Skip to content

Commit

Permalink
Merge pull request #30 from commonprefix/dencun
Browse files Browse the repository at this point in the history
Add support for Dencun hardfork
  • Loading branch information
shresthagrawal authored May 20, 2024
2 parents 99a7307 + 655d73b commit de3678d
Show file tree
Hide file tree
Showing 16 changed files with 6,279 additions and 571 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ lib/
yarn-error.log
.env
patronum.log
coverage/
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Ethereum RPC proxy that verifies RPC responses against given trusted block hashe
```ts
import { VerifyingProvider, startServer } from 'patronum';

const provider = new VerifyingProvider(
const provider = await VerifyingProvider.create(
trustlessRPCURL,
trustedBlockNumber,
trustedBlockHash,
Expand All @@ -24,7 +24,7 @@ await startServer(provider, PORT);
```ts
import { VerifyingProvider } from 'patronum';

const provider = new VerifyingProvider(
const provider = await VerifyingProvider.create(
trustlessRPCURL,
trustedBlockNumber,
trustedBlockHash,
Expand Down
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,21 @@
"build": "tsc",
"prepare": "npm run build && husky install",
"test": "jest",
"test:coverage": "jest --coverage",
"prettier:check": "npx prettier -c .",
"prettier:fix": "npx prettier -w ."
},
"dependencies": {
"@ethereumjs/block": "^4.3.0",
"@ethereumjs/blockchain": "^6.3.0",
"@ethereumjs/common": "^3.2.0",
"@ethereumjs/trie": "^5.1.0",
"@ethereumjs/tx": "^4.2.0",
"@ethereumjs/util": "^8.1.0",
"@ethereumjs/vm": "^6.5.0",
"@ethereumjs/block": "^5.2.0",
"@ethereumjs/blockchain": "^7.2.0",
"@ethereumjs/common": "^4.3.0",
"@ethereumjs/trie": "^6.2.0",
"@ethereumjs/tx": "^5.3.0",
"@ethereumjs/util": "^9.0.3",
"@ethereumjs/vm": "^8.0.0",
"axios": "0.27.2",
"json-rpc-2.0": "1.4.1",
"kzg-wasm": "^0.4.0",
"lodash": "4.17.21",
"rlp": "3.0.0",
"web3": "1.7.5",
Expand Down
7 changes: 4 additions & 3 deletions scripts/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@ import { VerifyingProvider, startServer } from '../src';
const RPC_URL = process.env.RPC_URL || '';
const RPC_URL_WS = process.env.RPC_URL_WS;
// Metamask doesn't allow same RPC URL for different networks
const PORT = process.env.CHAIN_ID === '5' ? 8547 : 8546;
const PORT = process.env.PORT || (process.env.CHAIN_ID === '5' ? 8547 : 8546);
const CHAIN = process.env.CHAIN_ID === '5' ? Chain.Goerli : Chain.Mainnet;
const POLLING_DELAY = 13 * 1000; //13s

async function main() {
const web3 = new Web3(RPC_URL);
const block = await web3.eth.getBlock('latest');
const provider = new VerifyingProvider(
const provider = await VerifyingProvider.create(
RPC_URL,
BigInt(block.number),
block.hash,
CHAIN,
);

if (RPC_URL_WS) {
const web3Sub = new Web3(RPC_URL_WS);
web3Sub.eth
Expand All @@ -44,7 +45,7 @@ async function main() {
provider.update(block.hash, BigInt(block.number));
}, POLLING_DELAY);
}
await startServer(provider, PORT);
await startServer(provider, Number(PORT));
}

main();
97 changes: 60 additions & 37 deletions src/provider.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import _ from 'lodash';
import Web3 from 'web3';
import { loadKZG } from 'kzg-wasm';
import { Trie } from '@ethereumjs/trie';
import rlp from 'rlp';
import { Common, Chain, Hardfork } from '@ethereumjs/common';
import { Common, Chain, Hardfork, CustomCrypto } from '@ethereumjs/common';
import {
Address,
Account,
toType,
bufferToHex,
toBuffer,
bytesToHex,
hexToBytes,
TypeOutput,
setLengthLeft,
KECCAK256_NULL_S,
equalsBytes,
} from '@ethereumjs/util';
import { VM } from '@ethereumjs/vm';
import { BlockHeader, Block } from '@ethereumjs/block';
Expand Down Expand Up @@ -70,15 +72,34 @@ export class VerifyingProvider {
blockNumber: bigint | number,
blockHash: Bytes32,
chain: bigint | Chain = Chain.Mainnet,
customCrypto: CustomCrypto,
) {
this.rpc = new RPC({ URL: providerURL });
this.common = new Common({
chain,
hardfork: chain === Chain.Mainnet ? Hardfork.Shanghai : undefined,
});
const _blockNumber = BigInt(blockNumber);
this.latestBlockNumber = _blockNumber;
this.blockHashes[bigIntToHex(_blockNumber)] = blockHash;
this.common = new Common({
chain,
hardfork: Hardfork.Cancun,
customCrypto,
});
}

static async create(
providerURL: string,
blockNumber: bigint | number,
blockHash: Bytes32,
chain: bigint | Chain = Chain.Mainnet,
): Promise<VerifyingProvider> {
const kzg = await loadKZG();
const customCrypto: CustomCrypto = { kzg };
return new VerifyingProvider(
providerURL,
blockNumber,
blockHash,
chain,
customCrypto,
);
}

update(blockHash: Bytes32, blockNumber: bigint) {
Expand Down Expand Up @@ -235,12 +256,12 @@ export class VerifyingProvider {
gasLimit: toType(gasLimit, TypeOutput.BigInt),
gasPrice: toType(gasPrice || maxPriorityFeePerGas, TypeOutput.BigInt),
value: toType(value, TypeOutput.BigInt),
data: data ? toBuffer(data) : undefined,
data: data ? hexToBytes(data) : undefined,
block: { header },
};
const { execResult } = await vm.evm.runCall(runCallOpts);

return bufferToHex(execResult.returnValue);
return bytesToHex(execResult.returnValue);
} catch (error: any) {
throw new InternalError(error.message.toString());
}
Expand Down Expand Up @@ -342,10 +363,10 @@ export class VerifyingProvider {
throw new InternalError(`RPC request failed`);
}

const tx = TransactionFactory.fromSerializedData(toBuffer(signedTx), {
const tx = TransactionFactory.fromSerializedData(hexToBytes(signedTx), {
common: this.common,
});
return bufferToHex(tx.hash());
return bytesToHex(tx.hash());
}

async getTransactionReceipt(txHash: Bytes32): Promise<JSONRPCReceipt | null> {
Expand All @@ -359,7 +380,7 @@ export class VerifyingProvider {
const header = await this.getBlockHeader(receipt.blockNumber);
const block = await this.getBlock(header);
const index = block.transactions.findIndex(
tx => bufferToHex(tx.hash()) === txHash.toLowerCase(),
tx => bytesToHex(tx.hash()) === txHash.toLowerCase(),
);
if (index === -1) {
throw new InternalError('the recipt provided by the RPC is invalid');
Expand All @@ -369,7 +390,7 @@ export class VerifyingProvider {
return {
transactionHash: txHash,
transactionIndex: bigIntToHex(index),
blockHash: bufferToHex(block.hash()),
blockHash: bytesToHex(block.hash()),
blockNumber: bigIntToHex(block.header.number),
from: tx.getSenderAddress().toString(),
to: tx.to?.toString() ?? null,
Expand Down Expand Up @@ -420,13 +441,14 @@ export class VerifyingProvider {
const blockData = blockDataFromWeb3Response(blockInfo);
const block = Block.fromBlockData(blockData, { common: this.common });

if (!block.header.hash().equals(header.hash())) {
if (!equalsBytes(block.header.hash(), header.hash())) {
throw new InternalError(
`BN(${header.number}): blockhash doest match the blockData provided by the RPC`,
`BN(${header.number}): blockhash doesn't match the blockData provided by the RPC`,
);
}

if (!(await block.validateTransactionsTrie())) {
// TODO: block.validateBlobTransactions(), etc.?
if (!(await block.transactionsTrieIsValid())) {
throw new InternalError(
`transactionTree doesn't match the transactions provided by the RPC`,
);
Expand Down Expand Up @@ -486,14 +508,14 @@ export class VerifyingProvider {
const blockchain = await Blockchain.create({ common: this.common });
// path the blockchain to return the correct blockhash
(blockchain as any).getBlock = async (blockId: number) => {
const _hash = toBuffer(await this.getBlockHash(BigInt(blockId)));
const _hash = hexToBytes(await this.getBlockHash(BigInt(blockId)));
return {
hash: () => _hash,
};
};
this.vm = await VM.create({ common: this.common, blockchain });
}
return await this.vm!.copy();
return await this.vm!.shallowCopy();
}

private async getVM(tx: RPCTx, header: BlockHeader): Promise<VM> {
Expand Down Expand Up @@ -590,13 +612,13 @@ export class VerifyingProvider {
for (let storageAccess of storageAccesses) {
await vm.stateManager.putContractStorage(
address,
setLengthLeft(toBuffer(storageAccess.key), 32),
setLengthLeft(toBuffer(storageAccess.value), 32),
setLengthLeft(hexToBytes(storageAccess.key), 32),
setLengthLeft(hexToBytes(storageAccess.value), 32),
);
}

if (code !== '0x')
await vm.stateManager.putContractCode(address, toBuffer(code));
await vm.stateManager.putContractCode(address, hexToBytes(code));
}
await vm.stateManager.commit();
return vm;
Expand All @@ -611,7 +633,7 @@ export class VerifyingProvider {
const hash = this.blockHashes[bigIntToHex(lastVerifiedBlockNumber)];
const header = await this.getBlockHeaderByHash(hash);
lastVerifiedBlockNumber--;
const parentBlockHash = bufferToHex(header.parentHash);
const parentBlockHash = bytesToHex(header.parentHash);
const parentBlockNumberHex = bigIntToHex(lastVerifiedBlockNumber);
if (
parentBlockNumberHex in this.blockHashes &&
Expand Down Expand Up @@ -640,8 +662,7 @@ export class VerifyingProvider {

const headerData = headerDataFromWeb3Response(blockInfo);
const header = new BlockHeader(headerData, { common: this.common });

if (!header.hash().equals(toBuffer(blockHash))) {
if (!equalsBytes(header.hash(), hexToBytes(blockHash))) {
throw new InternalError(
`blockhash doesn't match the blockInfo provided by the RPC`,
);
Expand All @@ -653,49 +674,51 @@ export class VerifyingProvider {

private verifyCodeHash(code: Bytes, codeHash: Bytes32): boolean {
return (
(code === '0x' && codeHash === '0x' + KECCAK256_NULL_S) ||
(code === '0x' && codeHash === KECCAK256_NULL_S) ||
Web3.utils.keccak256(code) === codeHash
);
}

private async verifyProof(
address: Address,
storageKeys: Bytes32[],
stateRoot: Buffer,
stateRoot: Uint8Array,
proof: GetProof,
): Promise<boolean> {
const trie = new Trie();
const key = Web3.utils.keccak256(address.toString());
const expectedAccountRLP = await trie.verifyProof(
stateRoot,
toBuffer(key),
proof.accountProof.map(a => toBuffer(a)),
Buffer.from(stateRoot),
hexToBytes(key),
proof.accountProof.map(a => hexToBytes(a)),
);
const account = Account.fromAccountData({
nonce: BigInt(proof.nonce),
balance: BigInt(proof.balance),
storageRoot: proof.storageHash,
codeHash: proof.codeHash,
});
const isAccountValid = account
.serialize()
.equals(expectedAccountRLP ? expectedAccountRLP : emptyAccountSerialize);
const isAccountValid = equalsBytes(
account.serialize(),
expectedAccountRLP ?? emptyAccountSerialize,
);

if (!isAccountValid) return false;

for (let i = 0; i < storageKeys.length; i++) {
const sp = proof.storageProof[i];
const key = Web3.utils.keccak256(
bufferToHex(setLengthLeft(toBuffer(storageKeys[i]), 32)),
bytesToHex(setLengthLeft(hexToBytes(storageKeys[i]), 32)),
);
const expectedStorageRLP = await trie.verifyProof(
toBuffer(proof.storageHash),
toBuffer(key),
sp.proof.map(a => toBuffer(a)),
hexToBytes(proof.storageHash),
hexToBytes(key),
sp.proof.map(a => hexToBytes(a)),
);
const isStorageValid =
(!expectedStorageRLP && sp.value === '0x0') ||
(!!expectedStorageRLP &&
expectedStorageRLP.equals(rlp.encode(sp.value)));
equalsBytes(expectedStorageRLP, rlp.encode(sp.value)));
if (!isStorageValid) return false;
}

Expand Down
19 changes: 11 additions & 8 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
setLengthLeft,
toBuffer,
hexToBytes,
bigIntToHex,
bufferToHex,
bytesToHex,
intToHex,
} from '@ethereumjs/util';
import { HeaderData, BlockData, Block } from '@ethereumjs/block';
Expand Down Expand Up @@ -38,6 +38,9 @@ export function headerDataFromWeb3Response(blockInfo: any): HeaderData {
? BigInt(blockInfo.baseFeePerGas)
: undefined,
withdrawalsRoot: blockInfo.withdrawalsRoot,
excessBlobGas: blockInfo.excessBlobGas,
blobGasUsed: blockInfo.blobGasUsed,
parentBeaconBlockRoot: blockInfo.parentBeaconBlockRoot,
};
}

Expand All @@ -50,7 +53,7 @@ export function txDataFromWeb3Response(
gasPrice: BigInt(txInfo.gasPrice),
gasLimit: txInfo.gas,
to: isTruthy(txInfo.to)
? setLengthLeft(toBuffer(txInfo.to), 20)
? setLengthLeft(hexToBytes(txInfo.to), 20)
: undefined,
value: BigInt(txInfo.value),
maxFeePerGas: isTruthy(txInfo.maxFeePerGas)
Expand All @@ -76,7 +79,7 @@ export function toJSONRPCTx(
): JSONRPCTx {
const txJSON = tx.toJSON();
return {
blockHash: block ? bufferToHex(block.hash()) : null,
blockHash: block ? bytesToHex(block.hash()) : null,
blockNumber: block ? bigIntToHex(block.header.number) : null,
from: tx.getSenderAddress().toString(),
gas: txJSON.gasLimit!,
Expand All @@ -86,7 +89,7 @@ export function toJSONRPCTx(
type: intToHex(tx.type),
accessList: txJSON.accessList,
chainId: txJSON.chainId,
hash: bufferToHex(tx.hash()),
hash: bytesToHex(tx.hash()),
input: txJSON.data!,
nonce: txJSON.nonce!,
to: tx.to?.toString() ?? null,
Expand All @@ -109,11 +112,11 @@ export function toJSONRPCBlock(
const transactions = block.transactions.map((tx, txIndex) =>
includeTransactions
? toJSONRPCTx(tx, block, txIndex)
: bufferToHex(tx.hash()),
: bytesToHex(tx.hash()),
);
return {
number: header.number!,
hash: bufferToHex(block.hash()),
hash: bytesToHex(block.hash()),
parentHash: header.parentHash!,
mixHash: header.mixHash,
nonce: header.nonce!,
Expand All @@ -131,7 +134,7 @@ export function toJSONRPCBlock(
gasUsed: header.gasUsed!,
timestamp: header.timestamp!,
transactions,
uncles: uncleHeaderHashes.map(bufferToHex),
uncles: uncleHeaderHashes.map(bytesToHex),
baseFeePerGas: header.baseFeePerGas,
};
}
Loading

0 comments on commit de3678d

Please sign in to comment.