diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index 2fdf890320..4f23e30141 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -116,6 +116,35 @@ func pricedDataTransaction(nonce uint64, gaslimit uint64, gasprice *big.Int, key return tx } +// pricedDataTransactionWithFixedSignature generates a signed transaction with fixed-size data, +// and ensures that the resulting signature components (r and s) are exactly 32 bytes each. +// +// This avoids variability in transaction size caused by leading zeros being omitted in +// RLP encoding of r/s. Since r and s are derived from ECDSA, they occasionally have leading +// zeros and thus can be shorter than 32 bytes. +// +// For example: +// +// r: 0 leading zeros, bytesSize: 32, bytes: [221 ... 101] +// s: 1 leading zeros, bytesSize: 31, bytes: [0 75 ... 47] +// +// This function retries signing up to 10 times to find a signature where both r and s are +// exactly 32 bytes long. +// 10 attempts is statistically sufficient since leading zeros in ECDSA signatures are rare and randomly distributed. +// +// This ensures consistent transaction size (especially important when testing tx size limits). +func pricedDataTransactionWithFixedSignature(nonce uint64, gaslimit uint64, gasprice *big.Int, key *ecdsa.PrivateKey, bytes uint64) *types.Transaction { + var tx *types.Transaction + for i := 0; i < 10; i++ { + tx = pricedDataTransaction(nonce, gaslimit, gasprice, key, bytes) + _, r, s := tx.RawSignatureValues() + if len(r.Bytes()) == 32 && len(s.Bytes()) == 32 { + break + } + } + return tx +} + func dynamicFeeTx(nonce uint64, gaslimit uint64, gasFee *big.Int, tip *big.Int, key *ecdsa.PrivateKey) *types.Transaction { tx, _ := types.SignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.DynamicFeeTx{ ChainID: params.TestChainConfig.ChainID, @@ -1237,12 +1266,12 @@ func TestAllowedTxSize(t *testing.T) { // Find the maximum data length for the kind of transaction which will // be generated in the pool.addRemoteSync calls below. const largeDataLength = txMaxSize - 200 // enough to have a 5 bytes RLP encoding of the data length number - txWithLargeData := pricedDataTransaction(0, pool.currentHead.Load().GasLimit, big.NewInt(1), key, largeDataLength) + txWithLargeData := pricedDataTransactionWithFixedSignature(0, pool.currentHead.Load().GasLimit, big.NewInt(1), key, largeDataLength) maxTxLengthWithoutData := txWithLargeData.Size() - largeDataLength // 103 bytes - maxTxDataLength := txMaxSize - maxTxLengthWithoutData // 131072 - 103 = 130953 bytes + maxTxDataLength := txMaxSize - maxTxLengthWithoutData // 131072 - 103 = 130969 bytes // Try adding a transaction with maximal allowed size - tx := pricedDataTransaction(0, pool.currentHead.Load().GasLimit, big.NewInt(1), key, maxTxDataLength) + tx := pricedDataTransactionWithFixedSignature(0, pool.currentHead.Load().GasLimit, big.NewInt(1), key, maxTxDataLength) if err := pool.addRemoteSync(tx); err != nil { t.Fatalf("failed to add transaction of size %d, close to maximal: %v", int(tx.Size()), err) } @@ -1251,7 +1280,7 @@ func TestAllowedTxSize(t *testing.T) { t.Fatalf("failed to add transaction of random allowed size: %v", err) } // Try adding a transaction above maximum size by one - if err := pool.addRemoteSync(pricedDataTransaction(2, pool.currentHead.Load().GasLimit, big.NewInt(1), key, maxTxDataLength+1)); err == nil { + if err := pool.addRemoteSync(pricedDataTransactionWithFixedSignature(2, pool.currentHead.Load().GasLimit, big.NewInt(1), key, maxTxDataLength+1)); err == nil { t.Fatalf("expected rejection on slightly oversize transaction") } // Try adding a transaction above maximum size by more than one