Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(evm): preverify transaction and evict invalid transaction from pool #820

Merged
merged 15 commits into from
Jan 16, 2025
Merged
6 changes: 4 additions & 2 deletions packages/configuration-generator/bin/create-genesis-block.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ async function run() {
// },
});

for (const tag of ["evm", "ephemeral"]) {
await app.getTagged(AppIdentifiers.Evm.Instance, "instance", tag).dispose();
for (const tag of ["evm", "validator", "transaction-pool"]) {
if (app.isBoundTagged(AppIdentifiers.Evm.Instance, "instance", tag)) {
await app.getTagged(AppIdentifiers.Evm.Instance, "instance", tag).dispose();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ describe<{
mnemonicGenerator: MnemonicGenerator;
}>("GenesisBlockGenerator", ({ it, assert, afterEach, beforeEach }) => {
afterEach(async (context) => {
await context.app.getTagged<Contracts.Evm.Instance>(AppIdentifiers.Evm.Instance, "instance", "evm").dispose();
for (const tag of ["evm", "validator", "transaction-pool"]) {
await context.app.getTagged<Contracts.Evm.Instance>(AppIdentifiers.Evm.Instance, "instance", tag).dispose();
}
});

beforeEach(async (context) => {
Expand Down
22 changes: 22 additions & 0 deletions packages/contracts/source/contracts/evm/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface GenesisInfo {

export interface Instance extends CommitHandler {
prepareNextCommit(context: PrepareNextCommitContext): Promise<void>;
preverifyTransaction(txContext: PreverifyTransactionContext): Promise<PreverifyTransactionResult>;
process(txContext: TransactionContext): Promise<ProcessResult>;
view(viewContext: TransactionViewContext): Promise<ViewResult>;
initializeGenesis(commit: GenesisInfo): Promise<void>;
Expand Down Expand Up @@ -43,6 +44,12 @@ export interface ViewResult {
readonly output?: Buffer;
}

export interface PreverifyTransactionResult {
readonly success: boolean;
readonly initialGasUsed: bigint;
readonly error?: string;
}

export interface CommitResult {}

export interface AccountInfo {
Expand Down Expand Up @@ -80,6 +87,21 @@ export interface PrepareNextCommitContext {
readonly commitKey: CommitKey;
}

export interface PreverifyTransactionContext {
readonly caller: string;
/** Omit recipient when deploying a contract */
readonly recipient?: string;
readonly gasLimit: bigint;
readonly value: bigint;
readonly gasPrice?: bigint;
readonly nonce: bigint;
readonly data: Buffer;
readonly txHash: string;
readonly sequence?: number;
readonly specId: SpecId;
readonly blockGasLimit: bigint;
}

export interface TransactionContext {
readonly caller: string;
/** Omit recipient when deploying a contract */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface WorkerFlags extends KeyValuePair {}
export interface WorkerScriptHandler {
boot(flags: WorkerFlags): Promise<void>;
getTransactions(): Promise<string[]>;
removeTransaction(address: string, id: string): Promise<void>;
commit(height: number, sendersAddresses: string[]): Promise<void>;
setPeer(ip: string): Promise<void>;
forgetPeer(ip: string): Promise<void>;
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/source/contracts/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type TransactionHandlerContext = {
export interface TransactionHandler {
verify(transaction: Transaction): Promise<boolean>;

throwIfCannotBeApplied(transaction: Transaction, sender: Wallet): Promise<void>;
throwIfCannotBeApplied(transaction: Transaction, sender: Wallet, evm: Instance): Promise<void>;

apply(context: TransactionHandlerContext, transaction: Transaction): Promise<TransactionReceipt>;

Expand Down
9 changes: 9 additions & 0 deletions packages/contracts/source/exceptions/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ export class TransactionPoolFullError extends PoolError {
}
}

export class TransactionFailedToPreverifyError extends PoolError {
public readonly error: Error;

public constructor(transaction: Transaction, error: Error) {
super(`tx ${transaction.id} cannot be preverified: ${error.message}`, "ERR_PREVERIFY");
this.error = error;
}
}

export class TransactionFailedToApplyError extends PoolError {
public readonly error: Error;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class EvmCallTransactionHandler extends Handlers.TransactionHandler {

return receipt;
} catch (error) {
return this.app.terminate("invalid EVM call", error);
throw new Error(`invalid EVM call: ${error.message}`);
}
}

Expand Down
17 changes: 14 additions & 3 deletions packages/evm-service/source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,24 @@ export class ServiceProvider extends Providers.ServiceProvider {
.bind(Identifiers.Evm.Instance)
.to(EphemeralInstance)
.inRequestScope()
.when(Selectors.anyAncestorOrTargetTaggedFirst("instance", "ephemeral"));
.when(Selectors.anyAncestorOrTargetTaggedFirst("instance", "validator"));

this.app
.bind(Identifiers.Evm.Instance)
.to(EphemeralInstance)
.inRequestScope()
.when(Selectors.anyAncestorOrTargetTaggedFirst("instance", "transaction-pool"));
}

public async boot(): Promise<void> {}

public async dispose(): Promise<void> {
await this.app.getTagged<EvmInstance>(Identifiers.Evm.Instance, "instance", "ephemeral").dispose();
await this.app.getTagged<EvmInstance>(Identifiers.Evm.Instance, "instance", "evm").dispose();
for (const tag of ["evm", "validator", "transaction-pool"]) {
if (this.app.isBoundTagged(Identifiers.Evm.Instance, "instance", tag)) {
{
await this.app.getTagged<EvmInstance>(Identifiers.Evm.Instance, "instance", tag).dispose();
}
}
}
}
}
83 changes: 69 additions & 14 deletions packages/evm-service/source/instances/evm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,20 +544,20 @@ describe<{
it("should revert transaction if it exceeds gas limit", async ({ instance }) => {
const [sender] = wallets;

const commitKey = { height: BigInt(0), round: BigInt(0) };
const { receipt } = await instance.process({
caller: sender.address,
value: 0n,
nonce: 0n,
data: Buffer.from(MainsailERC20.bytecode.slice(2), "hex"),
blockContext: { ...blockContext, commitKey },
txHash: getRandomTxHash(),
gasLimit: 30_000n,
specId: Contracts.Evm.SpecId.SHANGHAI,
});

assert.false(receipt.success);
assert.equal(receipt.gasUsed, 30_000n);
await assert.rejects(
async () =>
instance.process({
caller: sender.address,
value: 0n,
nonce: 0n,
data: Buffer.from(MainsailERC20.bytecode.slice(2), "hex"),
blockContext: { ...blockContext, commitKey: { height: BigInt(0), round: BigInt(0) } },
txHash: getRandomTxHash(),
gasLimit: 30_000n,
specId: Contracts.Evm.SpecId.SHANGHAI,
}),
"transaction validation error: call gas cost exceeds the gas limit",
);
});

it("should reject invalid specId", async ({ instance }) => {
Expand Down Expand Up @@ -722,6 +722,61 @@ describe<{
const balance = ethers.toBigInt(slot);
assert.equal(balance, ethers.parseEther("100000000"));
});

it("should preverify transaction", async ({ instance }) => {
const [sender, recipient] = wallets;

const initialSupply = ethers.parseEther("100");

await instance.initializeGenesis({
account: sender.address,
initialSupply,
deployerAccount: ethers.ZeroAddress,
usernameContract: ethers.ZeroAddress,
validatorContract: ethers.ZeroAddress,
});

const ctx = {
nonce: 0n,
data: Buffer.alloc(0),
txHash: getRandomTxHash(),
blockContext: { ...blockContext, commitKey: { height: BigInt(0), round: BigInt(0) } },
specId: Contracts.Evm.SpecId.SHANGHAI,
};

// Succeeds
let result = await instance.preverifyTransaction({
...ctx,
caller: sender.address,
recipient: recipient.address,
value: initialSupply - 21_000n * ethers.parseUnits("5", "gwei"),
data: Buffer.alloc(0),
gasLimit: 21_000n,
gasPrice: 5n,
blockGasLimit: 10_000_000n,
});

assert.equal(result, { success: true, initialGasUsed: 21000n });

// Fails
result = await instance.preverifyTransaction({
...ctx,
caller: sender.address,
recipient: undefined,
value: 0n,
nonce: 0n,
data: Buffer.from(MainsailERC20.bytecode.slice(2), "hex"),
gasLimit: 21_000n,
gasPrice: 5n,
blockGasLimit: 10_000_000n,
});

assert.equal(result, {
success: false,
initialGasUsed: 0n,
error: "preverify failed: transaction validation error: call gas cost exceeds the gas limit",
});
});
});

const getRandomTxHash = () => Buffer.from(randomBytes(32)).toString("hex");
Expand Down
6 changes: 6 additions & 0 deletions packages/evm-service/source/instances/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ export class EvmInstance implements Contracts.Evm.Instance {
return this.#evm.prepareNextCommit(context);
}

public async preverifyTransaction(
txContext: Contracts.Evm.PreverifyTransactionContext,
): Promise<Contracts.Evm.PreverifyTransactionResult> {
return this.#evm.preverifyTransaction(txContext);
}

public async view(viewContext: Contracts.Evm.TransactionViewContext): Promise<Contracts.Evm.ViewResult> {
return this.#evm.view(viewContext);
}
Expand Down
67 changes: 67 additions & 0 deletions packages/evm/bindings/src/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ pub struct JsTransactionContext {
pub spec_id: JsString,
}

#[napi(object)]
pub struct JsPreverifyTransactionContext {
pub caller: JsString,
/// Omit recipient when deploying a contract
pub recipient: Option<JsString>,
pub gas_limit: JsBigInt,
pub gas_price: Option<JsBigInt>,
pub value: JsBigInt,
pub nonce: JsBigInt,
pub data: JsBuffer,
pub tx_hash: JsString,
pub spec_id: JsString,
pub block_gas_limit: JsBigInt,
}

#[napi(object)]
pub struct JsTransactionViewContext {
pub caller: JsString,
Expand Down Expand Up @@ -82,6 +97,21 @@ pub struct PrepareNextCommitContext {
pub commit_key: CommitKey,
}

#[derive(Debug)]
pub struct PreverifyTxContext {
pub caller: Address,
/// Omit recipient when deploying a contract
pub recipient: Option<Address>,
pub gas_limit: u64,
pub gas_price: Option<U256>,
pub value: U256,
pub nonce: u64,
pub data: Bytes,
pub tx_hash: B256,
pub spec_id: SpecId,
pub block_gas_limit: U256,
}

#[derive(Debug)]
pub struct TxContext {
pub caller: Address,
Expand Down Expand Up @@ -260,6 +290,43 @@ impl TryFrom<JsTransactionContext> for TxContext {
}
}

impl TryFrom<JsPreverifyTransactionContext> for PreverifyTxContext {
type Error = anyhow::Error;

fn try_from(value: JsPreverifyTransactionContext) -> std::result::Result<Self, Self::Error> {
let buf = value.data.into_value()?;

let recipient = if let Some(recipient) = value.recipient {
Some(utils::create_address_from_js_string(recipient)?)
} else {
None
};

let gas_price = if let Some(gas_price) = value.gas_price {
Some(utils::convert_bigint_to_u256(gas_price)?)
} else {
None
};

let tx_ctx = PreverifyTxContext {
recipient,
gas_limit: value.gas_limit.try_into()?,
gas_price,
caller: utils::create_address_from_js_string(value.caller)?,
value: utils::convert_bigint_to_u256(value.value)?,
nonce: value.nonce.get_u64()?.0,
data: Bytes::from(buf.as_ref().to_owned()),
tx_hash: B256::try_from(
&Bytes::from_str(value.tx_hash.into_utf8()?.as_str()?)?.as_ref()[..],
)?,
block_gas_limit: utils::convert_bigint_to_u256(value.block_gas_limit)?,
spec_id: parse_spec_id(value.spec_id)?,
};

Ok(tx_ctx)
}
}

impl TryFrom<JsTransactionViewContext> for TxViewContext {
type Error = anyhow::Error;

Expand Down
Loading
Loading