From a0c518cb4f68f7d7047a2ba88ae4ab465b2a0445 Mon Sep 17 00:00:00 2001 From: tersec Date: Tue, 17 Aug 2021 08:07:17 +0000 Subject: [PATCH] sync committee/aggregate signature signing and verification (#2784) * sync committee/aggregate signature signing and verification * add message signature tests --- AllTests-mainnet.md | 17 +- beacon_chain/spec/signatures.nim | 94 ++++++- beacon_chain/validators/validator_pool.nim | 54 +++- tests/all_tests.nim | 1 + tests/test_message_signatures.nim | 286 +++++++++++++++++++++ 5 files changed, 442 insertions(+), 10 deletions(-) create mode 100644 tests/test_message_signatures.nim diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index c763c324f..8bc895da1 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -38,12 +38,13 @@ OK: 12/12 Fail: 0/12 Skip: 0/12 OK: 1/1 Fail: 0/1 Skip: 0/1 ## Bit fields ```diff ++ isZeros OK + iterating words OK + overlaps OK + roundtrips BitArray OK + roundtrips BitSeq OK ``` -OK: 4/4 Fail: 0/4 Skip: 0/4 +OK: 5/5 Fail: 0/5 Skip: 0/5 ## Block pool processing [Preset: mainnet] ```diff + Adding the same block twice returns a Duplicate error [Preset: mainnet] OK @@ -114,6 +115,18 @@ OK: 3/3 Fail: 0/3 Skip: 0/3 + Mocked start private key OK ``` OK: 3/3 Fail: 0/3 Skip: 0/3 +## Message signatures +```diff ++ Aggregate and proof signatures OK ++ Attestation signatures OK ++ Deposit signatures OK ++ Slot signatures OK ++ Sync committee message signatures OK ++ Sync committee selection proof signatures OK ++ Sync committee signed contribution and proof signatures OK ++ Voluntary exit signatures OK +``` +OK: 8/8 Fail: 0/8 Skip: 0/8 ## Old database versions [Preset: mainnet] ```diff + pre-1.1.0 OK @@ -257,4 +270,4 @@ OK: 3/3 Fail: 0/3 Skip: 0/3 OK: 1/1 Fail: 0/1 Skip: 0/1 ---TOTAL--- -OK: 131/131 Fail: 0/131 Skip: 0/131 +OK: 140/140 Fail: 0/140 Skip: 0/140 diff --git a/beacon_chain/spec/signatures.nim b/beacon_chain/spec/signatures.nim index 3e59061a2..f5513df9a 100644 --- a/beacon_chain/spec/signatures.nim +++ b/beacon_chain/spec/signatures.nim @@ -32,7 +32,7 @@ func compute_slot_root*( epoch = compute_epoch_at_slot(slot) domain = get_domain( fork, DOMAIN_SELECTION_PROOF, epoch, genesis_validators_root) - result = compute_signing_root(slot, domain) + compute_signing_root(slot, domain) # https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/validator.md#aggregation-selection func get_slot_signature*( @@ -55,9 +55,8 @@ proc verify_slot_signature*( func compute_epoch_root*( fork: Fork, genesis_validators_root: Eth2Digest, epoch: Epoch ): Eth2Digest = - let - domain = get_domain(fork, DOMAIN_RANDAO, epoch, genesis_validators_root) - result = compute_signing_root(epoch, domain) + let domain = get_domain(fork, DOMAIN_RANDAO, epoch, genesis_validators_root) + compute_signing_root(epoch, domain) # https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/validator.md#randao-reveal func get_epoch_signature*( @@ -82,7 +81,7 @@ func compute_block_root*( epoch = compute_epoch_at_slot(slot) domain = get_domain( fork, DOMAIN_BEACON_PROPOSER, epoch, genesis_validators_root) - result = compute_signing_root(root, domain) + compute_signing_root(root, domain) # https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/validator.md#signature func get_block_signature*( @@ -110,7 +109,7 @@ func compute_aggregate_and_proof_root*(fork: Fork, genesis_validators_root: Eth2 epoch = compute_epoch_at_slot(aggregate_and_proof.aggregate.data.slot) domain = get_domain( fork, DOMAIN_AGGREGATE_AND_PROOF, epoch, genesis_validators_root) - result = compute_signing_root(aggregate_and_proof, domain) + compute_signing_root(aggregate_and_proof, domain) # https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/validator.md#broadcast-aggregate func get_aggregate_and_proof_signature*(fork: Fork, genesis_validators_root: Eth2Digest, @@ -140,7 +139,39 @@ func compute_attestation_root*( epoch = attestation_data.target.epoch domain = get_domain( fork, DOMAIN_BEACON_ATTESTER, epoch, genesis_validators_root) - result = compute_signing_root(attestation_data, domain) + compute_signing_root(attestation_data, domain) + +# https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.7/specs/altair/validator.md#prepare-sync-committee-message +func sync_committee_msg_signing_root*( + fork: Fork, epoch: Epoch, + genesis_validators_root: Eth2Digest, + block_root: Eth2Digest): Eth2Digest = + let domain = get_domain(fork, DOMAIN_SYNC_COMMITTEE, epoch, genesis_validators_root) + compute_signing_root(block_root, domain) + +# https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.7/specs/altair/validator.md#signature +func contribution_and_proof_signing_root*( + fork: Fork, + genesis_validators_root: Eth2Digest, + msg: ContributionAndProof): Eth2Digest = + let domain = get_domain(fork, DOMAIN_CONTRIBUTION_AND_PROOF, + msg.contribution.slot.epoch, + genesis_validators_root) + compute_signing_root(msg, domain) + +# https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.7/specs/altair/validator.md#aggregation-selection +proc sync_committee_selection_proof_signing_root*( + fork: Fork, + genesis_validators_root: Eth2Digest, + slot: Slot, + subcommittee_index: uint64): Eth2Digest = + let + domain = get_domain(fork, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, + slot.epoch, genesis_validators_root) + signing_data = SyncAggregatorSelectionData( + slot: slot, + subcommittee_index: subcommittee_index) + compute_signing_root(signing_data, domain) # https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/validator.md#aggregate-signature func get_attestation_signature*( @@ -211,3 +242,52 @@ proc verify_voluntary_exit_signature*( signing_root = compute_signing_root(voluntary_exit, domain) blsVerify(pubkey, signing_root.data, signature) + +proc verify_sync_committee_message_signature*( + epoch: Epoch, + beacon_block_root: Eth2Digest, + fork: Fork, + genesis_validators_root: Eth2Digest, + pubkey: CookedPubKey, + signature: CookedSig): bool = + let + domain = get_domain( + fork, DOMAIN_SYNC_COMMITTEE, epoch, genesis_validators_root) + signing_root = compute_signing_root(beacon_block_root, domain) + + blsVerify(pubkey, signing_root.data, signature) + +# https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.7/specs/altair/validator.md#aggregation-selection +proc is_sync_committee_aggregator*(signature: ValidatorSig): bool = + let + signatureDigest = eth2digest(signature.blob) + modulo = max(1'u64, (SYNC_COMMITTEE_SIZE div SYNC_COMMITTEE_SUBNET_COUNT) div TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE) + bytes_to_uint64(signatureDigest.data.toOpenArray(0, 7)) mod modulo == 0 + +proc verify_signed_contribution_and_proof_signature*( + msg: SignedContributionAndProof, + fork: Fork, + genesis_validators_root: Eth2Digest, + pubkey: ValidatorPubKey | CookedPubKey): bool = + let + domain = get_domain( + fork, DOMAIN_CONTRIBUTION_AND_PROOF, msg.message.contribution.slot.epoch, genesis_validators_root) + signing_root = compute_signing_root(msg.message, domain) + + blsVerify(pubkey, signing_root.data, msg.signature) + +proc verify_selection_proof_signature*( + msg: ContributionAndProof, + fork: Fork, + genesis_validators_root: Eth2Digest, + pubkey: ValidatorPubKey | CookedPubKey): bool = + let + slot = msg.contribution.slot + domain = get_domain( + fork, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, slot.epoch, genesis_validators_root) + signing_data = SyncAggregatorSelectionData( + slot: slot, + subcommittee_index: msg.contribution.subcommittee_index) + signing_root = compute_signing_root(signing_data, domain) + + blsVerify(pubkey, signing_root.data, msg.selection_proof) diff --git a/beacon_chain/validators/validator_pool.nim b/beacon_chain/validators/validator_pool.nim index ab3dd8654..9b415f8aa 100644 --- a/beacon_chain/validators/validator_pool.nim +++ b/beacon_chain/validators/validator_pool.nim @@ -13,7 +13,7 @@ import json_serialization/std/[sets, net], eth/db/[kvstore, kvstore_sqlite3], ../spec/[signatures, helpers], - ../spec/datatypes/base, + ../spec/datatypes/[phase0, altair], ../beacon_node_types, ./slashing_protection @@ -152,6 +152,58 @@ proc signAggregateAndProof*(v: AttachedValidator, aggregate_and_proof) await signWithRemoteValidator(v, root) +# https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.7/specs/altair/validator.md#prepare-sync-committee-message +proc signSyncCommitteeMessage*(v: AttachedValidator, + slot: Slot, + fork: Fork, + genesis_validators_root: Eth2Digest, + block_root: Eth2Digest): Future[SyncCommitteeMessage] {.async.} = + let + signing_root = sync_committee_msg_signing_root( + fork, slot.epoch, genesis_validators_root, block_root) + + let signature = if v.kind == inProcess: + blsSign(v.privkey, signing_root.data).toValidatorSig + else: + await signWithRemoteValidator(v, signing_root) + + return SyncCommitteeMessage( + slot: slot, + beacon_block_root: block_root, + validator_index: v.index.get.uint64, + signature: signature) + +# https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.7/specs/altair/validator.md#aggregation-selection +proc getSyncCommitteeSelectionProof*( + v: AttachedValidator, + fork: Fork, + genesis_validators_root: Eth2Digest, + slot: Slot, + subcommittee_index: uint64): Future[ValidatorSig] {.async.} = + let + signing_root = sync_committee_selection_proof_signing_root( + fork, genesis_validators_root, slot, subcommittee_index) + + return if v.kind == inProcess: + blsSign(v.privkey, signing_root.data).toValidatorSig + else: + await signWithRemoteValidator(v, signing_root) + +# https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.7/specs/altair/validator.md#signature +proc sign*( + v: AttachedValidator, + msg: ref SignedContributionAndProof, + fork: Fork, + genesis_validators_root: Eth2Digest) {.async.} = + let + signing_root = contribution_and_proof_signing_root( + fork, genesis_validators_root, msg.message) + + msg.signature = if v.kind == inProcess: + blsSign(v.privkey, signing_root.data).toValidatorSig + else: + await signWithRemoteValidator(v, signing_root) + # https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/validator.md#randao-reveal func genRandaoReveal*(k: ValidatorPrivKey, fork: Fork, genesis_validators_root: Eth2Digest, diff --git a/tests/all_tests.nim b/tests/all_tests.nim index e84e4e8dc..c6089bc0b 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -21,6 +21,7 @@ import # Unit test ./test_helpers, ./test_honest_validator, ./test_interop, + ./test_message_signatures, ./test_peer_pool, ./test_ssz, ./test_statediff, diff --git a/tests/test_message_signatures.nim b/tests/test_message_signatures.nim new file mode 100644 index 000000000..337e91d79 --- /dev/null +++ b/tests/test_message_signatures.nim @@ -0,0 +1,286 @@ +# beacon_chain +# Copyright (c) 2018-2021 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 + unittest2, + ../beacon_chain/spec/[crypto, signatures], + ./mocking/mock_validator_keys + +suite "Message signatures": + let + fork0 = Fork(current_version: Version [byte 0x40, 0x21, 0x8c, 0xe8]) + fork1 = Fork(current_version: Version [byte 0x3b, 0x4e, 0xf6, 0x1d]) + genesis_validators_root0 = Eth2Digest.fromHex( + "0x8fbd3b999e4873fb182569b20fb090400332849240da5ceb925db7ff7a8d984b") + genesis_validators_root1 = Eth2Digest.fromHex( + "0x78fb3f89983b990a841b98bab7951dccc73a757d2394f496e318db3c4826654e") + pubkey0 = MockPubKeys[0] + privkey0 = MockPrivKeys[0] + privkey1 = MockPrivKeys[1] + + test "Slot signatures": + let + slot = default(Slot) + root = default(Eth2Digest) + + check: + # Matching public/private keys and genesis validator roots + verify_block_signature( + fork0, genesis_validators_root0, slot, root, pubkey0, + get_block_signature( + fork0, genesis_validators_root0, slot, root, privkey0).toValidatorSig) + + # Mismatched public/private keys + not verify_block_signature( + fork0, genesis_validators_root0, slot, root, pubkey0, + get_block_signature( + fork0, genesis_validators_root0, slot, root, privkey1).toValidatorSig) + + # Mismatched forks + not verify_block_signature( + fork0, genesis_validators_root0, slot, root, pubkey0, + get_block_signature( + fork1, genesis_validators_root0, slot, root, privkey0).toValidatorSig) + + # Mismatched genesis validator roots + not verify_block_signature( + fork0, genesis_validators_root0, slot, root, pubkey0, + get_block_signature( + fork0, genesis_validators_root1, slot, root, privkey0).toValidatorSig) + + test "Aggregate and proof signatures": + let aggregate_and_proof = AggregateAndProof( + aggregate: Attestation(aggregation_bits: CommitteeValidatorsBits.init(8))) + + check: + # Matching public/private keys and genesis validator roots + verify_aggregate_and_proof_signature( + fork0, genesis_validators_root0, aggregate_and_proof, pubkey0, + get_aggregate_and_proof_signature( + fork0, genesis_validators_root0, aggregate_and_proof, + privkey0).toValidatorSig) + + # Mismatched public/private keys + not verify_aggregate_and_proof_signature( + fork0, genesis_validators_root0, aggregate_and_proof, pubkey0, + get_aggregate_and_proof_signature( + fork0, genesis_validators_root0, aggregate_and_proof, + privkey1).toValidatorSig) + + # Mismatched forks + not verify_aggregate_and_proof_signature( + fork0, genesis_validators_root0, aggregate_and_proof, pubkey0, + get_aggregate_and_proof_signature( + fork1, genesis_validators_root0, aggregate_and_proof, + privkey0).toValidatorSig) + + # Mismatched genesis validator roots + not verify_aggregate_and_proof_signature( + fork0, genesis_validators_root0, aggregate_and_proof, pubkey0, + get_aggregate_and_proof_signature( + fork0, genesis_validators_root1, aggregate_and_proof, + privkey0).toValidatorSig) + + test "Attestation signatures": + let attestation_data = default(AttestationData) + + check: + # Matching public/private keys and genesis validator roots + verify_attestation_signature( + fork0, genesis_validators_root0, attestation_data, [pubkey0], + get_attestation_signature( + fork0, genesis_validators_root0, attestation_data, + privkey0).toValidatorSig) + + # Mismatched public/private keys + not verify_attestation_signature( + fork0, genesis_validators_root0, attestation_data, [pubkey0], + get_attestation_signature( + fork0, genesis_validators_root0, attestation_data, + privkey1).toValidatorSig) + + # Mismatched forks + not verify_attestation_signature( + fork0, genesis_validators_root0, attestation_data, [pubkey0], + get_attestation_signature( + fork1, genesis_validators_root0, attestation_data, + privkey0).toValidatorSig) + + # Mismatched genesis validator roots + not verify_attestation_signature( + fork0, genesis_validators_root0, attestation_data, [pubkey0], + get_attestation_signature( + fork0, genesis_validators_root1, attestation_data, + privkey0).toValidatorSig) + + test "Deposit signatures": + let preset = default(RuntimeConfig) + + check: + # Matching public/private keys and genesis validator roots + verify_deposit_signature( + preset, DepositData( + pubkey: pubkey0, + signature: get_deposit_signature( + preset, DepositData(pubkey: pubkey0), privkey0).toValidatorSig)) + + # Mismatched public/private keys + not verify_deposit_signature( + preset, DepositData( + pubkey: pubkey0, + signature: get_deposit_signature( + preset, DepositData(pubkey: pubkey0), privkey1).toValidatorSig)) + + test "Voluntary exit signatures": + let voluntary_exit = default(VoluntaryExit) + + check: + # Matching public/private keys and genesis validator roots + verify_voluntary_exit_signature( + fork0, genesis_validators_root0, voluntary_exit, pubkey0, + get_voluntary_exit_signature( + fork0, genesis_validators_root0, voluntary_exit, + privkey0).toValidatorSig) + + # Mismatched public/private keys + not verify_voluntary_exit_signature( + fork0, genesis_validators_root0, voluntary_exit, pubkey0, + get_voluntary_exit_signature( + fork0, genesis_validators_root0, voluntary_exit, + privkey1).toValidatorSig) + + # Mismatched forks + not verify_voluntary_exit_signature( + fork0, genesis_validators_root0, voluntary_exit, pubkey0, + get_voluntary_exit_signature( + fork1, genesis_validators_root0, voluntary_exit, + privkey0).toValidatorSig) + + # Mismatched genesis validator roots + not verify_voluntary_exit_signature( + fork0, genesis_validators_root0, voluntary_exit, pubkey0, + get_voluntary_exit_signature( + fork0, genesis_validators_root1, voluntary_exit, + privkey0).toValidatorSig) + + test "Sync committee message signatures": + let + epoch = default(Epoch) + block_root = default(Eth2Digest) + + check: + # Matching public/private keys and genesis validator roots + verify_sync_committee_message_signature( + epoch, block_root, fork0, genesis_validators_root0, load(pubkey0).get, + blsSign(privkey0, sync_committee_msg_signing_root( + fork0, epoch, genesis_validators_root0, block_root).data)) + + # Mismatched public/private keys + not verify_sync_committee_message_signature( + epoch, block_root, fork0, genesis_validators_root0, load(pubkey0).get, + blsSign(privkey1, sync_committee_msg_signing_root( + fork0, epoch, genesis_validators_root0, block_root).data)) + + # Mismatched forks + not verify_sync_committee_message_signature( + epoch, block_root, fork0, genesis_validators_root0, load(pubkey0).get, + blsSign(privkey0, sync_committee_msg_signing_root( + fork1, epoch, genesis_validators_root0, block_root).data)) + + # Mismatched genesis validator roots + not verify_sync_committee_message_signature( + epoch, block_root, fork0, genesis_validators_root0, load(pubkey0).get, + blsSign(privkey0, sync_committee_msg_signing_root( + fork0, epoch, genesis_validators_root1, block_root).data)) + + test "Sync committee signed contribution and proof signatures": + let signed_contribution_and_proof = default(SignedContributionAndProof) + + check: + # Matching public/private keys and genesis validator roots + verify_signed_contribution_and_proof_signature( + SignedContributionAndProof(signature: blsSign( + privkey0, contribution_and_proof_signing_root( + fork0, genesis_validators_root0, + signed_contribution_and_proof.message).data).toValidatorSig), + fork0, genesis_validators_root0, load(pubkey0).get) + + # Mismatched public/private keys + not verify_signed_contribution_and_proof_signature( + SignedContributionAndProof(signature: blsSign( + privkey1, contribution_and_proof_signing_root( + fork0, genesis_validators_root0, + signed_contribution_and_proof.message).data).toValidatorSig), + fork0, genesis_validators_root0, load(pubkey0).get) + + # Mismatched forks + not verify_signed_contribution_and_proof_signature( + SignedContributionAndProof(signature: blsSign( + privkey0, contribution_and_proof_signing_root( + fork1, genesis_validators_root0, + signed_contribution_and_proof.message).data).toValidatorSig), + fork0, genesis_validators_root0, load(pubkey0).get) + + # Mismatched genesis validator roots + not verify_signed_contribution_and_proof_signature( + SignedContributionAndProof(signature: blsSign( + privkey0, contribution_and_proof_signing_root( + fork0, genesis_validators_root0, + signed_contribution_and_proof.message).data).toValidatorSig), + fork0, genesis_validators_root1, load(pubkey0).get) + + test "Sync committee selection proof signatures": + let + slot = default(Slot) + subcommittee_index = default(uint64) + + check: + # Matching public/private keys and genesis validator roots + verify_selection_proof_signature( + ContributionAndProof( + contribution: SyncCommitteeContribution( + slot: slot, subcommittee_index: subcommittee_index), + selection_proof: blsSign( + privkey0, sync_committee_selection_proof_signing_root( + fork0, genesis_validators_root0, slot, + subcommittee_index).data).toValidatorSig), + fork0, genesis_validators_root0, load(pubkey0).get) + + # Mismatched public/private keys + not verify_selection_proof_signature( + ContributionAndProof( + contribution: SyncCommitteeContribution( + slot: slot, subcommittee_index: subcommittee_index), + selection_proof: blsSign( + privkey1, sync_committee_selection_proof_signing_root( + fork0, genesis_validators_root0, slot, + subcommittee_index).data).toValidatorSig), + fork0, genesis_validators_root0, load(pubkey0).get) + + # Mismatched forks + not verify_selection_proof_signature( + ContributionAndProof( + contribution: SyncCommitteeContribution( + slot: slot, subcommittee_index: subcommittee_index), + selection_proof: blsSign( + privkey0, sync_committee_selection_proof_signing_root( + fork0, genesis_validators_root0, slot, + subcommittee_index).data).toValidatorSig), + fork1, genesis_validators_root0, load(pubkey0).get) + + # Mismatched genesis validator roots + not verify_selection_proof_signature( + ContributionAndProof( + contribution: SyncCommitteeContribution( + slot: slot, subcommittee_index: subcommittee_index), + selection_proof: blsSign( + privkey1, sync_committee_selection_proof_signing_root( + fork0, genesis_validators_root0, slot, + subcommittee_index).data).toValidatorSig), + fork0, genesis_validators_root1, load(pubkey0).get)