Compile time SHA256 override? #702

issue gmaxwell openend this issue on December 18, 2019
  1. gmaxwell commented at 12:22 pm on December 18, 2019: contributor

    It’s a bit silly that the library packs in its own sha256 even when the user has their own and on small devices it’s a costly waste of space. In places like Bitcoin Core the environment has a fast SIMD or SHA-NI sha256, … it actually makes a measurable difference in signing time to use SHA-NI.

    It would be nice if there were some -Dlibsecp2561k_sha256init=x -Dlibsecp2561k_sha256update=y -Dlibsecp2561k_sha256final=z settings that could be used to compile time substitute in another library and leave the internal one out.

  2. elichai commented at 12:25 pm on December 18, 2019: contributor

    Last time we discussed this on IRC some people said that providing 3 functions might be a bit much and having a single sha256 function instead is better. I think that a single one might be a bit awkward because it requires allocating a big buffer on the stack that can contain everything (and then more things you need to clean).

    What are your thoughts on this?

  3. real-or-random commented at 4:48 pm on December 18, 2019: contributor
    I think you’ll want this feature only if you seriously care about performance. And in this case, you probably have the more complex API with 3 functions anyway.
  4. apoelstra commented at 5:51 pm on December 18, 2019: contributor

    Also, in practice a single function is often harder to use - if you want to hash a series of things it’s nice to just throw them all into a hash engine … if you need to create a buffer first then (depending on your language) you may need to think about buffer allocation and indexing.

    Another alternate might be to just conditionally compile out the sha256 functions, and require the user provide replacements with exactly the correct names in order for linking to succeed. This is what we do for the default error callbacks for example.

  5. gmaxwell commented at 8:08 pm on December 18, 2019: contributor

    only if you seriously care about performance.

    I think the vast majority of users would be ones that don’t want to have an extra 19kilobytes in their binary.

    Another alternate might be to just conditionally compile out the sha256 functions, and require the user provide replacements with exactly the correct names in order for linking to succeed.

    That would be ducky too.

  6. elichai commented at 9:13 am on December 20, 2019: contributor
    Unless anyone already works on this I’d like to give it a try :)
  7. real-or-random commented at 5:12 pm on April 24, 2020: contributor

    Unless anyone already works on this I’d like to give it a try :)

    Could you look into this?

  8. elichai commented at 5:46 pm on April 24, 2020: contributor

    Could you look into this?

    IIRC the problem I encountered was the SHA2 context. I remember a few solutions but none of them was great:

    1. Decide that the context needs to be size X, provide an opaque struct with unsigned array size X, and the implementer can memcpy this into his own Context back and forth (or type pun it? need to look into what the standard says on that).

    2. The implementer also needs to provide a header with the name sha256.h that defines the struct+functions using the same names/symbols we use.

    3. Provide a c_void pointer to the context in the function’s API. (it then requires a reset function to reset the context, and it limits us because we can’t hash two things “in parallel”)

    Any feedback on which is best or maybe a different solution is appreciated.

  9. sipa commented at 5:58 pm on April 24, 2020: contributor

    One possibility is keeping the SHA256 padding/chopping into blocks on the secp256k1 side, and only require external implementations to provide a SHA256 transformation function.

    The API would look like:

    0/** Update the state (array of 8 uint32_t) pointed to by state with 64*blocks bytes of input pointed to by data. */
    1void sha256_transform(uint32_t *state, size_t blocks, unsigned char *data);
    

    This means the external implementation may need to copy data from/to “array of 8 uint32_t” to their own internal representation - but I suspect almost any C-like implementation already uses that representation anyway.

  10. elichai commented at 7:23 am on April 26, 2020: contributor

    That’s an interesting idea. I do think it will probably require the user to slightly modify his implementation to match this (I’ve looked at a few sha2 implementations I’ve found on google, and they all have their own quirks when it comes to the “transform” function)

    And is there any real advantage with passing a bunch of blocks at the same time? won’t it just call the transform function in a loop? or is there some SIMD/SHA-NI magic this enables?

  11. elichai commented at 3:05 pm on April 26, 2020: contributor

    I played with this, it works pretty nice, but then I realized that one of the reasons were binary size, so I measured using counting asm lines in godbolt(I’m not sure if it’s a good way to compare), and exposing RFC6979+HMAC+SHA2+SHA2_transform. with -Os (for minimal size): As-is: 3,523 lines of asm. Omitting the transform function: 429 lines. Omitting all of SHA2: 242 lines.

    with -O2: As-is: 3,844 lines. Omitting transform: 511 lines. Omitting all of SHA2: 236 lines.

    with -O3: As-is: 6,852 lines. Omitting transform: 3,470 lines. Omitting all of SHA2: 565 lines. (-O3 is crazy lol)

    Anyone who wants to play with it: https://godbolt.org/z/MPF3w9 add -DUSE_EXTERNAL_SHA2_TRANSFORM to omit the transform function. add -DUSE_EXTERNAL_SHA2 to omit all of sha2 functions and leave only HMAC+RFC6979

  12. real-or-random commented at 4:20 pm on April 26, 2020: contributor

    For binary size you can really just look at the file size of the binary, see #700 (comment).

    add -DUSE_EXTERNAL_SHA2 to omit all of sha2 functions and leave only HMAC+RFC6979

    If at all, I think we would want the opposite. We’ll need SHA256 for Schnorr sigs and ECDH, but HMAC/RFC6979 is only used for deriving nonces, and you can use SHA256, too.

  13. elichai commented at 8:45 am on April 27, 2020: contributor

    If at all, I think we would want the opposite. We’ll need SHA256 for Schnorr sigs and ECDH, but HMAC/RFC6979 is only used for deriving nonces, and you can use SHA256, too.

    I just had to expose some end result through godbolt to get asm, I used RFC6979 because it doesn’t have any complex logic and it uses SHA256 in a complex way that prevent inlining everything and optimizing write together with finalize and initialize (so it’s an example to show the asm code of SHA2 not of RFC6979)

  14. real-or-random commented at 9:39 am on April 28, 2020: contributor
    This really depends on whether we have a compile-time override or a runtime override but we should think about a self-test in the spirit of https://github.com/bitcoin/bitcoin/blob/99813a9745fe10a58bedd7a4cb721faf14f907a4/src/crypto/sha256.cpp#L465 .
  15. elichai commented at 9:32 pm on April 28, 2020: contributor
    I think the main advantage is actually code size, because my tests don’t show big advantages for optimized sha2 implementations, although I haven’t tested with SHA-NI but I don’t think it’s going to be a huge improvement in terms of performance, as SHA2 is really fast compared to anything EC related
  16. switck commented at 2:21 pm on February 1, 2021: none

    Im making embedded library containing libsecp256k1, see https://github.com/switck/libngu

    thoughts:

    • already have a few sha256’s functions in my codebase, do not want again in my binary (especially a vanilla version like this one)
    • would like to use the hmac-sha256 being carried in your library, but it’s declared static
    • definitely want your deterministic nonce, since that impacts private key security
    • this is a code-size problem, not a performance concern

    RE: sharing SHA256 code

    • a single-shot function is best if you plan to share code.
    • all s/w implementations use init/update/finalize but the context is never predictable
    • hardware SHA256 has context that isn’t just a memory pointer+length (ie. chip resources)
    • you are hashing short messages, not megabytes, so stop/restart not needed
    • allocating space on the stack is instant; copying/appending messages onto stack is quick
    • a single-shot function can optimize for specific message lengths, since complete message size is known at start.
  17. real-or-random commented at 3:23 pm on December 23, 2021: contributor
    @elichai Any update here? Maybe, if you currently don’t have a lot of time, it would still be good to share your WIP branch, so someone else could adopt the PR.
  18. elichai commented at 6:12 pm on December 23, 2021: contributor

    @elichai Any update here? Maybe, if you currently don’t have a lot of time, it would still be good to share your WIP branch, so someone else could adopt the PR.

    Sadly I can’t find it :(

    One possibility is keeping the SHA256 padding/chopping into blocks on the secp256k1 side, and only require external implementations to provide a SHA256 transformation function.

    The API would look like:

    0/** Update the state (array of 8 uint32_t) pointed to by state with 64*blocks bytes of input pointed to by data. */
    1void sha256_transform(uint32_t *state, size_t blocks, unsigned char *data);
    

    This means the external implementation may need to copy data from/to “array of 8 uint32_t” to their own internal representation - but I suspect almost any C-like implementation already uses that representation anyway.

    Do you know if implementations would want to keep the state as a __m128i instead of uint32_t? (I see that bitcoin’s implementations load it to vector registers each time, but wondering if that’s optimal)

  19. real-or-random commented at 1:34 pm on January 2, 2022: contributor

    It’s a bit silly that the library packs in its own sha256 even when the user has their own and on small devices it’s a costly waste of space.

    In places like Bitcoin Core the environment has a fast SIMD or SHA-NI sha256, … it actually makes a measurable difference in signing time to use SHA-NI.

    Going a step back, I believe that these are separate concerns. As per the discussion here, an override helps mostly to save space.

    If we want to profit from hardware optimizations, this is somewhat orthogonal. An override would help here but only if the caller controls the compilation. This is true for Bitcoin Core but in general it’s rather an exception. Also, if you look at Core’s SHA256 implementation (https://github.com/bitcoin/bitcoin/blob/master/src/crypto/sha256.cpp), it’s not clear how the override would look like. Depending on the available hardware, the essential function is either Transform, Transform_2way, Transform_4way, Transform_8way.

    This is an argument in favor of optimizing our SHA256 implementation (independent of the possibility to override), and I tend to believe that this is desirable. We didn’t want to do this in the past because SHA256 was an implementation detail. But as it’s an integral part of Schnorr verification and signing now, things have changed. It’s just somewhat messy: We’d need to duplicate code from Core (and worse, change it to C). Or move SHA256 (and maybe other stuff such as ChaCha20 for #760) to a separate library that’s linked to secp256k1 and Core. Or expose the SHA256 here and make Core use them. None of this sounds great.

    Thoughts?

  20. benma commented at 8:44 pm on July 14, 2024: contributor
    I’d very much like being able to plug in a different sha256 implementation to save binary space.

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: 2024-12-23 04:15 UTC

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