diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index eb63d82e1..2a5eec613 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -24,9 +24,10 @@ OK: 1/1 Fail: 0/1 Skip: 0/1 + find ancestors [Preset: mainnet] OK + sanity check blocks [Preset: mainnet] OK + sanity check genesis roundtrip [Preset: mainnet] OK ++ sanity check state diff roundtrip [Preset: mainnet] OK + sanity check states [Preset: mainnet] OK ``` -OK: 5/5 Fail: 0/5 Skip: 0/5 +OK: 6/6 Fail: 0/6 Skip: 0/6 ## Beacon node ```diff + Compile OK @@ -266,6 +267,12 @@ OK: 1/1 Fail: 0/1 Skip: 0/1 + HashArray OK ``` OK: 1/1 Fail: 0/1 Skip: 0/1 +## state diff tests [Preset: mainnet] +```diff ++ delta-encoding/decoding roundtrip sanity [Preset: mainnet] OK ++ random slot differences [Preset: mainnet] OK +``` +OK: 2/2 Fail: 0/2 Skip: 0/2 ---TOTAL--- -OK: 145/154 Fail: 0/154 Skip: 9/154 +OK: 148/157 Fail: 0/157 Skip: 9/157 diff --git a/beacon_chain/beacon_chain_db.nim b/beacon_chain/beacon_chain_db.nim index 5f17a8bcb..c2e3e11e9 100644 --- a/beacon_chain/beacon_chain_db.nim +++ b/beacon_chain/beacon_chain_db.nim @@ -88,6 +88,13 @@ type ## that we were not able to verify against a `deposit_root` served ## by the web3 provider. This may happen on Geth nodes that serve ## only recent contract state data (i.e. only recent `deposit_roots`). + kHashToStateDiff + ## Instead of storing full BeaconStates, one can store only the diff from + ## a different state. As 75% of a typical BeaconState's serialized form's + ## the validators, which are mostly immutable and append-only, just using + ## a simple append-diff representation helps significantly. Various roots + ## are stored in a mod-increment pattern across fixed-sized arrays, which + ## addresses most of the rest of the BeaconState sizes. BeaconBlockSummary* = object slot*: Slot @@ -120,6 +127,9 @@ func subkey(kind: type SignedBeaconBlock, key: Eth2Digest): auto = func subkey(kind: type BeaconBlockSummary, key: Eth2Digest): auto = subkey(kHashToBlockSummary, key.data) +func subkey(kind: type BeaconStateDiff, key: Eth2Digest): auto = + subkey(kHashToStateDiff, key.data) + func subkey(root: Eth2Digest, slot: Slot): array[40, byte] = var ret: array[40, byte] # big endian to get a naturally ascending order on slots in sorted indices @@ -324,6 +334,9 @@ proc putStateRoot*(db: BeaconChainDB, root: Eth2Digest, slot: Slot, value: Eth2Digest) = db.put(subkey(root, slot), value) +proc putStateDiff*(db: BeaconChainDB, root: Eth2Digest, value: BeaconStateDiff) = + db.put(subkey(BeaconStateDiff, root), value) + proc delBlock*(db: BeaconChainDB, key: Eth2Digest) = db.backend.del(subkey(SignedBeaconBlock, key)).expect("working database") db.backend.del(subkey(BeaconBlockSummary, key)).expect("working database") @@ -334,6 +347,9 @@ proc delState*(db: BeaconChainDB, key: Eth2Digest) = proc delStateRoot*(db: BeaconChainDB, root: Eth2Digest, slot: Slot) = db.backend.del(subkey(root, slot)).expect("working database") +proc delStateDiff*(db: BeaconChainDB, root: Eth2Digest) = + db.backend.del(subkey(BeaconStateDiff, root)).expect("working database") + proc putHeadBlock*(db: BeaconChainDB, key: Eth2Digest) = db.put(subkey(kHeadBlock), key) @@ -397,6 +413,12 @@ proc getStateRoot*(db: BeaconChainDB, slot: Slot): Opt[Eth2Digest] = db.get(subkey(root, slot), Eth2Digest) +proc getStateDiff*(db: BeaconChainDB, + root: Eth2Digest): Opt[BeaconStateDiff] = + result.ok(BeaconStateDiff()) + if db.get(subkey(BeaconStateDiff, root), result.get) != GetResult.found: + result.err + proc getHeadBlock*(db: BeaconChainDB): Opt[Eth2Digest] = db.get(subkey(kHeadBlock), Eth2Digest) @@ -431,6 +453,9 @@ proc containsBlock*(db: BeaconChainDB, key: Eth2Digest): bool = proc containsState*(db: BeaconChainDB, key: Eth2Digest): bool = db.backend.contains(subkey(BeaconState, key)).expect("working database") +proc containsStateDiff*(db: BeaconChainDB, key: Eth2Digest): bool = + db.backend.contains(subkey(BeaconStateDiff, key)).expect("working database") + iterator getAncestors*(db: BeaconChainDB, root: Eth2Digest): TrustedSignedBeaconBlock = ## Load a chain of ancestors for blck - returns a list of blocks with the diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index af30eee7d..6f557fc64 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -461,6 +461,71 @@ type branch*: array[DEPOSIT_CONTRACT_TREE_DEPTH, Eth2Digest] deposit_count*: array[32, byte] # Uint256 + # https://github.com/ethereum/eth2.0-specs/blob/v1.0.0/specs/phase0/beacon-chain.md#validator + ValidatorStatus* = object + # This is a validator without the expensive, immutable, append-only parts + + effective_balance*: uint64 ##\ + ## Balance at stake + + slashed*: bool + + # Status epochs + activation_eligibility_epoch*: Epoch ##\ + ## When criteria for activation were met + + activation_epoch*: Epoch + exit_epoch*: Epoch + + withdrawable_epoch*: Epoch ##\ + ## When validator can withdraw or transfer funds + + BeaconStateDiff* = object + # Small and/or static; always include + slot*: Slot + latest_block_header*: BeaconBlockHeader + + # Mod-increment/circular + block_roots*: array[SLOTS_PER_EPOCH, Eth2Digest] + state_roots*: array[SLOTS_PER_EPOCH, Eth2Digest] + + # Append only; either 0 or 1 per epoch + historical_root_added*: bool + historical_root*: Eth2Digest + + # Replace + eth1_data*: Eth1Data + + eth1_data_votes_replaced*: bool + eth1_data_votes*: + List[Eth1Data, Limit(EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH)] + + # Replace + eth1_deposit_index*: uint64 + + # Validators come in two parts, the immutable public key and mutable + # entrance/exit/slashed information about that validator. + validator_statuses*: + List[ValidatorStatus, Limit VALIDATOR_REGISTRY_LIMIT] + + # Represent in full + balances*: List[uint64, Limit VALIDATOR_REGISTRY_LIMIT] + + # Mod-increment + randao_mix*: Eth2Digest + slashing*: uint64 + + # To start with, always overwrite, not append + previous_epoch_attestations*: + HashList[PendingAttestation, Limit(MAX_ATTESTATIONS * SLOTS_PER_EPOCH)] + current_epoch_attestations*: + HashList[PendingAttestation, Limit(MAX_ATTESTATIONS * SLOTS_PER_EPOCH)] + + justification_bits*: uint8 + previous_justified_checkpoint*: Checkpoint + current_justified_checkpoint*: Checkpoint + finalized_checkpoint*: Checkpoint + func shortValidatorKey*(state: BeaconState, validatorIdx: int): string = ($state.validators[validatorIdx].pubkey)[0..7] diff --git a/beacon_chain/statediff.nim b/beacon_chain/statediff.nim new file mode 100644 index 000000000..39404a71b --- /dev/null +++ b/beacon_chain/statediff.nim @@ -0,0 +1,208 @@ +# beacon_chain +# Copyright (c) 2020 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: [Defect].} + +import + ssz/types, + spec/[datatypes, digest, helpers] + +func diffModIncEpoch[T, U](hl: HashArray[U, T], startSlot: uint64): + array[SLOTS_PER_EPOCH, T] = + static: doAssert U.uint64 mod SLOTS_PER_EPOCH == 0 + doAssert startSlot mod SLOTS_PER_EPOCH == 0 + for i in startSlot ..< startSlot + SLOTS_PER_EPOCH: + result[i mod SLOTS_PER_EPOCH] = hl[i mod U.uint64] + +func applyModIncrement[T, U]( + ha: var HashArray[U, T], hl: array[SLOTS_PER_EPOCH, T], slot: uint64) = + var indexSlot = slot + + for item in hl: + ha[indexSlot mod U.uint64] = item + indexSlot += 1 + +func getImmutableValidatorData*(validator: Validator): ImmutableValidatorData = + ImmutableValidatorData( + pubkey: validator.pubkey, + withdrawal_credentials: validator.withdrawal_credentials) + +func applyValidatorIdentities( + validators: var HashList[Validator, Limit VALIDATOR_REGISTRY_LIMIT], + hl: auto) = + for item in hl: + validators.add Validator( + pubkey: item.pubkey, + withdrawal_credentials: item.withdrawal_credentials) + +func getValidatorStatus(validator: Validator): ValidatorStatus = + ValidatorStatus( + effective_balance: validator.effective_balance, + slashed: validator.slashed, + activation_eligibility_epoch: validator.activation_eligibility_epoch, + activation_epoch: validator.activation_epoch, + exit_epoch: validator.exit_epoch, + withdrawable_epoch: validator.withdrawable_epoch) + +func getValidatorStatuses(state: BeaconState): + List[ValidatorStatus, Limit VALIDATOR_REGISTRY_LIMIT] = + for validator in state.validators: + result.add getValidatorStatus(validator) + +func setValidatorStatuses( + validators: var HashList[Validator, Limit VALIDATOR_REGISTRY_LIMIT], + hl: List[ValidatorStatus, Limit VALIDATOR_REGISTRY_LIMIT]) = + doAssert validators.len == hl.len + + for i in 0 ..< hl.len: + validators[i].effective_balance = hl[i].effective_balance + validators[i].slashed = hl[i].slashed + + validators[i].activation_eligibility_epoch = + hl[i].activation_eligibility_epoch + validators[i].activation_epoch = hl[i].activation_epoch + validators[i].exit_epoch = hl[i].exit_epoch + validators[i].withdrawable_epoch = hl[i].withdrawable_epoch + +func deltaEncodeBalances*[T, U](balances: HashList[T, U]): List[T, U] = + if balances.len == 0: + return + + result.add balances[0] + + for i in 1 ..< balances.len: + result.add balances[i] - balances[i - 1] + + doAssert balances.len == result.len + +func deltaDecodeBalances*[T, U](encodedBalances: List[T, U]): HashList[T, U] = + var accum = 0'u64 + for i in 0 ..< encodedBalances.len: + accum += encodedBalances[i] + result.add accum + + doAssert encodedBalances.len == result.len + +func replaceOrAddEncodeEth1Votes[T, U](votes0, votes1: HashList[T, U]): + (bool, List[T, U]) = + let + num_votes0 = votes0.len + lower_bound = + if votes1.len < num_votes0 or + (num_votes0 > 0 and votes0[num_votes0 - 1] != votes1[num_votes0 - 1]): + # EPOCHS_PER_ETH1_VOTING_PERIOD epochs have passed, and + # eth1_data_votes has been reset/cleared. Because their + # deposit_index counts increase monotonically, it works + # to use only the last element for comparison. + 0 + else: + num_votes0 + + result[0] = lower_bound == 0 + for i in lower_bound ..< votes1.len: + result[1].add votes1[i] + +func replaceOrAddDecodeEth1Votes[T, U]( + votes0: var HashList[T, U], eth1_data_votes_replaced: bool, + votes1: List[T, U]) = + if eth1_data_votes_replaced: + votes0 = HashList[T, U]() + + for item in votes1: + votes0.add item + +func diffStates*(state0, state1: BeaconState): BeaconStateDiff = + doAssert state1.slot > state0.slot + doAssert state0.slot.isEpoch + doAssert state1.slot == state0.slot + SLOTS_PER_EPOCH + # TODO not here, but in chainDag, an isancestorof check + + doAssert state0.genesis_time == state1.genesis_time + doAssert state0.genesis_validators_root == state1.genesis_validators_root + doAssert state0.fork == state1.fork + doAssert state1.historical_roots.len - state0.historical_roots.len in [0, 1] + + let + historical_root_added = + state0.historical_roots.len != state1.historical_roots.len + (eth1_data_votes_replaced, eth1_data_votes) = + replaceOrAddEncodeEth1Votes(state0.eth1_data_votes, state1.eth1_data_votes) + + BeaconStateDiff( + slot: state1.slot, + latest_block_header: state1.latest_block_header, + + block_roots: diffModIncEpoch(state1.block_roots, state0.slot.uint64), + state_roots: diffModIncEpoch(state1.state_roots, state0.slot.uint64), + historical_root_added: historical_root_added, + historical_root: + if historical_root_added: + state1.historical_roots[state0.historical_roots.len] + else: + default(Eth2Digest), + eth1_data: state1.eth1_data, + eth1_data_votes_replaced: eth1_data_votes_replaced, + eth1_data_votes: eth1_data_votes, + eth1_deposit_index: state1.eth1_deposit_index, + + validatorStatuses: getValidatorStatuses(state1), + balances: deltaEncodeBalances(state1.balances), + + # RANDAO mixes gets updated every block, in place + randao_mix: state1.randao_mixes[state0.slot.compute_epoch_at_slot.uint64 mod + EPOCHS_PER_HISTORICAL_VECTOR.uint64], + slashing: state1.slashings[state0.slot.compute_epoch_at_slot.uint64 mod + EPOCHS_PER_HISTORICAL_VECTOR.uint64], + + previous_epoch_attestations: state1.previous_epoch_attestations, + current_epoch_attestations: state1.current_epoch_attestations, + + justification_bits: state1.justification_bits, + previous_justified_checkpoint: state1.previous_justified_checkpoint, + current_justified_checkpoint: state1.current_justified_checkpoint, + finalized_checkpoint: state1.finalized_checkpoint + ) + +func applyDiff*( + state: var BeaconState, + immutableValidators: openArray[ImmutableValidatorData], + stateDiff: BeaconStateDiff) = + # Carry over unchanged genesis_time, genesis_validators_root, and fork. + state.latest_block_header = stateDiff.latest_block_header + + applyModIncrement(state.block_roots, stateDiff.block_roots, state.slot.uint64) + applyModIncrement(state.state_roots, stateDiff.state_roots, state.slot.uint64) + if stateDiff.historical_root_added: + state.historical_roots.add stateDiff.historical_root + + state.eth1_data = stateDiff.eth1_data + replaceOrAddDecodeEth1Votes( + state.eth1_data_votes, stateDiff.eth1_data_votes_replaced, + stateDiff.eth1_data_votes) + state.eth1_deposit_index = stateDiff.eth1_deposit_index + + applyValidatorIdentities(state.validators, immutableValidators) + setValidatorStatuses(state.validators, stateDiff.validator_statuses) + state.balances = deltaDecodeBalances(stateDiff.balances) + + # RANDAO mixes gets updated every block, in place, so ensure there's always + # >=1 value from it + let epochIndex = + state.slot.epoch.uint64 mod EPOCHS_PER_HISTORICAL_VECTOR.uint64 + state.randao_mixes[epochIndex] = stateDiff.randao_mix + state.slashings[epochIndex] = stateDiff.slashing + + state.previous_epoch_attestations = stateDiff.previous_epoch_attestations + state.current_epoch_attestations = stateDiff.current_epoch_attestations + + state.justification_bits = stateDiff.justification_bits + state.previous_justified_checkpoint = stateDiff.previous_justified_checkpoint + state.current_justified_checkpoint = stateDiff.current_justified_checkpoint + state.finalized_checkpoint = stateDiff.finalized_checkpoint + + # Don't update slot until the end, because various other updates depend on it + state.slot = stateDiff.slot diff --git a/tests/all_tests.nim b/tests/all_tests.nim index ea9f168b2..e6a549ee3 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -16,6 +16,7 @@ import # Unit test ./test_beacon_node, ./test_beaconstate, ./test_bitseqs, + ./test_statediff, ./test_block_pool, ./test_datatypes, ./test_helpers, diff --git a/tests/test_beacon_chain_db.nim b/tests/test_beacon_chain_db.nim index ea56f7907..13788e433 100644 --- a/tests/test_beacon_chain_db.nim +++ b/tests/test_beacon_chain_db.nim @@ -144,6 +144,28 @@ suiteReport "Beacon chain DB" & preset(): check db.containsState(root) let state2 = db.getStateRef(root) + db.delState(root) + check not db.containsState(root) + db.close() + + check: + hash_tree_root(state2[]) == root + + wrappedTimedTest "sanity check state diff roundtrip" & preset(): + var + db = BeaconChainDB.init(defaultRuntimePreset, "", inMemory = true) + + # TODO htr(diff) probably not interesting/useful, but stand-in + let + stateDiff = BeaconStateDiff() + root = hash_tree_root(stateDiff) + + db.putStateDiff(root, stateDiff) + + check db.containsStateDiff(root) + let state2 = db.getStateDiff(root) + db.delStateDiff(root) + check not db.containsStateDiff(root) db.close() check: diff --git a/tests/test_statediff.nim b/tests/test_statediff.nim new file mode 100644 index 000000000..a972807eb --- /dev/null +++ b/tests/test_statediff.nim @@ -0,0 +1,173 @@ +# beacon_chain +# Copyright (c) 2018-2020 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.} + +import + options, sequtils, unittest, + ./testutil, + ./helpers/math_helpers, + ./mocking/mock_deposits, + ../beacon_chain/spec/[beaconstate, datatypes, digest, helpers, + state_transition, presets], + ../beacon_chain/[beacon_node_types, ssz, statediff], + ../beacon_chain/block_pools/[chain_dag, quarantine, clearance] + +when isMainModule: + import chronicles # or some random compile error happens... + +proc valid_deposit(state: var BeaconState) = + # TODO copy/pasted from foo; refactor + const deposit_amount = MAX_EFFECTIVE_BALANCE + let validator_index = state.validators.len + let deposit = mockUpdateStateForNewDeposit( + state, + uint64 validator_index, + deposit_amount, + flags = {} + ) + + let pre_val_count = state.validators.len + let pre_balance = if validator_index < pre_val_count: + state.balances[validator_index] + else: + 0 + check: + process_deposit(defaultRuntimePreset(), state, deposit, {}).isOk + state.validators.len == pre_val_count + 1 + state.balances.len == pre_val_count + 1 + state.balances[validator_index] == pre_balance + deposit.data.amount + state.validators[validator_index].effective_balance == + round_multiple_down( + min(MAX_EFFECTIVE_BALANCE, state.balances[validator_index]), + EFFECTIVE_BALANCE_INCREMENT + ) + +proc getTestStates(initialState: HashedBeaconState): + seq[ref HashedBeaconState] = + # Randomly generated slot numbers, with a jump to around + # SLOTS_PER_HISTORICAL_ROOT to force wraparound of those + # slot-based mod/increment fields. + const stateEpochs = [ + 0, 1, + + # Around minimal wraparound SLOTS_PER_HISTORICAL_ROOT wraparound + 5, 6, 7, 8, 9, + + 39, 40, 97, 98, 99, 113, 114, 115, 116, 130, 131, 145, 146, 192, 193, + 232, 233, 237, 238, + + # Approaching and passing SLOTS_PER_HISTORICAL_ROOT wraparound + 254, 255, 256, 257, 258] + + var + tmpState = assignClone(initialState) + cache = StateCache() + + for i, epoch in stateEpochs: + let slot = epoch.Epoch.compute_start_slot_at_epoch + if tmpState.data.slot < slot: + doAssert process_slots(tmpState[], slot, cache) + if i mod 3 == 0: + valid_deposit(tmpState.data) + doAssert tmpState.data.slot == slot + result.add assignClone(tmpState[]) + +template wrappedTimedTest(name: string, body: untyped) = + # `check` macro takes a copy of whatever it's checking, on the stack! + # This leads to stack overflow + # We can mitigate that by wrapping checks in proc + block: # Symbol namespacing + proc wrappedTest() = + timedTest name: + body + wrappedTest() + +suiteReport "state diff tests" & preset(): + setup: + var + db = makeTestDB(SLOTS_PER_EPOCH) + dag = init(ChainDAGRef, defaultRuntimePreset, db) + + wrappedTimedTest "random slot differences" & preset(): + let testStates = getTestStates(dag.headState.data) + + for i in 0 ..< testStates.len: + for j in (i+1) ..< testStates.len: + doAssert testStates[i].data.slot < testStates[j].data.slot + if testStates[i].data.slot + SLOTS_PER_EPOCH != testStates[j].data.slot: + continue + var tmpStateApplyBase = assignClone(testStates[i].data) + let diff = diffStates(testStates[i].data, testStates[j].data) + # Immutable parts of validators stored separately, so aren't part of + # the state diff. Synthesize required portion here for testing. + applyDiff( + tmpStateApplyBase[], + mapIt(testStates[j].data.validators.asSeq[ + testStates[i].data.validators.len .. + testStates[j].data.validators.len - 1], + it.getImmutableValidatorData), + diff) + check hash_tree_root(testStates[j].data) == + hash_tree_root(tmpStateApplyBase[]) + + wrappedTimedTest "delta-encoding/decoding roundtrip sanity" & preset(): + const + balances0 = [ + 18441870559'u64, 33446800397'u64, 11147100626'u64, 42603154274'u64, + 35932339237'u64, 59867680015'u64, 19647051219'u64, 63570367156'u64, + 43824455480'u64, 47579598334'u64, 22175553574'u64, 13601246675'u64, + 40046565997'u64, 19862192832'u64, 14541260920'u64, 25776220537'u64, + 53093805050'u64, 47082111792'u64, 24773067164'u64, 25673826779'u64, + 45827636611'u64, 31759878136'u64, 58103054360'u64, 50512782241'u64, + 31182839614'u64] + + balances1 = [ + 42080447134'u64, 9723866886'u64, 21528919469'u64, 60580554318'u64, + 37463193877'u64, 18143243334'u64, 32030042150'u64, 51881718936'u64, + 17259308484'u64, 18169637307'u64, 48769712906'u64, 51088432822'u64, + 52895655180'u64, 26116017983'u64, 39305430230'u64, 24222097345'u64, + 39462882494'u64, 39596015040'u64, 37160795641'u64, 35339479924'u64, + 33636108383'u64, 15242724015'u64, 60815628681'u64, 32706350007'u64, + 8978429438'u64, 21322048864'u64, 22997808541'u64, 37068275007'u64, + 50938101702'u64, 14620153832'u64, 55162721187'u64, 26298968647'u64, + 17648055143'u64, 59996602297'u64, 30878159440'u64, 22415848926'u64, + 20768842475'u64] + + balances2 = [ + 21675589964'u64, 13993227022'u64, 26438767944'u64, 41440196317'u64, + 41766461882'u64, 52661505859'u64, 42126387709'u64, 54445893868'u64, + 41509802863'u64, 36976355380'u64, 46813612650'u64, 41196532827'u64, + 23300952618'u64, 39031444988'u64, 37599530900'u64, 51850708563'u64, + 42648477675'u64, 48123583384'u64, 17001259539'u64, 41801119284'u64, + 44028789526'u64, 18179258736'u64, 50904978474'u64, 61199002779'u64, + 24333838181'u64, 39569287366'u64, 37714257632'u64, 27622624307'u64, + 63524818041'u64, 9470549646'u64, 41890932546'u64, 35929754455'u64, + 18073815159'u64, 61164677670'u64, 46599755663'u64, 39969979788'u64, + 19044350776'u64, 54818254044'u64, 48961544925'u64, 32004978192'u64, + 26380608851'u64, 31055862486'u64, 16774301884'u64, 34387075525'u64, + 30929489373'u64, 59224634642'u64, 39883929054'u64, 46052767920'u64, + 53119984525'u64] + + balances_empty: array[0, uint64] = [] + + balances_single = [26971116287'u64] + + template test_roundtrip_balances(state_balances: untyped) = + var balances = HashList[uint64, Limit VALIDATOR_REGISTRY_LIMIT]() + for balance in state_balances: + balances.add balance + + check deltaDecodeBalances[uint64, Limit VALIDATOR_REGISTRY_LIMIT]( + deltaEncodeBalances[uint64, Limit VALIDATOR_REGISTRY_LIMIT]( + balances)) == balances + + test_roundtrip_balances(balances_empty) + test_roundtrip_balances(balances_single) + test_roundtrip_balances(balances0) + test_roundtrip_balances(balances1) + test_roundtrip_balances(balances2)