From ef90593bf80dea586556bcce1c3fbda5f6d97c33 Mon Sep 17 00:00:00 2001 From: caleb Date: Wed, 22 Oct 2025 18:13:31 -0500 Subject: [PATCH 01/11] legacy tx support --- contracts/src/RLP/RLPTxBreakdown.sol | 188 +++++++++++++++++++++++++-- 1 file changed, 179 insertions(+), 9 deletions(-) diff --git a/contracts/src/RLP/RLPTxBreakdown.sol b/contracts/src/RLP/RLPTxBreakdown.sol index 59fce3c..e215677 100644 --- a/contracts/src/RLP/RLPTxBreakdown.sol +++ b/contracts/src/RLP/RLPTxBreakdown.sol @@ -5,18 +5,24 @@ import {RLPReader} from "./RLPReader.sol"; /** * @title RLPTxBreakdown - * @notice A library for decoding raw EIP-1559 transactions and breaking them down into their components. - * @dev This library expects a raw transaction beginning with 0x02 and 12 RLP items. + * @notice A library for decoding raw Ethereum transactions and breaking them down into their components. + * @dev Supports both legacy transactions and EIP-1559 transactions (type 0x02). */ library RLPTxBreakdown { using RLPReader for bytes; using RLPReader for RLPReader.RLPItem; + enum TxType { + Legacy, + EIP1559 + } + struct DecodedTransaction { + TxType txType; uint256 chainId; uint256 nonce; - uint256 maxPriorityFeePerGas; - uint256 maxFeePerGas; + uint256 maxPriorityFeePerGas; // For legacy: same as gasPrice + uint256 maxFeePerGas; // For legacy: same as gasPrice uint256 gasLimit; uint256 value; bytes data; @@ -32,12 +38,26 @@ library RLPTxBreakdown { */ function decodeTx(bytes calldata txData) external pure returns (DecodedTransaction memory) { require(txData.length > 0, "Empty tx"); - require(txData[0] == 0x02, "Not EIP-1559"); + + // Check if it's an EIP-1559 transaction (starts with 0x02) + if (txData[0] == 0x02) { + return _decodeEIP1559Tx(txData); + } else { + return _decodeLegacyTx(txData); + } + } + + /** + * @notice Decode an EIP-1559 transaction. + * @param txData The raw transaction data. + * @return decodedTx The decoded transaction. + */ + function _decodeEIP1559Tx(bytes calldata txData) internal pure returns (DecodedTransaction memory) { // Remove the type byte. bytes memory rlpTx = _slice(txData, 1, txData.length - 1); RLPReader.RLPItem memory txItem = rlpTx.toRlpItem(); RLPReader.RLPItem[] memory items = txItem.toList(); - require(items.length == 12, "Invalid tx"); + require(items.length == 12, "Invalid EIP-1559 tx"); bool isContractDeployment = items[5].toBytes().length == 0; address toAddress = isContractDeployment ? address(0) : items[5].toAddress(); @@ -64,6 +84,7 @@ library RLPTxBreakdown { items[8].toRlpBytes() ); return DecodedTransaction({ + txType: TxType.EIP1559, chainId: items[0].toUint(), nonce: items[1].toUint(), maxPriorityFeePerGas: items[2].toUint(), @@ -72,18 +93,59 @@ library RLPTxBreakdown { value: items[6].toUint(), data: items[7].toBytes(), to: toAddress, - from: _getAddress(unsignedPayload, items), + from: _getAddressEIP1559(unsignedPayload, items), + isContractDeployment: isContractDeployment + }); + } + + /** + * @notice Decode a legacy transaction. + * @param txData The raw transaction data. + * @return decodedTx The decoded transaction. + */ + function _decodeLegacyTx(bytes calldata txData) internal pure returns (DecodedTransaction memory) { + RLPReader.RLPItem memory txItem = txData.toRlpItem(); + RLPReader.RLPItem[] memory items = txItem.toList(); + require(items.length == 9, "Invalid legacy tx"); + + bool isContractDeployment = items[3].toBytes().length == 0; + address toAddress = isContractDeployment ? address(0) : items[3].toAddress(); + + // Build unsigned payload from first 6 RLP items + chainId for EIP-155 + uint256 v = items[6].toUint(); + uint256 chainId; + + // Extract chainId from v (EIP-155: v = chainId * 2 + 35 + {0,1}) + if (v >= 35) { + chainId = (v - 35) / 2; + } else { + chainId = 0; // Pre-EIP-155 transaction + } + + uint256 gasPrice = items[1].toUint(); + + return DecodedTransaction({ + txType: TxType.Legacy, + chainId: chainId, + nonce: items[0].toUint(), + maxPriorityFeePerGas: gasPrice, // Legacy uses gasPrice for both + maxFeePerGas: gasPrice, + gasLimit: items[2].toUint(), + value: items[4].toUint(), + data: items[5].toBytes(), + to: toAddress, + from: _getAddressLegacy(items, chainId), isContractDeployment: isContractDeployment }); } /** - * @notice Given the unsigned payload, recovers the sender address. + * @notice Given the unsigned payload, recovers the sender address for EIP-1559 transactions. * @param unsignedPayload The unsigned payload of the transaction. * @param items The RLP items of the transaction. * @return sender The sender address */ - function _getAddress(bytes memory unsignedPayload, RLPReader.RLPItem[] memory items) + function _getAddressEIP1559(bytes memory unsignedPayload, RLPReader.RLPItem[] memory items) internal pure returns (address sender) @@ -118,6 +180,86 @@ library RLPTxBreakdown { return ecrecover(msgHash, v, r, s); } + /** + * @notice Recovers the sender address for legacy transactions. + * @param items The RLP items of the transaction. + * @param chainId The chain ID extracted from v. + * @return sender The sender address + */ + function _getAddressLegacy(RLPReader.RLPItem[] memory items, uint256 chainId) + internal + pure + returns (address sender) + { + // Build unsigned payload for legacy transaction + bytes memory unsignedPayload; + + if (chainId == 0) { + // Pre-EIP-155: [nonce, gasPrice, gasLimit, to, value, data] + unsignedPayload = abi.encodePacked( + items[0].toRlpBytes(), // nonce + items[1].toRlpBytes(), // gasPrice + items[2].toRlpBytes(), // gasLimit + items[3].toRlpBytes(), // to + items[4].toRlpBytes(), // value + items[5].toRlpBytes() // data + ); + } else { + // EIP-155: [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0] + unsignedPayload = abi.encodePacked( + items[0].toRlpBytes(), // nonce + items[1].toRlpBytes(), // gasPrice + items[2].toRlpBytes(), // gasLimit + items[3].toRlpBytes(), // to + items[4].toRlpBytes(), // value + items[5].toRlpBytes(), // data + _encodeUint(chainId), // chainId + uint8(0x80), // empty value (0) + uint8(0x80) // empty value (0) + ); + } + + // RLP-encode the unsigned payload + bytes memory encodedUnsigned; + if (unsignedPayload.length < 56) { + encodedUnsigned = abi.encodePacked(uint8(0xc0 + unsignedPayload.length), unsignedPayload); + } else { + uint256 len = unsignedPayload.length; + uint256 lenLen; + uint256 tmp = len; + while (tmp != 0) { + lenLen++; + tmp >>= 8; + } + bytes memory lenBytes = new bytes(lenLen); + tmp = len; + for (uint256 i = 0; i < lenLen; i++) { + lenBytes[lenLen - 1 - i] = bytes1(uint8(tmp & 0xFF)); + tmp >>= 8; + } + encodedUnsigned = abi.encodePacked(uint8(0xf7 + lenLen), lenBytes, unsignedPayload); + } + + bytes32 msgHash = keccak256(encodedUnsigned); + + // Extract signature values + uint256 v = items[6].toUint(); + bytes32 r = _toBytes32(items[7]); + bytes32 s = _toBytes32(items[8]); + + // Normalize v for ecrecover (should be 27 or 28) + uint8 normalizedV; + if (chainId == 0) { + // Pre-EIP-155: v is already 27 or 28 + normalizedV = uint8(v); + } else { + // EIP-155: v = chainId * 2 + 35 + {0,1}, so extract the {0,1} and add 27 + normalizedV = uint8((v - 35 - chainId * 2) + 27); + } + + return ecrecover(msgHash, normalizedV, r, s); + } + /** * @notice Internal helper function to slice a byte array. * @param data The byte array. @@ -145,4 +287,32 @@ library RLPTxBreakdown { result := mload(add(b, 32)) } } + + /** + * @notice Internal helper to RLP-encode a uint256 value. + * @param value The uint256 value to encode. + * @return The RLP-encoded bytes. + */ + function _encodeUint(uint256 value) internal pure returns (bytes memory) { + if (value == 0) { + return abi.encodePacked(uint8(0x80)); // RLP encoding of 0 + } else if (value < 0x80) { + return abi.encodePacked(uint8(value)); // Single byte + } else { + // Multi-byte encoding + uint256 len; + uint256 tmp = value; + while (tmp != 0) { + len++; + tmp >>= 8; + } + bytes memory valueBytes = new bytes(len); + tmp = value; + for (uint256 i = 0; i < len; i++) { + valueBytes[len - 1 - i] = bytes1(uint8(tmp & 0xFF)); + tmp >>= 8; + } + return abi.encodePacked(uint8(0x80 + len), valueBytes); + } + } } From 4fcb43b07d0cb96e81ac802d919c13b5160bae61 Mon Sep 17 00:00:00 2001 From: caleb Date: Thu, 23 Oct 2025 12:25:24 -0500 Subject: [PATCH 02/11] type 0 transaction support --- contracts/test/RandomnessSequencer.t.sol | 145 ++++++++++++++++++++--- 1 file changed, 130 insertions(+), 15 deletions(-) diff --git a/contracts/test/RandomnessSequencer.t.sol b/contracts/test/RandomnessSequencer.t.sol index 39ea5ad..c20a35f 100644 --- a/contracts/test/RandomnessSequencer.t.sol +++ b/contracts/test/RandomnessSequencer.t.sol @@ -134,7 +134,7 @@ contract RandomnessSequencerTest is Test { function testProcessTransactionWithoutRandomnessRequired() public { // Create a simple EIP-1559 transaction - bytes memory txn = _createMockTransaction(address(0x123), hex"12345678"); + bytes memory txn = _createType2MockTransaction(address(0x123), hex"12345678"); vm.prank(sequencerRole); sequencer.processTransaction(txn); @@ -151,7 +151,7 @@ contract RandomnessSequencerTest is Test { vm.prank(functionSelectorAdmin); sequencer.addToFunctionAllowlist(targetContract, selector); - bytes memory txn = _createMockTransaction(targetContract, abi.encodePacked(selector)); + bytes memory txn = _createType2MockTransaction(targetContract, abi.encodePacked(selector)); vm.prank(sequencerRole); vm.expectEmit(false, false, false, true); @@ -163,7 +163,7 @@ contract RandomnessSequencerTest is Test { } function testProcessTransactionUnauthorized() public { - bytes memory txn = _createMockTransaction(address(0x123), hex"12345678"); + bytes memory txn = _createType2MockTransaction(address(0x123), hex"12345678"); vm.prank(unauthorized); vm.expectRevert(); @@ -172,8 +172,8 @@ contract RandomnessSequencerTest is Test { function testProcessTransactionsBulk() public { bytes[] memory txns = new bytes[](2); - txns[0] = _createMockTransaction(address(0x123), hex"12345678"); - txns[1] = _createMockTransaction(address(0x456), hex"87654321"); + txns[0] = _createType2MockTransaction(address(0x123), hex"12345678"); + txns[1] = _createType2MockTransaction(address(0x456), hex"87654321"); vm.prank(sequencerRole); sequencer.processTransactionsBulk(txns); @@ -184,7 +184,7 @@ contract RandomnessSequencerTest is Test { function testProcessTransactionsBulkUnauthorized() public { bytes[] memory txns = new bytes[](1); - txns[0] = _createMockTransaction(address(0x123), hex"12345678"); + txns[0] = _createType2MockTransaction(address(0x123), hex"12345678"); vm.prank(unauthorized); vm.expectRevert(); @@ -200,8 +200,8 @@ contract RandomnessSequencerTest is Test { sequencer.addToFunctionAllowlist(targetContract, selector); // Add transactions to mempool - bytes memory txn1 = _createMockTransaction(targetContract, abi.encodePacked(selector)); - bytes memory txn2 = _createMockTransaction(targetContract, abi.encodePacked(selector)); + bytes memory txn1 = _createType2MockTransaction(targetContract, abi.encodePacked(selector)); + bytes memory txn2 = _createType2MockTransaction(targetContract, abi.encodePacked(selector)); vm.prank(sequencerRole); sequencer.processTransaction(txn1); @@ -211,7 +211,7 @@ contract RandomnessSequencerTest is Test { assertEq(sequencer.getMempoolLength(), 2); // Add randomness transaction - bytes memory randomnessTx = _createMockTransaction(address(0x999), hex"abcdef"); + bytes memory randomnessTx = _createType2MockTransaction(address(0x999), hex"abcdef"); vm.prank(randomnessRole); vm.expectEmit(false, false, false, false); @@ -227,7 +227,7 @@ contract RandomnessSequencerTest is Test { } function testProcessRandomTransactionUnauthorized() public { - bytes memory randomnessTx = _createMockTransaction(address(0x999), hex"abcdef"); + bytes memory randomnessTx = _createType2MockTransaction(address(0x999), hex"abcdef"); vm.prank(unauthorized); vm.expectRevert(); @@ -235,7 +235,7 @@ contract RandomnessSequencerTest is Test { } function testProcessRandomTransactionWithEmptyMempool() public { - bytes memory randomnessTx = _createMockTransaction(address(0x999), hex"abcdef"); + bytes memory randomnessTx = _createType2MockTransaction(address(0x999), hex"abcdef"); vm.prank(randomnessRole); sequencer.processRandomTransaction(randomnessTx); @@ -253,8 +253,8 @@ contract RandomnessSequencerTest is Test { sequencer.addToFunctionAllowlist(targetContract, selector); // Process multiple transactions - bytes memory txn1 = _createMockTransaction(targetContract, abi.encodePacked(selector)); - bytes memory txn2 = _createMockTransaction(targetContract, abi.encodePacked(selector)); + bytes memory txn1 = _createType2MockTransaction(targetContract, abi.encodePacked(selector)); + bytes memory txn2 = _createType2MockTransaction(targetContract, abi.encodePacked(selector)); vm.prank(sequencerRole); sequencer.processTransaction(txn1); @@ -277,8 +277,123 @@ contract RandomnessSequencerTest is Test { assertEq(funcs.length, 2); } - // Helper function to create a real RLP-encoded EIP-1559 transaction - function _createMockTransaction(address to, bytes memory data) internal view returns (bytes memory) { + function testProcessLegacyTransactionWithoutRandomnessRequired() public { + // Create a legacy transaction + bytes memory txn = _createLegacyMockTransaction(address(0x123), hex"12345678"); + + vm.prank(sequencerRole); + sequencer.processTransaction(txn); + + assertEq(sequencer.getMempoolLength(), 0); + assertEq(mockChain.getProcessedCount(), 1); + } + + function testProcessLegacyTransactionWithRandomnessRequired() public { + address targetContract = address(0x123); + bytes4 selector = bytes4(hex"12345678"); + + // Add function selector to require randomness + vm.prank(functionSelectorAdmin); + sequencer.addToFunctionAllowlist(targetContract, selector); + + bytes memory txn = _createLegacyMockTransaction(targetContract, abi.encodePacked(selector)); + + vm.prank(sequencerRole); + vm.expectEmit(false, false, false, true); + emit MempoolUpdated(1, txn); + sequencer.processTransaction(txn); + + assertEq(sequencer.getMempoolLength(), 1); + assertEq(mockChain.getProcessedCount(), 0); + } + + function testProcessMixedTransactionTypes() public { + address targetContract = address(0x123); + bytes4 selector = bytes4(hex"12345678"); + + // Add function selector to require randomness + vm.prank(functionSelectorAdmin); + sequencer.addToFunctionAllowlist(targetContract, selector); + + // Add both legacy and EIP-1559 transactions to mempool + bytes memory legacyTxn = _createLegacyMockTransaction(targetContract, abi.encodePacked(selector)); + bytes memory eip1559Txn = _createType2MockTransaction(targetContract, abi.encodePacked(selector)); + + vm.prank(sequencerRole); + sequencer.processTransaction(legacyTxn); + vm.prank(sequencerRole); + sequencer.processTransaction(eip1559Txn); + + assertEq(sequencer.getMempoolLength(), 2); + + // Process randomness transaction + bytes memory randomnessTx = _createType2MockTransaction(address(0x999), hex"abcdef"); + + vm.prank(randomnessRole); + sequencer.processRandomTransaction(randomnessTx); + + // Both transaction types should be processed + assertEq(sequencer.getMempoolLength(), 0); + assertEq(mockChain.getProcessedCount(), 1); + assertEq(mockChain.getProcessedBulkCount(), 1); + } + + // Helper function to create a real RLP-encoded legacy transaction (Type 0) + function _createLegacyMockTransaction(address to, bytes memory data) internal pure returns (bytes memory) { + uint256 privateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; // Test private key + + // Legacy transaction parameters + uint256 chainId = 1; + uint256 nonce = 0; + uint256 gasPrice = 10 gwei; + uint256 gasLimit = 100000; + uint256 value = 0; + + // Build the unsigned transaction payload for EIP-155 + // [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0] + bytes memory unsignedPayload = abi.encodePacked( + _encodeUint(nonce), + _encodeUint(gasPrice), + _encodeUint(gasLimit), + _encodeAddress(to), + _encodeUint(value), + _encodeBytes(data), + _encodeUint(chainId), + uint8(0x80), // RLP encoding of 0 + uint8(0x80) // RLP encoding of 0 + ); + + // Wrap in RLP list + bytes memory rlpUnsigned = _encodeList(unsignedPayload); + + // Hash for signing + bytes32 txHash = keccak256(rlpUnsigned); + + // Sign the transaction + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, txHash); + + // Calculate EIP-155 v value: chainId * 2 + 35 + {0,1} + uint256 vValue = chainId * 2 + 35 + (v - 27); + + // Build the signed transaction (9 items: 6 unsigned - chainId,0,0 + v, r, s) + bytes memory signedPayload = abi.encodePacked( + _encodeUint(nonce), + _encodeUint(gasPrice), + _encodeUint(gasLimit), + _encodeAddress(to), + _encodeUint(value), + _encodeBytes(data), + _encodeUint(vValue), + _encodeBytes32(r), + _encodeBytes32(s) + ); + + // Wrap in RLP list (no type prefix for legacy) + return _encodeList(signedPayload); + } + + // Helper function to create a real RLP-encoded EIP-1559 transaction (Type 2) + function _createType2MockTransaction(address to, bytes memory data) internal pure returns (bytes memory) { uint256 privateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; // Test private key // EIP-1559 transaction parameters From a2d0445f5f86cb52c0102a95e7bec1d35977b48f Mon Sep 17 00:00:00 2001 From: caleb Date: Fri, 24 Oct 2025 10:11:47 -0500 Subject: [PATCH 03/11] type 1 tx support --- contracts/src/RLP/RLPTxBreakdown.sol | 100 ++++++++++++++++++++++- contracts/test/RandomnessSequencer.t.sol | 84 ++++++++++++++++++- 2 files changed, 179 insertions(+), 5 deletions(-) diff --git a/contracts/src/RLP/RLPTxBreakdown.sol b/contracts/src/RLP/RLPTxBreakdown.sol index e215677..f739daf 100644 --- a/contracts/src/RLP/RLPTxBreakdown.sol +++ b/contracts/src/RLP/RLPTxBreakdown.sol @@ -14,6 +14,7 @@ library RLPTxBreakdown { enum TxType { Legacy, + EIP2930, EIP1559 } @@ -39,14 +40,68 @@ library RLPTxBreakdown { function decodeTx(bytes calldata txData) external pure returns (DecodedTransaction memory) { require(txData.length > 0, "Empty tx"); - // Check if it's an EIP-1559 transaction (starts with 0x02) - if (txData[0] == 0x02) { + // Check transaction type based on first byte + if (txData[0] == 0x01) { + return _decodeEIP2930Tx(txData); + } else if (txData[0] == 0x02) { return _decodeEIP1559Tx(txData); } else { return _decodeLegacyTx(txData); } } + /** + * @notice Decode an EIP-2930 transaction. + * @param txData The raw transaction data. + * @return decodedTx The decoded transaction. + */ + function _decodeEIP2930Tx(bytes calldata txData) internal pure returns (DecodedTransaction memory) { + // Remove the type byte. + bytes memory rlpTx = _slice(txData, 1, txData.length - 1); + RLPReader.RLPItem memory txItem = rlpTx.toRlpItem(); + RLPReader.RLPItem[] memory items = txItem.toList(); + require(items.length == 11, "Invalid EIP-2930 tx"); + + bool isContractDeployment = items[4].toBytes().length == 0; + address toAddress = isContractDeployment ? address(0) : items[4].toAddress(); + + // Build unsigned payload from first 8 RLP items. + bytes memory unsignedPayload = abi.encodePacked( + // chainId - Chain ID of the network + items[0].toRlpBytes(), + // nonce - Transaction nonce of the sender account + items[1].toRlpBytes(), + // gasPrice - Gas price the sender is willing to pay + items[2].toRlpBytes(), + // gasLimit - Gas limit for the transaction + items[3].toRlpBytes(), + // to - Recipient address (20-byte Ethereum address) + items[4].toRlpBytes(), + // value - Amount of ETH (in wei) to transfer + items[5].toRlpBytes(), + // data - Transaction payload + items[6].toRlpBytes(), + // accessList - EIP-2930 access list + items[7].toRlpBytes() + ); + + uint256 gasPrice = items[2].toUint(); + + return DecodedTransaction({ + txType: TxType.EIP2930, + chainId: items[0].toUint(), + nonce: items[1].toUint(), + maxPriorityFeePerGas: gasPrice, // EIP-2930 uses gasPrice + maxFeePerGas: gasPrice, + gasLimit: items[3].toUint(), + value: items[5].toUint(), + data: items[6].toBytes(), + to: toAddress, + from: _getAddressEIP2930(unsignedPayload, items), + isContractDeployment: isContractDeployment + }); + } + /** * @notice Decode an EIP-1559 transaction. * @param txData The raw transaction data. @@ -139,6 +194,47 @@ library RLPTxBreakdown { }); } + /** + * @notice Given the unsigned payload, recovers the sender address for EIP-2930 transactions. + * @param unsignedPayload The unsigned payload of the transaction. + * @param items The RLP items of the transaction. + * @return sender The sender address + */ + function _getAddressEIP2930(bytes memory unsignedPayload, RLPReader.RLPItem[] memory items) + internal + pure + returns (address sender) + { + // RLP-encode the unsigned payload. + bytes memory encodedUnsigned; + if (unsignedPayload.length < 56) { + encodedUnsigned = abi.encodePacked(uint8(0xc0 + unsignedPayload.length), unsignedPayload); + } else { + uint256 len = unsignedPayload.length; + uint256 lenLen; + uint256 tmp = len; + while (tmp != 0) { + lenLen++; + tmp >>= 8; + } + bytes memory lenBytes = new bytes(lenLen); + tmp = len; + for (uint256 i = 0; i < lenLen; i++) { + lenBytes[lenLen - 1 - i] = bytes1(uint8(tmp & 0xFF)); + tmp >>= 8; + } + encodedUnsigned = abi.encodePacked(uint8(0xf7 + lenLen), lenBytes, unsignedPayload); + } + // Prepend type byte 0x01. + bytes32 msgHash = keccak256(abi.encodePacked(bytes1(0x01), encodedUnsigned)); + + // Extract and normalize v (EIP-2930 uses v values 0 or 1, need to add 27) + uint8 v = uint8(uint256(items[8].toUint())) + 27; + bytes32 r = _toBytes32(items[9]); + bytes32 s = _toBytes32(items[10]); + return ecrecover(msgHash, v, r, s); + } + /** * @notice Given the unsigned payload, recovers the sender address for EIP-1559 transactions. * @param unsignedPayload The unsigned payload of the transaction. diff --git a/contracts/test/RandomnessSequencer.t.sol b/contracts/test/RandomnessSequencer.t.sol index c20a35f..ad8b0b5 100644 --- a/contracts/test/RandomnessSequencer.t.sol +++ b/contracts/test/RandomnessSequencer.t.sol @@ -307,6 +307,36 @@ contract RandomnessSequencerTest is Test { assertEq(mockChain.getProcessedCount(), 0); } + function testProcessType1TransactionWithoutRandomnessRequired() public { + // Create an EIP-2930 transaction + bytes memory txn = _createType1MockTransaction(address(0x123), hex"12345678"); + + vm.prank(sequencerRole); + sequencer.processTransaction(txn); + + assertEq(sequencer.getMempoolLength(), 0); + assertEq(mockChain.getProcessedCount(), 1); + } + + function testProcessType1TransactionWithRandomnessRequired() public { + address targetContract = address(0x123); + bytes4 selector = bytes4(hex"12345678"); + + // Add function selector to require randomness + vm.prank(functionSelectorAdmin); + sequencer.addToFunctionAllowlist(targetContract, selector); + + bytes memory txn = _createType1MockTransaction(targetContract, abi.encodePacked(selector)); + + vm.prank(sequencerRole); + vm.expectEmit(false, false, false, true); + emit MempoolUpdated(1, txn); + sequencer.processTransaction(txn); + + assertEq(sequencer.getMempoolLength(), 1); + assertEq(mockChain.getProcessedCount(), 0); + } + function testProcessMixedTransactionTypes() public { address targetContract = address(0x123); bytes4 selector = bytes4(hex"12345678"); @@ -315,16 +345,19 @@ contract RandomnessSequencerTest is Test { vm.prank(functionSelectorAdmin); sequencer.addToFunctionAllowlist(targetContract, selector); - // Add both legacy and EIP-1559 transactions to mempool + // Add legacy, EIP-2930, and EIP-1559 transactions to mempool bytes memory legacyTxn = _createLegacyMockTransaction(targetContract, abi.encodePacked(selector)); + bytes memory eip2930Txn = _createType1MockTransaction(targetContract, abi.encodePacked(selector)); bytes memory eip1559Txn = _createType2MockTransaction(targetContract, abi.encodePacked(selector)); vm.prank(sequencerRole); sequencer.processTransaction(legacyTxn); vm.prank(sequencerRole); + sequencer.processTransaction(eip2930Txn); + vm.prank(sequencerRole); sequencer.processTransaction(eip1559Txn); - assertEq(sequencer.getMempoolLength(), 2); + assertEq(sequencer.getMempoolLength(), 3); // Process randomness transaction bytes memory randomnessTx = _createType2MockTransaction(address(0x999), hex"abcdef"); @@ -332,12 +365,57 @@ contract RandomnessSequencerTest is Test { vm.prank(randomnessRole); sequencer.processRandomTransaction(randomnessTx); - // Both transaction types should be processed + // All transaction types should be processed assertEq(sequencer.getMempoolLength(), 0); assertEq(mockChain.getProcessedCount(), 1); assertEq(mockChain.getProcessedBulkCount(), 1); } + // Helper function to create a real RLP-encoded EIP-2930 transaction (Type 1) + function _createType1MockTransaction(address to, bytes memory data) internal pure returns (bytes memory) { + uint256 privateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; // Test private key + + // EIP-2930 transaction parameters + uint256 chainId = 1; + uint256 nonce = 0; + uint256 gasPrice = 10 gwei; + uint256 gasLimit = 100000; + uint256 value = 0; + bytes memory accessList = hex"c0"; // Empty access list + + // Build the unsigned transaction payload (8 items) + bytes memory unsignedPayload = abi.encodePacked( + _encodeUint(chainId), + _encodeUint(nonce), + _encodeUint(gasPrice), + _encodeUint(gasLimit), + _encodeAddress(to), + _encodeUint(value), + _encodeBytes(data), + accessList + ); + + // Wrap in RLP list + bytes memory rlpUnsigned = _encodeList(unsignedPayload); + + // Hash for signing: keccak256(0x01 || rlp(unsigned_tx)) + bytes32 txHash = keccak256(abi.encodePacked(bytes1(0x01), rlpUnsigned)); + + // Sign the transaction + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, txHash); + + // Build the signed transaction (11 items: 8 unsigned + v, r, s) + bytes memory signedPayload = abi.encodePacked( + unsignedPayload, + _encodeUint(v - 27), // EIP-2930 uses v - 27 (0 or 1) + _encodeBytes32(r), + _encodeBytes32(s) + ); + + // Wrap in RLP list and prepend 0x01 + return abi.encodePacked(bytes1(0x01), _encodeList(signedPayload)); + } + // Helper function to create a real RLP-encoded legacy transaction (Type 0) function _createLegacyMockTransaction(address to, bytes memory data) internal pure returns (bytes memory) { uint256 privateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; // Test private key From 1005defdd9243f46f5ae3ef430f59ced8cf205cb Mon Sep 17 00:00:00 2001 From: caleb Date: Fri, 24 Oct 2025 10:16:36 -0500 Subject: [PATCH 04/11] rm isContractDeployment from DecodedTransaction & check to address instead --- contracts/src/RLP/RLPTxBreakdown.sol | 24 ++++++++++-------------- contracts/src/RandomnessSequencer.sol | 3 ++- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/contracts/src/RLP/RLPTxBreakdown.sol b/contracts/src/RLP/RLPTxBreakdown.sol index f739daf..703555e 100644 --- a/contracts/src/RLP/RLPTxBreakdown.sol +++ b/contracts/src/RLP/RLPTxBreakdown.sol @@ -27,9 +27,8 @@ library RLPTxBreakdown { uint256 gasLimit; uint256 value; bytes data; - address to; + address to; // address(0) for contract deployments address from; - bool isContractDeployment; } /** @@ -62,8 +61,8 @@ library RLPTxBreakdown { RLPReader.RLPItem[] memory items = txItem.toList(); require(items.length == 11, "Invalid EIP-2930 tx"); - bool isContractDeployment = items[4].toBytes().length == 0; - address toAddress = isContractDeployment ? address(0) : items[4].toAddress(); + // If 'to' field is empty, it's a contract deployment (address(0)) + address toAddress = items[4].toBytes().length == 0 ? address(0) : items[4].toAddress(); // Build unsigned payload from first 8 RLP items. bytes memory unsignedPayload = abi.encodePacked( @@ -97,8 +96,7 @@ library RLPTxBreakdown { value: items[5].toUint(), data: items[6].toBytes(), to: toAddress, - from: _getAddressEIP2930(unsignedPayload, items), - isContractDeployment: isContractDeployment + from: _getAddressEIP2930(unsignedPayload, items) }); } @@ -114,8 +112,8 @@ library RLPTxBreakdown { RLPReader.RLPItem[] memory items = txItem.toList(); require(items.length == 12, "Invalid EIP-1559 tx"); - bool isContractDeployment = items[5].toBytes().length == 0; - address toAddress = isContractDeployment ? address(0) : items[5].toAddress(); + // If 'to' field is empty, it's a contract deployment (address(0)) + address toAddress = items[5].toBytes().length == 0 ? address(0) : items[5].toAddress(); // Build unsigned payload from first 9 RLP items. bytes memory unsignedPayload = abi.encodePacked( @@ -148,8 +146,7 @@ library RLPTxBreakdown { value: items[6].toUint(), data: items[7].toBytes(), to: toAddress, - from: _getAddressEIP1559(unsignedPayload, items), - isContractDeployment: isContractDeployment + from: _getAddressEIP1559(unsignedPayload, items) }); } @@ -163,8 +160,8 @@ library RLPTxBreakdown { RLPReader.RLPItem[] memory items = txItem.toList(); require(items.length == 9, "Invalid legacy tx"); - bool isContractDeployment = items[3].toBytes().length == 0; - address toAddress = isContractDeployment ? address(0) : items[3].toAddress(); + // If 'to' field is empty, it's a contract deployment (address(0)) + address toAddress = items[3].toBytes().length == 0 ? address(0) : items[3].toAddress(); // Build unsigned payload from first 6 RLP items + chainId for EIP-155 uint256 v = items[6].toUint(); @@ -189,8 +186,7 @@ library RLPTxBreakdown { value: items[4].toUint(), data: items[5].toBytes(), to: toAddress, - from: _getAddressLegacy(items, chainId), - isContractDeployment: isContractDeployment + from: _getAddressLegacy(items, chainId) }); } diff --git a/contracts/src/RandomnessSequencer.sol b/contracts/src/RandomnessSequencer.sol index decfc7e..6b73588 100644 --- a/contracts/src/RandomnessSequencer.sol +++ b/contracts/src/RandomnessSequencer.sol @@ -114,7 +114,8 @@ contract RandomnessSequencer is AccessControl, ISequencingChain { function _processTransaction(bytes memory txn) internal { RLPTxBreakdown.DecodedTransaction memory decodedTx = RLPTxBreakdown.decodeTx(txn); - if (decodedTx.data.length > 0 && !decodedTx.isContractDeployment) { + // Skip function selector checking for contract deployments (to == address(0)) + if (decodedTx.data.length > 0 && decodedTx.to != address(0)) { bytes4 selector = getFunctionSelector(decodedTx.data); if (isRandomnessRequired[decodedTx.to][selector]) { transactionNonces[decodedTx.to][decodedTx.from][selector]++; From a5e8a0559b8ae8286124231167c263548cbbf984 Mon Sep 17 00:00:00 2001 From: caleb Date: Fri, 24 Oct 2025 10:24:07 -0500 Subject: [PATCH 05/11] GH actions --- .github/CODEOWNERS | 2 + .github/workflows/foundry-tests.yaml | 67 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/foundry-tests.yaml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b028f82 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# root files +/*.* @WillPapper @sammdec @daniilrrr @ericvelazquez @ibremseth @jorgemmsilva @tsite @Br1ght0ne @calebguy diff --git a/.github/workflows/foundry-tests.yaml b/.github/workflows/foundry-tests.yaml new file mode 100644 index 0000000..63987eb --- /dev/null +++ b/.github/workflows/foundry-tests.yaml @@ -0,0 +1,67 @@ +name: Foundry Tests + +on: + # Run workflow on every push to main. This ensures that cross-service PRs that + # depend on synd-contracts are tested. + push: + branches: + - main + # Only run on PRs that touch shared contracts + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true + +env: + FOUNDRY_PROFILE: ci + +jobs: + contracts-check: + name: Contracts Check + runs-on: ubuntu-latest + defaults: + run: + working-directory: "contracts" + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Cache Forge dependencies + uses: actions/cache@v3 + with: + path: | + ~/.foundry + contracts/cache + contracts/out + key: ${{ runner.os }}-foundry-${{ hashFiles('contracts/foundry.toml') }} + restore-keys: | + ${{ runner.os }}-foundry- + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build + id: build + + - name: Run Forge tests with gas report + run: | + forge test -vvv --gas-report + id: test + + - name: Run Coverage + run: | + forge coverage + id: coverage From 5734cb3dcd43e1ed331a397132ff52e9d70881ba Mon Sep 17 00:00:00 2001 From: caleb Date: Fri, 24 Oct 2025 10:25:10 -0500 Subject: [PATCH 06/11] fmt --- contracts/src/RLP/RLPReader.sol | 20 ++++---------------- contracts/src/RLP/RLPTxBreakdown.sol | 8 ++++---- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/contracts/src/RLP/RLPReader.sol b/contracts/src/RLP/RLPReader.sol index 517b67d..a92be49 100644 --- a/contracts/src/RLP/RLPReader.sol +++ b/contracts/src/RLP/RLPReader.sol @@ -35,10 +35,7 @@ library RLPReader { uint256 itemLength = _itemLength(ptr); self.nextPtr = ptr + itemLength; - return RLPItem({ - len: itemLength, - memPtr: ptr - }); + return RLPItem({len: itemLength, memPtr: ptr}); } /* @@ -60,10 +57,7 @@ library RLPReader { memPtr := add(item, 0x20) } - return RLPItem({ - len: item.length, - memPtr: memPtr - }); + return RLPItem({len: item.length, memPtr: memPtr}); } /* @@ -75,10 +69,7 @@ library RLPReader { require(isList(self)); uint256 ptr = self.memPtr + _payloadOffset(self.memPtr); - return Iterator({ - item: self, - nextPtr: ptr - }); + return Iterator({item: self, nextPtr: ptr}); } /* @@ -120,10 +111,7 @@ library RLPReader { uint256 dataLen; for (uint256 i = 0; i < items; i++) { dataLen = _itemLength(memPtr); - result[i] = RLPItem({ - len: dataLen, - memPtr: memPtr - }); + result[i] = RLPItem({len: dataLen, memPtr: memPtr}); memPtr = memPtr + dataLen; } diff --git a/contracts/src/RLP/RLPTxBreakdown.sol b/contracts/src/RLP/RLPTxBreakdown.sol index 703555e..3a476f2 100644 --- a/contracts/src/RLP/RLPTxBreakdown.sol +++ b/contracts/src/RLP/RLPTxBreakdown.sol @@ -294,7 +294,7 @@ library RLPTxBreakdown { items[2].toRlpBytes(), // gasLimit items[3].toRlpBytes(), // to items[4].toRlpBytes(), // value - items[5].toRlpBytes() // data + items[5].toRlpBytes() // data ); } else { // EIP-155: [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0] @@ -305,9 +305,9 @@ library RLPTxBreakdown { items[3].toRlpBytes(), // to items[4].toRlpBytes(), // value items[5].toRlpBytes(), // data - _encodeUint(chainId), // chainId - uint8(0x80), // empty value (0) - uint8(0x80) // empty value (0) + _encodeUint(chainId), // chainId + uint8(0x80), // empty value (0) + uint8(0x80) // empty value (0) ); } From 4cde8e4f1b3dda890293f9a9fed111fea270aa77 Mon Sep 17 00:00:00 2001 From: caleb Date: Fri, 24 Oct 2025 10:29:14 -0500 Subject: [PATCH 07/11] WF updates --- contracts/test/Random.t.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/test/Random.t.sol b/contracts/test/Random.t.sol index 48f59fb..9514bd1 100644 --- a/contracts/test/Random.t.sol +++ b/contracts/test/Random.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.25; import {Test} from "forge-std/Test.sol"; import {Random} from "../src/Random.sol"; -import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; contract RandomTest is Test { Random public randomContract; From a38d9272bc83ee3011119051731debf1b883cc26 Mon Sep 17 00:00:00 2001 From: caleb Date: Fri, 24 Oct 2025 10:30:28 -0500 Subject: [PATCH 08/11] gh wf update --- .github/workflows/foundry-tests.yaml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/foundry-tests.yaml b/.github/workflows/foundry-tests.yaml index 63987eb..846b66f 100644 --- a/.github/workflows/foundry-tests.yaml +++ b/.github/workflows/foundry-tests.yaml @@ -31,17 +31,6 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 - - name: Cache Forge dependencies - uses: actions/cache@v3 - with: - path: | - ~/.foundry - contracts/cache - contracts/out - key: ${{ runner.os }}-foundry-${{ hashFiles('contracts/foundry.toml') }} - restore-keys: | - ${{ runner.os }}-foundry- - - name: Show Forge version run: | forge --version @@ -60,8 +49,3 @@ jobs: run: | forge test -vvv --gas-report id: test - - - name: Run Coverage - run: | - forge coverage - id: coverage From 41a54453fc8dec57d1e28669014013a510db3de0 Mon Sep 17 00:00:00 2001 From: caleb Date: Fri, 24 Oct 2025 10:34:35 -0500 Subject: [PATCH 09/11] fix submodules --- .github/workflows/foundry-tests.yaml | 4 ++++ .gitmodules | 3 +++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/foundry-tests.yaml b/.github/workflows/foundry-tests.yaml index 846b66f..f1037d6 100644 --- a/.github/workflows/foundry-tests.yaml +++ b/.github/workflows/foundry-tests.yaml @@ -31,6 +31,10 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 + - name: Install dependencies + run: | + forge install + - name: Show Forge version run: | forge --version diff --git a/.gitmodules b/.gitmodules index 62f0dfc..460a2ec 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "contracts/lib/openzeppelin-contracts"] path = contracts/lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "contracts/lib/forge-std"] + path = contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std From ef4d648f9e3dcc18b6d0d6e55c19aaa377b7d0ba Mon Sep 17 00:00:00 2001 From: caleb Date: Fri, 24 Oct 2025 10:37:32 -0500 Subject: [PATCH 10/11] fix --- contracts/foundry.lock | 3 +++ contracts/lib/forge-std | 1 + 2 files changed, 4 insertions(+) create mode 160000 contracts/lib/forge-std diff --git a/contracts/foundry.lock b/contracts/foundry.lock index b071289..81831a9 100644 --- a/contracts/foundry.lock +++ b/contracts/foundry.lock @@ -1,4 +1,7 @@ { + "lib/forge-std": { + "rev": "a6d71da563bbb8d6eef8fbec3a16c61c603d2764" + }, "lib/openzeppelin-contracts": { "tag": { "name": "v5.4.0", diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std new file mode 160000 index 0000000..a6d71da --- /dev/null +++ b/contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit a6d71da563bbb8d6eef8fbec3a16c61c603d2764 From 9cabba61c3da8357f4cb7d260ff7f9cf2502487e Mon Sep 17 00:00:00 2001 From: caleb Date: Fri, 24 Oct 2025 10:43:59 -0500 Subject: [PATCH 11/11] fix gh submodules --- .gitmodules | 6 +++--- contracts/lib/openzeppelin-contracts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) create mode 160000 contracts/lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index 460a2ec..5c7d5d6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ -[submodule "contracts/lib/openzeppelin-contracts"] - path = contracts/lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts [submodule "contracts/lib/forge-std"] path = contracts/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "contracts/lib/openzeppelin-contracts"] + path = contracts/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/contracts/lib/openzeppelin-contracts b/contracts/lib/openzeppelin-contracts new file mode 160000 index 0000000..c64a1ed --- /dev/null +++ b/contracts/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit c64a1edb67b6e3f4a15cca8909c9482ad33a02b0