Skip to content

Commit

Permalink
support eip2930
Browse files Browse the repository at this point in the history
  • Loading branch information
zjb0807 committed Jan 17, 2024
1 parent 819b280 commit 57efc57
Show file tree
Hide file tree
Showing 3 changed files with 373 additions and 1 deletion.
2 changes: 2 additions & 0 deletions primitives/src/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ed25519::Signature> for AcalaMultiSignature {
Expand Down
48 changes: 47 additions & 1 deletion primitives/src/unchecked_extrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(&eth_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!(
Expand Down
324 changes: 324 additions & 0 deletions ts-tests/tests/test-sign-eip2930.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});

0 comments on commit 57efc57

Please sign in to comment.