diff --git a/fluffy/network/history/experimental/beacon_chain_block_proof.nim b/fluffy/network/history/experimental/beacon_chain_block_proof.nim index 197608dec..afd97063d 100644 --- a/fluffy/network/history/experimental/beacon_chain_block_proof.nim +++ b/fluffy/network/history/experimental/beacon_chain_block_proof.nim @@ -99,14 +99,14 @@ func getHistoricalRootsIndex*(slot: Slot): uint64 = func getHistoricalRootsIndex*(blockHeader: BeaconBlockHeader): uint64 = getHistoricalRootsIndex(blockHeader.slot) -func getBlockRootsIndex*( - slot: Slot, historicalRootIndex: uint64): uint64 = - uint64(slot - historicalRootIndex * SLOTS_PER_HISTORICAL_ROOT) +func getBlockRootsIndex*(slot: Slot): uint64 = + slot mod SLOTS_PER_HISTORICAL_ROOT -func getBlockRootsIndex*( - blockHeader: BeaconBlockHeader, historicalRootIndex: uint64): uint64 = - getBlockRootsIndex(blockHeader.slot, historicalRootIndex) +func getBlockRootsIndex*(blockHeader: BeaconBlockHeader): uint64 = + getBlockRootsIndex(blockHeader.slot) +# Builds proof to be able to verify that the EL block hash is part of +# BeaconBlockBody for given root. func buildProof*( blockBody: bellatrix.BeaconBlockBody): Result[BeaconBlockBodyProof, string] = # 16 as there are 10 fields @@ -121,6 +121,8 @@ func buildProof*( ok(proof) +# Builds proof to be able to verify that the CL BlockBody root is part of +# BeaconBlockHeader for given root. func buildProof*( blockHeader: BeaconBlockHeader): Result[BeaconBlockHeaderProof, string] = # 5th field of container with 5 fields -> 7 + 5 @@ -131,6 +133,8 @@ func buildProof*( ok(proof) +# Builds proof to be able to verify that a BeaconBlock root is part of the +# HistoricalBatch for given root. func buildProof*( batch: HistoricalBatch, blockRootIndex: uint64): Result[HistoricalRootsProof, string] = @@ -148,8 +152,7 @@ func buildProof*( blockBody: bellatrix.BeaconBlockBody): Result[BeaconChainBlockProof, string] = let - historicalRootsIndex = getHistoricalRootsIndex(blockHeader) - blockRootIndex = getBlockRootsIndex(blockHeader, historicalRootsIndex) + blockRootIndex = getBlockRootsIndex(blockHeader) beaconBlockBodyProof = ? blockBody.buildProof() beaconBlockHeaderProof = ? blockHeader.buildProof() @@ -197,7 +200,7 @@ func verifyProof*( blockHash: Digest): bool = let historicalRootsIndex = getHistoricalRootsIndex(proof.slot) - blockRootIndex = getBlockRootsIndex(proof.slot, historicalRootsIndex) + blockRootIndex = getBlockRootsIndex(proof.slot) blockHash.verifyProof( proof.beaconBlockBodyProof, proof.beaconBlockBodyRoot) and diff --git a/fluffy/network/history/experimental/beacon_chain_block_proof_capella.nim b/fluffy/network/history/experimental/beacon_chain_block_proof_capella.nim new file mode 100644 index 000000000..f0734d06c --- /dev/null +++ b/fluffy/network/history/experimental/beacon_chain_block_proof_capella.nim @@ -0,0 +1,169 @@ +# Nimbus +# Copyright (c) 2023 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +# This is a PoC of how execution block headers in the Portal history network +# could be proven to be part of the canonical chain by means of a proof that +# exists out a chain of proofs. +# +# It is the equivalent of beacon_chain_block_proof.nim but after Capella fork +# making use of historical_summaries instead of the frozen historical_roots. +# +# +# The usage of this PoC can be seen in +# ./fluffy/tests/test_beacon_chain_block_proof_capella.nim +# +# TODO: Fit both beacon_chain_block_proof.nim and +# beacon_chain_block_proof_capella.nim better together and add fork selection +# on top of it. +# + +{.push raises: [].} + +import + stew/results, + ssz_serialization, ssz_serialization/[proofs, merkleization], + beacon_chain/spec/eth2_ssz_serialization, + beacon_chain/spec/presets, + beacon_chain/spec/datatypes/capella + +type + BeaconBlockBodyProof* = array[8, Digest] + BeaconBlockHeaderProof* = array[3, Digest] + HistoricalSummariesProof* = array[13, Digest] + + BeaconChainBlockProof* = object + # Total size (8 + 1 + 3 + 1 + 13) * 32 bytes + 4 bytes = 836 bytes + beaconBlockBodyProof: BeaconBlockBodyProof + beaconBlockBodyRoot: Digest + beaconBlockHeaderProof: BeaconBlockHeaderProof + beaconBlockHeaderRoot: Digest + historicalSummariesProof: HistoricalSummariesProof + slot: Slot + +func getHistoricalRootsIndex*(slot: Slot, cfg: RuntimeConfig): uint64 = + (slot - cfg.CAPELLA_FORK_EPOCH * SLOTS_PER_EPOCH) div SLOTS_PER_HISTORICAL_ROOT + +func getHistoricalRootsIndex*( + blockHeader: BeaconBlockHeader, cfg: RuntimeConfig): uint64 = + getHistoricalRootsIndex(blockHeader.slot, cfg) + +func getBlockRootsIndex*(slot: Slot): uint64 = + slot mod SLOTS_PER_HISTORICAL_ROOT + +func getBlockRootsIndex*(blockHeader: BeaconBlockHeader): uint64 = + getBlockRootsIndex(blockHeader.slot) + +# Builds proof to be able to verify that the EL block hash is part of +# BeaconBlockBody for given root. +func buildProof*( + blockBody: capella.BeaconBlockBody): Result[BeaconBlockBodyProof, string] = + # 16 as there are 10 fields + # 9 as index (pos) of field = 9 + let gIndexTopLevel = (1 * 1 * 16 + 9) + # 16 as there are 14 fields + # 12 as pos of field = 12 + let gIndex = GeneralizedIndex(gIndexTopLevel * 1 * 16 + 12) + + var proof: BeaconBlockBodyProof + ? blockBody.build_proof(gIndex, proof) + + ok(proof) + +# Builds proof to be able to verify that the CL BlockBody root is part of +# BeaconBlockHeader for given root. +func buildProof*( + blockHeader: BeaconBlockHeader): Result[BeaconBlockHeaderProof, string] = + # 5th field of container with 5 fields -> 7 + 5 + let gIndex = GeneralizedIndex(12) + + var proof: BeaconBlockHeaderProof + ? blockHeader.build_proof(gIndex, proof) + + ok(proof) + +# Builds proof to be able to verify that a BeaconBlock root is part of the +# block_roots for given root. +func buildProof*( + blockRoots: array[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest], + blockRootIndex: uint64): + Result[HistoricalSummariesProof, string] = + # max list size * 1 is start point of leaves + let gIndex = GeneralizedIndex(SLOTS_PER_HISTORICAL_ROOT + blockRootIndex) + + var proof: HistoricalSummariesProof + ? blockRoots.build_proof(gIndex, proof) + + ok(proof) + +# Put all 3 above proofs together to be able to verify that an EL block hash +# is part of historical_summaries and thus canonical. +func buildProof*( + blockRoots: array[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest], + blockHeader: BeaconBlockHeader, + blockBody: capella.BeaconBlockBody, + cfg: RuntimeConfig): + Result[BeaconChainBlockProof, string] = + let + blockRootIndex = getBlockRootsIndex(blockHeader) + + beaconBlockBodyProof = ? blockBody.buildProof() + beaconBlockHeaderProof = ? blockHeader.buildProof() + historicalSummariesProof = ? blockRoots.buildProof(blockRootIndex) + + ok(BeaconChainBlockProof( + beaconBlockBodyProof: beaconBlockBodyProof, + beaconBlockBodyRoot: hash_tree_root(blockBody), + beaconBlockHeaderProof: beaconBlockHeaderProof, + beaconBlockHeaderRoot: hash_tree_root(blockHeader), + historicalSummariesProof: historicalSummariesProof, + slot: blockHeader.slot + )) + +func verifyProof*( + blockHash: Digest, + proof: BeaconBlockBodyProof, + blockBodyRoot: Digest): bool = + let + gIndexTopLevel = (1 * 1 * 16 + 9) + gIndex = GeneralizedIndex(gIndexTopLevel * 1 * 16 + 12) + + verify_merkle_multiproof(@[blockHash], proof, @[gIndex], blockBodyRoot) + +func verifyProof*( + blockBodyRoot: Digest, + proof: BeaconBlockHeaderProof, + blockHeaderRoot: Digest): bool = + let gIndex = GeneralizedIndex(12) + + verify_merkle_multiproof(@[blockBodyRoot], proof, @[gIndex], blockHeaderRoot) + +func verifyProof*( + blockHeaderRoot: Digest, + proof: HistoricalSummariesProof, + historicalRoot: Digest, + blockRootIndex: uint64): bool = + let gIndex = GeneralizedIndex(SLOTS_PER_HISTORICAL_ROOT + blockRootIndex) + + verify_merkle_multiproof(@[blockHeaderRoot], proof, @[gIndex], historicalRoot) + +func verifyProof*( + historical_summaries: HashList[HistoricalSummary, Limit HISTORICAL_ROOTS_LIMIT], + proof: BeaconChainBlockProof, + blockHash: Digest, + cfg: RuntimeConfig): bool = + let + historicalRootsIndex = getHistoricalRootsIndex(proof.slot, cfg) + blockRootIndex = getBlockRootsIndex(proof.slot) + + blockHash.verifyProof( + proof.beaconBlockBodyProof, proof.beaconBlockBodyRoot) and + proof.beaconBlockBodyRoot.verifyProof( + proof.beaconBlockHeaderProof, proof.beaconBlockHeaderRoot) and + proof.beaconBlockHeaderRoot.verifyProof( + proof.historicalSummariesProof, + historical_summaries[historicalRootsIndex].block_summary_root, + blockRootIndex) diff --git a/fluffy/tests/all_fluffy_tests.nim b/fluffy/tests/all_fluffy_tests.nim index 67dad1fb1..d127cedb2 100644 --- a/fluffy/tests/all_fluffy_tests.nim +++ b/fluffy/tests/all_fluffy_tests.nim @@ -16,5 +16,6 @@ import ./test_content_db, ./test_discovery_rpc, ./test_beacon_chain_block_proof, + ./test_beacon_chain_block_proof_capella, ./test_beacon_chain_historical_roots, ./test_beacon_chain_historical_summaries diff --git a/fluffy/tests/test_beacon_chain_block_proof.nim b/fluffy/tests/test_beacon_chain_block_proof.nim index d5701da6c..580658708 100644 --- a/fluffy/tests/test_beacon_chain_block_proof.nim +++ b/fluffy/tests/test_beacon_chain_block_proof.nim @@ -80,7 +80,7 @@ suite "Beacon Chain Block Proofs": let beaconBlock = blocks[i].message historicalRootsIndex = getHistoricalRootsIndex(beaconBlock.slot) - blockRootIndex = getBlockRootsIndex(beaconBlock.slot, historicalRootsIndex) + blockRootIndex = getBlockRootsIndex(beaconBlock.slot) let res = buildProof(batch, blockRootIndex) check res.isOk() diff --git a/fluffy/tests/test_beacon_chain_block_proof_capella.nim b/fluffy/tests/test_beacon_chain_block_proof_capella.nim new file mode 100644 index 000000000..8df98fe96 --- /dev/null +++ b/fluffy/tests/test_beacon_chain_block_proof_capella.nim @@ -0,0 +1,172 @@ +# Nimbus +# Copyright (c) 2022-2023 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +{.used.} + +{.push raises: [].} + +import + unittest2, + beacon_chain/spec/forks, + beacon_chain/spec/datatypes/capella, + beacon_chain/../tests/testblockutil, + # Mock helpers + beacon_chain/../tests/mocking/mock_genesis, + + ../network/history/experimental/beacon_chain_block_proof_capella + +# Test suite for the proofs: +# - historicalSummariesProof +# - BeaconBlockHeaderProof +# - BeaconBlockBodyProof +# and as last +# - the chain of proofs, BeaconChainBlockProof: +# BlockHash || BlockHeader +# -> BeaconBlockBodyProof +# -> BeaconBlockHeaderProof +# -> historicalSummariesProof +# historical_summaries +# +# Note: The last test makes the others redundant, but keeping them all around +# for now as it might be sufficient to go with just historicalSummariesProof +# (and perhaps BeaconBlockHeaderProof), see comments in beacon_chain_proofs.nim. +# +# TODO: +# - Add more blocks to reach 1+ historical summaries, to make sure that +# indexing is properly tested. +# - Adjust tests to test usage of historical_summaries and historical_roots +# together. + +suite "Beacon Chain Block Proofs": + let + cfg = block: + var res = defaultRuntimeConfig + res.ALTAIR_FORK_EPOCH = GENESIS_EPOCH + res.BELLATRIX_FORK_EPOCH = GENESIS_EPOCH + # res.CAPELLA_FORK_EPOCH = GENESIS_EPOCH + res.CAPELLA_FORK_EPOCH = Epoch(256) + res + state = newClone(initGenesisState(cfg = cfg)) + var cache = StateCache() + + var blocks: seq[capella.SignedBeaconBlock] + # Note: + # Adding 8192*2 blocks. First block is genesis block and not one of these. + # Then one extra block is needed to get the historical roots, block + # roots and state roots processed. + # index i = 0 is second block. + # index i = 8190 is 8192th block and last one that is part of the first + # historical root + + # genesis + 8191 slots, next one will be capella fork + for i in 0.. 16383 + for i in 0..= ConsensusFork.Capella: + let historical_summaries = forkyState.data.historical_summaries + + # for i in 0..<(SLOTS_PER_HISTORICAL_ROOT - 1): # Test all blocks + for i in blocksToTest: + let + beaconBlock = blocks[i].message + historicalRootsIndex = getHistoricalRootsIndex(beaconBlock.slot, cfg) + blockRootIndex = getBlockRootsIndex(beaconBlock.slot) + + let res = buildProof(blockRoots, blockRootIndex) + check res.isOk() + let proof = res.get() + + check verifyProof( + blocks[i].root, proof, + historical_summaries[historicalRootsIndex].block_summary_root, blockRootIndex) + + test "BeaconBlockHeaderProof for BeaconBlockBody": + # for i in 0..<(SLOTS_PER_HISTORICAL_ROOT - 1): # Test all blocks + for i in blocksToTest: + let + beaconBlock = blocks[i].message + beaconBlockHeader = BeaconBlockHeader( + slot: beaconBlock.slot, + proposer_index: beaconBlock.proposer_index, + parent_root: beaconBlock.parent_root, + state_root: beaconBlock.state_root, + body_root: hash_tree_root(beaconBlock.body) + ) + beaconBlockBody = beaconBlock.body + + let res = buildProof(beaconBlockHeader) + check res.isOk() + let proof = res.get() + + let leave = hash_tree_root(beaconBlockBody) + check verifyProof(leave, proof, blocks[i].root) + + test "BeaconBlockBodyProof for Execution BlockHeader": + # for i in 0..<(SLOTS_PER_HISTORICAL_ROOT - 1): # Test all blocks + for i in blocksToTest: + let beaconBlockBody = blocks[i].message.body + + let res = buildProof(beaconBlockBody) + check res.isOk() + let proof = res.get() + + let leave = beaconBlockBody.execution_payload.block_hash + let root = hash_tree_root(beaconBlockBody) + check verifyProof(leave, proof, root) + + test "BeaconChainBlockProof for Execution BlockHeader": + let + blockRoots = getStateField(state[], block_roots).data + + withState(state[]): + when consensusFork >= ConsensusFork.Capella: + let historical_summaries = forkyState.data.historical_summaries + + # for i in 0..<(SLOTS_PER_HISTORICAL_ROOT - 1): # Test all blocks + for i in blocksToTest: + let + beaconBlock = blocks[i].message + beaconBlockHeader = BeaconBlockHeader( + slot: beaconBlock.slot, + proposer_index: beaconBlock.proposer_index, + parent_root: beaconBlock.parent_root, + state_root: beaconBlock.state_root, + body_root: hash_tree_root(beaconBlock.body) + ) + beaconBlockBody = beaconBlock.body + + # Normally we would have an execution BlockHeader that holds this + # value, but we skip the creation of that header for now and just take + # the blockHash from the execution payload. + blockHash = beaconBlockBody.execution_payload.block_hash + + let proofRes = buildProof(blockRoots, beaconBlockHeader, beaconBlockBody, cfg) + check proofRes.isOk() + let proof = proofRes.get() + + check verifyProof(historical_summaries, proof, blockHash, cfg)