Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions go/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,17 @@ func ComputeAddress(config SystemConfigResponse, publicKey string) string {
}

func GrindKey(keySeed string, keyValLimit *big.Int) string {
sha256EcMaxDigest := new(big.Int)
sha256EcMaxDigest.SetString("1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000", 16)
// SHA256_EC_MAX_DIGEST is 2^256, the size of the SHA-256 output space.
sha256EcMaxDigest := new(big.Int).Lsh(big.NewInt(1), 256)
maxAllowedVal := new(big.Int).Sub(sha256EcMaxDigest, new(big.Int).Mod(sha256EcMaxDigest, keyValLimit))

i := 0
key := hashKeyWithIndex(keySeed, i)
i++

// Make sure the produced key is divided by the Stark EC order, and falls within the range
// [0, maxAllowedVal).
for key.Cmp(maxAllowedVal) < 0 {
// [0, maxAllowedVal). Keep grinding while the key is out of range (>= maxAllowedVal).
for key.Cmp(maxAllowedVal) >= 0 {
key = hashKeyWithIndex(keySeed, i)
i++
}
Expand All @@ -92,15 +92,22 @@ func GrindKey(keySeed string, keyValLimit *big.Int) string {
return fmt.Sprintf("0x%x", result)
}

// paddedHex returns the hex string with a leading '0' prepended when its length
// is odd, ensuring it decodes to a whole number of bytes.
func paddedHex(h string) string {
if len(h)%2 != 0 {
return "0" + h
}
return h
}

func hashKeyWithIndex(keySeed string, index int) *big.Int {
// Remove '0x' prefix if present
key := strings.TrimPrefix(keySeed, "0x")

// Convert index to hex and pad to 2 bytes
indexHex := fmt.Sprintf("%02x", index)

// Combine key and index
data := key + indexHex
// Combine key and index, each padded to an even number of hex digits so the
// concatenation decodes cleanly to bytes (matching the reference padded_hex).
data := paddedHex(key) + paddedHex(fmt.Sprintf("%x", index))

// Decode hex string to bytes
dataBytes, err := hex.DecodeString(data)
Expand Down
79 changes: 79 additions & 0 deletions go/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package main

import (
"math/big"
"testing"

"github.com/stretchr/testify/require"
)

// starkEcOrder is the Stark curve order (EC_ORDER) used as the key value limit,
// matching the constant used by paradex-py and paradex.js.
const starkEcOrder = "0800000000000010ffffffffffffffffb781126dcae7b2321e66a241adc64d2f"

func ecOrder(t *testing.T) *big.Int {
t.Helper()
n, ok := new(big.Int).SetString(starkEcOrder, 16)
require.True(t, ok, "failed to parse Stark EC order")
return n
}

// TestGrindKey pins GrindKey's output to golden values generated from the
// reference implementations (paradex-py / paradex.js grind_key). Any divergence
// in the loop condition, the SHA256_EC_MAX_DIGEST constant, or the seed/index
// hex padding changes the derived key and fails here.
func TestGrindKey(t *testing.T) {
n := ecOrder(t)

cases := []struct {
name string
seed string
want string
}{
// Typical 64-hex-char seed (the real path derives the seed from the
// first 32 bytes of an Ethereum signature, i.e. always 64 chars).
{
name: "all_a_64",
seed: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
want: "0x20c20907b449bacb0a16d9a297ea531aa98eadd80424215dc17da8604868ebf",
},
{
name: "typical",
seed: "1b2e3d4c5f6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f70819a2b3c4d5e6",
want: "0x520e4af362957fe86d08d4524b84de5c1d061b3673320863382d95c19365753",
},
// Odd-length seed: the reference pads it to even length before hashing.
// A naive Go port (no padding) panics in hex.DecodeString.
{
name: "odd_len",
seed: "abc",
want: "0x5fe775158c41e99ecc67b2ebf8d33da7cd3666e6b50a7b5ae761dd5abf993a2",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := GrindKey(tc.seed, n)
require.Equal(t, tc.want, got)
})
}
}

// TestGrindKeyAcceptsHexPrefix ensures a "0x"-prefixed seed grinds to the same
// key as the bare seed.
func TestGrindKeyAcceptsHexPrefix(t *testing.T) {
n := ecOrder(t)
seed := "1b2e3d4c5f6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f70819a2b3c4d5e6"
require.Equal(t, GrindKey(seed, n), GrindKey("0x"+seed, n))
}

// TestGrindKeyInRange checks the produced key is a valid Stark private key,
// i.e. it lies in [0, EC_ORDER).
func TestGrindKeyInRange(t *testing.T) {
n := ecOrder(t)
out := GrindKey("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", n)
key, ok := new(big.Int).SetString(out[2:], 16) // strip "0x"
require.True(t, ok)
require.Equal(t, -1, key.Cmp(n), "key must be < EC_ORDER")
require.GreaterOrEqual(t, key.Sign(), 0, "key must be >= 0")
}