docs: Clarify that callback can be called more than once #1727

pull real-or-random wants to merge 2 commits into bitcoin-core:master from real-or-random:202508-illegal-twice changing 1 files +22 −20
  1. real-or-random commented at 8:09 am on August 20, 2025: contributor

    The tests in #1698 reminded me that some functions, e.g., secp256k1_ec_pubkey_cmp, may call the illegal callback more than once (see #1390 (review) for more context). This PR clarifies the API docs to state explicitly that this is possible.

    This is the simplest solution. Any production code should crash anyway if it encounters a callback. And in debug code or in our test code, it doesn’t really matter whether you see an error message once or twice.

    The alternative is to provide a guarantee that the callback is called only once. But that would make our code more complex for no good reason.

    The second commit fixes a few typos, wording, and consistency.

  2. real-or-random added the label user-documentation on Aug 20, 2025
  3. real-or-random added the label tweak/refactor on Aug 20, 2025
  4. sipa commented at 1:16 am on September 3, 2025: contributor
    concept ack
  5. real-or-random commented at 11:30 am on September 3, 2025: contributor
    @stratospher Want to review this? :)
  6. in include/secp256k1.h:341 in d2793799c7 outdated
    335@@ -336,34 +336,35 @@ SECP256K1_API void secp256k1_context_destroy(
    336  *
    337  *  The philosophy is that these shouldn't be dealt with through a
    338  *  specific return value, as calling code should not have branches to deal with
    339- *  the case that this code itself is broken.
    340+ *  the case that this code itself is broken. On the other hand, during debug
    341+ *  stage, one would want to be informed about such mistakes, and the default
    342+ *  (crashing) may be inadvisable.
    


    stratospher commented at 6:28 pm on September 3, 2025:
    • When this callback is triggered, the API function called is guaranteed not
    • to cause a crash, though its return value and output arguments are
    • undefined.

    when I initially read the comments on master, I assumed “API not crashing guarantee” is only for debug stage (no abort call) since they were in 1 para. but after seeing this diff just realised that crashing in a callback is acceptable and still a stable API regardless of debug/production build. so I think the comments made it clearer for me!


    real-or-random commented at 6:51 pm on September 3, 2025:

    Nice, paragraph breaks matter. :)

    Though I think it is relatively clear even on master. When the callback calls abort, then the entire program will crash immediately. And from that point on, any guarantee you get from the API is vacuous.


    stratospher commented at 7:28 pm on September 3, 2025:

    ok that makes sense. but now I’m confused. It’s just a line break difference but I interpreted the meaning before and after as:

    1. before:
    0*  The philosophy is that these shouldn't be dealt with through a
    1 *  specific return value, as calling code should not have branches to deal with
    2 *  the case that this code itself is broken.
    3 *
    4 *  On the other hand, during debug stage, one would want to be informed about
    5 *  such mistakes, and the default (crashing) may be inadvisable.
    6 *  When this callback is triggered, the API function called is guaranteed not
    7 *  to cause a crash, though its return value and output arguments are
    

    API function called is guaranteed not to cause a crash in debug stage (in tests where it’s mostly the callback function counting_callback_fn that just counts and no abort)

    1. Now:
    0*  The philosophy is that these shouldn't be dealt with through a
    1 *  specific return value, as calling code should not have branches to deal with
    2 *  the case that this code itself is broken. On the other hand, during debug
    3 *  stage, one would want to be informed about such mistakes, and the default
    4 *  (crashing) may be inadvisable.
    5 *
    6 *  When this callback is triggered, the API function called is guaranteed not
    7 *  to cause a crash, though its return value and output arguments are
    8 *  undefined. A single API call may trigger the callback more than once.
    

    API function called is guaranteed not to cause a crash in any stage (tests or IRL) => because it’s the callback doing the crash + handling invalid user inputs + user responsibility and so API is stable?

    do we want 1 or 2?


    real-or-random commented at 9:33 am on September 5, 2025:

    Huh, I think the guarantee we want to provide is: “An API function called is guaranteed not to crash after the callback returns.”

    This guarantee holds no matter what the callback function is set to, which corresponds to what you called “in any “stage”. Now, of course, this guarantee doesn’t say anything if the callback itself causes a crash (and the default callback does) because then the callback never returns, so there’s no “after the callback returns”.


    Now that I’ve written this, I’m not sure what this actually means. In simple cases, the only thing the API function does after the callback returns is to return 0. This shouldn’t crash. But there are more complex cases:

    • Some functions try to read more arguments from memory, e.g., secp256k1_ec_pubkey_cmp tries to read the memory at pubkey1 even after an invalid pubkey0 triggered the callback. This may crash if pubkey1 points to an invalid memory region.
    • Some functions write to their output arguments after the callback has returned (see #1736 for another rabbit hole…).

    Perhaps what we should want to say here is something like this: “An API function called is guaranteed not to crash after the callback returns (unless the crash is unrelated to the violation that triggered the callback)”. But this is imprecise and ugly.

    Perhaps we should drop this sentence entirely. It creates more confusion than it resolves. What about this?

    “Should the callback return, the return value and output arguments of the API function call are undefined. Moreover, the same API call may trigger the callback again in this case.”


    Note that all of this is about API guarantees, so the audience is API users. As a result, “debug stage” means “debug stage of a program using libsecp256k1”. (And as a side remark, note that the callbacks can be set at runtime, so strictly speaking, this is not (necessarily) about debug vs. release builds.) So if you develop a program (an application or another library) that uses the libsecp256k1 API, you may want to set a callback that does not crash for debugging purposes. I’m not entirely sure how useful this functionality is to API users, but this is what the docs here describe.

    In practice, where this is useful is exactly in our internal tests, as you correctly point out. But what I’m trying to say is that these are our internal tests, where we may do everything because we control the implementation of the API functions. So we could also have an internal way of setting a counting callback that is not exposed through the public API.

    If you ask me, the main purpose of setting callback functions is that you can control how the library crashes in production code. Maybe in your application, you don’t want abort() but a more graceful termination. Or you can’t use the default callback because it writes an error message to stdout but you don’t have stdout because you’re developing for a hardware wallet. (In the latter case, setting callbacks at runtime won’t help you because the program won’t compile in the first place. We’ve added compile-time overrides for this case.)

    And on a last note, I don’t think all of this is optimally designed. I believe we’d do some things differently if we had a chance to start from scratch.


    stratospher commented at 6:55 am on September 17, 2025:

    Thank you for the awesome explanation! This helped me understand what the API is actually guaranteeing.

    “Should the callback return, the return value and output arguments of the API function call are undefined. Moreover, the same API call may trigger the callback again in this case.”

    I like this wording better!


    real-or-random commented at 11:00 am on September 22, 2025:
    Ok nice, force-pushed to switch to (a variant of) this.
  7. stratospher commented at 6:29 pm on September 3, 2025: contributor
    ACK d279379. verified that illegal callback can be called more than once. and the simple route makes sense.
  8. josibake commented at 9:53 am on September 8, 2025: member

    Concept ACK

    While working on #1698, I was very confused when I started to see errors stating the illegal callback had been called more than once. Having some documentation to indicated what is allowed/expected would have been very helpful. Haven’t had a chance to dig into the exact wording in this PR but generally agree we should have something.

  9. theStack commented at 2:19 pm on September 10, 2025: contributor
    Concept ACK
  10. theStack approved
  11. theStack commented at 6:11 pm on September 12, 2025: contributor
    ACK d2793799c70e33cc73d0b3bb94c342935d3cedc5
  12. docs: Clarify that callback can be called more than once 895f53d1cf
  13. docs: Improve API docs of _context_set_illegal_callback 4d90585fea
  14. real-or-random force-pushed on Sep 22, 2025
  15. stratospher commented at 12:39 pm on September 22, 2025: contributor
    ACK 4d90585.
  16. theStack approved
  17. theStack commented at 4:50 am on September 24, 2025: contributor
    re-ACK 4d90585feaa52711133deb0c5a3cb32fca73d042
  18. real-or-random merged this on Sep 24, 2025
  19. real-or-random closed this on Sep 24, 2025

  20. real-or-random added the label needs-changelog on Sep 24, 2025

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin-core/secp256k1. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-11-04 02:15 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me