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

fix: resolve hardfork for blocks by blocknumber and timestamp #4455

Merged
merged 16 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions src/chains/ethereum/ethereum/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,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 @@ -442,7 +443,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 Expand Up @@ -1113,10 +1114,7 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
const to = hasToAddress ? new Address(transaction.to.toBuffer()) : null;

const common = this.fallback
? this.fallback.getCommonForBlockNumber(
this.common,
BigInt(transaction.block.header.number.toString())
)
? this.fallback.getCommonForBlock(this.common, transaction.block.header)
: this.common;

const gasLeft =
Expand Down Expand Up @@ -1250,10 +1248,7 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
} as any;

const common = this.fallback
? this.fallback.getCommonForBlockNumber(
this.common,
BigInt(block.header.number.toString())
)
? this.fallback.getCommonForBlock(this.common, block.header)
: this.common;

// TODO: prefixCodeHashes should eventually be conditional
Expand Down
38 changes: 22 additions & 16 deletions src/chains/ethereum/ethereum/src/data-managers/block-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
BlockRawTransaction,
EthereumRawBlock,
EthereumRawBlockHeader,
GanacheRawBlock,
Head,
serialize,
WithdrawalRaw
Expand All @@ -21,7 +22,7 @@ import {
} from "@ganache/ethereum-transaction";
import { GanacheLevelUp } from "../database";
import { Ethereum } from "../api-types";
import { encode } from "@ganache/rlp";
import { decode, encode } from "@ganache/rlp";

const LATEST_INDEX_KEY = BUFFER_ZERO;

Expand Down Expand Up @@ -171,10 +172,10 @@ export default class BlockManager extends Manager<Block> {
if (json == null) {
return null;
} else {
const common = fallback.getCommonForBlockNumber(
this.#common,
BigInt(json.number)
);
const common = fallback.getCommonForBlock(this.#common, {
number: BigInt(json.number),
timestamp: BigInt(json.timestamp)
});

return BlockManager.rawFromJSON(json, common);
}
Expand Down Expand Up @@ -226,12 +227,12 @@ export default class BlockManager extends Manager<Block> {
true
]);
if (json) {
const blockNumber = BigInt(json.number);
if (blockNumber <= fallback.blockNumber.toBigInt()) {
const common = fallback.getCommonForBlockNumber(
this.#common,
blockNumber
);
const number = BigInt(json.number);
if (number <= fallback.blockNumber.toBigInt()) {
const common = fallback.getCommonForBlock(this.#common, {
number,
timestamp: BigInt(json.timestamp)
});
return new Block(BlockManager.rawFromJSON(json, common), common);
}
}
Expand Down Expand Up @@ -272,9 +273,14 @@ export default class BlockManager extends Manager<Block> {
if (fallback) {
const block = await this.fromFallback(blockNumber);
if (block) {
const header: EthereumRawBlockHeader =
decode<GanacheRawBlock>(block)[0];
return new Block(
block,
fallback.getCommonForBlockNumber(common, blockNumber.toBigInt())
fallback.getCommonForBlock(common, {
number: blockNumber.toBigInt(),
timestamp: Quantity.toBigInt(header[11])
})
);
}
}
Expand Down Expand Up @@ -319,10 +325,10 @@ export default class BlockManager extends Manager<Block> {
{ disableCache: true }
);
if (json) {
const common = fallback.getCommonForBlockNumber(
this.#common,
BigInt(json.number)
);
const common = fallback.getCommonForBlock(this.#common, {
number: BigInt(json.number),
timestamp: BigInt(json.timestamp)
});
return new Block(BlockManager.rawFromJSON(json, common), common);
}
} else {
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,20 +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 common = fallback.getCommonForBlockNumber(
fallback.common,
blockNumber.toBigInt()
);
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
38 changes: 18 additions & 20 deletions src/chains/ethereum/ethereum/src/forking/fork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,11 @@ export class Fork {
cacheProm,
this.#setCommonFromChain(chainIdPromise)
]);
const common = this.getCommonForBlockNumber(
this.common,
this.blockNumber.toBigInt()
);

