Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Commit

Permalink
Shuffle around transaction serialization logic so that we don't need …
Browse files Browse the repository at this point in the history
…to instantiate a transaction in order to serialize it for the database. Except where we need the effective gas price for an eip-1559 fee market transaction, because we need to know the block basefeepergas :(
  • Loading branch information
jeffsmale90 committed Jun 26, 2023
1 parent 4717d19 commit bd80d60
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 54 deletions.
5 changes: 3 additions & 2 deletions src/chains/ethereum/ethereum/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ import {
calculateIntrinsicGas,
InternalTransactionReceipt,
VmTransaction,
TypedTransaction
TypedTransaction,
serializeForDb
} from "@ganache/ethereum-transaction";
import { Block, RuntimeBlock, Snapshots } from "@ganache/ethereum-block";
import {
Expand Down Expand Up @@ -445,7 +446,7 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
// TODO: the block has already done most of the work serializing the tx
// we should reuse it, if possible
// https://github.com/trufflesuite/ganache/issues/4341
const serialized = tx.serializeForDb(blockHash, blockNumberQ, index);
const serialized = serializeForDb(tx, blockHash, blockNumberQ, index);
this.transactions.set(hash, serialized);

// save receipt to the database
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import Blockchain from "../blockchain";
import PromiseQueue from "@ganache/promise-queue";
import type { Common } from "@ethereumjs/common";
import { Data, Quantity } from "@ganache/utils";
import { Address } from "@ganache/ethereum-address";
import {
GanacheRawExtraTx,
TransactionFactory,
Transaction,
TypedTransaction
TypedTransaction,
serializeRpcForDb
} from "@ganache/ethereum-transaction";
import { GanacheLevelUp } from "../database";

Expand Down Expand Up @@ -57,27 +56,7 @@ export default class TransactionManager extends Manager<NoOp> {
// fallback's blocknumber because it doesn't exist in our local chain.
if (!fallback.isValidForkBlockNumber(blockNumber)) return null;

const extra: GanacheRawExtraTx = [
Address.toBuffer(tx.from),
Data.toBuffer((tx as any).hash, 32),
blockHash.toBuffer(),
blockNumber.toBuffer(),
index.toBuffer(),
Quantity.toBuffer(tx.gasPrice)
];
const block = await fallback.request<any>("eth_getBlockByNumber", [
blockNumber.toString(),
false
]);
if (block == null) return null;

const common = fallback.getCommonForBlock(fallback.common, {
number: blockNumber.toBigInt(),
timestamp: Quantity.toBigInt(block.timestamp)
});

const runTx = TransactionFactory.fromRpc(tx, common, extra);
return runTx.serializeForDb(blockHash, blockNumber, index);
return serializeRpcForDb(tx, blockHash, blockNumber, index);
};

public async getRaw(transactionHash: Buffer): Promise<Buffer> {
Expand Down
1 change: 1 addition & 0 deletions src/chains/ethereum/transaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from "./src/transaction-receipt";
export * from "./src/transaction-factory";
export * from "./src/transaction-types";
export * from "./src/vm-transaction";
export * from "./src/transaction-serialization";
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,22 @@ export class EIP1559FeeMarketTransaction extends RuntimeTransaction {
public updateEffectiveGasPrice(baseFeePerGas: bigint) {
const maxFeePerGas = this.maxFeePerGas.toBigInt();
const maxPriorityFeePerGas = this.maxPriorityFeePerGas.toBigInt();
const effectiveGasPrice = EIP1559FeeMarketTransaction.getEffectiveGasPrice(
baseFeePerGas,
maxFeePerGas,
maxPriorityFeePerGas
);
this.effectiveGasPrice = Quantity.from(effectiveGasPrice);
}

public static getEffectiveGasPrice(
baseFeePerGas: bigint,
maxFeePerGas: bigint,
maxPriorityFeePerGas: bigint
): bigint {
const a = maxFeePerGas - baseFeePerGas;
const tip = a < maxPriorityFeePerGas ? a : maxPriorityFeePerGas;
this.effectiveGasPrice = Quantity.from(baseFeePerGas + tip);

return baseFeePerGas + tip;
}
}
27 changes: 0 additions & 27 deletions src/chains/ethereum/transaction/src/runtime-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import { Transaction } from "./rpc-transaction";
import type { Common } from "@ethereumjs/common";
import { GanacheRawExtraTx, TypedRawTransaction } from "./raw";
import type { RunTxResult } from "@ethereumjs/vm";
import { EncodedPart, encode } from "@ganache/rlp";
import { BaseTransaction } from "./base-transaction";
import { InternalTransactionReceipt } from "./transaction-receipt";
import { Address } from "@ganache/ethereum-address";
import { encodeWithPrefix } from "./signing";

export const toValidLengthAddress = (address: string, fieldName: string) => {
const buffer = Data.toBuffer(address);
Expand Down Expand Up @@ -98,31 +96,6 @@ export abstract class RuntimeTransaction extends BaseTransaction {
*/
protected abstract signAndHash(privateKey: Buffer);

public serializeForDb(
blockHash: Data,
blockNumber: Quantity,
transactionIndex: Quantity
): Buffer {
const legacy = this.raw.length === 9;
// todo(perf):make this work with encodeRange and digest
const txAndExtraData: [TypedRawTransaction, GanacheRawExtraTx] = [
// todo: this is encoded differently in the tx table than it is in the
// block table. we should migrate the tx table to use the same format as
// the block (`Buffer.concat([type, encode(raw)])`) so that we can avoid
// block it twice for each block save step.
legacy ? this.raw : ([this.type.toBuffer(), ...this.raw] as any),
[
this.from.toBuffer(),
this.hash.toBuffer(),
blockHash.toBuffer(),
blockNumber.toBuffer(),
transactionIndex.toBuffer(),
this.effectiveGasPrice.toBuffer()
]
];
return encode(txAndExtraData);
}

abstract toJSON(common: Common);

/**
Expand Down
1 change: 1 addition & 0 deletions src/chains/ethereum/transaction/src/transaction-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export class TransactionFactory {
JsonRpcErrorCode.METHOD_NOT_FOUND
);
}

/**
* Create a transaction from a `txData` object
*
Expand Down
154 changes: 154 additions & 0 deletions src/chains/ethereum/transaction/src/transaction-serialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Quantity, Data, JsonRpcErrorCode } from "@ganache/utils";
import { Address } from "@ganache/ethereum-address";
import { GanacheRawExtraTx, TypedRawTransaction } from "./raw";
import { encode } from "@ganache/rlp";
import { TransactionType } from "./transaction-factory";
import { Transaction } from "./rpc-transaction";
import { AccessLists } from "./access-lists";
import { CodedError } from "@ganache/ethereum-utils";
import { EIP1559FeeMarketTransaction } from "./eip1559-fee-market-transaction";

export function serializeRpcForDb(
tx: Transaction,
blockHash: Data,
blockNumber: Quantity,
transactionIndex: Quantity
) {
let type: number;
if (!("type" in tx) || tx.type === undefined) {
type = TransactionType.Legacy;
} else {
type = parseInt(tx.type, 16);
}

let effectiveGasPrice: Quantity;

switch (type) {
case TransactionType.Legacy:
case TransactionType.EIP2930AccessList:
effectiveGasPrice = Quantity.from(tx.gasPrice);
break;
case TransactionType.EIP1559AccessList:
effectiveGasPrice = Quantity.from(
EIP1559FeeMarketTransaction.getEffectiveGasPrice(
// this becomes problematic because we need the baseFeePerGas which
// comes from the block :(
0n,
Quantity.toBigInt(tx.maxFeePerGas),
Quantity.toBigInt(tx.maxPriorityFeePerGas)
)
);
break;
}
const txData = {
raw: rawFromRpc(tx, type),
from: Address.from(tx.from),
hash: Data.from((tx as any).hash, 32),
effectiveGasPrice,
type: Quantity.from(type)
};

return serializeForDb(txData, blockHash, blockNumber, transactionIndex);
}

export function serializeForDb(
tx: {
raw: TypedRawTransaction;
from: Address;
hash: Data;
effectiveGasPrice: Quantity;
type: Quantity;
},
blockHash: Data,
blockNumber: Quantity,
transactionIndex: Quantity
): Buffer {
const legacy = tx.raw.length === 9;
// todo(perf):make this work with encodeRange and digest
const txAndExtraData: [TypedRawTransaction, GanacheRawExtraTx] = [
// todo: this is encoded differently in the tx table than it is in the
// block table. we should migrate the tx table to use the same format as
// the block (`Buffer.concat([type, encode(raw)])`) so that we can avoid
// block it twice for each block save step.
legacy ? tx.raw : ([tx.type.toBuffer(), ...tx.raw] as any),
[
tx.from.toBuffer(),
tx.hash.toBuffer(),
blockHash.toBuffer(),
blockNumber.toBuffer(),
transactionIndex.toBuffer(),
tx.effectiveGasPrice.toBuffer()
]
];
return encode(txAndExtraData);
}

export function rawFromRpc(
txData: Transaction,
txType: number
): TypedRawTransaction {
// if no access list is provided, we convert to legacy
const targetType =
txType === TransactionType.EIP2930AccessList &&
txData.accessList === undefined
? TransactionType.Legacy
: txType;

switch (targetType) {
case TransactionType.Legacy:
return [
Quantity.toBuffer(txData.nonce),
Quantity.toBuffer(txData.gasPrice),
Quantity.toBuffer(txData.gas || txData.gasLimit),
// todo: use address?
Data.toBuffer(txData.to, 20),
Quantity.toBuffer(txData.value),
Data.toBuffer(txData.data || txData.input),
Data.toBuffer((txData as any).v),
Data.toBuffer((txData as any).r),
Data.toBuffer((txData as any).s)
];
case TransactionType.EIP2930AccessList:
return [
Quantity.toBuffer(txData.chainId),
Quantity.toBuffer(txData.nonce),
Quantity.toBuffer(txData.gasPrice),
Quantity.toBuffer(txData.gas || txData.gasLimit),
// todo: use address?
Data.toBuffer(txData.to, 20),
Quantity.toBuffer(txData.value),
Data.toBuffer(txData.data || txData.input),
// accesslists is _always_ set, otherwise it's legacy
txData.accessList
? AccessLists.getAccessListData(txData.accessList).accessList
: [],
Data.toBuffer((txData as any).v),
Data.toBuffer((txData as any).r),
Data.toBuffer((txData as any).s)
];
// todo: should this be TransactionType.EIP1559FeeMarket?
case TransactionType.EIP1559AccessList:
return [
Quantity.toBuffer(txData.chainId),
Quantity.toBuffer(txData.nonce),
Quantity.toBuffer(txData.maxPriorityFeePerGas),
Quantity.toBuffer(txData.maxFeePerGas),
Quantity.toBuffer(txData.gas || txData.gasLimit),
// todo: use address?
Data.toBuffer(txData.to, 20),
Quantity.toBuffer(txData.value),
Data.toBuffer(txData.data || txData.input),
txData.accessList
? AccessLists.getAccessListData(txData.accessList).accessList
: [],
Data.toBuffer((txData as any).v),
Data.toBuffer((txData as any).r),
Data.toBuffer((txData as any).s)
];
default:
throw new CodedError(
`Tx instantiation with supplied type not supported`,
JsonRpcErrorCode.METHOD_NOT_FOUND
);
}
}

0 comments on commit bd80d60

Please sign in to comment.