From 5288ee076ae5f5bcfdc714fc3eb36f618260a7af Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Fri, 7 Feb 2025 08:51:20 +0800 Subject: [PATCH 1/2] check tip in eth_call --- runtime/acala/src/lib.rs | 7 +- runtime/karura/src/lib.rs | 7 +- runtime/mandala/src/lib.rs | 7 +- ts-tests/tests/test-sign-eth.ts | 463 ++++++++++++++++++++++++++++++++ 4 files changed, 481 insertions(+), 3 deletions(-) diff --git a/runtime/acala/src/lib.rs b/runtime/acala/src/lib.rs index 589ebed22..15522ddd5 100644 --- a/runtime/acala/src/lib.rs +++ b/runtime/acala/src/lib.rs @@ -38,7 +38,7 @@ use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, traits::{ AccountIdConversion, AccountIdLookup, BadOrigin, BlakeTwo256, Block as BlockT, Bounded, Convert, - IdentityLookup, SaturatedConversion, StaticLookup, + IdentityLookup, SaturatedConversion, StaticLookup, Zero, }, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, ArithmeticError, DispatchResult, FixedPointNumber, Perbill, Percent, Permill, Perquintill, @@ -2567,6 +2567,11 @@ impl Convert<(RuntimeCall, SignedExtra), Result<(EthereumTransactionMessage, Sig let nonce = check_nonce.nonce; let tip = charge.0; + // ensure tip is zero to prevent miner attack + if !tip.is_zero() { + return Err(InvalidTransaction::BadProof); + } + extra.5.mark_as_ethereum_tx(valid_until); Ok(( diff --git a/runtime/karura/src/lib.rs b/runtime/karura/src/lib.rs index e7fe7b8f9..e6a101d6a 100644 --- a/runtime/karura/src/lib.rs +++ b/runtime/karura/src/lib.rs @@ -39,7 +39,7 @@ use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, traits::{ AccountIdConversion, AccountIdLookup, BadOrigin, BlakeTwo256, Block as BlockT, Bounded, Convert, - IdentityLookup, SaturatedConversion, StaticLookup, + IdentityLookup, SaturatedConversion, StaticLookup, Zero, }, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, ArithmeticError, DispatchResult, FixedPointNumber, Perbill, Percent, Permill, Perquintill, @@ -2549,6 +2549,11 @@ impl Convert<(RuntimeCall, SignedExtra), Result<(EthereumTransactionMessage, Sig let nonce = check_nonce.nonce; let tip = charge.0; + // ensure tip is zero to prevent miner attack + if !tip.is_zero() { + return Err(InvalidTransaction::BadProof); + } + extra.5.mark_as_ethereum_tx(valid_until); Ok(( diff --git a/runtime/mandala/src/lib.rs b/runtime/mandala/src/lib.rs index 3080760a6..a7e382a2e 100644 --- a/runtime/mandala/src/lib.rs +++ b/runtime/mandala/src/lib.rs @@ -77,7 +77,7 @@ use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, traits::{ AccountIdConversion, BadOrigin, BlakeTwo256, Block as BlockT, Bounded, Convert, IdentityLookup, - SaturatedConversion, StaticLookup, + SaturatedConversion, StaticLookup, Zero, }, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, ArithmeticError, DispatchResult, FixedPointNumber, RuntimeDebug, @@ -1931,6 +1931,11 @@ impl Convert<(RuntimeCall, SignedExtra), Result<(EthereumTransactionMessage, Sig let nonce = check_nonce.nonce; let tip = charge.0; + // ensure tip is zero to prevent miner attack + if !tip.is_zero() { + return Err(InvalidTransaction::BadProof); + } + extra.5.mark_as_ethereum_tx(valid_until); Ok(( diff --git a/ts-tests/tests/test-sign-eth.ts b/ts-tests/tests/test-sign-eth.ts index ecccb621f..30cddb29c 100644 --- a/ts-tests/tests/test-sign-eth.ts +++ b/ts-tests/tests/test-sign-eth.ts @@ -316,3 +316,466 @@ describeWithAcala("Acala RPC (Sign eth)", (context) => { expect((await erc20.balanceOf(receiver)).toString()).to.equal("100"); }); }); + +describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { + let alice: BodhiSigner; + let signer: Wallet; + let subAddr: string; + let factory: ContractFactory; + let contract: string; + + beforeAll(async function () { + [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 () { + const chainId = +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 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 = { + // to: "0x0000000000000000000000000000000000000000", + nonce: nonce, + gasLimit: tx_gas_limit.toNumber(), + gasPrice: tx_gas_price.toHexString(), + data: deploy.data, + value: 0, + chainId: chainId, + } + + const signedTx = await signer.signTransaction(value) + const rawtx = ethers.utils.parseTransaction(signedTx) + + expect(rawtx).to.deep.include({ + nonce: 0, + // gasPrice: BigNumber.from(200000209209), + // gasLimit: BigNumber.from(12116000), + // to: '0x0000000000000000000000000000000000000000', + // value: BigNumber.from(0), + data: deploy.data, + chainId: 595, + // v: 1226, + // r: '0xff8ff25480f5e1d1b38603b8fa1f10d64faf81707768dd9016fc4dd86d5474d2', + // s: '0x6c2cfd5acd5b0b820e1c107efd5e7ce2c452b81742091f43f5c793a835c8644f', + from: '0x14791697260E4c9A71f18484C9f997B308e59325', + // hash: '0x456d37c868520b362bbf5baf1b19752818eba49cc92c1a512e2e80d1ccfbc18b', + type: null + }); + + expect(rawtx.gasPrice?.toNumber()).to.eq(200000209209); + expect(rawtx.gasLimit?.toNumber()).to.eq(12116000); + expect(rawtx.value?.toNumber()).to.eq(0); + + // 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 as any, + value.value, + input_gas_limit.toNumber(), + input_storage_limit.toNumber(), + [], // accessList + input_valid_until + ); + + const sig = ethers.utils.joinSignature({ r: rawtx.r!, s: rawtx.s, v: rawtx.v }) + + tx.addSignature(subAddr, { Ethereum: 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: 0, + transactionVersion: 0, // ignored + }); + + expect(tx.toString()).to.equal( + `{ + "signature": { + "signer": { + "id": "5EMjsczQH4R2WZaB5Svau8HWZp1aAfMqjxfv3GeLWotYSkLc" + }, + "signature": { + "ethereum": "${sig}" + }, + "era": { + "immortalEra": "0x00" + }, + "nonce": 0, + "tip": 0 + }, + "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("create should fail", async function () { + const chainId = +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 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 = { + // to: "0x0000000000000000000000000000000000000000", + nonce: nonce, + gasLimit: tx_gas_limit.toNumber(), + gasPrice: tx_gas_price.toHexString(), + data: deploy.data, + value: 0, + chainId: chainId, + } + + const signedTx = await signer.signTransaction(value) + const rawtx = ethers.utils.parseTransaction(signedTx) + + expect(rawtx).to.deep.include({ + nonce: 1, + // gasPrice: BigNumber.from(200000209209), + // gasLimit: BigNumber.from(12116000), + // to: '0x0000000000000000000000000000000000000000', + // value: BigNumber.from(0), + data: deploy.data, + chainId: 595, + // v: 1226, + // r: '0xff8ff25480f5e1d1b38603b8fa1f10d64faf81707768dd9016fc4dd86d5474d2', + // s: '0x6c2cfd5acd5b0b820e1c107efd5e7ce2c452b81742091f43f5c793a835c8644f', + from: '0x14791697260E4c9A71f18484C9f997B308e59325', + // hash: '0x456d37c868520b362bbf5baf1b19752818eba49cc92c1a512e2e80d1ccfbc18b', + type: null + }); + + expect(rawtx.gasPrice?.toNumber()).to.eq(200000209209); + expect(rawtx.gasLimit?.toNumber()).to.eq(12116000); + expect(rawtx.value?.toNumber()).to.eq(0); + + // 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 as any, + value.value, + input_gas_limit.toNumber(), + input_storage_limit.toNumber(), + [], // accessList + input_valid_until + ); + + const sig = ethers.utils.joinSignature({ r: rawtx.r!, s: rawtx.s, v: rawtx.v }) + + tx.addSignature(subAddr, { Ethereum: 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: 1, // verify tip must be zero + transactionVersion: 0, // ignored + }); + + expect(tx.toString()).to.equal( + `{ + "signature": { + "signer": { + "id": "5EMjsczQH4R2WZaB5Svau8HWZp1aAfMqjxfv3GeLWotYSkLc" + }, + "signature": { + "ethereum": "${sig}" + }, + "era": { + "immortalEra": "0x00" + }, + "nonce": 1, + "tip": 1 + }, + "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, '') + ); + + try { + await new Promise((resolve, reject) => { + tx.send((result) => { + // console.log('Status:', result.status.type); + + if (result.status.isInvalid) { + console.log('Invalid transaction detected'); + const error = result.toHuman(); + reject(new Error(`Invalid transaction: ${JSON.stringify(error)}`)); + return; + } + + if (result.status.isFinalized || result.status.isInBlock) { + console.log('Transaction finalized/inBlock'); + resolve(undefined); + return; + } + + // console.log('Other status:', result.status.type); + }).catch(error => { + // console.log('Send error:', error); + reject(error); + }); + }); + } catch (error: any) { + console.log('Caught error:', error); + expect(error.toString()).to.contain('RpcError: 1010: {"invalid":{"badProof":null}}'); + } + }); + + it("call should fail", async function () { + const chainId = +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 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 = { + to: contract, + nonce: nonce, + gasLimit: tx_gas_limit.toNumber(), + gasPrice: tx_gas_price.toHexString(), + data: input.data, + value: 0, + chainId: chainId, + } + + const signedTx = await signer.signTransaction(value) + const rawtx = ethers.utils.parseTransaction(signedTx) + + expect(rawtx).to.deep.include({ + nonce: 1, + // gasPrice: BigNumber.from(200000208912), + // gasLimit: BigNumber.from(722000), + to: ethers.utils.getAddress(contract), + // value: BigNumber.from(0), + data: input.data, + chainId: 595, + // v: 1225, + // r: '0xf84345a6459785986a1b2df711fe02597d70c1393757a243f8f924ea541d2ecb', + // s: '0x51476de1aa437cd820d59e1d9836e37e643fec711fe419464e637cab59291875', + from: '0x14791697260E4c9A71f18484C9f997B308e59325', + // hash: '0x67274cd0347795d0e2986021a19b1347948a0a93e1fb31a315048320fbfcae8a', + type: null + }); + expect(rawtx.gasPrice?.toNumber()).to.eq(200000208912); + expect(rawtx.gasLimit?.toNumber()).to.eq(722000); + expect(rawtx.value?.toNumber()).to.eq(0); + + // 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 as any, + value.value, + input_gas_limit.toNumber(), + input_storage_limit.toNumber(), + [], // accessList + input_valid_until + ); + + const sig = ethers.utils.joinSignature({ r: rawtx.r!, s: rawtx.s, v: rawtx.v }) + + tx.addSignature(subAddr, { Ethereum: 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: 1, // verify tip must be zero + transactionVersion: 0, // ignored + }); + + expect(tx.toString()).to.equal( + `{ + "signature": { + "signer": { + "id": "5EMjsczQH4R2WZaB5Svau8HWZp1aAfMqjxfv3GeLWotYSkLc" + }, + "signature": { + "ethereum": "${sig}" + }, + "era": { + "immortalEra": "0x00" + }, + "nonce": 1, + "tip": 1 + }, + "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, '') + ); + + try { + await new Promise((resolve, reject) => { + tx.send((result) => { + // console.log('Status:', result.status.type); + + if (result.status.isInvalid) { + console.log('Invalid transaction detected'); + const error = result.toHuman(); + reject(new Error(`Invalid transaction: ${JSON.stringify(error)}`)); + return; + } + + if (result.status.isFinalized || result.status.isInBlock) { + console.log('Transaction finalized/inBlock'); + resolve(undefined); + return; + } + + // console.log('Other status:', result.status.type); + }).catch(error => { + // console.log('Send error:', error); + reject(error); + }); + }); + } catch (error: any) { + console.log('Caught error:', error); + expect(error.toString()).to.contain('RpcError: 1010: {"invalid":{"badProof":null}}'); + } + }); +}); From b8363bbb20842e96cfba5645204e022cf0d30f99 Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Thu, 13 Feb 2025 21:58:29 +0800 Subject: [PATCH 2/2] fix --- primitives/src/unchecked_extrinsic.rs | 13 + runtime/acala/src/lib.rs | 7 +- runtime/karura/src/lib.rs | 7 +- runtime/mandala/src/lib.rs | 7 +- ts-tests/tests/test-sign-eip2930.ts | 595 +++++++++++++++++++++++++- ts-tests/tests/test-sign-eth.ts | 433 ++++++++++++------- 6 files changed, 877 insertions(+), 185 deletions(-) diff --git a/primitives/src/unchecked_extrinsic.rs b/primitives/src/unchecked_extrinsic.rs index a97a87fbe..497978974 100644 --- a/primitives/src/unchecked_extrinsic.rs +++ b/primitives/src/unchecked_extrinsic.rs @@ -17,6 +17,7 @@ // along with this program. If not, see . use crate::{evm::EthereumTransactionMessage, signature::AcalaMultiSignature, to_bytes, Address, Balance}; +use frame_support::ensure; use frame_support::{ dispatch::{DispatchInfo, GetDispatchInfo}, traits::{ExtrinsicCall, Get}, @@ -106,6 +107,12 @@ where target: "evm", "Ethereum eth_msg: {:?}", eth_msg ); + // module_evm::Call::eth_call, ensure tip is zero to prevent miner attacks + // module_evm::Call::eth_call_v2, tip encoded in gas_price + if eth_msg.gas_price.is_zero() { + ensure!(eth_msg.tip.is_zero(), InvalidTransaction::BadProof); + } + if !eth_msg.access_list.len().is_zero() { // Not yet supported, require empty return Err(InvalidTransaction::BadProof.into()); @@ -154,6 +161,12 @@ where target: "evm", "Eip2930 eth_msg: {:?}", eth_msg ); + // module_evm::Call::eth_call, ensure tip is zero to prevent miner attacks + // module_evm::Call::eth_call_v2, tip encoded in gas_price + if eth_msg.gas_price.is_zero() { + ensure!(eth_msg.tip.is_zero(), InvalidTransaction::BadProof); + } + 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)? diff --git a/runtime/acala/src/lib.rs b/runtime/acala/src/lib.rs index 15522ddd5..589ebed22 100644 --- a/runtime/acala/src/lib.rs +++ b/runtime/acala/src/lib.rs @@ -38,7 +38,7 @@ use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, traits::{ AccountIdConversion, AccountIdLookup, BadOrigin, BlakeTwo256, Block as BlockT, Bounded, Convert, - IdentityLookup, SaturatedConversion, StaticLookup, Zero, + IdentityLookup, SaturatedConversion, StaticLookup, }, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, ArithmeticError, DispatchResult, FixedPointNumber, Perbill, Percent, Permill, Perquintill, @@ -2567,11 +2567,6 @@ impl Convert<(RuntimeCall, SignedExtra), Result<(EthereumTransactionMessage, Sig let nonce = check_nonce.nonce; let tip = charge.0; - // ensure tip is zero to prevent miner attack - if !tip.is_zero() { - return Err(InvalidTransaction::BadProof); - } - extra.5.mark_as_ethereum_tx(valid_until); Ok(( diff --git a/runtime/karura/src/lib.rs b/runtime/karura/src/lib.rs index e6a101d6a..e7fe7b8f9 100644 --- a/runtime/karura/src/lib.rs +++ b/runtime/karura/src/lib.rs @@ -39,7 +39,7 @@ use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, traits::{ AccountIdConversion, AccountIdLookup, BadOrigin, BlakeTwo256, Block as BlockT, Bounded, Convert, - IdentityLookup, SaturatedConversion, StaticLookup, Zero, + IdentityLookup, SaturatedConversion, StaticLookup, }, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, ArithmeticError, DispatchResult, FixedPointNumber, Perbill, Percent, Permill, Perquintill, @@ -2549,11 +2549,6 @@ impl Convert<(RuntimeCall, SignedExtra), Result<(EthereumTransactionMessage, Sig let nonce = check_nonce.nonce; let tip = charge.0; - // ensure tip is zero to prevent miner attack - if !tip.is_zero() { - return Err(InvalidTransaction::BadProof); - } - extra.5.mark_as_ethereum_tx(valid_until); Ok(( diff --git a/runtime/mandala/src/lib.rs b/runtime/mandala/src/lib.rs index a7e382a2e..3080760a6 100644 --- a/runtime/mandala/src/lib.rs +++ b/runtime/mandala/src/lib.rs @@ -77,7 +77,7 @@ use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, traits::{ AccountIdConversion, BadOrigin, BlakeTwo256, Block as BlockT, Bounded, Convert, IdentityLookup, - SaturatedConversion, StaticLookup, Zero, + SaturatedConversion, StaticLookup, }, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, ArithmeticError, DispatchResult, FixedPointNumber, RuntimeDebug, @@ -1931,11 +1931,6 @@ impl Convert<(RuntimeCall, SignedExtra), Result<(EthereumTransactionMessage, Sig let nonce = check_nonce.nonce; let tip = charge.0; - // ensure tip is zero to prevent miner attack - if !tip.is_zero() { - return Err(InvalidTransaction::BadProof); - } - extra.5.mark_as_ethereum_tx(valid_until); Ok(( diff --git a/ts-tests/tests/test-sign-eip2930.ts b/ts-tests/tests/test-sign-eip2930.ts index 2c38af52b..536644c35 100644 --- a/ts-tests/tests/test-sign-eip2930.ts +++ b/ts-tests/tests/test-sign-eip2930.ts @@ -8,7 +8,12 @@ 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) => { +// const GAS_MASK = 100000; +const STORAGE_MASK = 100; +const GAS_LIMIT_CHUNK = BigNumber.from(30000); +const TEN_GWEI = BigNumber.from(10000000000); + +describeWithAcala("Acala RPC (Sign eip2930 with ethCall)", (context) => { let alice: BodhiSigner; let signer: Wallet; let subAddr: string; @@ -50,8 +55,6 @@ describeWithAcala("Acala RPC (Sign eip2930)", (context) => { 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)); @@ -123,7 +126,7 @@ describeWithAcala("Acala RPC (Sign eip2930)", (context) => { method: "Bytes", // don't know that is this nonce: nonce, specVersion: 0, // ignored - tip: tip, + tip: 0, transactionVersion: 0, // ignored }); @@ -140,7 +143,7 @@ describeWithAcala("Acala RPC (Sign eip2930)", (context) => { "immortalEra": "0x00" }, "nonce": 0, - "tip": ${tip} + "tip": 0 }, "method": { "callIndex": "0xb400", @@ -186,8 +189,6 @@ describeWithAcala("Acala RPC (Sign eip2930)", (context) => { 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)); @@ -260,7 +261,7 @@ describeWithAcala("Acala RPC (Sign eip2930)", (context) => { method: "Bytes", // don't know that is this nonce: nonce, specVersion: 0, // ignored - tip: tip, + tip: 0, transactionVersion: 0, // ignored }); @@ -277,7 +278,285 @@ describeWithAcala("Acala RPC (Sign eip2930)", (context) => { "immortalEra": "0x00" }, "nonce": 1, - "tip": ${tip} + "tip": 0 + }, + "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"); + }); + + it("create should fail", async function () { + 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 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: 2, + 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 as any, + 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: 1, // verify tip must be zero + transactionVersion: 0, // ignored + }); + + expect(tx.toString()).to.equal( + `{ + "signature": { + "signer": { + "id": "5EMjsczQH4R2WZaB5Svau8HWZp1aAfMqjxfv3GeLWotYSkLc" + }, + "signature": { + "eip2930": "${sig}" + }, + "era": { + "immortalEra": "0x00" + }, + "nonce": 2, + "tip": 1 + }, + "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, '') + ); + + try { + await new Promise((resolve, reject) => { + tx.send((result) => { + // console.log('Status:', result.status.type); + + if (result.status.isInvalid) { + console.log('Invalid transaction detected'); + const error = result.toHuman(); + reject(new Error(`Invalid transaction: ${JSON.stringify(error)}`)); + return; + } + + if (result.status.isFinalized || result.status.isInBlock) { + console.log('Transaction finalized/inBlock'); + resolve(undefined); + return; + } + + // console.log('Other status:', result.status.type); + }).catch(error => { + // console.log('Send error:', error); + reject(error); + }); + }); + } catch (error: any) { + console.log('Caught error:', error); + expect(error.toString()).to.contain('RpcError: 1010: {"invalid":{"badProof":null}}'); + } + }); + + it("call should fail", async function () { + 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 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: 2, + 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 as any, + 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: 1, // verify tip must be zero + transactionVersion: 0, // ignored + }); + + expect(tx.toString()).to.equal( + `{ + "signature": { + "signer": { + "id": "5EMjsczQH4R2WZaB5Svau8HWZp1aAfMqjxfv3GeLWotYSkLc" + }, + "signature": { + "eip2930": "${sig}" + }, + "era": { + "immortalEra": "0x00" + }, + "nonce": 2, + "tip": 1 }, "method": { "callIndex": "0xb400", @@ -296,6 +575,304 @@ describeWithAcala("Acala RPC (Sign eip2930)", (context) => { }`.toString().replace(/\s/g, '') ); + try { + await new Promise((resolve, reject) => { + tx.send((result) => { + // console.log('Status:', result.status.type); + + if (result.status.isInvalid) { + console.log('Invalid transaction detected'); + const error = result.toHuman(); + reject(new Error(`Invalid transaction: ${JSON.stringify(error)}`)); + return; + } + + if (result.status.isFinalized || result.status.isInBlock) { + console.log('Transaction finalized/inBlock'); + resolve(undefined); + return; + } + + // console.log('Other status:', result.status.type); + }).catch(error => { + // console.log('Send error:', error); + reject(error); + }); + }); + } catch (error: any) { + console.log('Caught error:', error); + expect(error.toString()).to.contain('RpcError: 1010: {"invalid":{"badProof":null}}'); + } + }); +}); + +describeWithAcala("Acala RPC (Sign eth with ethCallV2)", (context) => { + let alice: BodhiSigner; + let signer: Wallet; + let subAddr: string; + let factory: ContractFactory; + let contract: string; + + beforeAll(async function () { + [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); + }); + + it("create should sign and verify", async function () { + const chainId = +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 = BigNumber.from('2100000'); + + // 10% + const tipNumber = BigNumber.from('1'); + // 100 Gwei + const txFeePerGas = BigNumber.from('110000000000'); + const txGasPrice = txFeePerGas.add(validUntil); + const encodedGasLimit = gasLimit.div(GAS_LIMIT_CHUNK).add(1); + const encodedStorageLimit = Math.ceil(Math.log2(storageLimit)); + // tx fee = 100_00000 + const txGasLimit = BigNumber.from('10000000').add(encodedGasLimit.mul(STORAGE_MASK)).add(encodedStorageLimit); + const tip = txGasPrice.sub(tipNumber.mul(TEN_GWEI)).mul(txGasLimit).mul(tipNumber).div(10).div(1000000); + + const deploy = factory.getDeployTransaction(100000); + + const value = { + type: 1, // EIP-2930 + // to: "0x0000000000000000000000000000000000000000", + nonce: nonce, + gasLimit: txGasLimit.toNumber(), + gasPrice: txGasPrice.toHexString(), + data: deploy.data, + value: 0, + chainId: chainId, + 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', + }); + expect(rawtx.gasPrice?.toNumber()).toMatchInlineSnapshot(`110000000106`) + expect(rawtx.gasLimit?.toNumber()).toMatchInlineSnapshot(`10007115`) + + const tx = context.provider.api.tx.evm.ethCallV2( + { Create: null }, + value.data as any, + value.value, + txGasPrice.toNumber(), + txGasLimit.toNumber(), + [], // accessList + ); + + 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.toString(), + transactionVersion: 0, // ignored + }); + + expect(tx.toString()).to.equal( + `{ + "signature": { + "signer": { + "id": "5EMjsczQH4R2WZaB5Svau8HWZp1aAfMqjxfv3GeLWotYSkLc" + }, + "signature": { + "eip2930": "${sig}" + }, + "era": { + "immortalEra": "0x00" + }, + "nonce": 0, + "tip": ${tip} + }, + "method": { + "callIndex": "0xb40f", + "args": { + "action": { + "create": null + }, + "input": "${deploy.data}", + "value": 0, + "gas_price": ${rawtx.gasPrice}, + "gas_limit": ${rawtx.gasLimit}, + "access_list": [] + } + } + }`.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 () { + const chainId = +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 = BigNumber.from('210000'); + + // 10% + const tipNumber = BigNumber.from('1'); + // 100 Gwei + const txFeePerGas = BigNumber.from('110000000000'); + const txGasPrice = txFeePerGas.add(validUntil); + const encodedGasLimit = gasLimit.div(GAS_LIMIT_CHUNK).add(1); + const encodedStorageLimit = Math.ceil(Math.log2(storageLimit)); + // tx fee = 100_00000 + const txGasLimit = BigNumber.from('10000000').add(encodedGasLimit.mul(STORAGE_MASK)).add(encodedStorageLimit); + const tip = txGasPrice.sub(tipNumber.mul(TEN_GWEI)).mul(txGasLimit).mul(tipNumber).div(10).div(1000000); + + const receiver = '0x1111222233334444555566667777888899990000'; + const input = await factory.attach(contract).populateTransaction.transfer(receiver, 100); + + const value = { + type: 1, // EIP-2930 + to: contract, + nonce: nonce, + gasLimit: txGasLimit.toNumber(), + gasPrice: txGasPrice.toHexString(), + data: input.data, + value: 0, + chainId: chainId, + 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', + }); + expect(rawtx.gasPrice?.toNumber()).toMatchInlineSnapshot(`110000000107`) + expect(rawtx.gasLimit?.toNumber()).toMatchInlineSnapshot(`10000810`) + + const tx = context.provider.api.tx.evm.ethCallV2( + { Call: value.to }, + value.data as any, + value.value, + txGasPrice.toNumber(), + txGasLimit.toNumber(), + [], // accessList + ); + + 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.toString(), + transactionVersion: 0, // ignored + }); + + expect(tx.toString()).to.equal( + `{ + "signature": { + "signer": { + "id": "5EMjsczQH4R2WZaB5Svau8HWZp1aAfMqjxfv3GeLWotYSkLc" + }, + "signature": { + "eip2930": "${sig}" + }, + "era": { + "immortalEra": "0x00" + }, + "nonce": 1, + "tip": ${tip} + }, + "method": { + "callIndex": "0xb40f", + "args": { + "action": { + "call": "${contract}" + }, + "input": "${input.data}", + "value": 0, + "gas_price": ${rawtx.gasPrice}, + "gas_limit": ${rawtx.gasLimit}, + "access_list": [] + } + } + }`.toString().replace(/\s/g, '') + ); + await new Promise(async (resolve) => { tx.send((result) => { if (result.status.isFinalized || result.status.isInBlock) { diff --git a/ts-tests/tests/test-sign-eth.ts b/ts-tests/tests/test-sign-eth.ts index 30cddb29c..7bf5b69c5 100644 --- a/ts-tests/tests/test-sign-eth.ts +++ b/ts-tests/tests/test-sign-eth.ts @@ -8,7 +8,12 @@ import { hexToU8a, u8aConcat, stringToU8a } from "@polkadot/util"; import { ethers, BigNumber, ContractFactory } from "ethers"; import Erc20DemoContract from "../build/Erc20DemoContract.json" -describeWithAcala("Acala RPC (Sign eth)", (context) => { +// const GAS_MASK = 100000; +const STORAGE_MASK = 100; +const GAS_LIMIT_CHUNK = BigNumber.from(30000); +const TEN_GWEI = BigNumber.from(10000000000); + +describeWithAcala("Acala RPC (Sign eth with ethCall)", (context) => { let alice: BodhiSigner; let signer: Wallet; let subAddr: string; @@ -315,45 +320,8 @@ describeWithAcala("Acala RPC (Sign eth)", (context) => { expect((await erc20.balanceOf(signer.address)).toString()).to.equal("99900"); expect((await erc20.balanceOf(receiver)).toString()).to.equal("100"); }); -}); - -describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { - let alice: BodhiSigner; - let signer: Wallet; - let subAddr: string; - let factory: ContractFactory; - let contract: string; - - beforeAll(async function () { - [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 () { + it("create should fail", async function () { const chainId = +context.provider.api.consts.evmAccounts.chainId.toString() const nonce = await getEvmNonce(context.provider, signer.address); @@ -387,7 +355,7 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { const rawtx = ethers.utils.parseTransaction(signedTx) expect(rawtx).to.deep.include({ - nonce: 0, + nonce: 2, // gasPrice: BigNumber.from(200000209209), // gasLimit: BigNumber.from(12116000), // to: '0x0000000000000000000000000000000000000000', @@ -432,7 +400,7 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { method: "Bytes", // don't know that is this nonce: nonce, specVersion: 0, // ignored - tip: 0, + tip: 1, // verify tip must be zero transactionVersion: 0, // ignored }); @@ -448,8 +416,8 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { "era": { "immortalEra": "0x00" }, - "nonce": 0, - "tip": 0 + "nonce": 2, + "tip": 1 }, "method": { "callIndex": "0xb400", @@ -468,34 +436,43 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { }`.toString().replace(/\s/g, '') ); - await new Promise(async (resolve) => { - tx.send((result) => { - if (result.status.isFinalized || result.status.isInBlock) { - resolve(undefined); - } - }); - }); + try { + await new Promise((resolve, reject) => { + tx.send((result) => { + // console.log('Status:', result.status.type); - 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()); + if (result.status.isInvalid) { + console.log('Invalid transaction detected'); + const error = result.toHuman(); + reject(new Error(`Invalid transaction: ${JSON.stringify(error)}`)); + return; + } - 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()) + if (result.status.isFinalized || result.status.isInBlock) { + console.log('Transaction finalized/inBlock'); + resolve(undefined); + return; + } - // get address - contract = event[0].event.data[1].toString(); + // console.log('Other status:', result.status.type); + }).catch(error => { + // console.log('Send error:', error); + reject(error); + }); + }); + } catch (error: any) { + console.log('Caught error:', error); + expect(error.toString()).to.contain('RpcError: 1010: {"invalid":{"badProof":null}}'); + } }); - it("create should fail", async function () { - const chainId = +context.provider.api.consts.evmAccounts.chainId.toString() + it("call should fail", async function () { + const chainId = +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 validUntil = (await context.provider.api.rpc.chain.getHeader()).number.toNumber() + 100; + const storageLimit = 1000; + const gasLimit = 210000; const block_period = bigNumDiv(BigNumber.from(validUntil), BigNumber.from(30)); const storage_entry_limit = bigNumDiv(BigNumber.from(storageLimit), BigNumber.from(64)); @@ -507,14 +484,15 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { // 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 receiver = '0x1111222233334444555566667777888899990000'; + const input = await factory.attach(contract).populateTransaction.transfer(receiver, 100); const value = { - // to: "0x0000000000000000000000000000000000000000", + to: contract, nonce: nonce, gasLimit: tx_gas_limit.toNumber(), gasPrice: tx_gas_price.toHexString(), - data: deploy.data, + data: input.data, value: 0, chainId: chainId, } @@ -523,23 +501,22 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { const rawtx = ethers.utils.parseTransaction(signedTx) expect(rawtx).to.deep.include({ - nonce: 1, - // gasPrice: BigNumber.from(200000209209), - // gasLimit: BigNumber.from(12116000), - // to: '0x0000000000000000000000000000000000000000', + nonce: 2, + // gasPrice: BigNumber.from(200000208912), + // gasLimit: BigNumber.from(722000), + to: ethers.utils.getAddress(contract), // value: BigNumber.from(0), - data: deploy.data, + data: input.data, chainId: 595, - // v: 1226, - // r: '0xff8ff25480f5e1d1b38603b8fa1f10d64faf81707768dd9016fc4dd86d5474d2', - // s: '0x6c2cfd5acd5b0b820e1c107efd5e7ce2c452b81742091f43f5c793a835c8644f', + // v: 1225, + // r: '0xf84345a6459785986a1b2df711fe02597d70c1393757a243f8f924ea541d2ecb', + // s: '0x51476de1aa437cd820d59e1d9836e37e643fec711fe419464e637cab59291875', from: '0x14791697260E4c9A71f18484C9f997B308e59325', - // hash: '0x456d37c868520b362bbf5baf1b19752818eba49cc92c1a512e2e80d1ccfbc18b', + // hash: '0x67274cd0347795d0e2986021a19b1347948a0a93e1fb31a315048320fbfcae8a', type: null }); - - expect(rawtx.gasPrice?.toNumber()).to.eq(200000209209); - expect(rawtx.gasLimit?.toNumber()).to.eq(12116000); + expect(rawtx.gasPrice?.toNumber()).to.eq(200000208912); + expect(rawtx.gasLimit?.toNumber()).to.eq(722000); expect(rawtx.value?.toNumber()).to.eq(0); // tx data to user input @@ -550,7 +527,7 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { 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 }, + { Call: value.to }, value.data as any, value.value, input_gas_limit.toNumber(), @@ -584,19 +561,19 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { "era": { "immortalEra": "0x00" }, - "nonce": 1, + "nonce": 2, "tip": 1 }, "method": { "callIndex": "0xb400", "args": { "action": { - "create": null + "call": "${contract}" }, - "input": "${deploy.data}", + "input": "${input.data}", "value": 0, - "gas_limit": 2100000, - "storage_limit": 20032, + "gas_limit": 210000, + "storage_limit": 1024, "access_list": [], "valid_until": 120 } @@ -633,24 +610,182 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { expect(error.toString()).to.contain('RpcError: 1010: {"invalid":{"badProof":null}}'); } }); +}); - it("call should fail", async function () { +describeWithAcala("Acala RPC (Sign eth with ethCallV2)", (context) => { + let alice: BodhiSigner; + let signer: Wallet; + let subAddr: string; + let factory: ContractFactory; + let contract: string; + + beforeAll(async function () { + [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); + }); + + it("create should sign and verify", async function () { + const chainId = +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 = BigNumber.from('2100000'); + + // 10% + const tipNumber = BigNumber.from('1'); + // 100 Gwei + const txFeePerGas = BigNumber.from('110000000000'); + const txGasPrice = txFeePerGas.add(validUntil); + const encodedGasLimit = gasLimit.div(GAS_LIMIT_CHUNK).add(1); + const encodedStorageLimit = Math.ceil(Math.log2(storageLimit)); + // tx fee = 100_00000 + const txGasLimit = BigNumber.from('10000000').add(encodedGasLimit.mul(STORAGE_MASK)).add(encodedStorageLimit); + const tip = txGasPrice.sub(tipNumber.mul(TEN_GWEI)).mul(txGasLimit).mul(tipNumber).div(10).div(1000000); + + const deploy = factory.getDeployTransaction(100000); + + const value = { + // to: "0x0000000000000000000000000000000000000000", + nonce: nonce, + gasLimit: txGasLimit.toNumber(), + gasPrice: txGasPrice.toHexString(), + data: deploy.data, + value: 0, + chainId: chainId, + } + + const signedTx = await signer.signTransaction(value) + const rawtx = ethers.utils.parseTransaction(signedTx) + + expect(rawtx).to.deep.include({ + nonce: 0, + // gasPrice: BigNumber.from(110000000105), + // gasLimit: BigNumber.from(10007115), + // to: '0x0000000000000000000000000000000000000000', + // value: BigNumber.from(0), + data: deploy.data, + chainId: 595, + // v: 1226, + // r: '0xff8ff25480f5e1d1b38603b8fa1f10d64faf81707768dd9016fc4dd86d5474d2', + // s: '0x6c2cfd5acd5b0b820e1c107efd5e7ce2c452b81742091f43f5c793a835c8644f', + from: '0x14791697260E4c9A71f18484C9f997B308e59325', + // hash: '0x456d37c868520b362bbf5baf1b19752818eba49cc92c1a512e2e80d1ccfbc18b', + type: null + }); + expect(rawtx.gasPrice?.toNumber()).toMatchInlineSnapshot(`110000000106`) + expect(rawtx.gasLimit?.toNumber()).toMatchInlineSnapshot(`10007115`) + expect(rawtx.value?.toNumber()).to.eq(0); + + const tx = context.provider.api.tx.evm.ethCallV2( + { Create: null }, + value.data as any, + value.value, + txGasPrice.toNumber(), + txGasLimit.toNumber(), + [], // accessList + ); + + const sig = ethers.utils.joinSignature({ r: rawtx.r!, s: rawtx.s, v: rawtx.v }) + + tx.addSignature(subAddr, { Ethereum: 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.toString(), + transactionVersion: 0, // ignored + }); + + expect(tx.toString()).to.equal( + `{ + "signature": { + "signer": { + "id": "5EMjsczQH4R2WZaB5Svau8HWZp1aAfMqjxfv3GeLWotYSkLc" + }, + "signature": { + "ethereum": "${sig}" + }, + "era": { + "immortalEra": "0x00" + }, + "nonce": 0, + "tip": ${tip} + }, + "method": { + "callIndex": "0xb40f", + "args": { + "action": { + "create": null + }, + "input": "${deploy.data}", + "value": 0, + "gas_price": ${rawtx.gasPrice}, + "gas_limit": ${rawtx.gasLimit}, + "access_list": [] + } + } + }`.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 () { const chainId = +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 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 gasLimit = BigNumber.from('210000'); + + // 10% + const tipNumber = BigNumber.from('1'); + // 100 Gwei + const txFeePerGas = BigNumber.from('110000000000'); + const txGasPrice = txFeePerGas.add(validUntil); + const encodedGasLimit = gasLimit.div(GAS_LIMIT_CHUNK).add(1); + const encodedStorageLimit = Math.ceil(Math.log2(storageLimit)); + // tx fee = 100_00000 + const txGasLimit = BigNumber.from('10000000').add(encodedGasLimit.mul(STORAGE_MASK)).add(encodedStorageLimit); + const tip = txGasPrice.sub(tipNumber.mul(TEN_GWEI)).mul(txGasLimit).mul(tipNumber).div(10).div(1000000); const receiver = '0x1111222233334444555566667777888899990000'; const input = await factory.attach(contract).populateTransaction.transfer(receiver, 100); @@ -658,8 +793,8 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { const value = { to: contract, nonce: nonce, - gasLimit: tx_gas_limit.toNumber(), - gasPrice: tx_gas_price.toHexString(), + gasLimit: txGasLimit.toNumber(), + gasPrice: txGasPrice.toHexString(), data: input.data, value: 0, chainId: chainId, @@ -670,8 +805,8 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { expect(rawtx).to.deep.include({ nonce: 1, - // gasPrice: BigNumber.from(200000208912), - // gasLimit: BigNumber.from(722000), + // gasPrice: BigNumber.from(110000000106), + // gasLimit: BigNumber.from(10000810), to: ethers.utils.getAddress(contract), // value: BigNumber.from(0), data: input.data, @@ -683,25 +818,17 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { // hash: '0x67274cd0347795d0e2986021a19b1347948a0a93e1fb31a315048320fbfcae8a', type: null }); - expect(rawtx.gasPrice?.toNumber()).to.eq(200000208912); - expect(rawtx.gasLimit?.toNumber()).to.eq(722000); + expect(rawtx.gasPrice?.toNumber()).toMatchInlineSnapshot(`110000000107`) + expect(rawtx.gasLimit?.toNumber()).toMatchInlineSnapshot(`10000810`) expect(rawtx.value?.toNumber()).to.eq(0); - // 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( + const tx = context.provider.api.tx.evm.ethCallV2( { Call: value.to }, value.data as any, value.value, - input_gas_limit.toNumber(), - input_storage_limit.toNumber(), + txGasPrice.toNumber(), + txGasLimit.toNumber(), [], // accessList - input_valid_until ); const sig = ethers.utils.joinSignature({ r: rawtx.r!, s: rawtx.s, v: rawtx.v }) @@ -713,69 +840,59 @@ describeWithAcala("Acala RPC (Sign eth with tip)", (context) => { method: "Bytes", // don't know that is this nonce: nonce, specVersion: 0, // ignored - tip: 1, // verify tip must be zero + tip: tip.toString(), transactionVersion: 0, // ignored }); expect(tx.toString()).to.equal( `{ "signature": { - "signer": { + "signer": { "id": "5EMjsczQH4R2WZaB5Svau8HWZp1aAfMqjxfv3GeLWotYSkLc" - }, - "signature": { + }, + "signature": { "ethereum": "${sig}" - }, - "era": { + }, + "era": { "immortalEra": "0x00" - }, - "nonce": 1, - "tip": 1 + }, + "nonce": 1, + "tip": ${tip} }, "method": { - "callIndex": "0xb400", - "args": { + "callIndex": "0xb40f", + "args": { "action": { - "call": "${contract}" + "call": "${contract}" }, "input": "${input.data}", "value": 0, - "gas_limit": 210000, - "storage_limit": 1024, - "access_list": [], - "valid_until": 120 - } + "gas_price": ${rawtx.gasPrice}, + "gas_limit": ${rawtx.gasLimit}, + "access_list": [] + } } - }`.toString().replace(/\s/g, '') + }`.toString().replace(/\s/g, '') ); - try { - await new Promise((resolve, reject) => { - tx.send((result) => { - // console.log('Status:', result.status.type); - - if (result.status.isInvalid) { - console.log('Invalid transaction detected'); - const error = result.toHuman(); - reject(new Error(`Invalid transaction: ${JSON.stringify(error)}`)); - return; - } + await new Promise(async (resolve) => { + tx.send((result) => { + if (result.status.isFinalized || result.status.isInBlock) { + resolve(undefined); + } + }); + }); - if (result.status.isFinalized || result.status.isInBlock) { - console.log('Transaction finalized/inBlock'); - resolve(undefined); - return; - } + 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); + } + })); + }); - // console.log('Other status:', result.status.type); - }).catch(error => { - // console.log('Send error:', error); - reject(error); - }); - }); - } catch (error: any) { - console.log('Caught error:', error); - expect(error.toString()).to.contain('RpcError: 1010: {"invalid":{"badProof":null}}'); - } + 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"); }); });