From afe5b662bef55afb1dfe4ae4007c74d0043521d3 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 29 Oct 2024 16:10:17 +0100 Subject: [PATCH 1/2] Add EIP1559 and EIP2930 signer contracts --- contracts/contracts/EIP1559Signer.sol | 125 ++++++++ contracts/contracts/EIP2930Signer.sol | 122 ++++++++ contracts/contracts/EIPTypes.sol | 43 +++ contracts/contracts/tests/EIPTests.sol | 123 ++++++++ contracts/test/eip1559_2930.ts | 387 +++++++++++++++++++++++++ 5 files changed, 800 insertions(+) create mode 100644 contracts/contracts/EIP1559Signer.sol create mode 100644 contracts/contracts/EIP2930Signer.sol create mode 100644 contracts/contracts/EIPTypes.sol create mode 100644 contracts/contracts/tests/EIPTests.sol create mode 100644 contracts/test/eip1559_2930.ts diff --git a/contracts/contracts/EIP1559Signer.sol b/contracts/contracts/EIP1559Signer.sol new file mode 100644 index 00000000..0bc45fde --- /dev/null +++ b/contracts/contracts/EIP1559Signer.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; +import {Sapphire} from "./Sapphire.sol"; +import {EthereumUtils, SignatureRSV} from "./EthereumUtils.sol"; +import {RLPWriter} from "./RLPWriter.sol"; +import {EIPTypes} from "./EIPTypes.sol"; + +/** + * @title Ethereum EIP-1559 transaction signer & encoder + */ +library EIP1559Signer { + struct EIP1559Tx { + uint64 nonce; + uint256 maxPriorityFeePerGas; + uint256 maxFeePerGas; + uint64 gasLimit; + address to; + uint256 value; + bytes data; + EIPTypes.AccessList accessList; + uint256 chainId; + } + + /** + * @notice Encode an unsigned EIP-1559 transaction for signing + * @param rawTx Transaction to encode + */ + function encodeUnsignedTx(EIP1559Tx memory rawTx) + internal + pure + returns (bytes memory) + { + bytes[] memory b = new bytes[](9); + b[0] = RLPWriter.writeUint(rawTx.chainId); + b[1] = RLPWriter.writeUint(rawTx.nonce); + b[2] = RLPWriter.writeUint(rawTx.maxPriorityFeePerGas); + b[3] = RLPWriter.writeUint(rawTx.maxFeePerGas); + b[4] = RLPWriter.writeUint(rawTx.gasLimit); + b[5] = RLPWriter.writeAddress(rawTx.to); + b[6] = RLPWriter.writeUint(rawTx.value); + b[7] = RLPWriter.writeBytes(rawTx.data); + b[8] = EIPTypes.encodeAccessList(rawTx.accessList); + + // RLP encode the transaction data + bytes memory rlpEncodedTx = RLPWriter.writeList(b); + + // Return the unsigned transaction with EIP-1559 type prefix + return abi.encodePacked(hex"02", rlpEncodedTx); + } + + /** + * @notice Encode a signed EIP-1559 transaction + * @param rawTx Transaction which was signed + * @param rsv R, S & V parameters of signature + */ + function encodeSignedTx(EIP1559Tx memory rawTx, SignatureRSV memory rsv) + internal + pure + returns (bytes memory) + { + bytes[] memory b = new bytes[](12); + b[0] = RLPWriter.writeUint(rawTx.chainId); + b[1] = RLPWriter.writeUint(rawTx.nonce); + b[2] = RLPWriter.writeUint(rawTx.maxPriorityFeePerGas); + b[3] = RLPWriter.writeUint(rawTx.maxFeePerGas); + b[4] = RLPWriter.writeUint(rawTx.gasLimit); + b[5] = RLPWriter.writeAddress(rawTx.to); + b[6] = RLPWriter.writeUint(rawTx.value); + b[7] = RLPWriter.writeBytes(rawTx.data); + b[8] = EIPTypes.encodeAccessList(rawTx.accessList); + b[9] = RLPWriter.writeUint(uint256(rsv.v)); + b[10] = RLPWriter.writeUint(uint256(rsv.r)); + b[11] = RLPWriter.writeUint(uint256(rsv.s)); + + // RLP encode the transaction data + bytes memory rlpEncodedTx = RLPWriter.writeList(b); + + // Return the signed transaction with EIP-1559 type prefix + return abi.encodePacked(hex"02", rlpEncodedTx); + } + + /** + * @notice Sign a raw transaction + * @param rawTx Transaction to sign + * @param pubkeyAddr Ethereum address of secret key + * @param secretKey Secret key used to sign + */ + function signRawTx( + EIP1559Tx memory rawTx, + address pubkeyAddr, + bytes32 secretKey + ) internal view returns (SignatureRSV memory ret) { + // First encode the transaction without signature fields + bytes memory encoded = encodeUnsignedTx(rawTx); + + // Hash the encoded unsigned transaction + bytes32 digest = keccak256(encoded); + + // Sign the hash + ret = EthereumUtils.sign(pubkeyAddr, secretKey, digest); + } + + /** + * @notice Sign a transaction, returning it in EIP-1559 encoded form + * @param publicAddress Ethereum address of secret key + * @param secretKey Secret key used to sign + * @param transaction Transaction to sign + */ + function sign( + address publicAddress, + bytes32 secretKey, + EIP1559Tx memory transaction + ) internal view returns (bytes memory) { + SignatureRSV memory rsv = signRawTx( + transaction, + publicAddress, + secretKey + ); + + // For EIP-1559, we only need to normalize v to 0/1 + rsv.v = rsv.v - 27; + + return encodeSignedTx(transaction, rsv); + } +} \ No newline at end of file diff --git a/contracts/contracts/EIP2930Signer.sol b/contracts/contracts/EIP2930Signer.sol new file mode 100644 index 00000000..741fe18b --- /dev/null +++ b/contracts/contracts/EIP2930Signer.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; +import {Sapphire} from "./Sapphire.sol"; +import {EthereumUtils, SignatureRSV} from "./EthereumUtils.sol"; +import {RLPWriter} from "./RLPWriter.sol"; +import {EIPTypes} from "./EIPTypes.sol"; + +/** + * @title Ethereum EIP-2930 transaction signer & encoder + */ +library EIP2930Signer { + struct EIP2930Tx { + uint64 nonce; + uint256 gasPrice; + uint64 gasLimit; + address to; + uint256 value; + bytes data; + EIPTypes.AccessList accessList; + uint256 chainId; + } + + /** + * @notice Encode an unsigned EIP-2930 transaction for signing + * @param rawTx Transaction to encode + */ + function encodeUnsignedTx(EIP2930Tx memory rawTx) + internal + pure + returns (bytes memory) + { + bytes[] memory b = new bytes[](8); + b[0] = RLPWriter.writeUint(rawTx.chainId); + b[1] = RLPWriter.writeUint(rawTx.nonce); + b[2] = RLPWriter.writeUint(rawTx.gasPrice); + b[3] = RLPWriter.writeUint(rawTx.gasLimit); + b[4] = RLPWriter.writeAddress(rawTx.to); + b[5] = RLPWriter.writeUint(rawTx.value); + b[6] = RLPWriter.writeBytes(rawTx.data); + b[7] = EIPTypes.encodeAccessList(rawTx.accessList); + + // RLP encode the transaction data + bytes memory rlpEncodedTx = RLPWriter.writeList(b); + + // Return the unsigned transaction with EIP-2930 type prefix + return abi.encodePacked(hex"01", rlpEncodedTx); + } + + /** + * @notice Encode a signed EIP-2930 transaction + * @param rawTx Transaction which was signed + * @param rsv R, S & V parameters of signature + */ + function encodeSignedTx(EIP2930Tx memory rawTx, SignatureRSV memory rsv) + internal + pure + returns (bytes memory) + { + bytes[] memory b = new bytes[](11); + b[0] = RLPWriter.writeUint(rawTx.chainId); + b[1] = RLPWriter.writeUint(rawTx.nonce); + b[2] = RLPWriter.writeUint(rawTx.gasPrice); + b[3] = RLPWriter.writeUint(rawTx.gasLimit); + b[4] = RLPWriter.writeAddress(rawTx.to); + b[5] = RLPWriter.writeUint(rawTx.value); + b[6] = RLPWriter.writeBytes(rawTx.data); + b[7] = EIPTypes.encodeAccessList(rawTx.accessList); + b[8] = RLPWriter.writeUint(uint256(rsv.v)); + b[9] = RLPWriter.writeUint(uint256(rsv.r)); + b[10] = RLPWriter.writeUint(uint256(rsv.s)); + + // RLP encode the transaction data + bytes memory rlpEncodedTx = RLPWriter.writeList(b); + + // Return the signed transaction with EIP-2930 type prefix + return abi.encodePacked(hex"01", rlpEncodedTx); + } + + /** + * @notice Sign a raw transaction + * @param rawTx Transaction to sign + * @param pubkeyAddr Ethereum address of secret key + * @param secretKey Secret key used to sign + */ + function signRawTx( + EIP2930Tx memory rawTx, + address pubkeyAddr, + bytes32 secretKey + ) internal view returns (SignatureRSV memory ret) { + // First encode the transaction without signature fields + bytes memory encoded = encodeUnsignedTx(rawTx); + + // Hash the encoded unsigned transaction + bytes32 digest = keccak256(encoded); + + // Sign the hash + ret = EthereumUtils.sign(pubkeyAddr, secretKey, digest); + } + + /** + * @notice Sign a transaction, returning it in EIP-2930 encoded form + * @param publicAddress Ethereum address of secret key + * @param secretKey Secret key used to sign + * @param transaction Transaction to sign + */ + function sign( + address publicAddress, + bytes32 secretKey, + EIP2930Tx memory transaction + ) internal view returns (bytes memory) { + SignatureRSV memory rsv = signRawTx( + transaction, + publicAddress, + secretKey + ); + + // For EIP-2930, we only need to normalize v to 0/1 + rsv.v = rsv.v - 27; + + return encodeSignedTx(transaction, rsv); + } +} \ No newline at end of file diff --git a/contracts/contracts/EIPTypes.sol b/contracts/contracts/EIPTypes.sol new file mode 100644 index 00000000..04b80fbc --- /dev/null +++ b/contracts/contracts/EIPTypes.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; +import {RLPWriter} from "./RLPWriter.sol"; + +library EIPTypes { + struct AccessList { + AccessListItem[] items; + } + + struct AccessListItem { + address addr; + bytes32[] storageKeys; + } + + /** + * @notice Encode an access list for EIP-1559 and EIP-2930 transactions + */ + function encodeAccessList(AccessList memory list) + internal + pure + returns (bytes memory) + { + bytes[] memory items = new bytes[](list.items.length); + + for (uint i = 0; i < list.items.length; i++) { + bytes[] memory item = new bytes[](2); + // Encode the address + item[0] = RLPWriter.writeAddress(list.items[i].addr); + + // Encode storage keys + bytes[] memory storageKeys = new bytes[](list.items[i].storageKeys.length); + for (uint j = 0; j < list.items[i].storageKeys.length; j++) { + // Use writeBytes for the full storage key + storageKeys[j] = RLPWriter.writeBytes(abi.encodePacked(list.items[i].storageKeys[j])); + } + item[1] = RLPWriter.writeList(storageKeys); + + items[i] = RLPWriter.writeList(item); + } + + return RLPWriter.writeList(items); + } +} \ No newline at end of file diff --git a/contracts/contracts/tests/EIPTests.sol b/contracts/contracts/tests/EIPTests.sol new file mode 100644 index 00000000..886116ca --- /dev/null +++ b/contracts/contracts/tests/EIPTests.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {EthereumUtils} from "../EthereumUtils.sol"; +import {EIP1559Signer} from "../EIP1559Signer.sol"; +import {EIP2930Signer} from "../EIP2930Signer.sol"; + +contract EIPTests { + address public immutable SENDER_ADDRESS; + bytes32 public immutable SECRET_KEY; + + // New state variables to simulate storage access + uint256 public storedNumber1; + uint256 public storedNumber2; + bytes32 public storedBytes1; + bytes32 public storedBytes2; + + constructor() payable { + // Deploy test contract + (SENDER_ADDRESS, SECRET_KEY) = EthereumUtils.generateKeypair(); + payable(SENDER_ADDRESS).transfer(msg.value); + + // Initialize state variables + storedNumber1 = 42; + storedNumber2 = 84; + storedBytes1 = keccak256(abi.encodePacked("first slot")); + storedBytes2 = keccak256(abi.encodePacked("second slot")); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } + + event HasChainId(uint256); + function emitChainId() external { + emit HasChainId(block.chainid); + } + + // In your contract + function getStorageSlots() public pure returns (bytes32, bytes32, bytes32, bytes32) { + bytes32 slot0; + bytes32 slot1; + bytes32 slot2; + bytes32 slot3; + + assembly { + // Get storage slots for each state variable + // Note: immutable variables don't have storage slots + slot0 := storedNumber1.slot // uint256 public storedNumber1 + slot1 := storedNumber2.slot // uint256 public storedNumber2 + slot2 := storedBytes1.slot // bytes32 public storedBytes1 + slot3 := storedBytes2.slot // bytes32 public storedBytes2 + } + + return (slot0, slot1, slot2, slot3); + } + + // EIP-1559 signing functions + function signEIP1559(EIP1559Signer.EIP1559Tx memory transaction) + external + view + returns (bytes memory) + { + transaction.data = abi.encodeWithSelector(this.example.selector); + transaction.chainId = block.chainid; + return EIP1559Signer.sign(SENDER_ADDRESS, SECRET_KEY, transaction); + } + + function signEIP1559WithSecret( + EIP1559Signer.EIP1559Tx memory transaction, + address fromPublicAddr, + bytes32 fromSecret + ) external view returns (bytes memory) { + transaction.data = abi.encodeWithSelector(this.example.selector); + transaction.chainId = block.chainid; + return EIP1559Signer.sign(fromPublicAddr, fromSecret, transaction); + } + + // EIP-2930 signing functions + function signEIP2930(EIP2930Signer.EIP2930Tx memory transaction) + external + view + returns (bytes memory) + { + transaction.data = abi.encodeWithSelector(this.example.selector); + transaction.chainId = block.chainid; + return EIP2930Signer.sign(SENDER_ADDRESS, SECRET_KEY, transaction); + } + + function signEIP2930WithSecret( + EIP2930Signer.EIP2930Tx memory transaction, + address fromPublicAddr, + bytes32 fromSecret + ) external view returns (bytes memory) { + transaction.data = abi.encodeWithSelector(this.example.selector); + transaction.chainId = block.chainid; + return EIP2930Signer.sign(fromPublicAddr, fromSecret, transaction); + } + + event ExampleEvent(bytes32 x); + function example() external returns (uint256 result) { + emit ExampleEvent( + 0xfedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 + ); + // First access to storage variables (cold access without access list) + uint256 sum1 = storedNumber1 + storedNumber2; + bytes32 hash1 = keccak256(abi.encodePacked(storedBytes1, storedBytes2)); + emit ExampleEvent(hash1); + + // Second access (would be cold again without access list) + uint256 sum2 = storedNumber1 * storedNumber2; + bytes32 hash2 = keccak256(abi.encodePacked(storedBytes2, storedBytes1)); + emit ExampleEvent(hash2); + + // Third access (cold again without access list) + uint256 sum3 = (storedNumber1 ** 2) + (storedNumber2 ** 2); + bytes32 hash3 = keccak256(abi.encodePacked(storedBytes1, storedBytes2)); + emit ExampleEvent(hash3); + + // Use all computed values to prevent optimization + result = sum1 + sum2 + uint256(hash1) + uint256(hash2) + uint256(hash3) + sum3; + } +} \ No newline at end of file diff --git a/contracts/test/eip1559_2930.ts b/contracts/test/eip1559_2930.ts new file mode 100644 index 00000000..0205667a --- /dev/null +++ b/contracts/test/eip1559_2930.ts @@ -0,0 +1,387 @@ +import { expect } from 'chai'; +import hre, { ethers } from 'hardhat'; +import { wrapEthersSigner } from '@oasisprotocol/sapphire-ethers-v6'; +import { EIPTests__factory } from '../typechain-types/factories/contracts/tests'; +import { EIPTests } from '../typechain-types/contracts/tests/EIPTests'; +import { HardhatNetworkHDAccountsConfig } from 'hardhat/types'; +import { Transaction } from 'ethers'; + +const EXPECTED_EVENT = '0xfedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210'; +const EXPECTED_ENTROPY_ENCRYPTED = 3.8; + +// Shannon entropy calculation +function entropy(str: string) { + return [...new Set(str)] + .map((chr) => { + return str.match(new RegExp(chr, 'g'))!.length; + }) + .reduce((sum, frequency) => { + let p = frequency / str.length; + return sum + p * Math.log2(1 / p); + }, 0); +} + +function getWallet(index: number) { + const accounts = hre.network.config.accounts as HardhatNetworkHDAccountsConfig; + if (!accounts.mnemonic) { + return new ethers.Wallet((accounts as unknown as string[])[0]); + } + return ethers.HDNodeWallet.fromMnemonic( + ethers.Mnemonic.fromPhrase(accounts.mnemonic), + accounts.path + `/${index}`, + ); +} + +async function verifyTxReceipt(response: any, expectedData?: string) { + const receipt = await response.wait(); + if (!receipt) throw new Error('No transaction receipt received'); + + if (expectedData) { + expect(receipt.logs[0].data).to.equal(expectedData); + } + return receipt; +} + +describe('EIP-1559 and EIP-2930 Tests', function () { + let testContract: EIPTests; + let calldata: string; + + before(async () => { + const factory = (await ethers.getContractFactory( + 'EIPTests', + )) as unknown as EIPTests__factory; + testContract = await factory.deploy({ + value: ethers.parseEther('10'), + }); + await testContract.waitForDeployment(); + calldata = testContract.interface.encodeFunctionData('example'); + }); + + it('Has correct block.chainid', async () => { + const provider = ethers.provider; + const expectedChainId = (await provider.getNetwork()).chainId; + + const tx = await testContract.emitChainId(); + const receipt = await tx.wait(); + if (!receipt || receipt.status != 1) throw new Error('tx failed'); + expect(receipt.logs![0].data).eq(expectedChainId); + + const onchainChainId = await testContract.getChainId(); + expect(onchainChainId).eq(expectedChainId); + }); + + describe('EIP-1559', function() { + it('Other-Signed EIP-1559 transaction submission via un-wrapped provider', async function () { + const provider = ethers.provider; + const feeData = await provider.getFeeData(); + + const publicAddr = await testContract.SENDER_ADDRESS(); + const secretKey = await testContract.SECRET_KEY(); + console.log(`Public Address: ${publicAddr}`); + console.log(`Secret Key: ${secretKey}`); + + // Ensure fee data is valid + if (!feeData.gasPrice) { + throw new Error('Failed to fetch fee data'); + } + + // Set custom values for maxPriorityFeePerGas and maxFeePerGas + const maxPriorityFeePerGas = ethers.parseUnits('20', 'gwei'); // Custom value for maxPriorityFeePerGas + const maxFeePerGas = ethers.parseUnits('120', 'gwei'); // Custom value for maxFeePerGas + + const signedTx = await testContract.signEIP1559({ + nonce: await provider.getTransactionCount(await testContract.SENDER_ADDRESS()), + maxPriorityFeePerGas: maxPriorityFeePerGas, + maxFeePerGas: maxFeePerGas, + gasLimit: 250000, + to: await testContract.getAddress(), + value: 0, + data: '0x', + accessList: { items: [] }, + chainId: 0, + }); + + let plainResp = await provider.broadcastTransaction(signedTx); + await plainResp.wait(); + let receipt = await provider.getTransactionReceipt(plainResp.hash); + expect(plainResp.data).eq(calldata); + expect(receipt!.logs[0].data).equal(EXPECTED_EVENT); + }); + + it('Should compare Self-Signed EIP-1559 transactions with and without access list', async function () { + const provider = ethers.provider; + const privateKey = await testContract.SECRET_KEY(); + const wp = new ethers.Wallet(privateKey, provider); + const wallet = wrapEthersSigner(wp); + + const maxPriorityFeePerGas = ethers.parseUnits('20', 'gwei'); + const maxFeePerGas = ethers.parseUnits('200', 'gwei'); + const contractAddress = await testContract.getAddress(); + + // Get storage slots + const [slot0, slot1, slot2, slot3] = await testContract.getStorageSlots(); + + // Test cases + const testCases = [ + { + name: "Without access list", + accessList: [] + }, + { + name: "With access list", + accessList: [ + { + address: contractAddress, + storageKeys: [slot0, slot1, slot2, slot3] + } + ] + } + ]; + + for (const testCase of testCases) { + console.log(`\nTesting: ${testCase.name}`); + + const tx = Transaction.from({ + gasLimit: 250000, + to: contractAddress, + value: 0, + data: calldata, + chainId: (await provider.getNetwork()).chainId, + maxPriorityFeePerGas, + maxFeePerGas, + nonce: await provider.getTransactionCount(wallet.address), + type: 2, // EIP-1559 + accessList: testCase.accessList + }); + + const signedTx = await wallet.signTransaction(tx); + let response = await provider.broadcastTransaction(signedTx); + const receipt = await verifyTxReceipt(response); + + console.log(`Gas used: ${receipt.gasUsed} gas`); + + // Verify transaction succeeded and produced expected results + expect(entropy(response.data)).gte(EXPECTED_ENTROPY_ENCRYPTED); + expect(response.data).not.eq(calldata); + expect(receipt.logs[0].data).equal(EXPECTED_EVENT); + + // Optional: Print the decoded transaction to verify access list + const decodedTx = Transaction.from(signedTx); + console.log('Access List:', decodedTx.accessList); + } + }); + }); + + describe('EIP-2930', function() { + it('Other-Signed EIP-2930 transaction submission via un-wrapped provider', async function () { + const provider = ethers.provider; + const feeData = await provider.getFeeData(); + + const signedTx = await testContract.signEIP2930({ + nonce: await provider.getTransactionCount(await testContract.SENDER_ADDRESS()), + gasPrice: feeData.gasPrice as bigint, + gasLimit: 250000, + to: await testContract.getAddress(), + value: 0, + data: '0x', + accessList: { items: [] }, + chainId: 0, + }); + + let plainResp = await provider.broadcastTransaction(signedTx); + await plainResp.wait(); + let receipt = await provider.getTransactionReceipt(plainResp.hash); + expect(plainResp.data).eq(calldata); + expect(receipt!.logs[0].data).equal(EXPECTED_EVENT); + }); + + it('Self-Signed EIP-2930 transaction submission via wrapped wallet', async function () { + const provider = ethers.provider; + const wp = getWallet(0).connect(provider); + const wallet = wrapEthersSigner(wp); + const feeData = await provider.getFeeData(); + + const tx = Transaction.from({ + gasLimit: 250000, + to: await testContract.getAddress(), + value: 0, + data: calldata, + chainId: (await provider.getNetwork()).chainId, + gasPrice: feeData.gasPrice, + nonce: await provider.getTransactionCount(wallet.address), + type: 1, // EIP-2930 + accessList: [ + { + address: await testContract.getAddress(), + storageKeys: [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001", + ], + }, + ], + }); + + const signedTx = await wallet.signTransaction(tx); + let response = await provider.broadcastTransaction(signedTx); + await response.wait(); + expect(entropy(response.data)).gte(EXPECTED_ENTROPY_ENCRYPTED); + expect(response.data).not.eq(calldata); + + let receipt = await provider.getTransactionReceipt(response.hash); + expect(receipt!.logs[0].data).equal(EXPECTED_EVENT); + }); + + it('should fail with invalid storage key length', async function () { + const provider = ethers.provider; + const publicAddr = await testContract.SENDER_ADDRESS(); + + // Create an access list with invalid storage key length + const accessList = { + items: [ + { + addr: await testContract.getAddress(), + storageKeys: [ + ethers.zeroPadValue('0x01', 16) // Invalid: only 16 bytes instead of 32 + ] + } + ] + }; + + await expect( + testContract.signEIP2930({ + nonce: await provider.getTransactionCount(publicAddr), + gasPrice: ethers.parseUnits('100', 'gwei'), + gasLimit: 250000, + to: await testContract.getAddress(), + value: 0, + data: '0x', + accessList, + chainId: (await provider.getNetwork()).chainId, + }) + ).to.be.revertedWithCustomError; + }); + }); + + describe('Access List Gas Tests', function() { + + describe('Gas Usage Comparison', function() { + it('should compare gas usage with and without access lists for EIP-1559', async function() { + const provider = ethers.provider; + const publicAddr = await testContract.SENDER_ADDRESS(); + const contractAddress = await testContract.getAddress(); + + const [slot0, slot1, slot2, slot3] = await testContract.getStorageSlots(); + + // Test cases with different access list configurations + const testCases = [ + { + name: "No access list", + accessList: { items: [] } + }, + { + name: "With storedNumber1 and storedNumber2", + accessList: { + items: [{ + addr: contractAddress, + storageKeys: [slot0, slot1] + }] + } + }, + { + name: "With all storage slots", + accessList: { + items: [{ + addr: contractAddress, + storageKeys: [slot0, slot1, slot2, slot3] + }] + } + } + ]; + + for (const testCase of testCases) { + console.log(`\nTesting: ${testCase.name}`); + + const signedTx = await testContract.signEIP1559({ + nonce: await provider.getTransactionCount(publicAddr), + maxPriorityFeePerGas: ethers.parseUnits('20', 'gwei'), + maxFeePerGas: ethers.parseUnits('120', 'gwei'), + gasLimit: 500000, + to: contractAddress, + value: 0, + data: '0x', + accessList: testCase.accessList, + chainId: (await provider.getNetwork()).chainId, + }); + + const response = await provider.broadcastTransaction(signedTx); + const receipt = await verifyTxReceipt(response); + + console.log(`Gas used: ${receipt.gasUsed}`); + } + }); + + it('should compare gas usage with and without access lists for EIP-2930', async function() { + const provider = ethers.provider; + const publicAddr = await testContract.SENDER_ADDRESS(); + const contractAddress = await testContract.getAddress(); + + const [slot0, slot1, slot2, slot3] = await testContract.getStorageSlots(); + + const testCases = [ + { + name: "No access list", + accessList: { items: [] } + }, + { + name: "With number slots", + accessList: { + items: [{ + addr: contractAddress, + storageKeys: [slot0, slot1] + }] + } + }, + { + name: "With bytes slots", + accessList: { + items: [{ + addr: contractAddress, + storageKeys: [slot2, slot3] + }] + } + }, + { + name: "With all slots", + accessList: { + items: [{ + addr: contractAddress, + storageKeys: [slot0, slot1, slot2, slot3] + }] + } + } + ]; + + for (const testCase of testCases) { + console.log(`\nTesting: ${testCase.name}`); + + const signedTx = await testContract.signEIP2930({ + nonce: await provider.getTransactionCount(publicAddr), + gasPrice: ethers.parseUnits('100', 'gwei'), + gasLimit: 500000, + to: contractAddress, + value: 0, + data: '0x', + accessList: testCase.accessList, + chainId: (await provider.getNetwork()).chainId, + }); + + const response = await provider.broadcastTransaction(signedTx); + const receipt = await verifyTxReceipt(response); + + console.log(`Gas used: ${receipt.gasUsed}`); + } + }); + }); + }); + +}); \ No newline at end of file From b971cb2f81df3ba0b50e2269aa58d0d6d2c37a3f Mon Sep 17 00:00:00 2001 From: root Date: Mon, 4 Nov 2024 15:46:37 +0100 Subject: [PATCH 2/2] Format signer contracts --- contracts/contracts/EIP1559Signer.sol | 18 +- contracts/contracts/EIP2930Signer.sol | 18 +- contracts/contracts/EIPTypes.sol | 26 +-- contracts/test/eip1559_2930.ts | 268 ++++++++++++++------------ 4 files changed, 175 insertions(+), 155 deletions(-) diff --git a/contracts/contracts/EIP1559Signer.sol b/contracts/contracts/EIP1559Signer.sol index 0bc45fde..77c0505d 100644 --- a/contracts/contracts/EIP1559Signer.sol +++ b/contracts/contracts/EIP1559Signer.sol @@ -40,10 +40,10 @@ library EIP1559Signer { b[6] = RLPWriter.writeUint(rawTx.value); b[7] = RLPWriter.writeBytes(rawTx.data); b[8] = EIPTypes.encodeAccessList(rawTx.accessList); - + // RLP encode the transaction data bytes memory rlpEncodedTx = RLPWriter.writeList(b); - + // Return the unsigned transaction with EIP-1559 type prefix return abi.encodePacked(hex"02", rlpEncodedTx); } @@ -71,10 +71,10 @@ library EIP1559Signer { b[9] = RLPWriter.writeUint(uint256(rsv.v)); b[10] = RLPWriter.writeUint(uint256(rsv.r)); b[11] = RLPWriter.writeUint(uint256(rsv.s)); - + // RLP encode the transaction data bytes memory rlpEncodedTx = RLPWriter.writeList(b); - + // Return the signed transaction with EIP-1559 type prefix return abi.encodePacked(hex"02", rlpEncodedTx); } @@ -92,10 +92,10 @@ library EIP1559Signer { ) internal view returns (SignatureRSV memory ret) { // First encode the transaction without signature fields bytes memory encoded = encodeUnsignedTx(rawTx); - + // Hash the encoded unsigned transaction bytes32 digest = keccak256(encoded); - + // Sign the hash ret = EthereumUtils.sign(pubkeyAddr, secretKey, digest); } @@ -116,10 +116,10 @@ library EIP1559Signer { publicAddress, secretKey ); - + // For EIP-1559, we only need to normalize v to 0/1 rsv.v = rsv.v - 27; - + return encodeSignedTx(transaction, rsv); } -} \ No newline at end of file +} diff --git a/contracts/contracts/EIP2930Signer.sol b/contracts/contracts/EIP2930Signer.sol index 741fe18b..5c22d17d 100644 --- a/contracts/contracts/EIP2930Signer.sol +++ b/contracts/contracts/EIP2930Signer.sol @@ -38,10 +38,10 @@ library EIP2930Signer { b[5] = RLPWriter.writeUint(rawTx.value); b[6] = RLPWriter.writeBytes(rawTx.data); b[7] = EIPTypes.encodeAccessList(rawTx.accessList); - + // RLP encode the transaction data bytes memory rlpEncodedTx = RLPWriter.writeList(b); - + // Return the unsigned transaction with EIP-2930 type prefix return abi.encodePacked(hex"01", rlpEncodedTx); } @@ -68,10 +68,10 @@ library EIP2930Signer { b[8] = RLPWriter.writeUint(uint256(rsv.v)); b[9] = RLPWriter.writeUint(uint256(rsv.r)); b[10] = RLPWriter.writeUint(uint256(rsv.s)); - + // RLP encode the transaction data bytes memory rlpEncodedTx = RLPWriter.writeList(b); - + // Return the signed transaction with EIP-2930 type prefix return abi.encodePacked(hex"01", rlpEncodedTx); } @@ -89,10 +89,10 @@ library EIP2930Signer { ) internal view returns (SignatureRSV memory ret) { // First encode the transaction without signature fields bytes memory encoded = encodeUnsignedTx(rawTx); - + // Hash the encoded unsigned transaction bytes32 digest = keccak256(encoded); - + // Sign the hash ret = EthereumUtils.sign(pubkeyAddr, secretKey, digest); } @@ -113,10 +113,10 @@ library EIP2930Signer { publicAddress, secretKey ); - + // For EIP-2930, we only need to normalize v to 0/1 rsv.v = rsv.v - 27; - + return encodeSignedTx(transaction, rsv); } -} \ No newline at end of file +} diff --git a/contracts/contracts/EIPTypes.sol b/contracts/contracts/EIPTypes.sol index 04b80fbc..7c895415 100644 --- a/contracts/contracts/EIPTypes.sol +++ b/contracts/contracts/EIPTypes.sol @@ -6,12 +6,12 @@ library EIPTypes { struct AccessList { AccessListItem[] items; } - + struct AccessListItem { address addr; bytes32[] storageKeys; } - + /** * @notice Encode an access list for EIP-1559 and EIP-2930 transactions */ @@ -21,23 +21,27 @@ library EIPTypes { returns (bytes memory) { bytes[] memory items = new bytes[](list.items.length); - - for (uint i = 0; i < list.items.length; i++) { + + for (uint256 i = 0; i < list.items.length; i++) { bytes[] memory item = new bytes[](2); // Encode the address item[0] = RLPWriter.writeAddress(list.items[i].addr); - + // Encode storage keys - bytes[] memory storageKeys = new bytes[](list.items[i].storageKeys.length); - for (uint j = 0; j < list.items[i].storageKeys.length; j++) { + bytes[] memory storageKeys = new bytes[]( + list.items[i].storageKeys.length + ); + for (uint256 j = 0; j < list.items[i].storageKeys.length; j++) { // Use writeBytes for the full storage key - storageKeys[j] = RLPWriter.writeBytes(abi.encodePacked(list.items[i].storageKeys[j])); + storageKeys[j] = RLPWriter.writeBytes( + abi.encodePacked(list.items[i].storageKeys[j]) + ); } item[1] = RLPWriter.writeList(storageKeys); - + items[i] = RLPWriter.writeList(item); } - + return RLPWriter.writeList(items); } -} \ No newline at end of file +} diff --git a/contracts/test/eip1559_2930.ts b/contracts/test/eip1559_2930.ts index 0205667a..2c6fd626 100644 --- a/contracts/test/eip1559_2930.ts +++ b/contracts/test/eip1559_2930.ts @@ -6,7 +6,8 @@ import { EIPTests } from '../typechain-types/contracts/tests/EIPTests'; import { HardhatNetworkHDAccountsConfig } from 'hardhat/types'; import { Transaction } from 'ethers'; -const EXPECTED_EVENT = '0xfedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210'; +const EXPECTED_EVENT = + '0xfedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210'; const EXPECTED_ENTROPY_ENCRYPTED = 3.8; // Shannon entropy calculation @@ -22,7 +23,8 @@ function entropy(str: string) { } function getWallet(index: number) { - const accounts = hre.network.config.accounts as HardhatNetworkHDAccountsConfig; + const accounts = hre.network.config + .accounts as HardhatNetworkHDAccountsConfig; if (!accounts.mnemonic) { return new ethers.Wallet((accounts as unknown as string[])[0]); } @@ -35,7 +37,7 @@ function getWallet(index: number) { async function verifyTxReceipt(response: any, expectedData?: string) { const receipt = await response.wait(); if (!receipt) throw new Error('No transaction receipt received'); - + if (expectedData) { expect(receipt.logs[0].data).to.equal(expectedData); } @@ -70,7 +72,7 @@ describe('EIP-1559 and EIP-2930 Tests', function () { expect(onchainChainId).eq(expectedChainId); }); - describe('EIP-1559', function() { + describe('EIP-1559', function () { it('Other-Signed EIP-1559 transaction submission via un-wrapped provider', async function () { const provider = ethers.provider; const feeData = await provider.getFeeData(); @@ -79,18 +81,20 @@ describe('EIP-1559 and EIP-2930 Tests', function () { const secretKey = await testContract.SECRET_KEY(); console.log(`Public Address: ${publicAddr}`); console.log(`Secret Key: ${secretKey}`); - + // Ensure fee data is valid if (!feeData.gasPrice) { - throw new Error('Failed to fetch fee data'); - } - + throw new Error('Failed to fetch fee data'); + } + // Set custom values for maxPriorityFeePerGas and maxFeePerGas const maxPriorityFeePerGas = ethers.parseUnits('20', 'gwei'); // Custom value for maxPriorityFeePerGas const maxFeePerGas = ethers.parseUnits('120', 'gwei'); // Custom value for maxFeePerGas const signedTx = await testContract.signEIP1559({ - nonce: await provider.getTransactionCount(await testContract.SENDER_ADDRESS()), + nonce: await provider.getTransactionCount( + await testContract.SENDER_ADDRESS(), + ), maxPriorityFeePerGas: maxPriorityFeePerGas, maxFeePerGas: maxFeePerGas, gasLimit: 250000, @@ -113,72 +117,74 @@ describe('EIP-1559 and EIP-2930 Tests', function () { const privateKey = await testContract.SECRET_KEY(); const wp = new ethers.Wallet(privateKey, provider); const wallet = wrapEthersSigner(wp); - + const maxPriorityFeePerGas = ethers.parseUnits('20', 'gwei'); const maxFeePerGas = ethers.parseUnits('200', 'gwei'); const contractAddress = await testContract.getAddress(); - + // Get storage slots const [slot0, slot1, slot2, slot3] = await testContract.getStorageSlots(); - + // Test cases const testCases = [ - { - name: "Without access list", - accessList: [] - }, - { - name: "With access list", - accessList: [ - { - address: contractAddress, - storageKeys: [slot0, slot1, slot2, slot3] - } - ] - } + { + name: 'Without access list', + accessList: [], + }, + { + name: 'With access list', + accessList: [ + { + address: contractAddress, + storageKeys: [slot0, slot1, slot2, slot3], + }, + ], + }, ]; - + for (const testCase of testCases) { - console.log(`\nTesting: ${testCase.name}`); - - const tx = Transaction.from({ - gasLimit: 250000, - to: contractAddress, - value: 0, - data: calldata, - chainId: (await provider.getNetwork()).chainId, - maxPriorityFeePerGas, - maxFeePerGas, - nonce: await provider.getTransactionCount(wallet.address), - type: 2, // EIP-1559 - accessList: testCase.accessList - }); - - const signedTx = await wallet.signTransaction(tx); - let response = await provider.broadcastTransaction(signedTx); - const receipt = await verifyTxReceipt(response); - - console.log(`Gas used: ${receipt.gasUsed} gas`); - - // Verify transaction succeeded and produced expected results - expect(entropy(response.data)).gte(EXPECTED_ENTROPY_ENCRYPTED); - expect(response.data).not.eq(calldata); - expect(receipt.logs[0].data).equal(EXPECTED_EVENT); - - // Optional: Print the decoded transaction to verify access list - const decodedTx = Transaction.from(signedTx); - console.log('Access List:', decodedTx.accessList); + console.log(`\nTesting: ${testCase.name}`); + + const tx = Transaction.from({ + gasLimit: 250000, + to: contractAddress, + value: 0, + data: calldata, + chainId: (await provider.getNetwork()).chainId, + maxPriorityFeePerGas, + maxFeePerGas, + nonce: await provider.getTransactionCount(wallet.address), + type: 2, // EIP-1559 + accessList: testCase.accessList, + }); + + const signedTx = await wallet.signTransaction(tx); + let response = await provider.broadcastTransaction(signedTx); + const receipt = await verifyTxReceipt(response); + + console.log(`Gas used: ${receipt.gasUsed} gas`); + + // Verify transaction succeeded and produced expected results + expect(entropy(response.data)).gte(EXPECTED_ENTROPY_ENCRYPTED); + expect(response.data).not.eq(calldata); + expect(receipt.logs[0].data).equal(EXPECTED_EVENT); + + // Optional: Print the decoded transaction to verify access list + const decodedTx = Transaction.from(signedTx); + console.log('Access List:', decodedTx.accessList); } - }); + }); }); - describe('EIP-2930', function() { + describe('EIP-2930', function () { it('Other-Signed EIP-2930 transaction submission via un-wrapped provider', async function () { const provider = ethers.provider; const feeData = await provider.getFeeData(); - + const signedTx = await testContract.signEIP2930({ - nonce: await provider.getTransactionCount(await testContract.SENDER_ADDRESS()), + nonce: await provider.getTransactionCount( + await testContract.SENDER_ADDRESS(), + ), gasPrice: feeData.gasPrice as bigint, gasLimit: 250000, to: await testContract.getAddress(), @@ -210,12 +216,12 @@ describe('EIP-1559 and EIP-2930 Tests', function () { gasPrice: feeData.gasPrice, nonce: await provider.getTransactionCount(wallet.address), type: 1, // EIP-2930 - accessList: [ + accessList: [ { address: await testContract.getAddress(), storageKeys: [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000001", + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000001', ], }, ], @@ -234,17 +240,17 @@ describe('EIP-1559 and EIP-2930 Tests', function () { it('should fail with invalid storage key length', async function () { const provider = ethers.provider; const publicAddr = await testContract.SENDER_ADDRESS(); - + // Create an access list with invalid storage key length const accessList = { items: [ { addr: await testContract.getAddress(), storageKeys: [ - ethers.zeroPadValue('0x01', 16) // Invalid: only 16 bytes instead of 32 - ] - } - ] + ethers.zeroPadValue('0x01', 16), // Invalid: only 16 bytes instead of 32 + ], + }, + ], }; await expect( @@ -257,50 +263,54 @@ describe('EIP-1559 and EIP-2930 Tests', function () { data: '0x', accessList, chainId: (await provider.getNetwork()).chainId, - }) + }), ).to.be.revertedWithCustomError; }); }); - describe('Access List Gas Tests', function() { - - describe('Gas Usage Comparison', function() { - it('should compare gas usage with and without access lists for EIP-1559', async function() { + describe('Access List Gas Tests', function () { + describe('Gas Usage Comparison', function () { + it('should compare gas usage with and without access lists for EIP-1559', async function () { const provider = ethers.provider; const publicAddr = await testContract.SENDER_ADDRESS(); const contractAddress = await testContract.getAddress(); - - const [slot0, slot1, slot2, slot3] = await testContract.getStorageSlots(); - + + const [slot0, slot1, slot2, slot3] = + await testContract.getStorageSlots(); + // Test cases with different access list configurations const testCases = [ { - name: "No access list", - accessList: { items: [] } + name: 'No access list', + accessList: { items: [] }, }, { - name: "With storedNumber1 and storedNumber2", + name: 'With storedNumber1 and storedNumber2', accessList: { - items: [{ - addr: contractAddress, - storageKeys: [slot0, slot1] - }] - } + items: [ + { + addr: contractAddress, + storageKeys: [slot0, slot1], + }, + ], + }, }, { - name: "With all storage slots", + name: 'With all storage slots', accessList: { - items: [{ - addr: contractAddress, - storageKeys: [slot0, slot1, slot2, slot3] - }] - } - } + items: [ + { + addr: contractAddress, + storageKeys: [slot0, slot1, slot2, slot3], + }, + ], + }, + }, ]; - + for (const testCase of testCases) { console.log(`\nTesting: ${testCase.name}`); - + const signedTx = await testContract.signEIP1559({ nonce: await provider.getTransactionCount(publicAddr), maxPriorityFeePerGas: ethers.parseUnits('20', 'gwei'), @@ -312,58 +322,65 @@ describe('EIP-1559 and EIP-2930 Tests', function () { accessList: testCase.accessList, chainId: (await provider.getNetwork()).chainId, }); - + const response = await provider.broadcastTransaction(signedTx); const receipt = await verifyTxReceipt(response); - + console.log(`Gas used: ${receipt.gasUsed}`); } }); - - it('should compare gas usage with and without access lists for EIP-2930', async function() { + + it('should compare gas usage with and without access lists for EIP-2930', async function () { const provider = ethers.provider; const publicAddr = await testContract.SENDER_ADDRESS(); const contractAddress = await testContract.getAddress(); - - const [slot0, slot1, slot2, slot3] = await testContract.getStorageSlots(); - + + const [slot0, slot1, slot2, slot3] = + await testContract.getStorageSlots(); + const testCases = [ { - name: "No access list", - accessList: { items: [] } + name: 'No access list', + accessList: { items: [] }, }, { - name: "With number slots", + name: 'With number slots', accessList: { - items: [{ - addr: contractAddress, - storageKeys: [slot0, slot1] - }] - } + items: [ + { + addr: contractAddress, + storageKeys: [slot0, slot1], + }, + ], + }, }, { - name: "With bytes slots", + name: 'With bytes slots', accessList: { - items: [{ - addr: contractAddress, - storageKeys: [slot2, slot3] - }] - } + items: [ + { + addr: contractAddress, + storageKeys: [slot2, slot3], + }, + ], + }, }, { - name: "With all slots", + name: 'With all slots', accessList: { - items: [{ - addr: contractAddress, - storageKeys: [slot0, slot1, slot2, slot3] - }] - } - } + items: [ + { + addr: contractAddress, + storageKeys: [slot0, slot1, slot2, slot3], + }, + ], + }, + }, ]; - + for (const testCase of testCases) { console.log(`\nTesting: ${testCase.name}`); - + const signedTx = await testContract.signEIP2930({ nonce: await provider.getTransactionCount(publicAddr), gasPrice: ethers.parseUnits('100', 'gwei'), @@ -374,14 +391,13 @@ describe('EIP-1559 and EIP-2930 Tests', function () { accessList: testCase.accessList, chainId: (await provider.getNetwork()).chainId, }); - + const response = await provider.broadcastTransaction(signedTx); const receipt = await verifyTxReceipt(response); - + console.log(`Gas used: ${receipt.gasUsed}`); } }); }); }); - -}); \ No newline at end of file +});