diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index 75190920e..8e26093a9 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -19,26 +19,6 @@ import export extras, phase0, altair, merge -# https://github.com/ethereum/consensus-specs/blob/v1.0.1/specs/phase0/beacon-chain.md#is_valid_merkle_branch -func is_valid_merkle_branch*(leaf: Eth2Digest, branch: openArray[Eth2Digest], - depth: int, index: uint64, - root: Eth2Digest): bool {.nbench.}= - ## Check if ``leaf`` at ``index`` verifies against the Merkle ``root`` and - ## ``branch``. - var - value = leaf - buf: array[64, byte] - - for i in 0 ..< depth: - if (index div (1'u64 shl i)) mod 2 != 0: - buf[0..31] = branch[i].data - buf[32..63] = value.data - else: - buf[0..31] = value.data - buf[32..63] = branch[i].data - value = eth2digest(buf) - value == root - # https://github.com/ethereum/consensus-specs/blob/v1.0.1/specs/phase0/beacon-chain.md#increase_balance func increase_balance*(balance: var Gwei, delta: Gwei) = balance += delta diff --git a/beacon_chain/spec/datatypes/altair.nim b/beacon_chain/spec/datatypes/altair.nim index 738128b89..b76fd4a13 100644 --- a/beacon_chain/spec/datatypes/altair.nim +++ b/beacon_chain/spec/datatypes/altair.nim @@ -28,7 +28,7 @@ # stew/byteutils, import - std/[macros, typetraits], + std/[macros, typetraits, sets, hashes], chronicles, stew/[assign2, bitops2], json_serialization/types as jsonTypes @@ -174,6 +174,12 @@ type fork_version*: Version ##\ ## Fork version for the aggregate signature + # https://github.com/ethereum/consensus-specs/blob/v1.1.0-beta.4/specs/altair/sync-protocol.md#lightclientstore + LightClientStore* = object + snapshot*: LightClientSnapshot + valid_updates*: HashSet[LightClientUpdate] + ## TODO: This will benefit from being an ordered set + # https://github.com/ethereum/consensus-specs/blob/v1.1.0-beta.4/specs/altair/beacon-chain.md#beaconstate BeaconState* = object # Versioning @@ -518,3 +524,7 @@ func shortLog*(v: SyncAggregate): auto = $(v.sync_committee_bits) chronicles.formatIt SyncCommitteeMessage: shortLog(it) + +func hash*(x: LightClientUpdate): Hash = + hash(x.header.state_root.data) + diff --git a/beacon_chain/spec/helpers.nim b/beacon_chain/spec/helpers.nim index 1b404864a..7ab91304b 100644 --- a/beacon_chain/spec/helpers.nim +++ b/beacon_chain/spec/helpers.nim @@ -13,7 +13,7 @@ import # Standard lib std/[math, tables], # Third-party - stew/[byteutils, endians2], + stew/[byteutils, endians2, bitops2], # Internal ./datatypes/[phase0, altair, merge], ./eth2_merkleization, ./ssz_codec @@ -47,6 +47,26 @@ template epoch*(slot: Slot): Epoch = template isEpoch*(slot: Slot): bool = (slot mod SLOTS_PER_EPOCH) == 0 +# https://github.com/ethereum/consensus-specs/blob/v1.0.1/specs/phase0/beacon-chain.md#is_valid_merkle_branch +func is_valid_merkle_branch*(leaf: Eth2Digest, branch: openArray[Eth2Digest], + depth: int, index: uint64, + root: Eth2Digest): bool = + ## Check if ``leaf`` at ``index`` verifies against the Merkle ``root`` and + ## ``branch``. + var + value = leaf + buf: array[64, byte] + + for i in 0 ..< depth: + if (index div (1'u64 shl i)) mod 2 != 0: + buf[0..31] = branch[i].data + buf[32..63] = value.data + else: + buf[0..31] = value.data + buf[32..63] = branch[i].data + value = eth2digest(buf) + value == root + const SLOTS_PER_SYNC_COMMITTEE_PERIOD* = EPOCHS_PER_SYNC_COMMITTEE_PERIOD * SLOTS_PER_EPOCH @@ -213,3 +233,8 @@ func add_flag*(flags: ParticipationFlags, flag_index: int): ParticipationFlags = func has_flag*(flags: ParticipationFlags, flag_index: int): bool = let flag = ParticipationFlags(1'u8 shl flag_index) (flags and flag) == flag + +# https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-beta.3/specs/altair/sync-protocol.md#get_subtree_index +func get_subtree_index*(idx: GeneralizedIndex): uint64 = + uint64(idx mod (type(idx)(1) shl log2trunc(idx))) + diff --git a/beacon_chain/spec/light_client_sync.nim b/beacon_chain/spec/light_client_sync.nim new file mode 100644 index 000000000..3544752e1 --- /dev/null +++ b/beacon_chain/spec/light_client_sync.nim @@ -0,0 +1,113 @@ +import + std/sets, + stew/bitops2, + datatypes/altair, + helpers + +func branchIsAllZeros(branch: openarray[Eth2Digest]): bool = + for node in branch: + if node != Eth2Digest(): + return false + + return true + +# https://github.com/ethereum/consensus-specs/blob/v1.1.0-beta.3/specs/altair/sync-protocol.md#validate_light_client_update +proc validate_light_client_update*(snapshot: LightClientSnapshot, + update: LightClientUpdate, + genesis_validators_root: Eth2Digest): bool = + # Verify update slot is larger than snapshot slot + if update.header.slot <= snapshot.header.slot: + return false + + # Verify update does not skip a sync committee period + var snapshot_period = compute_epoch_at_slot(snapshot.header.slot) div EPOCHS_PER_SYNC_COMMITTEE_PERIOD + var update_period = compute_epoch_at_slot(update.header.slot) div EPOCHS_PER_SYNC_COMMITTEE_PERIOD + if update_period notin [snapshot_period, snapshot_period + 1]: + return false + + # Verify update header root is the finalized root of the finality header, if specified + # TODO: Use a view type instead of `unsafeAddr` + let signed_header = if update.finality_header == BeaconBlockHeader(): + if not branchIsAllZeros(update.finality_branch): + return false + unsafeAddr update.header + else: + if not is_valid_merkle_branch(hash_tree_root(update.header), + update.finality_branch, + log2trunc(FINALIZED_ROOT_INDEX), + get_subtree_index(FINALIZED_ROOT_INDEX), + update.finality_header.state_root): + return false + unsafeAddr update.finality_header + + # Verify update next sync committee if the update period incremented + # TODO: Use a view type instead of `unsafeAddr` + let sync_committee = if update_period == snapshot_period: + if not branchIsAllZeros(update.next_sync_committee_branch): + return false + unsafeAddr snapshot.current_sync_committee + else: + if not is_valid_merkle_branch(hash_tree_root(update.next_sync_committee), + update.next_sync_committee_branch, + log2trunc(NEXT_SYNC_COMMITTEE_INDEX), + get_subtree_index(NEXT_SYNC_COMMITTEE_INDEX), + update.header.state_root): + return false + unsafeAddr snapshot.next_sync_committee + + # Verify sync committee has sufficient participants + if countOnes(update.sync_committee_bits) < MIN_SYNC_COMMITTEE_PARTICIPANTS: + return false + + # Verify sync committee aggregate signature + # participant_pubkeys = [pubkey for (bit, pubkey) in zip(update.sync_committee_bits, sync_committee.pubkeys) if bit] + var participant_pubkeys: seq[ValidatorPubKey] + for idx, bit in update.sync_committee_bits: + if bit: + participant_pubkeys.add(sync_committee.pubkeys[idx]) + + let domain = compute_domain(DOMAIN_SYNC_COMMITTEE, update.fork_version, genesis_validators_root) + let signing_root = compute_signing_root(signed_header[], domain) + + blsFastAggregateVerify(participant_pubkeys, signing_root.data, update.sync_committee_signature) + +# https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-beta.3/specs/altair/sync-protocol.md#apply_light_client_update +proc apply_light_client_update(snapshot: var LightClientSnapshot, update: LightClientUpdate) = + let snapshot_period = compute_epoch_at_slot(snapshot.header.slot) div EPOCHS_PER_SYNC_COMMITTEE_PERIOD + let update_period = compute_epoch_at_slot(update.header.slot) div EPOCHS_PER_SYNC_COMMITTEE_PERIOD + if update_period == snapshot_period + 1: + snapshot.current_sync_committee = snapshot.next_sync_committee + snapshot.next_sync_committee = update.next_sync_committee + snapshot.header = update.header + +# https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-beta.3/specs/altair/sync-protocol.md#process_light_client_update +proc process_light_client_update(store: var LightClientStore, + update: LightClientUpdate, + current_slot: Slot, + genesis_validators_root: Eth2Digest): bool = + if not validate_light_client_update(store.snapshot, update, genesis_validators_root): + return false + store.valid_updates.incl(update) + + var update_timeout = SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD + let sync_committee_participants_count = countOnes(update.sync_committee_bits) + if sync_committee_participants_count * 3 >= update.sync_committee_bits.len * 2 and + update.finality_header != BeaconBlockHeader(): + # Apply update if (1) 2/3 quorum is reached and (2) we have a finality proof. + # Note that (2) means that the current light client design needs finality. + # It may be changed to re-organizable light client design. See the on-going issue eth2.0-specs#2182. + apply_light_client_update(store.snapshot, update) + store.valid_updates.clear() + elif current_slot > store.snapshot.header.slot + update_timeout: + var best_update_participants = 0 + var best_update: LightClientUpdate + for update in store.valid_updates: + let update_participants = countOnes(update.sync_committee_bits) + if update_participants > best_update_participants: + best_update = update + best_update_participants = update_participants + + # Forced best update when the update timeout has elapsed + apply_light_client_update(store.snapshot, best_update) + store.valid_updates.clear() + return true diff --git a/beacon_chain/spec/presets/mainnet/altair_preset.nim b/beacon_chain/spec/presets/mainnet/altair_preset.nim index e2b4bb765..f53ec4871 100644 --- a/beacon_chain/spec/presets/mainnet/altair_preset.nim +++ b/beacon_chain/spec/presets/mainnet/altair_preset.nim @@ -22,4 +22,4 @@ const # Sync protocol # --------------------------------------------------------------- # 1 - MIN_SYNC_COMMITTEE_PARTICIPANTS*: uint64 = 1 + MIN_SYNC_COMMITTEE_PARTICIPANTS* = 1 diff --git a/beacon_chain/spec/presets/minimal/altair_preset.nim b/beacon_chain/spec/presets/minimal/altair_preset.nim index ade62dcc9..e0bdc1a8d 100644 --- a/beacon_chain/spec/presets/minimal/altair_preset.nim +++ b/beacon_chain/spec/presets/minimal/altair_preset.nim @@ -22,4 +22,4 @@ const # Sync protocol # --------------------------------------------------------------- # 1 - MIN_SYNC_COMMITTEE_PARTICIPANTS*: uint64 = 1 + MIN_SYNC_COMMITTEE_PARTICIPANTS* = 1 diff --git a/beacon_chain/ssz/merkleization.nim b/beacon_chain/ssz/merkleization.nim index eaaee18e5..1d4b6a9d9 100644 --- a/beacon_chain/ssz/merkleization.nim +++ b/beacon_chain/ssz/merkleization.nim @@ -32,6 +32,8 @@ func binaryTreeHeight*(totalElements: Limit): int = bitWidth nextPow2(uint64 totalElements) type + GeneralizedIndex* = uint32 + SszMerkleizerImpl = object combinedChunks: ptr UncheckedArray[Eth2Digest] totalChunks: uint64