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

Commit

Permalink
fix: resolve hardfork for blocks by blocknumber and timestamp (#4455)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffsmale90 authored Jul 3, 2023
1 parent 47bec2e commit 893b01b
Show file tree
Hide file tree
Showing 12 changed files with 796 additions and 92 deletions.
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
46 changes: 31 additions & 15 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 @@ -278,15 +280,20 @@ export class Fork {
/**
* 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`.
* original `chainId` (hardforks are applied if they are scheduled on the
* given chain on or after the blocknumber or timestamp of the given `block`).
* If the remote chain's `chainId` is not "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 +302,22 @@ 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)) {
hardfork = hf.name;
} else {
break;
if (hf.timestamp) {
const hfTimestamp = BigInt(hf.timestamp);
if (block.timestamp >= hfTimestamp) {
hardfork = hf.name;
} else {
break;
}
} else if (hf.block) {
if (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

0 comments on commit 893b01b

Please sign in to comment.