const common = this.getCommonForBlock(this.common, {
timestamp: BigInt(block.timestamp),
number: BigInt(block.number)
});
this.block = new Block(BlockManager.rawFromJSON(block, common), common);
if (!chainOptions.time && minerOptions.timestampIncrement !== "clock") {
chainOptions.time = new Date(
Expand All @@ -238,6 +239,7 @@ export class Fork {
1000
);
}

if (cache) await this.initCache(cache);
}
private async initCache(cache: PersistentCache) {
Expand Down Expand Up @@ -275,18 +277,11 @@ export class Fork {
: this.blockNumber;
}

/**
* If the `blockNumber` is before our `fork.blockNumber`, return a `Common`
* instance, applying the rules from the remote chain's `common` via its
* original `chainId`. If the remote chain's `chainId` is now "known", return
* a `Common` with our local `common`'s rules applied, but with the remote
* chain's `chainId`. If the block is greater than or equal to our
* `fork.blockNumber` return `common`.
* @param common -
* @param blockNumber -
*/
public getCommonForBlockNumber(common: Common, blockNumber: BigInt) {
if (blockNumber <= this.blockNumber.toBigInt()) {
public getCommonForBlock(
common: Common,
block: { number: bigint; timestamp: bigint }
): Common {
if (block.number <= this.blockNumber.toBigInt()) {
// we are at or before our fork block

let forkCommon: Common;
Expand All @@ -295,13 +290,16 @@ export class Fork {
let hardfork;
// hardforks are iterated from earliest to latest
for (const hf of common.hardforks()) {
if (hf.block === null) continue;
if (blockNumber >= BigInt(hf.block)) {
if (hf.timestamp) {
const hfTimestamp = BigInt(hf.timestamp);
if (block.timestamp >= hfTimestamp) {
hardfork = hf.name;
}
} else if (hf.block && block.number >= BigInt(hf.block)) {
hardfork = hf.name;
} else {
break;
}
}

forkCommon = new Common({ chain: this.chainId, hardfork });
} else {
// we don't know about this chain or hardfork, so just carry on per usual,
Expand Down
130 changes: 130 additions & 0 deletions src/chains/ethereum/ethereum/tests/forking/fork.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { EthereumOptionsConfig } from "@ganache/ethereum-options";
import { Fork } from "../../src/forking/fork";
import { KNOWN_CHAINIDS, Quantity } from "@ganache/utils";
import { Common } from "@ethereumjs/common/dist/common";
import ganache from "../../../../../packages/core";
import Server from "../../../../../packages/core/lib/src/server";
import assert from "assert";
import { logging } from "./helpers";

describe("Fork", () => {
const port = 9999;
const networkId = 1;
const accounts = [];
const forkOptions = {
fork: {
url: `http://localhost:${port}`
},
logging
};

let remoteServer: Server;
let fork: Fork;

before(async () => {
remoteServer = ganache.server({
wallet: { deterministic: true },
chain: { networkId: networkId },
logging
});
await remoteServer.listen(port);
});

beforeEach(async () => {
const providerOptions = EthereumOptionsConfig.normalize(forkOptions);
fork = new Fork(providerOptions, accounts);
await fork.initialize();
});

afterEach(async () => {
await fork.close();
});

after(async () => {
await remoteServer.close();
});

describe("getCommonForBlock()", () => {
it("should return a Common for known chainIds", () => {
KNOWN_CHAINIDS.forEach(chainId => {
if (chainId === 42) {
// Skip kovan, because it is no longer supported by ethereumjs. To be
// removed in https://github.com/trufflesuite/ganache/issues/4461
} else {
assert.doesNotThrow(() => {
const parentCommon = new Common({ chain: chainId });

fork.getCommonForBlock(parentCommon, {
number: 0n,
timestamp: 0n
});
});
}
});
});

it("should resolve the correct hardfork based on block number for known chainId", () => {
const mainnet = 1;
const mergeBlocknumber = 15537394n;

// ensure that the "fork" blockNumber is after the "merge" hardfork blockNumber
fork.blockNumber = Quantity.from(mergeBlocknumber + 100n);
fork.chainId = mainnet;

const parentCommon = new Common({ chain: mainnet });
const blocknumberToHardfork: [bigint, string][] = [
[mergeBlocknumber - 1n, "grayGlacier"],
[mergeBlocknumber, "merge"],
[mergeBlocknumber + 1n, "merge"]
];

blocknumberToHardfork.forEach(([number, expectedHardfork]) => {
const common = fork.getCommonForBlock(parentCommon, {
number,
timestamp: 0n
});

const hf = common.hardfork();

assert.strictEqual(
hf,
expectedHardfork,
`Unexpected hardfork with blocknumber: ${number}`
);
});
});

it("should resolve the correct hardfork based on timestamp for known chainId", () => {
// we use sepolia because it has shanghai hf scheduled
const sepolia = 11155111;
const shanghaiTimestamp = 1677557088n;
const mergeForkIdTransitionBlockNumber = 1735371n;

// ensure that the "fork" blockNumber is after the "mergeForkIdTransition" hardfork blockNumber
fork.blockNumber = Quantity.from(mergeForkIdTransitionBlockNumber + 100n);
fork.chainId = sepolia;

const timstampToHardfork: [bigint, string][] = [
[shanghaiTimestamp - 1n, "mergeForkIdTransition"],
[shanghaiTimestamp, "shanghai"],
[shanghaiTimestamp + 1n, "shanghai"]
];

const parentCommon = new Common({ chain: sepolia });
timstampToHardfork.forEach(([timestamp, expectedHardfork]) => {
const common = fork.getCommonForBlock(parentCommon, {
number: mergeForkIdTransitionBlockNumber,
timestamp
});

const hf = common.hardfork();

assert.strictEqual(
hf,
expectedHardfork,
`Unexpected hardfork with timestamp: ${timestamp}`
);
});
});
});
});
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 @@ -241,6 +241,7 @@ export class EIP1559FeeMarketTransaction extends RuntimeTransaction {
const maxPriorityFeePerGas = this.maxPriorityFeePerGas.toBigInt();
const a = maxFeePerGas - baseFeePerGas;
const tip = a < maxPriorityFeePerGas ? a : maxPriorityFeePerGas;

this.effectiveGasPrice = Quantity.from(baseFeePerGas + tip);
}
}
8 changes: 8 additions & 0 deletions src/chains/ethereum/transaction/src/rpc-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ export type Transaction =
| EIP2930AccessListRpcTransaction
| EIP1559FeeMarketRpcTransaction;

export enum TransactionType {
Legacy = 0x0,
EIP2930AccessList = 0x1,
//todo: this should be EIP1559FeeMarket
//https://github.com/trufflesuite/ganache/issues/4462
EIP1559AccessList = 0x2
}

export type CallTransaction = Omit<Transaction, "from"> & { from?: string };

export type LegacyRpcTransaction = Readonly<RpcTransaction> & {
Expand Down
Loading