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

fukasa light client: implement Ssz and Log EIPs to enable merkle proof generation and validation #3452

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
30 changes: 30 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/block/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"tsc": "../../config/cli/ts-compile.sh"
},
"dependencies": {
"@chainsafe/ssz": "https://github.com/ChainSafe/ssz/raw/cayman/stable-container/packages/ssz/package.tgz",
"@ethereumjs/common": "^4.4.0",
"@ethereumjs/rlp": "^5.0.2",
"@ethereumjs/trie": "^6.2.1",
Expand Down
40 changes: 19 additions & 21 deletions packages/block/src/block/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { keccak256 } from 'ethereum-cryptography/keccak.js'
// TODO: See if there is an easier way to achieve the same result.
// See: https://github.com/microsoft/TypeScript/issues/47558
// (situation will eventually improve on Typescript and/or Eslint update)
import { genTransactionsSszRoot, genWithdrawalsSszRoot } from '../helpers.js'
import {
genRequestsTrieRoot,
genTransactionsTrieRoot,
Expand Down Expand Up @@ -226,7 +227,9 @@ export class Block {
* Generates transaction trie for validation.
*/
async genTxTrie(): Promise<Uint8Array> {
return genTransactionsTrieRoot(this.transactions, new Trie({ common: this.common }))
return this.common.isActivatedEIP(6493)
? genTransactionsSszRoot(this.transactions)
: genTransactionsTrieRoot(this.transactions, new Trie({ common: this.common }))
}

/**
Expand All @@ -235,16 +238,10 @@ export class Block {
* @returns True if the transaction trie is valid, false otherwise
*/
async transactionsTrieIsValid(): Promise<boolean> {
let result
if (this.transactions.length === 0) {
result = equalsBytes(this.header.transactionsTrie, KECCAK256_RLP)
return result
}

if (this.cache.txTrieRoot === undefined) {
this.cache.txTrieRoot = await this.genTxTrie()
}
result = equalsBytes(this.cache.txTrieRoot, this.header.transactionsTrie)
const result = equalsBytes(this.cache.txTrieRoot, this.header.transactionsTrie)
return result
}

Expand Down Expand Up @@ -364,7 +361,9 @@ export class Block {
}

if (!(await this.transactionsTrieIsValid())) {
const msg = this._errorMsg('invalid transaction trie')
const msg = this._errorMsg(
`invalid transaction trie expected=${bytesToHex(this.cache.txTrieRoot!)}`,
)
throw new Error(msg)
}

Expand Down Expand Up @@ -453,6 +452,12 @@ export class Block {
return equalsBytes(this.keccakFunction(raw), this.header.uncleHash)
}

async genWithdrawalsTrie(): Promise<Uint8Array> {
return this.common.isActivatedEIP(6493)
? genWithdrawalsSszRoot(this.withdrawals!)
: genWithdrawalsTrieRoot(this.withdrawals!, new Trie({ common: this.common }))
}

/**
* Validates the withdrawal root
* @returns true if the withdrawals trie root is valid, false otherwise
Expand All @@ -462,19 +467,10 @@ export class Block {
throw new Error('EIP 4895 is not activated')
}

let result
if (this.withdrawals!.length === 0) {
result = equalsBytes(this.header.withdrawalsRoot!, KECCAK256_RLP)
return result
}

if (this.cache.withdrawalsTrieRoot === undefined) {
this.cache.withdrawalsTrieRoot = await genWithdrawalsTrieRoot(
this.withdrawals!,
new Trie({ common: this.common }),
)
this.cache.withdrawalsTrieRoot = await this.genWithdrawalsTrie()
}
result = equalsBytes(this.cache.withdrawalsTrieRoot, this.header.withdrawalsRoot!)
const result = equalsBytes(this.cache.withdrawalsTrieRoot, this.header.withdrawalsRoot!)
return result
}

Expand Down Expand Up @@ -543,7 +539,9 @@ export class Block {
toExecutionPayload(): ExecutionPayload {
const blockJSON = this.toJSON()
const header = blockJSON.header!
const transactions = this.transactions.map((tx) => bytesToHex(tx.serialize())) ?? []
const transactions = this.common.isActivatedEIP(6493)
? this.transactions.map((tx) => tx.toExecutionPayloadTx())
: this.transactions.map((tx) => bytesToHex(tx.serialize()))
const withdrawalsArr = blockJSON.withdrawals ? { withdrawals: blockJSON.withdrawals } : {}

const executionPayload: ExecutionPayload = {
Expand Down
43 changes: 34 additions & 9 deletions packages/block/src/block/constructors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type TxOptions,
createTx,
createTxFromBlockBodyData,
createTxFromExecutionPayloadTx,
createTxFromRLP,
normalizeTxParams,
} from '@ethereumjs/tx'
Expand All @@ -25,7 +26,13 @@ import {
} from '@ethereumjs/util'

import { generateCliqueBlockExtraData } from '../consensus/clique.js'
import { genRequestsTrieRoot, genTransactionsTrieRoot, genWithdrawalsTrieRoot } from '../helpers.js'
import {
genRequestsTrieRoot,
genTransactionsSszRoot,
genTransactionsTrieRoot,
genWithdrawalsSszRoot,
genWithdrawalsTrieRoot,
} from '../helpers.js'
import {
Block,
createBlockHeader,
Expand All @@ -46,6 +53,7 @@ import type {
RequestsBytes,
WithdrawalsBytes,
} from '../types.js'
import type { Common } from '@ethereumjs/common'
import type { TypedTransaction } from '@ethereumjs/tx'
import type {
CLRequest,
Expand Down Expand Up @@ -373,7 +381,7 @@ export const createBlockFromJSONRPCProvider = async (
*/
export async function createBlockFromExecutionPayload(
payload: ExecutionPayload,
opts?: BlockOptions,
opts: BlockOptions & { common: Common },
): Promise<Block> {
const {
blockNumber: number,
Expand All @@ -389,22 +397,39 @@ export async function createBlockFromExecutionPayload(
} = payload

const txs = []
for (const [index, serializedTx] of transactions.entries()) {
for (const [index, serializedTxOrPayload] of transactions.entries()) {
try {
const tx = createTxFromRLP(hexToBytes(serializedTx as PrefixedHexString), {
common: opts?.common,
})
let tx
if (opts.common.isActivatedEIP(6493)) {
if (typeof serializedTxOrPayload === 'string') {
throw Error('EIP 6493 activated for transaction bytes')
}
tx = createTxFromExecutionPayloadTx(serializedTxOrPayload, {
common: opts?.common,
})
} else {
if (typeof serializedTxOrPayload !== 'string') {
throw Error('EIP 6493 not activated for transaction payload')
}
tx = createTxFromRLP(hexToBytes(serializedTxOrPayload as PrefixedHexString), {
common: opts?.common,
})
}
txs.push(tx)
} catch (error) {
const validationError = `Invalid tx at index ${index}: ${error}`
throw validationError
}
}

const transactionsTrie = await genTransactionsTrieRoot(txs, new Trie({ common: opts?.common }))
const transactionsTrie = opts.common.isActivatedEIP(6493)
? await genTransactionsSszRoot(txs)
: await genTransactionsTrieRoot(txs, new Trie({ common: opts?.common }))
const withdrawals = withdrawalsData?.map((wData) => createWithdrawal(wData))
const withdrawalsRoot = withdrawals
? await genWithdrawalsTrieRoot(withdrawals, new Trie({ common: opts?.common }))
? opts.common.isActivatedEIP(6493)
? genWithdrawalsSszRoot(withdrawals)
: await genWithdrawalsTrieRoot(withdrawals, new Trie({ common: opts?.common }))
: undefined

const hasDepositRequests = depositRequests !== undefined && depositRequests !== null
Expand Down Expand Up @@ -478,7 +503,7 @@ export async function createBlockFromExecutionPayload(
*/
export async function createBlockFromBeaconPayloadJSON(
payload: BeaconPayloadJSON,
opts?: BlockOptions,
opts: BlockOptions & { common: Common },
): Promise<Block> {
const executionPayload = executionPayloadFromBeaconPayload(payload)
return createBlockFromExecutionPayload(executionPayload, opts)
Expand Down
77 changes: 73 additions & 4 deletions packages/block/src/from-beacon-payload.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { bigIntToHex } from '@ethereumjs/util'

import type { ExecutionPayload } from './types.js'
import type { NumericString, PrefixedHexString, VerkleExecutionWitness } from '@ethereumjs/util'
import type {
NumericString,
PrefixedHexString,
VerkleExecutionWitness,
ssz,
} from '@ethereumjs/util'

type BeaconWithdrawal = {
index: PrefixedHexString
Expand Down Expand Up @@ -30,7 +35,41 @@ type BeaconConsolidationRequest = {
target_pubkey: PrefixedHexString
}

// Payload JSON that one gets using the beacon apis
export type BeaconFeesPerGasV1 = {
regular: PrefixedHexString | null // Quantity 64 bytes
blob: PrefixedHexString | null // Quantity 64 bytes
}

export type BeaconAccessTupleV1 = {
address: PrefixedHexString // DATA 20 bytes
storage_keys: PrefixedHexString[] // Data 32 bytes MAX_ACCESS_LIST_STORAGE_KEYS array
}

export type BeaconTransactionPayloadV1 = {
type: PrefixedHexString | null // Quantity, 1 byte
chain_id: PrefixedHexString | null // Quantity 8 bytes
nonce: PrefixedHexString | null // Quantity 8 bytes
max_fees_per_gas: BeaconFeesPerGasV1 | null
gas: PrefixedHexString | null // Quantity 8 bytes
to: PrefixedHexString | null // DATA 20 bytes
value: PrefixedHexString | null // Quantity 64 bytes
input: PrefixedHexString | null // max MAX_CALLDATA_SIZE bytes,
access_list: BeaconAccessTupleV1[] | null
max_priority_fees_per_gas: BeaconFeesPerGasV1 | null
blob_versioned_hashes: PrefixedHexString[] | null // DATA 32 bytes array
}

export type BeaconTransactionSignatureV1 = {
from: PrefixedHexString | null // DATA 20 bytes
ecdsa_signature: PrefixedHexString | null // DATA 65 bytes or null
}

type BeaconTransactionV1 = {
payload: BeaconTransactionPayloadV1
signature: BeaconTransactionSignatureV1
}

// Payload json that one gets using the beacon apis
// curl localhost:5052/eth/v2/beacon/blocks/56610 | jq .data.message.body.execution_payload
export type BeaconPayloadJSON = {
parent_hash: PrefixedHexString
Expand All @@ -46,7 +85,7 @@ export type BeaconPayloadJSON = {
extra_data: PrefixedHexString
base_fee_per_gas: NumericString
block_hash: PrefixedHexString
transactions: PrefixedHexString[]
transactions: PrefixedHexString[] | BeaconTransactionV1[]
withdrawals?: BeaconWithdrawal[]
blob_gas_used?: NumericString
excess_blob_gas?: NumericString
Expand Down Expand Up @@ -121,6 +160,36 @@ function parseExecutionWitnessFromSnakeJSON({
* The JSON data can be retrieved from a consensus layer (CL) client on this Beacon API `/eth/v2/beacon/blocks/[block number]`
*/
export function executionPayloadFromBeaconPayload(payload: BeaconPayloadJSON): ExecutionPayload {
const transactions =
typeof payload.transactions[0] === 'object'
? (payload.transactions as BeaconTransactionV1[]).map((btxv1) => {
return {
payload: {
type: btxv1.payload.type,
chainId: btxv1.payload.chain_id,
nonce: btxv1.payload.nonce,
maxFeesPerGas: btxv1.payload.max_fees_per_gas,
to: btxv1.payload.to,
value: btxv1.payload.value,
input: btxv1.payload.input,
accessList:
btxv1.payload.access_list?.map((bal: BeaconAccessTupleV1) => {
return {
address: bal.address,
storageKeys: bal.storage_keys,
}
}) ?? null,
maxPriorityFeesPerGas: btxv1.payload.max_priority_fees_per_gas,
blobVersionedHashes: btxv1.payload.blob_versioned_hashes,
},
signature: {
from: btxv1.signature.from,
ecdsaSignature: btxv1.signature.ecdsa_signature,
},
} as ssz.TransactionV1
})
: (payload.transactions as PrefixedHexString[])

const executionPayload: ExecutionPayload = {
parentHash: payload.parent_hash,
feeRecipient: payload.fee_recipient,
Expand All @@ -135,7 +204,7 @@ export function executionPayloadFromBeaconPayload(payload: BeaconPayloadJSON): E
extraData: payload.extra_data,
baseFeePerGas: bigIntToHex(BigInt(payload.base_fee_per_gas)),
blockHash: payload.block_hash,
transactions: payload.transactions,
transactions,
}

if (payload.withdrawals !== undefined && payload.withdrawals !== null) {
Expand Down
Loading
Loading