secp256k1: Separate packages for field implementations. - #3757
Open
davecgh wants to merge 32 commits into
Open
Conversation
This updates the README.md and doc.go files to include some important caveats about the constant time and memory clearing behavior. Specifically, while the behavior has been manually verified by carefully reviewing the generated assembly as of the most recent version of Go, it is impossible to guarantee the compiler will not change in the future.
This adds a new test helper for some algebraic properties of scalar negation and calls it from the test that handles specific edge cases as well as the randomized test.
This adds a new test helper for some algebraic properties of modular multiplicative inverses of scalars and calls it from the test that handles specific edge cases as well as the randomized test.
This adds a new test helper for some algebraic properties of the scalar half order check and calls it from the test that handles specific edge cases as well as the randomized test.
This significantly optimizes the ModNScalar implementation by rewriting it to use saturated 4x64 arithmetic similar to the new 4x64 field arithmetic. The new implementation retains all of the important properties such as being constant time and only using temporary buffers on the stack that are cleared. Of particular interest is the new modular reduction implementation. It moves away from the 96-bit accumulate design and instead manually tracks and discards carries inline which provides a major speed advantage. Due to the complexity and potential mistakes in the arithmetic, all of the intermediate bounds and carry assumptions documented that the code relies upon have been formally verified to be correct. The formal proof is in a separate commit. The implementation is primarily targeted at 64-bit since it is by far the most common architecture in modern use, however, as the included benchmarks below show, it is faster for almost all operations on 32-bit architectures as well. The few operations where it is a bit slower on 32-bit architectures are not used anywhere near as frequently as the ones that get 10-30% better performance, so the end result is the overall implementation is notably faster on 32 bit too. For 64-bit architectures, every operation is significantly faster. For example, all of the primary operations that get heavy use get ~73-94% better performance. Finally, the tests have been updated to use values specifically crafted to exercise the new limbs versus the old values that were crafted for the old limbs. Comparison for 32-bit platforms: $ benchstat beforescalar_x86.txt afterscalar_x86.txt name old time/op new time/op delta ----------------------------------------------------------------------------------- ModNScalar 22.9ns ± 4% 23.2ns ± 5% ~ (p=0.184 n=10+10) ModNScalarZero 1.36ns ± 3% 6.59ns ± 6% +385.95% (p=0.000 n=9+9) ModNScalarIsZero 0.32ns ± 4% 0.34ns ±14% ~ (p=0.143 n=10+10) ModNScalarEquals 3.69ns ± 4% 0.30ns ± 7% -91.78% (p=0.000 n=10+10) ModNScalarAdd 25.1ns ± 6% 22.5ns ± 5% -10.40% (p=0.000 n=9+10) ModNScalarMul 257ns ± 5% 223ns ± 5% -13.07% (p=0.000 n=10+10) ModNScalarSquare 267ns ± 5% 172ns ± 6% -35.56% (p=0.000 n=10+10) ModNScalarNegate 12.8ns ± 9% 14.5ns ± 2% +13.39% (p=0.000 n=10+10) ModNScalarInverse 686ns ± 7% 623ns ± 3% -9.24% (p=0.000 n=10+9) ModNScalarIsOverHalfOrder 8.92ns ± 6% 6.55ns ± 3% -26.59% (p=0.000 n=10+10) Comparison for 64-bit platforms: $ benchstat beforescalar_x64.txt afterscalar_x64.txt name old time/op new time/op delta ModNScalar 12.1ns ± 1% 3.5ns ± 4% -71.25% (p=0.000 n=7+10) ModNScalarZero 0.64ns ± 9% 0.61ns ± 5% -4.08% (p=0.043 n=10+10) ModNScalarIsZero 0.31ns ± 8% 0.31ns ± 6% ~ (p=0.000 n=10+10) ModNScalarEquals 2.63ns ± 5% 0.33ns ± 9% -87.47% (p=0.000 n=10+10) ModNScalarAdd 15.1ns ± 4% 4.1ns ± 4% -73.25% (p=0.000 n=10+10) ModNScalarMul 214ns ± 6% 29ns ± 6% -86.30% (p=0.000 n=10+10) ModNScalarSquare 215ns ± 4% 23ns ± 6% -89.39% (p=0.000 n=10+10) ModNScalarNegate 5.78ns ± 4% 2.99ns ±10% -48.33% (p=0.000 n=10+10) ModNScalarInverse 452ns ± 4% 408ns ± 6% -9.89% (p=0.000 n=10+10) ModNScalarIsOverHalfOrder 5.52ns ± 6% 0.29ns ± 5% -94.66% (p=0.000 n=10+9)
This adds a formal verification proof of the scalar reduction arithmetic in the new 4x64 scalar implementation and updates the proofs README.md accordingly. It includes a formal proof for the following: - 512-bit modular reduction over the group order with saturated 64-bit limbs
With the ultimate goal of optimizing scalar multiplication by reducing the average number of point additions, this commit adds code to convert a balanced scalar representative into width-5 windowed non-adjacent form (wNAF). The current scalar multiplication implementation uses ordinary non-adjacent form (NAF), which corresponds to a window width of 2. This commit introduces only the width-5 wNAF recoder and does not yet integrate it into scalar multiplication. Future commits will add comprehensive tests and integrate the new implementation.
This adds comprehensive tests for the new wNAF implementation to help verify correctness. The tests assert the resulting encoding satisfies the required mathematical properties and reconstructs the original value for both deterministic edge cases and randomized balanced scalar representatives. The deterministic edge cases exercise enough small values to exhaustively test every residue modulo 2^w multiple times, and include additional edge cases such as carries introduced by the recoding, maximum values, and alternating bit patterns.
This updates the non-constant-time scalar multiplication implementation to use the new width-5 wNAF recoding. In particular, because wNAF requires precomputed odd multiples that were not needed by the previous implementation, this introduces a new dedicated precomputed table type along with a new helper to construct the required multiples. The previous implementation only needed to swap between two precomputed points depending on the scalar signs. However, since the new wNAF implementation uses larger precomputed tables, it instead tracks whether each decomposed scalar was negated and applies the corresponding sign adjustment to the appropriate half of each table. The following benchmark shows a before and after comparison of scalar multiplication: name old time/op new time/op delta --------------------------------------------------------------------------- ScalarMultNonConst 94.4µs ± 1% 84.3µs ± 1% -10.75% (p=0.000 n=10+10)
This removes the old ordinary NAF implementation that is no longer used now that the implementation has been updated to use a new width-5 wNAF recoder instead. It also removes the associated tests and benchmark accordingly.
The package currently defines several generic constant time helpers in the main package despite them not being specific to any particular type or algorithm. They are instead arithmetic primitives that are shared by multiple implementations. Notably, the helpers used by the field implementations that will be split into separate packages in future commits to prepare for architecture-specific backend selection. In order to smooth that transition, this moves the helpers into a new internal arith package and updates all call sites to use them. No functional changes are intended. The move is purely organizational and preserves the existing constant time semantics.
This adds dedicated tests for the constant time helpers in the new internal arith package. While the helpers are already well exercised by the upper layers that use them, testing the primitives directly helps ensure their correctness in isolation.
The package currently defines modulus-agnostic 512-bit multiplication and squaring primitives as part of the 64-bit field implementation despite them also being used by the scalar implementation. They are also shared by the field implementations that will be split into separate packages in future commits to prepare for architecture-specific backend selection. In order to smooth that transition, this moves the primitives into the internal arith package and updates all call sites to use them. No functional changes are intended. The move is purely organizational and preserves the existing arithmetic.
This adds dedicated tests for the 512-bit multiplication in the new internal arith package. While it is already already well exercised by the upper layers that use it, testing primitives directly helps ensure correctness in isolation.
This adds dedicated tests for the 512-bit squaring in the new internal arith package. While it is already already well exercised by the upper layers that use it, testing primitives directly helps ensure correctness in isolation.
The current CPU feature detection for the recently added MULX/ADX optimizations is done directly in the field64 code. There is nothing wrong with this from a purely functional standpoint, but it is difficult to extend and can't be reused across other packages in the module. With that in mind, this adds an internal package to detect support for CPU and operating system features that are relevant to architecture-specific optimizations throughout the secp256k1 package. The package intentionally provides a minimal abstraction consisting only of the feature detection required by the secp256k1 implementation. This provides a clean mechanism for detecting and supporting new hardware extensions in the future. Note that does not incorporate the package yet. It only introduces the new code.
This adds dedicated tests for the functions in the new internal cpufeat package. Since the specific values depend on the hardware, this avoids asserting specific values that would fail when not running on specific hardware and instead focuses on ensuring the functions are idempotent.
This updates the field64 code to use the new cpufeat package for feature detection instead of doing it manually inline.
This modifies the field64 amd64 benchmarks to be more consistent with the approach used for architecture-specific benchmarks in the rest of the code base. This approach is cleaner and makes it easier to add additional specialized implementations in the future.
This renames the field64Mul and field64Square funcs and their variants to include Reduce in their name to make it clear they are fused multiply/square-and-reduce operations unlike the generic arith.Mul512 and field64Reduce512 primitives.
The generic implementations of the fused field multiplication and squaring operations are currently duplicated in the amd64-specific code and the generic fallback code. Rather than duplicate that code, this moves the shared reduction implementation and generic fused multiply/square helpers into field64_generic.go and updates the architecture-specific dispatch to invoke the shared generic implementation when the optimized implementation is unavailable. To accomplish that, a new field64_noasm.go is introduced that only builds when the architecture-specific code is not built. Finally, it moves the amd64 instruction set architecture dispatch into field64isa_amd64.go file so that it only builds on amd64 and clearly separates ISA selection from the implementation itself. These modifications mirror the approach used for architecture-dependent code in the rest of the code base.
Historically, there has only ever been a single field implementation and
the current release of the module reflects that fact with a single
exported FieldVal for that implementation. However, recent work has
introduced a new FieldVal64 implementation which is significantly more
efficient on 64-bit hardware.
Ultimately, the main package will need to select the implementation
based on the target architecture, but having two different types with
different semantics exposed from the main package would be confusing.
At the same time, consumers should retain the flexibility to use a
specific implementation directly if their particular use case warrants
it.
The primary design goals are:
- Avoid breaking the public API in a way that would require a major
module version bump
- Make both variants available to consumers irrespective of the choice
the module makes for internally performing its own calculations over
the curve
- This allows them to make independent calculations using their
preferred implementation
- Ensure the public FieldVal documentation always reflects the most
restrictive semantics among all supported field implementations
- Note that a simple type alias that changes based on the architecture
does not provide this property
- Provide a clean structure that allows adding additional assembler
optimizations without bloating the main package
After considering a variety of approaches, the following design
satisfies all of those goals:
- Split the field implementations into separate subpackages and rename
them to Element within their respective packages
- Make FieldVal a thin wrapper over the chosen implementation
The required changes are largely mechanical, but they are quite
extensive.
So, with the goal of making it much easier to review and reason about
the correctness of the changes, this is the first in a series of commits
that will ultimately land on the described design.
This first commit moves the entire FieldVal implementation, tests, and
benchmarks into a separate package named field10x26 and only makes the
minimal required changes to ensure the code compiles and all tests pass.
It also temporarily aliases FieldVal to the new package. That alias
will be replaced with the wrapper in a subsequent commit.
No functional changes are intended by this commit. It is strictly a
refactoring that preserves the existing behavior while preparing for the
architecture-specific backend selection.
This renames FieldVal in the new field10x26 package to Element to more accurately reflect that it is an implementation-specific field element rather than the public compatibility type. It also renames all tests and benchmarks to match and updates various comments accordingly. While here, also update the internal terminology to consistently use the more technically accurate term "limbs". It is a large change in terms of lines changed, but it is entirely mechanical. No functional changes are intended by this commit. It is strictly a refactoring that preserves the existing behavior. This is part of a series of commits that will ultimately allow architecture-specific backend selection.
This replaces the public FieldVal type alias with a wrapper around the underlying field implementation. This preserves the existing API while allowing the concrete field implementation to vary independently of the public type. It also ensures the public documentation consistently describes the most restrictive semantics required by all supported implementations rather than those of whichever implementation happens to be selected. The wrapper forwards all methods directly to the underlying implementation, so no functional changes are intended. This is part of a series of commits that will ultimately allow architecture-specific backend selection.
This adds dedicated tests for the new FieldVal wrapper. The tests intentionally do not perform exhaustive testing of each of the methods since that is handled by each implementation. Instead, the focus is on making sure the wrapped methods return the same result as the underlying implementation and the methods that involve chaining return the same wrapped instance. This is part of a series of commits that will ultimately allow architecture-specific backend selection.
This is part of a series of commits that will ultimately allow architecture-specific backend selection.
This moves the entire FieldVal64 implementation, tests, and benchmarks into a separate package named field4x64 and only makes the minimal required changes to ensure the code compiles and all tests pass. It also modifies the Normalize method to match the signature used by FieldVal for API parity. No functional changes are intended by this commit. It is strictly a refactoring that preserves the existing behavior. This is part of a series of commits that will ultimately allow architecture-specific backend selection.
This renames FieldVal64 in the new field4x64 package to Element to more accurately reflect that it is an implementation-specific field element rather than the public compatibility type. It also renames all tests and benchmarks to match and updates various comments accordingly. It is a large change in terms of lines changed, but it is entirely mechanical. No functional changes are intended by this commit. It is strictly a refactoring that preserves the existing behavior. This is part of a series of commits that will ultimately allow architecture-specific backend selection.
This is part of a series of commits that will ultimately allow architecture-specific backend selection.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This requires #3754.
Historically, there has only ever been a single field implementation and the current release of the module reflects that fact with a single exported
FieldValfor that implementation. However, recent work has introduced a newFieldVal64implementation which is significantly more efficient on 64-bit hardware.Ultimately, the main package will need to select the implementation based on the target architecture, but having two different types with different semantics exposed from the main package would be confusing. At the same time, consumers should retain the flexibility to use a specific implementation directly if their particular use case warrants it.
The primary design goals are:
FieldValdocumentation always reflects the most restrictive semantics among all supported field implementationsAfter considering a variety of approaches, the following design satisfies all of those goals:
Elementwithin their respective packagesFieldVala thin wrapper over the chosen implementationThe required changes are largely mechanical, but they are quite extensive.
So, with the goal of making it much easier to review and reason about the correctness of the changes, this PR is split into a series of commits that will ultimately land on the described design.
The high-level overview of commits are as follows:
FieldValimplementation, tests, and benchmarks into a separate package namedfield10x26and only make the minimal required changes to ensure the code compiles and all tests passFieldValin the newfield10x26package toElementto more accurately reflect that it is an implementation-specific field element rather than the public compatibility typeFieldValwhich defers to a backend impelmentationfield10x26README.mdFieldVal64implementation, tests, and benchmarks into a separate package namedfield4x64and only make the minimal required changes to ensure the code compiles and all tests passFieldVal64in the newfield4x64package toElementfor the same reason as beforefield4x64README.mdSee the individual commit descriptions for more details.
No functional changes are intended by this PR. It is strictly a refactoring that preserves the existing behavior while preparing for the architecture-specific backend selection.