From cb4294da6340d5848fdd448880ff7fcd4d2a34e8 Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Thu, 18 Jan 2024 09:33:03 +0800 Subject: [PATCH] support eip2930 (#2689) --- primitives/src/signature.rs | 2 + primitives/src/unchecked_extrinsic.rs | 48 +++- ts-tests/tests/test-sign-eip2930.ts | 324 ++++++++++++++++++++++++++ 3 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 ts-tests/tests/test-sign-eip2930.ts diff --git a/primitives/src/signature.rs b/primitives/src/signature.rs index 6840be2542..3379650dc0 100644 --- a/primitives/src/signature.rs +++ b/primitives/src/signature.rs @@ -41,6 +41,8 @@ pub enum AcalaMultiSignature { Eip1559([u8; 65]), // An Ethereum SECP256k1 signature using Eip712 for message encoding. AcalaEip712([u8; 65]), + // An Ethereum SECP256k1 signature using Eip2930 for message encoding. + Eip2930([u8; 65]), } impl From for AcalaMultiSignature { diff --git a/primitives/src/unchecked_extrinsic.rs b/primitives/src/unchecked_extrinsic.rs index 0441a6a0f7..1bd4ddcbf7 100644 --- a/primitives/src/unchecked_extrinsic.rs +++ b/primitives/src/unchecked_extrinsic.rs @@ -21,7 +21,9 @@ use frame_support::{ dispatch::{DispatchInfo, GetDispatchInfo}, traits::{ExtrinsicCall, Get}, }; -use module_evm_utility::ethereum::{EIP1559TransactionMessage, LegacyTransactionMessage, TransactionAction}; +use module_evm_utility::ethereum::{ + EIP1559TransactionMessage, EIP2930TransactionMessage, LegacyTransactionMessage, TransactionAction, +}; use module_evm_utility_macro::keccak256; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; @@ -146,6 +148,50 @@ where function, }) } + Some((addr, AcalaMultiSignature::Eip2930(sig), extra)) => { + let (eth_msg, eth_extra) = ConvertEthTx::convert((function.clone(), extra))?; + log::trace!( + target: "evm", "Eip2930 eth_msg: {:?}", eth_msg + ); + + let (tx_gas_price, tx_gas_limit) = if eth_msg.gas_price.is_zero() { + recover_sign_data(ð_msg, TxFeePerGas::get(), StorageDepositPerByte::get()) + .ok_or(InvalidTransaction::BadProof)? + } else { + // eth_call_v2, the gas_price and gas_limit are encoded. + (eth_msg.gas_price as u128, eth_msg.gas_limit as u128) + }; + + let msg = EIP2930TransactionMessage { + chain_id: eth_msg.chain_id, + nonce: eth_msg.nonce.into(), + gas_price: tx_gas_price.into(), + gas_limit: tx_gas_limit.into(), + action: eth_msg.action, + value: eth_msg.value.into(), + input: eth_msg.input, + access_list: eth_msg.access_list, + }; + log::trace!( + target: "evm", "tx msg: {:?}", msg + ); + + let msg_hash = msg.hash(); // TODO: consider rewirte this to use `keccak_256` for hashing because it could be faster + + let signer = recover_signer(&sig, msg_hash.as_fixed_bytes()).ok_or(InvalidTransaction::BadProof)?; + + let account_id = lookup.lookup(Address::Address20(signer.into()))?; + let expected_account_id = lookup.lookup(addr)?; + + if account_id != expected_account_id { + return Err(InvalidTransaction::BadProof.into()); + } + + Ok(CheckedExtrinsic { + signed: Some((account_id, eth_extra)), + function, + }) + } Some((addr, AcalaMultiSignature::Eip1559(sig), extra)) => { let (eth_msg, eth_extra) = ConvertEthTx::convert((function.clone(), extra))?; log::trace!( diff --git a/ts-tests/tests/test-sign-eip2930.ts b/ts-tests/tests/test-sign-eip2930.ts new file mode 100644 index 0000000000..e61b573b83 --- /dev/null +++ b/ts-tests/tests/test-sign-eip2930.ts @@ -0,0 +1,324 @@ +import { expect } from "chai"; + +import { describeWithAcala, getEvmNonce, transfer } from "./util"; +import { BodhiSigner } from "@acala-network/bodhi"; +import { Wallet } from "@ethersproject/wallet"; +import { encodeAddress } from "@polkadot/keyring"; +import { hexToU8a, u8aConcat, stringToU8a } from "@polkadot/util"; +import { ethers, BigNumber, ContractFactory } from "ethers"; +import Erc20DemoContract from "../build/Erc20DemoContract.json" + +describeWithAcala("Acala RPC (Sign eip2930)", (context) => { + let alice: BodhiSigner; + let signer: Wallet; + let subAddr: string; + let factory: ContractFactory; + let contract: string; + + before("init", async function () { + this.timeout(15000); + [alice] = context.wallets; + + signer = new Wallet( + "0x0123456789012345678901234567890123456789012345678901234567890123" + ); + + subAddr = encodeAddress( + u8aConcat( + stringToU8a("evm:"), + hexToU8a(signer.address), + new Uint8Array(8).fill(0) + ) + ); + + expect(subAddr).to.equal("5EMjsczQH4R2WZaB5Svau8HWZp1aAfMqjxfv3GeLWotYSkLc"); + + await transfer(context, alice.substrateAddress, subAddr, 10000000000000); + + factory = new ethers.ContractFactory(Erc20DemoContract.abi, Erc20DemoContract.bytecode); + }); + + const bigNumDiv = (x: BigNumber, y: BigNumber) => { + const res = x.div(y); + return res.mul(y) === x + ? res + : res.add(1) + } + + it("create should sign and verify", async function () { + this.timeout(150000); + + const chain_id = +context.provider.api.consts.evmAccounts.chainId.toString() + const nonce = await getEvmNonce(context.provider, signer.address); + const validUntil = (await context.provider.api.rpc.chain.getHeader()).number.toNumber() + 100 + const storageLimit = 20000; + const gasLimit = 2100000; + const priorityFee = BigNumber.from(2); + const tip = priorityFee.mul(gasLimit).toNumber(); + + const block_period = bigNumDiv(BigNumber.from(validUntil), BigNumber.from(30)); + const storage_entry_limit = bigNumDiv(BigNumber.from(storageLimit), BigNumber.from(64)); + const storage_byte_deposit = BigNumber.from(context.provider.api.consts.evm.storageDepositPerByte.toString()); + const storage_entry_deposit = storage_byte_deposit.mul(64); + const tx_fee_per_gas = BigNumber.from(context.provider.api.consts.evm.txFeePerGas.toString()); + const tx_gas_price = tx_fee_per_gas.add(block_period.toNumber() << 16).add(storage_entry_limit); + // There is a loss of precision here, so the order of calculation must be guaranteed + // must ensure storage_deposit / tx_fee_per_gas * storage_limit + const tx_gas_limit = storage_entry_deposit.div(tx_fee_per_gas).mul(storage_entry_limit).add(gasLimit); + + const deploy = factory.getDeployTransaction(100000); + + const value = { + type: 1, // EIP-2930 + // to: "0x0000000000000000000000000000000000000000", + nonce: nonce, + gasPrice: tx_gas_price.toHexString(), + gasLimit: tx_gas_limit.toNumber(), + data: deploy.data, + value: 0, + chainId: chain_id, + accessList: [], + } + + const signedTx = await signer.signTransaction(value) + const rawtx = ethers.utils.parseTransaction(signedTx) + + expect(rawtx).to.deep.include({ + type: 1, + chainId: 595, + nonce: 0, + gasPrice: BigNumber.from(200000209209), + gasLimit: BigNumber.from(12116000), + to: null, + value: BigNumber.from(0), + data: deploy.data, + accessList: [], + // v: 1226, + // r: '0xff8ff25480f5e1d1b38603b8fa1f10d64faf81707768dd9016fc4dd86d5474d2', + // s: '0x6c2cfd5acd5b0b820e1c107efd5e7ce2c452b81742091f43f5c793a835c8644f', + from: '0x14791697260E4c9A71f18484C9f997B308e59325', + // hash: '0x456d37c868520b362bbf5baf1b19752818eba49cc92c1a512e2e80d1ccfbc18b', + }); + + // tx data to user input + const input_storage_entry_limit = tx_gas_price.and(0xffff); + const input_storage_limit = input_storage_entry_limit.mul(64); + const input_block_period = (tx_gas_price.sub(input_storage_entry_limit).sub(tx_fee_per_gas).toNumber()) >> 16; + const input_valid_until = input_block_period * 30; + const input_gas_limit = tx_gas_limit.sub(storage_entry_deposit.div(tx_fee_per_gas).mul(input_storage_entry_limit)); + + const tx = context.provider.api.tx.evm.ethCall( + { Create: null }, + value.data, + value.value, + input_gas_limit.toNumber(), + input_storage_limit.toNumber(), + value.accessList, + input_valid_until + ); + + const sig = ethers.utils.joinSignature({ r: rawtx.r!, s: rawtx.s, v: rawtx.v }) + + tx.addSignature(subAddr, { Eip2930: sig } as any, { + blockHash: '0x', // ignored + era: "0x00", // mortal + genesisHash: '0x', // ignored + method: "Bytes", // don't know that is this + nonce: nonce, + specVersion: 0, // ignored + tip: tip, + transactionVersion: 0, // ignored + }); + + expect(tx.toString()).to.equal( + `{ + "signature": { + "signer": { + "id": "5EMjsczQH4R2WZaB5Svau8HWZp1aAfMqjxfv3GeLWotYSkLc" + }, + "signature": { + "eip2930": "${sig}" + }, + "era": { + "immortalEra": "0x00" + }, + "nonce": 0, + "tip": ${tip} + }, + "method": { + "callIndex": "0xb400", + "args": { + "action": { + "create": null + }, + "input": "${deploy.data}", + "value": 0, + "gas_limit": 2100000, + "storage_limit": 20032, + "access_list": [], + "valid_until": 120 + } + } + }`.toString().replace(/\s/g, '') + ); + + await new Promise(async (resolve) => { + tx.send((result) => { + if (result.status.isFinalized || result.status.isInBlock) { + resolve(undefined); + } + }); + }); + + let current_block_number = (await context.provider.api.query.system.number()).toNumber(); + let block_hash = await context.provider.api.rpc.chain.getBlockHash(current_block_number); + const result = await context.provider.api.derive.tx.events(block_hash); + // console.log("current_block_number: ", current_block_number, " event: ", result.events.toString()); + + let event = result.events.filter(item => context.provider.api.events.evm.Created.is(item.event)); + expect(event.length).to.equal(1); + // console.log(event[0].toString()) + + // get address + contract = event[0].event.data[1].toString(); + }); + + it("call should sign and verify", async function () { + this.timeout(150000); + + const chain_id = +context.provider.api.consts.evmAccounts.chainId.toString(); + const nonce = await getEvmNonce(context.provider, signer.address); + const validUntil = (await context.provider.api.rpc.chain.getHeader()).number.toNumber() + 100; + const storageLimit = 1000; + const gasLimit = 210000; + const priorityFee = BigNumber.from(2); + const tip = priorityFee.mul(gasLimit).toNumber(); + + const block_period = bigNumDiv(BigNumber.from(validUntil), BigNumber.from(30)); + const storage_entry_limit = bigNumDiv(BigNumber.from(storageLimit), BigNumber.from(64)); + const storage_byte_deposit = BigNumber.from(context.provider.api.consts.evm.storageDepositPerByte.toString()); + const storage_entry_deposit = storage_byte_deposit.mul(64); + const tx_fee_per_gas = BigNumber.from(context.provider.api.consts.evm.txFeePerGas.toString()); + const tx_gas_price = tx_fee_per_gas.add(block_period.toNumber() << 16).add(storage_entry_limit); + // There is a loss of precision here, so the order of calculation must be guaranteed + // must ensure storage_deposit / tx_fee_per_gas * storage_limit + const tx_gas_limit = storage_entry_deposit.div(tx_fee_per_gas).mul(storage_entry_limit).add(gasLimit); + + const receiver = '0x1111222233334444555566667777888899990000'; + const input = await factory.attach(contract).populateTransaction.transfer(receiver, 100); + + const value = { + type: 1, // EIP-2930 + to: contract, + nonce: nonce, + gasPrice: tx_gas_price.toHexString(), + gasLimit: tx_gas_limit.toNumber(), + data: input.data, + value: 0, + chainId: chain_id, + accessList: [], + } + + const signedTx = await signer.signTransaction(value) + const rawtx = ethers.utils.parseTransaction(signedTx) + + expect(rawtx).to.deep.include({ + type: 1, + chainId: 595, + nonce: 1, + gasPrice: BigNumber.from(200000208912), + gasLimit: BigNumber.from(722000), + to: ethers.utils.getAddress(contract), + value: BigNumber.from(0), + data: input.data, + accessList: [], + // v: 1226, + // r: '0xff8ff25480f5e1d1b38603b8fa1f10d64faf81707768dd9016fc4dd86d5474d2', + // s: '0x6c2cfd5acd5b0b820e1c107efd5e7ce2c452b81742091f43f5c793a835c8644f', + from: '0x14791697260E4c9A71f18484C9f997B308e59325', + // hash: '0x456d37c868520b362bbf5baf1b19752818eba49cc92c1a512e2e80d1ccfbc18b', + }); + + // tx data to user input + const input_storage_entry_limit = tx_gas_price.and(0xffff); + const input_storage_limit = input_storage_entry_limit.mul(64); + const input_block_period = (tx_gas_price.sub(input_storage_entry_limit).sub(tx_fee_per_gas).toNumber()) >> 16; + const input_valid_until = input_block_period * 30; + const input_gas_limit = tx_gas_limit.sub(storage_entry_deposit.div(tx_fee_per_gas).mul(input_storage_entry_limit)); + + const tx = context.provider.api.tx.evm.ethCall( + { Call: value.to }, + value.data, + value.value, + input_gas_limit.toNumber(), + input_storage_limit.toNumber(), + value.accessList, + input_valid_until + ); + + const sig = ethers.utils.joinSignature({ r: rawtx.r!, s: rawtx.s, v: rawtx.v }) + + tx.addSignature(subAddr, { Eip2930: sig } as any, { + blockHash: '0x', // ignored + era: "0x00", // mortal + genesisHash: '0x', // ignored + method: "Bytes", // don't know that is this + nonce: nonce, + specVersion: 0, // ignored + tip: tip, + transactionVersion: 0, // ignored + }); + + expect(tx.toString()).to.equal( + `{ + "signature": { + "signer": { + "id": "5EMjsczQH4R2WZaB5Svau8HWZp1aAfMqjxfv3GeLWotYSkLc" + }, + "signature": { + "eip2930": "${sig}" + }, + "era": { + "immortalEra": "0x00" + }, + "nonce": 1, + "tip": ${tip} + }, + "method": { + "callIndex": "0xb400", + "args": { + "action": { + "call": "${contract}" + }, + "input": "${input.data}", + "value": 0, + "gas_limit": 210000, + "storage_limit": 1024, + "access_list": [], + "valid_until": 120 + } + } + }`.toString().replace(/\s/g, '') + ); + + await new Promise(async (resolve) => { + tx.send((result) => { + if (result.status.isFinalized || result.status.isInBlock) { + resolve(undefined); + } + }); + }); + + await new Promise(async (resolve) => { + context.provider.api.tx.sudo.sudo(context.provider.api.tx.evm.publishFree(contract)).signAndSend(alice.substrateAddress, ((result) => { + if (result.status.isFinalized || result.status.isInBlock) { + resolve(undefined); + } + })); + }); + + const erc20 = new ethers.Contract(contract, Erc20DemoContract.abi, alice); + expect((await erc20.balanceOf(signer.address)).toString()).to.equal("99900"); + expect((await erc20.balanceOf(receiver)).toString()).to.equal("100"); + }); +});