# beacon_chain # Copyright (c) 2022-2024 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. {.push raises: [].} {.used.} import std/[json, os, random, sequtils, strutils, times], chronos, stew/[base10, results], chronicles, unittest2, yaml, ../beacon_chain/beacon_chain_db, ../beacon_chain/spec/deposit_snapshots, ./consensus_spec/os_ops from eth/db/kvstore import kvStore from nimcrypto import toDigest from snappy import encode from stew/byteutils import hexToSeqByte const ROOT = "342cecb5a18945fbbda7c62ede3016f3" template databaseRoot: string = getTempDir().joinPath(ROOT) template key1: array[1, byte] = [byte(kOldDepositContractSnapshot)] type DepositSnapshotUpgradeProc = proc( old: OldDepositContractSnapshot ): DepositContractSnapshot {.gcsafe, raises: [].} proc ifNecessaryMigrateDCS(db: BeaconChainDB, upgradeProc: DepositSnapshotUpgradeProc) = if not db.hasDepositContractSnapshot(): let oldSnapshot = db.getUpgradableDepositSnapshot() if oldSnapshot.isSome: db.putDepositContractSnapshot upgradeProc(oldSnapshot.get) # Hexlified copy of # mainnet/metadata/genesis_deposit_contract_snapshot.ssz let ds1: seq[byte] = hexToSeqByte( """ eeea1373d4aa9e099d7c9deddb694db9aeb4577755ef83f9b6345ce4357d9abfca3bfce2c 304c4f52e0c83f96daf8c98a05f80281b62cf08f6be9c1bc10c0adbabcf2f74605a9eb36c f243bb5009259a3717d44df3caf02acc53ab49cfd2eeb6d4079d31e57638b3a6928ff3940 d0d06545ae164278597bb8d46053084c335eaf9585ef52fc5eaf1f11718df7988d3f414d8 b0be2e56e15d7ade9f5ee4cc7ee4a4c96f16c3a300034788ba8bf79c3125a697488006a4a 4288c38fdc4e9891891cae036d14b83ff1523749d4fabf5c91e8d455dce2f14eae3408dce 22f901efc7858ccad1a32af9e9796d3026ba18925103cad44cba4bdc1f3d3c23be125bba1 811f1e08405d5d180444147397ea0d4aebf12edff5cebc52cb05983c8d4bd2d4a93d66676 459ab2c5ca9d553a5c5599cc6992ed90edc939c51cc99d1820b5691914bfcab6eb8016c51 77e9e8f006e7893ea46b232b91b1f923b05273a927cd6d0aa14720bc149ce68f20809d6fe 55816acf09e72c14b54637dea24eb961558a7ac726d03ced287a817fa8fea71c90bd89955 b093d7c5908305177efa8289457190435298b2d5b2b67543e4dceaf2c8b7fdbdac12836a7 0ed910c34abcd10b3ddf53f640c85e35fef7e7ba4ab8c561fe9f1d763a32c65a1fbad5756 6bda135236257aa502116cb72c9347d10dca1b64a342b41a829cc7ba95e71499f57be2be3 cd00000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000005251 """.replace(" ", "").replace("\n", "") ) const ds1Root = toDigest("1a4c3cce02935defd159e4e207890ae26a325bf03e205c9ee94ca040ecce008a") proc fixture1() = ## Inserts a OldDepositContractSnapshot fixture. let compressed = snappy.encode(ds1) db = SqStoreRef.init(databaseRoot, "nbc").expect("") kv = kvStore(db.openKvStore("key_values", true).expect("")) kv.put(key1, compressed).expect("") db.close() proc inspectDCS( snapshot: OldDepositContractSnapshot | DepositContractSnapshot) = ## Inspects a DCS and checks if all of its data corresponds to ## what's encoded in ds1. const zero = toDigest("0000000000000000000000000000000000000000000000000000000000000000") const root = toDigest("1a4c3cce02935defd159e4e207890ae26a325bf03e205c9ee94ca040ecce008a") const want = [ "ca3bfce2c304c4f52e0c83f96daf8c98a05f80281b62cf08f6be9c1bc10c0adb", "abcf2f74605a9eb36cf243bb5009259a3717d44df3caf02acc53ab49cfd2eeb6", "d4079d31e57638b3a6928ff3940d0d06545ae164278597bb8d46053084c335ea", "f9585ef52fc5eaf1f11718df7988d3f414d8b0be2e56e15d7ade9f5ee4cc7ee4", "a4c96f16c3a300034788ba8bf79c3125a697488006a4a4288c38fdc4e9891891", "cae036d14b83ff1523749d4fabf5c91e8d455dce2f14eae3408dce22f901efc7", "858ccad1a32af9e9796d3026ba18925103cad44cba4bdc1f3d3c23be125bba18", "11f1e08405d5d180444147397ea0d4aebf12edff5cebc52cb05983c8d4bd2d4a", "93d66676459ab2c5ca9d553a5c5599cc6992ed90edc939c51cc99d1820b56919", "14bfcab6eb8016c5177e9e8f006e7893ea46b232b91b1f923b05273a927cd6d0", "aa14720bc149ce68f20809d6fe55816acf09e72c14b54637dea24eb961558a7a", "c726d03ced287a817fa8fea71c90bd89955b093d7c5908305177efa828945719", "0435298b2d5b2b67543e4dceaf2c8b7fdbdac12836a70ed910c34abcd10b3ddf", "53f640c85e35fef7e7ba4ab8c561fe9f1d763a32c65a1fbad57566bda1352362", "57aa502116cb72c9347d10dca1b64a342b41a829cc7ba95e71499f57be2be3cd", ] # Check eth1Block. check($snapshot.eth1Block == "eeea1373d4aa9e099d7c9deddb694db9aeb4577755ef83f9b6345ce4357d9abf") # Check branch. for i in 0..want.high(): check($snapshot.depositContractState.branch[i] == want[i]) for i in (want.high() + 1)..31: check(snapshot.depositContractState.branch[i] == zero) # Check deposit_count. check(snapshot.getDepositCountU64() == 21073) # Check deposit root. check(snapshot.getDepositRoot == root) proc inspectDCS(snapshot: DepositContractSnapshot, wantedBlockHeight: uint64) = inspectDCS(snapshot) check(snapshot.blockHeight == wantedBlockHeight) suite "DepositContractSnapshot": setup: randomize() teardown: # removeDir(databaseRoot) discard test "SSZ": var snapshot = OldDepositContractSnapshot() check(decodeSSZ(ds1, snapshot)) inspectDCS(snapshot) test "Migration": # Start with a fresh database. removeDir(databaseRoot) createDir(databaseRoot) # Make sure there's no DepositContractSnapshot yet. let db = BeaconChainDB.new(databaseRoot, inMemory=false) check(db.getDepositContractSnapshot().isErr()) # Setup fixture. fixture1() # Make sure there's still no DepositContractSnapshot as # BeaconChainDB::getDepositContractSnapshot() checks only for DCSv2. check(db.getDepositContractSnapshot().isErr()) # Migrate DB. db.ifNecessaryMigrateDCS do ( d: OldDepositContractSnapshot) -> DepositContractSnapshot: d.toDepositContractSnapshot(11052984) # Make sure now there actually is a snapshot. check(db.getDepositContractSnapshot().isOk()) # Inspect content. let snapshot = db.getDepositContractSnapshot().expect("") inspectDCS(snapshot, 11052984) test "depositCount": var rand = initRand(12345678) for i in 1..1000: let n = rand.next() let m = n mod 4294967296'u64 check(depositCountU64(depositCountBytes(m)) == m) test "isValid": const ZERO = toDigest("0000000000000000000000000000000000000000000000000000000000000000") # Use our hard-coded ds1 as a model. var model: OldDepositContractSnapshot check(decodeSSZ(ds1, model)) # Check initialization. blockHeight cannot be validated and may be 0. var dcs = model.toDepositContractSnapshot(11052984) check(dcs.isValid(ds1Root)) # Check eth1Block. dcs.eth1Block = ZERO check(not dcs.isValid(ds1Root)) dcs.eth1Block = model.eth1Block check(dcs.isValid(ds1Root)) # Check branch. for i in 0..len(dcs.depositContractState.branch)-1: dcs.depositContractState.branch[i] = ZERO check(not dcs.isValid(ds1Root)) dcs.depositContractState.branch = model.depositContractState.branch check(dcs.isValid(ds1Root)) # Check deposit count. for i in 0..len(dcs.depositContractState.deposit_count)-1: dcs.depositContractState.deposit_count[i] = 0 check(not dcs.isValid(ds1Root)) dcs.depositContractState.deposit_count = model.depositContractState.deposit_count check(dcs.isValid(ds1Root)) suite "EIP-4881": type DepositTestCase = object deposit_data: DepositData deposit_data_root: Eth2Digest eth1_data: Eth1Data block_height: uint64 snapshot: DepositTreeSnapshot proc loadTestCases( path: string ): seq[DepositTestCase] {.raises: [ IOError, KeyError, ValueError, YamlConstructionError, YamlParserError].} = yaml.loadToJson(os_ops.readFile(path))[0].mapIt: DepositTestCase( deposit_data: DepositData( pubkey: ValidatorPubKey.fromHex( it["deposit_data"]["pubkey"].getStr()).expect("valid"), withdrawal_credentials: Eth2Digest.fromHex( it["deposit_data"]["withdrawal_credentials"].getStr()), amount: Gwei(Base10.decode(uint64, it["deposit_data"]["amount"].getStr()).expect("valid")), signature: ValidatorSig.fromHex( it["deposit_data"]["signature"].getStr()).expect("valid")), deposit_data_root: Eth2Digest.fromHex(it["deposit_data_root"].getStr()), eth1_data: Eth1Data( deposit_root: Eth2Digest.fromHex( it["eth1_data"]["deposit_root"].getStr()), deposit_count: Base10.decode(uint64, it["eth1_data"]["deposit_count"].getStr()).expect("valid"), block_hash: Eth2Digest.fromHex( it["eth1_data"]["block_hash"].getStr())), block_height: uint64(it["block_height"].getInt()), snapshot: DepositTreeSnapshot( finalized: it["snapshot"]["finalized"].foldl((block: check: a[].add Eth2Digest.fromHex(b.getStr()) a), newClone default(List[ Eth2Digest, Limit DEPOSIT_CONTRACT_TREE_DEPTH]))[], deposit_root: Eth2Digest.fromHex( it["snapshot"]["deposit_root"].getStr()), deposit_count: uint64( it["snapshot"]["deposit_count"].getInt()), execution_block_hash: Eth2Digest.fromHex( it["snapshot"]["execution_block_hash"].getStr()), execution_block_height: uint64( it["snapshot"]["execution_block_height"].getInt()))) const path = currentSourcePath.rsplit(DirSep, 1)[0]/ ".."/"vendor"/"EIPs"/"assets"/"eip-4881"/"test_cases.yaml" let testCases = loadTestCases(path) for testCase in testCases: check testCase.deposit_data_root == hash_tree_root(testCase.deposit_data) test "empty_root": var empty = DepositsMerkleizer.init() check empty.getDepositsRoot() == Eth2Digest.fromHex( "0xd70a234731285c6804c2a4f56711ddb8c82c99740f207854891028af34e27e5e") test "deposit_cases": var tree = DepositsMerkleizer.init() for testCase in testCases: tree.addChunk testCase.deposit_data_root.data var snapshot = DepositsMerkleizer.init(tree.toDepositContractState()) let expected = testCase.eth1_data.deposit_root check: snapshot.getDepositsRoot() == expected tree.getDepositsRoot() == expected test "finalization": var tree = DepositsMerkleizer.init() for testCase in testCases[0 ..< 128]: tree.addChunk testCase.deposit_data_root.data let originalRoot = tree.getDepositsRoot() check originalRoot == testCases[127].eth1_data.deposit_root var finalized = DepositsMerkleizer.init() for testCase in testCases[0 .. 100]: finalized.addChunk testCase.deposit_data_root.data var snapshot = finalized.getTreeSnapshot( testCases[100].eth1_data.block_hash, testCases[100].block_height) check snapshot == testCases[100].snapshot var copy = DepositsMerkleizer.init(snapshot).expect("just produced") for testCase in testCases[101 ..< 128]: copy.addChunk testCase.deposit_data_root.data check tree.getDepositsRoot() == copy.getDepositsRoot() for testCase in testCases[101 .. 105]: finalized.addChunk testCase.deposit_data_root.data snapshot = finalized.getTreeSnapshot( testCases[105].eth1_data.block_hash, testCases[105].block_height) copy = DepositsMerkleizer.init(snapshot).expect("just produced") var fullTreeCopy = DepositsMerkleizer.init() for testCase in testCases[0 .. 105]: fullTreeCopy.addChunk testCase.deposit_data_root.data let depositRoots = testCases[106 ..< 128].mapIt(it.deposit_data_root) proofs1 = copy.addChunksAndGenMerkleProofs(depositRoots) proofs2 = fullTreeCopy.addChunksAndGenMerkleProofs(depositRoots) check proofs1 == proofs2 test "snapshot_cases": var tree = DepositsMerkleizer.init() for testCase in testCases: tree.addChunk testCase.deposit_data_root.data let snapshot = tree.getTreeSnapshot( testCase.eth1_data.block_hash, testCase.block_height) check snapshot == testCase.snapshot test "invalid_snapshot": let invalidSnapshot = DepositTreeSnapshot( finalized: default(FinalizedDepositTreeBranch), deposit_root: ZERO_HASH, deposit_count: 0, execution_block_hash: ZERO_HASH, execution_block_height: 0) check DepositsMerkleizer.init(invalidSnapshot).isNone()