Skip to content

Commit

Permalink
uint256: Add conversion from stdlib big int support.
Browse files Browse the repository at this point in the history
This adds a convenience method for converting a standard library big
integer to a uint256 (modulo 2^256) along with associated tests to
ensure proper functionality.

This is part of a series of commits to fully implement the uint256
package.
  • Loading branch information
davecgh committed Nov 16, 2021
1 parent 068d2c5 commit 9db96fa
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 0 deletions.
42 changes: 42 additions & 0 deletions internal/staging/primitives/uint256/uint256.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ var (
// zero32 is an array of 32 bytes used for the purposes of zeroing and is
// defined here to avoid extra allocations.
zero32 = [32]byte{}

// bigUint256Mask is the value 2^256 - 1 (aka max uint256) as a stdlib big
// integer. It is defined here to save allocations in the conversion code.
bigTwo256 = new(big.Int).Lsh(big.NewInt(1), 256)
bigUint256Mask = new(big.Int).Sub(bigTwo256, big.NewInt(1))
)

// Uint256 implements high-performance, zero-allocation, unsigned 256-bit
Expand Down Expand Up @@ -1775,3 +1780,40 @@ func (n *Uint256) ToBig() *big.Int {
n.PutBig(&out)
return &out
}

// SetBig sets the uint256 to the passed standard library big integer modulo
// 2^256.
//
// The resulting uint256 will be set to the 2's complement of the provided value
// when it is negative.
//
// The uint256 is returned to support chaining. This enables syntax like:
// n := new(Uint256).SetBig(n2).AddUint64(1) so that n = n2 + 1 where n2 is not
// modified.
//
// PERFORMANCE NOTE: When the caller expects values to potentially be larger
// than a max uint256, it is _highly_ recommended to reduce the value mod 2^256
// prior to calling this method for better performance.
//
// The reason is that this method requires an allocation and copy when the
// provided big integer is larger than a max uint256 in order to reduce it
// without modifying the provided arg. The caller can avoid this allocation by
// performing the mod 2^256 prior to calling this method with the value.
//
// More concretely, it is around 3 to 4 times faster to perform the reduction
// caller side as well as avoiding the allocation.
func (n *Uint256) SetBig(n2 *big.Int) *Uint256 {
// Take the value mod 2^256 if needed.
tmp := n2
if n2.BitLen() > 256 {
tmp = new(big.Int).And(n2, bigUint256Mask)
}

var buf [32]byte
tmp.FillBytes(buf[:]) // Requires Go 1.15.
n.SetBytes(&buf)
if tmp.Sign() < 0 {
n.Negate()
}
return n
}
95 changes: 95 additions & 0 deletions internal/staging/primitives/uint256/uint256_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3967,3 +3967,98 @@ func TestUint256ToBigRandom(t *testing.T) {
}
}
}

// TestUint256SetBig ensures that setting a uint256 to a standard library big
// integer works as expected for edge cases.
func TestUint256SetBig(t *testing.T) {
tests := []struct {
name string // test description
in string // hex encoded big int test value
want string // expected hex encoded uint256
}{{
name: "0",
in: "00",
want: "0",
}, {
name: "1",
in: "1",
want: "1",
}, {
name: "-1",
in: "-1",
want: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
}, {
name: "2",
in: "2",
want: "2",
}, {
name: "-2",
in: "-2",
want: "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe",
}, {
name: "max int256 (2^255 - 1)",
in: "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
want: "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
}, {
name: "min int256 (-2^255)",
in: "-8000000000000000000000000000000000000000000000000000000000000000",
want: "8000000000000000000000000000000000000000000000000000000000000000",
}, {
name: "max uint256 (2^256 - 1)",
in: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
want: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
}, {
name: "max uint256 + 1 (2^256)",
in: "10000000000000000000000000000000000000000000000000000000000000000",
want: "0",
}, {
name: "max uint256 + 2 (2^256 + 1)",
in: "10000000000000000000000000000000000000000000000000000000000000001",
want: "1",
}}

for _, test := range tests {
// Parse test vals.
in, ok := new(big.Int).SetString(test.in, 16)
if !ok {
t.Errorf("%q: big int parse err for string %s", test.name, test.in)
continue
}
want := hexToUint256(test.want)

// Ensure setting the uint256 to the test big int produces the expected
// value.
var n Uint256
n.SetBig(in)
if !n.Eq(want) {
t.Errorf("%q: unexpected result -- got: %x, want: %x", test.name, n,
want)
continue
}
}
}

// TestUint256SetBigRandom ensures that converting bit ints created from random
// values to uint256s works as expected.
func TestUint256SetBigRandom(t *testing.T) {
// Use a unique random seed each test instance and log it if the tests fail.
seed := time.Now().Unix()
rng := rand.New(rand.NewSource(seed))
defer func(t *testing.T, seed int64) {
if t.Failed() {
t.Logf("random seed: %d", seed)
}
}(t, seed)

for i := 0; i < 100; i++ {
// Generate big integer and uint256 pair.
bigN, wantUint256 := randBigIntAndUint256(t, rng)

// Convert the big int to a uint256 and ensure they match.
uint256Result := new(Uint256).SetBig(bigN)
if !uint256Result.Eq(wantUint256) {
t.Fatalf("mismatched uint256 conversion: %x -- got %x, want %x",
bigN, uint256Result, wantUint256)
}
}
}

0 comments on commit 9db96fa

Please sign in to comment.