Skip to content

Conversation

@meyer9
Copy link
Collaborator

@meyer9 meyer9 commented Jan 8, 2026

This allows testing things like Block-STM where the number of callers affects performance. If we use a single caller, every transaction conflicts with the next transaction on the balance/nonce of the caller.

Description

Testing

@cb-heimdall
Copy link
Collaborator

cb-heimdall commented Jan 8, 2026

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 1
Sum 2

keys := make([]*ecdsa.PrivateKey, numCallers)
addrs := make([]common.Address, numCallers)
for i := 0; i < numCallers; i++ {
key, err := ecdsa.GenerateKey(crypto.S256(), src)

Check failure

Code scanning / CodeQL

Use of insufficient randomness as the key of a cryptographic algorithm High

This cryptographic algorithm depends on a
random number
generated with a cryptographically weak RNG.

Copilot Autofix

AI 5 days ago

In general, cryptographic keys must be generated using a cryptographically secure pseudo-random number generator (CSPRNG), such as Go’s crypto/rand, and must not depend on math/rand. If deterministic derivation from a master key is desired (e.g., for reproducible simulations), it should be implemented using deterministic key-derivation techniques (e.g., hashing plus modular reduction) rather than by plugging a weak PRNG into a cryptographic key-generation API.

For this specific code, the simplest, least invasive fix is:

  • Stop using math/rand as the io.Reader passed to ecdsa.GenerateKey.
  • Instead, derive each new private key deterministically from the prefunded key using a cryptographic hash, and construct the ECDSA private key directly on the secp256k1 curve:
    • For each caller index i, compute skInt = keccak256(prefundedKey.D || i) mod N, where N is the curve order.
    • Ensure skInt is non-zero; if zero, tweak it (e.g., hash again with a suffix) or fall back to a minimal non-zero value.
    • Set privKey := &ecdsa.PrivateKey{D: skInt, PublicKey: *pubKey} where pubKey is computed via crypto.S256().ScalarBaseMult(skInt.Bytes()).
  • This preserves determinism while relying only on cryptographic primitives, not on math/rand, and removes the tainted rand.NewSource path entirely.

Concretely in runner/payload/simulator/worker.go:

  • Remove the use of math/rand inside generateCallerAccounts (we can leave the import alone if we’re not allowed to touch unused imports, but we will not use it).
  • Add a small helper inside generateCallerAccounts (or inline logic) to compute a derived scalar from prefundedKey.D and the index using crypto.Keccak256 and new(big.Int).Mod(...).
  • Use that derived scalar to populate keys[i] and addrs[i] instead of calling ecdsa.GenerateKey with src.

No new external dependencies are needed; we only use crypto/ecdsa, crypto/elliptic via crypto.S256(), math/big, and github.com/ethereum/go-ethereum/crypto (already imported).

Suggested changeset 1
runner/payload/simulator/worker.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/runner/payload/simulator/worker.go b/runner/payload/simulator/worker.go
--- a/runner/payload/simulator/worker.go
+++ b/runner/payload/simulator/worker.go
@@ -204,20 +204,46 @@
 		return []*ecdsa.PrivateKey{prefundedKey}, []common.Address{crypto.PubkeyToAddress(prefundedKey.PublicKey)}
 	}
 
-	// Use deterministic random source seeded from prefunded key
-	seed := int64(prefundedKey.D.Uint64())
-	src := rand.New(rand.NewSource(seed))
+	// Deterministically derive additional caller keys from the prefunded key using a cryptographic hash.
+	// This avoids using a non-cryptographic PRNG as the entropy source for key generation.
+	curve := crypto.S256()
+	N := curve.Params().N
 
 	keys := make([]*ecdsa.PrivateKey, numCallers)
 	addrs := make([]common.Address, numCallers)
