add helpers for processing EL block header to `libnimbus_lc.a` (#5199)

To obtain the correct `transactions_root` and `withdrawals_root`,
it is necessary to process execution block header. Light client updates
don't contain the correct MPT roots.
This commit is contained in:
Etan Kissling 2023-08-07 14:23:44 +02:00 committed by GitHub
parent 2c424cac87
commit d7afa1c78a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 267 additions and 8 deletions

View File

@ -1023,6 +1023,75 @@ ETH_RESULT_USE_CHECK
int ETHExecutionPayloadHeaderGetExcessBlobGas(
const ETHExecutionPayloadHeader *execution);
/**
* Execution block header.
*/
typedef struct ETHExecutionBlockHeader ETHExecutionBlockHeader;
/**
* Verifies that a JSON execution block header is valid and that it matches
* the given `executionHash`.
*
* - The JSON-RPC `eth_getBlockByHash` with params `[executionHash, false]`
* may be used to obtain execution block header data for a given execution
* block hash. Pass the response's `result` property to `blockHeaderJson`.
*
* - The execution block header must be destroyed with
* `ETHExecutionBlockHeaderDestroy` once no longer needed,
* to release memory.
*
* @param executionHash Execution block hash.
* @param blockHeaderJson Buffer with JSON encoded header. NULL-terminated.
*
* @return Pointer to an initialized execution block header - If successful.
* @return `NULL` - If the given `blockHeaderJson` is malformed or incompatible.
*
* @see https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getblockbyhash
*/
ETH_RESULT_USE_CHECK
ETHExecutionBlockHeader *ETHExecutionBlockHeaderCreateFromJson(
const ETHRoot *executionHash,
const char *blockHeaderJson);
/**
* Destroys an execution block header.
*
* - The execution block header must no longer be used after destruction.
*
* @param executionBlockHeader Execution block header.
*/
void ETHExecutionBlockHeaderDestroy(ETHExecutionBlockHeader *executionBlockHeader);
/**
* Obtains the transactions MPT root of a given execution block header.
*
* - The returned value is allocated in the given execution block header.
* It must neither be released nor written to, and the execution block
* header must not be released while the returned value is in use.
*
* @param executionBlockHeader Execution block header.
*
* @return Execution transactions root.
*/
ETH_RESULT_USE_CHECK
const ETHRoot *ETHExecutionBlockHeaderGetTransactionsRoot(
const ETHExecutionBlockHeader *executionBlockHeader);
/**
* Obtains the withdrawals MPT root of a given execution block header.
*
* - The returned value is allocated in the given execution block header.
* It must neither be released nor written to, and the execution block
* header must not be released while the returned value is in use.
*
* @param executionBlockHeader Execution block header.
*
* @return Execution withdrawals root.
*/
ETH_RESULT_USE_CHECK
const ETHRoot *ETHExecutionBlockHeaderGetWithdrawalsRoot(
const ETHExecutionBlockHeader *executionBlockHeader);
#if __has_feature(nullability)
#pragma clang assume_nonnull end
#endif

View File

@ -8,7 +8,13 @@
{.push raises: [].}
import
std/[json, times],
stew/saturation_arith,
eth/common/eth_types_rlp,
eth/p2p/discoveryv5/random2,
json_rpc/jsonmarshal,
web3/ethtypes,
../el/el_manager,
../spec/eth2_apis/[eth2_rest_serialization, rest_light_client_calls],
../spec/[helpers, light_client_sync],
../sync/light_client_sync_helpers,
@ -1147,22 +1153,173 @@ func ETHExecutionPayloadHeaderGetBaseFeePerGas(
func ETHExecutionPayloadHeaderGetBlobGasUsed(
execution: ptr ExecutionPayloadHeader): cint {.exported.} =
## Obtains the data gas used of a given execution payload header.
## Obtains the blob gas used of a given execution payload header.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Data gas used.
## * Blob gas used.
execution[].blob_gas_used.cint
func ETHExecutionPayloadHeaderGetExcessBlobGas(
execution: ptr ExecutionPayloadHeader): cint {.exported.} =
## Obtains the excess data gas of a given execution payload header.
## Obtains the excess blob gas of a given execution payload header.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Excess data gas.
## * Excess blob gas.
execution[].excess_blob_gas.cint
type ETHExecutionBlockHeader = object
txRoot: Eth2Digest
withdrawalsRoot: Eth2Digest
proc ETHExecutionBlockHeaderCreateFromJson(
executionHash: ptr Eth2Digest,
blockHeaderJson: cstring): ptr ETHExecutionBlockHeader {.exported.} =
## Verifies that a JSON execution block header is valid and that it matches
## the given `executionHash`.
##
## * The JSON-RPC `eth_getBlockByHash` with params `[executionHash, false]`
## may be used to obtain execution block header data for a given execution
## block hash. Pass the response's `result` property to `blockHeaderJson`.
##
## * The execution block header must be destroyed with
## `ETHExecutionBlockHeaderDestroy` once no longer needed,
## to release memory.
##
## Parameters:
## * `executionHash` - Execution block hash.
## * `blockHeaderJson` - Buffer with JSON encoded header. NULL-terminated.
##
## Returns:
## * Pointer to an initialized execution block header - If successful.
## * `NULL` - If the given `blockHeaderJson` is malformed or incompatible.
##
## See:
## * https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getblockbyhash
let node =
try:
parseJson($blockHeaderJson)
except Exception:
return nil
var bdata: BlockObject
try:
fromJson(node, argName = "", bdata)
except KeyError, ValueError:
return nil
if bdata == nil:
return nil
# Sanity check
if bdata.hash.asEth2Digest != executionHash[]:
return nil
# Check fork consistency
static: doAssert totalSerializedFields(BlockObject) == 25,
"Only update this number once code is adjusted to check new fields!"
if bdata.baseFeePerGas.isNone and (
bdata.withdrawals.isSome or bdata.withdrawalsRoot.isSome or
bdata.blobGasUsed.isSome or bdata.excessBlobGas.isSome):
return nil
if bdata.withdrawalsRoot.isNone and (
bdata.blobGasUsed.isSome or bdata.excessBlobGas.isSome):
return nil
if bdata.withdrawals.isSome != bdata.withdrawalsRoot.isSome:
return nil
if bdata.blobGasUsed.isSome != bdata.excessBlobGas.isSome:
return nil
# Construct block header
static: # `GasInt` is signed. We only use it for hashing.
doAssert sizeof(int64) == sizeof(bdata.gasLimit)
doAssert sizeof(int64) == sizeof(bdata.gasUsed)
if distinctBase(bdata.timestamp) > int64.high.uint64:
return nil
if bdata.nonce.isNone:
return nil
let blockHeader = ExecutionBlockHeader(
parentHash: bdata.parentHash.asEth2Digest,
ommersHash: bdata.sha3Uncles.asEth2Digest,
coinbase: distinctBase(bdata.miner),
stateRoot: bdata.stateRoot.asEth2Digest,
txRoot: bdata.transactionsRoot.asEth2Digest,
receiptRoot: bdata.receiptsRoot.asEth2Digest,
bloom: distinctBase(bdata.logsBloom),
difficulty: bdata.difficulty,
blockNumber: distinctBase(bdata.number).u256,
gasLimit: cast[int64](bdata.gasLimit),
gasUsed: cast[int64](bdata.gasUsed),
timestamp: fromUnix(int64.saturate distinctBase(bdata.timestamp)),
extraData: distinctBase(bdata.extraData),
mixDigest: bdata.mixHash.asEth2Digest,
nonce: distinctBase(bdata.nonce.get),
fee: bdata.baseFeePerGas,
withdrawalsRoot:
if bdata.withdrawalsRoot.isSome:
some(bdata.withdrawalsRoot.get.asEth2Digest)
else:
none(ExecutionHash256),
blobGasUsed:
if bdata.blobGasUsed.isSome:
some distinctBase(bdata.blobGasUsed.get)
else:
none(uint64),
excessBlobGas:
if bdata.excessBlobGas.isSome:
some distinctBase(bdata.excessBlobGas.get)
else:
none(uint64))
if rlpHash(blockHeader) != executionHash[]:
return nil
let executionBlockHeader = ETHExecutionBlockHeader.new()
executionBlockHeader[] = ETHExecutionBlockHeader(
txRoot: blockHeader.txRoot,
withdrawalsRoot: blockHeader.withdrawalsRoot.get(ZERO_HASH))
executionBlockHeader.toUnmanagedPtr()
proc ETHExecutionBlockHeaderDestroy(
executionBlockHeader: ptr ETHExecutionBlockHeader) {.exported.} =
## Destroys an execution block header.
##
## * The execution block header must no longer be used after destruction.
##
## Parameters:
## * `executionBlockHeader` - Execution block header.
executionBlockHeader.destroy()
func ETHExecutionBlockHeaderGetTransactionsRoot(
executionBlockHeader: ptr ETHExecutionBlockHeader
): ptr Eth2Digest {.exported.} =
## Obtains the transactions MPT root of a given execution block header.
##
## * The returned value is allocated in the given execution block header.
## It must neither be released nor written to, and the execution block
## header must not be released while the returned value is in use.
##
## Parameters:
## * `executionBlockHeader` - Execution block header.
##
## Returns:
## * Execution transactions root.
addr executionBlockHeader[].txRoot
func ETHExecutionBlockHeaderGetWithdrawalsRoot(
executionBlockHeader: ptr ETHExecutionBlockHeader
): ptr Eth2Digest {.exported.} =
## Obtains the withdrawals MPT root of a given execution block header.
##
## * The returned value is allocated in the given execution block header.
## It must neither be released nor written to, and the execution block
## header must not be released while the returned value is in use.
##
## Parameters:
## * `executionBlockHeader` - Execution block header.
##
## Returns:
## * Execution withdrawals root.
addr executionBlockHeader[].withdrawalsRoot

File diff suppressed because one or more lines are too long

View File

@ -344,11 +344,41 @@ int main(void)
int safetyThreshold = ETHLightClientStoreGetSafetyThreshold(store);
printf("- safety_threshold: %d\n", safetyThreshold);
ETHLightClientHeader *copiedHeader =
ETHLightClientHeaderCreateCopy(ETHLightClientStoreGetFinalizedHeader(store));
ETHLightClientStoreDestroy(store);
ETHBeaconClockDestroy(beaconClock);
ETHForkDigestsDestroy(forkDigests);
ETHRootDestroy(genesisValRoot);
ETHConsensusConfigDestroy(cfg);
ETHRandomNumberDestroy(rng);
ETHRoot *copiedExecutionHash = ETHLightClientHeaderCopyExecutionHash(copiedHeader, cfg);
void *blockHeaderJson = readEntireFile(
__DIR__ "/test_files/executionBlockHeader.json", /* numBytes: */ NULL);
ETHExecutionBlockHeader *executionBlockHeader =
ETHExecutionBlockHeaderCreateFromJson(copiedExecutionHash, blockHeaderJson);
check(executionBlockHeader);
free(blockHeaderJson);
ETHRootDestroy(copiedExecutionHash);
ETHLightClientHeaderDestroy(copiedHeader);
printf("- finalized_header (execution block header):\n");
const ETHRoot *executionTransactionsRoot =
ETHExecutionBlockHeaderGetTransactionsRoot(executionBlockHeader);
printf(" - transactions_root: ");
printHexString(executionTransactionsRoot, sizeof *executionTransactionsRoot);
printf("\n");
const ETHRoot *executionWithdrawalsRoot =
ETHExecutionBlockHeaderGetWithdrawalsRoot(executionBlockHeader);
printf(" - withdrawals_root: ");
printHexString(executionWithdrawalsRoot, sizeof *executionWithdrawalsRoot);
printf("\n");
ETHExecutionBlockHeaderDestroy(executionBlockHeader);
return 0;
}

View File

@ -32,8 +32,9 @@ func toEther*(gwei: Gwei): Ether =
(gwei div ETH_TO_GWEI).Ether
type
ExecutionHash256* = eth_types.Hash256
ExecutionWithdrawal = eth_types.Withdrawal
ExecutionBlockHeader = eth_types.BlockHeader
ExecutionBlockHeader* = eth_types.BlockHeader
FinalityCheckpoints* = object
justified*: Checkpoint
@ -383,7 +384,7 @@ func compute_timestamp_at_slot*(state: ForkyBeaconState, slot: Slot): uint64 =
state.genesis_time + slots_since_genesis * SECONDS_PER_SLOT
proc computeTransactionsTrieRoot*(
payload: ForkyExecutionPayload): Hash256 =
payload: ForkyExecutionPayload): ExecutionHash256 =
if payload.transactions.len == 0:
return EMPTY_ROOT_HASH
@ -405,7 +406,8 @@ func toExecutionWithdrawal*(
# https://eips.ethereum.org/EIPS/eip-4895
proc computeWithdrawalsTrieRoot*(
payload: capella.ExecutionPayload | deneb.ExecutionPayload): Hash256 =
payload: capella.ExecutionPayload | deneb.ExecutionPayload
): ExecutionHash256 =
if payload.withdrawals.len == 0:
return EMPTY_ROOT_HASH
@ -429,7 +431,7 @@ proc payloadToBlockHeader*(
when typeof(payload).toFork >= ConsensusFork.Capella:
some payload.computeWithdrawalsTrieRoot()
else:
none(Hash256)
none(ExecutionHash256)
blobGasUsed =
when typeof(payload).toFork >= ConsensusFork.Deneb:
some payload.blob_gas_used