-	for i := 0; i < numCallers; i++ {
-		key, err := ecdsa.GenerateKey(crypto.S256(), src)
-		if err != nil {
-			panic(fmt.Sprintf("failed to generate caller key: %v", err))
+
+	// First caller uses the prefunded key directly for consistency with the numCallers == 1 case.
+	keys[0] = prefundedKey
+	addrs[0] = crypto.PubkeyToAddress(prefundedKey.PublicKey)
+
+	for i := 1; i < numCallers; i++ {
+		// Derive a new private scalar from prefundedKey.D and the index using keccak256, reduced mod N.
+		// Encode index deterministically as base-10 string bytes.
+		indexBytes := []byte(strconv.Itoa(i))
+		baseDBytes := prefundedKey.D.Bytes()
+		derivedHash := crypto.Keccak256(baseDBytes, indexBytes)
+
+		d := new(big.Int).SetBytes(derivedHash)
+		d.Mod(d, N)
+		if d.Sign() == 0 {
+			// Extremely unlikely, but ensure we never use zero as a private scalar.
+			d.Add(d, big.NewInt(1))
 		}
-		keys[i] = key
-		addrs[i] = crypto.PubkeyToAddress(key.PublicKey)
+
+		x, y := curve.ScalarBaseMult(d.Bytes())
+		priv := &ecdsa.PrivateKey{
+			D: d,
+			PublicKey: ecdsa.PublicKey{
+				Curve: curve,
+				X:     x,
+				Y:     y,
+			},
+		}
+
+		keys[i] = priv
+		addrs[i] = crypto.PubkeyToAddress(priv.PublicKey)
 	}
+
 	return keys, addrs
 }
 
EOF
@@ -204,20 +204,46 @@
return []*ecdsa.PrivateKey{prefundedKey}, []common.Address{crypto.PubkeyToAddress(prefundedKey.PublicKey)}
}

// Use deterministic random source seeded from prefunded key
seed := int64(prefundedKey.D.Uint64())
src := rand.New(rand.NewSource(seed))
// Deterministically derive additional caller keys from the prefunded key using a cryptographic hash.
// This avoids using a non-cryptographic PRNG as the entropy source for key generation.
curve := crypto.S256()
N := curve.Params().N

keys := make([]*ecdsa.PrivateKey, numCallers)
addrs := make([]common.Address, numCallers)
for i := 0; i < numCallers; i++ {
key, err := ecdsa.GenerateKey(crypto.S256(), src)
if err != nil {
panic(fmt.Sprintf("failed to generate caller key: %v", err))

// First caller uses the prefunded key directly for consistency with the numCallers == 1 case.
keys[0] = prefundedKey
addrs[0] = crypto.PubkeyToAddress(prefundedKey.PublicKey)

for i := 1; i < numCallers; i++ {
// Derive a new private scalar from prefundedKey.D and the index using keccak256, reduced mod N.
// Encode index deterministically as base-10 string bytes.
indexBytes := []byte(strconv.Itoa(i))
baseDBytes := prefundedKey.D.Bytes()
derivedHash := crypto.Keccak256(baseDBytes, indexBytes)

d := new(big.Int).SetBytes(derivedHash)
d.Mod(d, N)
if d.Sign() == 0 {
// Extremely unlikely, but ensure we never use zero as a private scalar.
d.Add(d, big.NewInt(1))
}
keys[i] = key
addrs[i] = crypto.PubkeyToAddress(key.PublicKey)

x, y := curve.ScalarBaseMult(d.Bytes())
priv := &ecdsa.PrivateKey{
D: d,
PublicKey: ecdsa.PublicKey{
Curve: curve,
X: x,
Y: y,
},
}

keys[i] = priv
addrs[i] = crypto.PubkeyToAddress(priv.PublicKey)
}

return keys, addrs
}

Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used for testing - we want a deterministic key here.

@meyer9 meyer9 marked this pull request as ready for review January 8, 2026 20:32
This allows testing things like Block-STM where the number of callers affects performance. If we use a single caller, every transaction conflicts with the next transaction on the balance/nonce of the caller.
@meyer9 meyer9 force-pushed the meyer9/simulator-num-callers branch from 518daed to e4aab51 Compare January 9, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants