diff --git a/aptos-move/framework/supra-framework/doc/stake.md b/aptos-move/framework/supra-framework/doc/stake.md index 5c049239252..4f79d62654f 100644 --- a/aptos-move/framework/supra-framework/doc/stake.md +++ b/aptos-move/framework/supra-framework/doc/stake.md @@ -85,6 +85,8 @@ or if their stake drops below the min required, they would get removed at the en - [Function `remove_validators`](#0x1_stake_remove_validators) - [Function `initialize_stake_owner`](#0x1_stake_initialize_stake_owner) - [Function `initialize_validator`](#0x1_stake_initialize_validator) +- [Function `initialize_validator_genesis`](#0x1_stake_initialize_validator_genesis) +- [Function `initialize_validator_internal`](#0x1_stake_initialize_validator_internal) - [Function `validate_consensus_public_key`](#0x1_stake_validate_consensus_public_key) - [Function `assert_consensus_pubkey_unique_in_next_validator_set`](#0x1_stake_assert_consensus_pubkey_unique_in_next_validator_set) - [Function `assert_consensus_pubkey_unique_in_validator_list`](#0x1_stake_assert_consensus_pubkey_unique_in_validator_list) @@ -1733,6 +1735,16 @@ Cannot update stake pool's lockup to earlier than current lockup. + + +The submitted BLS multisig proof-of-possession does not verify against the multisig key. + + +
const EINVALID_PROOF_OF_POSSESSION: u64 = 24;
+
+
+
+
Invalid consensus public key
@@ -2710,7 +2722,83 @@ Initialize the validator account and give ownership to the signing account.
network_addresses: vector<u8>,
fullnode_addresses: vector<u8>
) acquires AllowedValidators, ValidatorSet, ValidatorConfig {
- validate_consensus_public_key(consensus_pubkey);
+ initialize_validator_internal(
+ account,
+ consensus_pubkey,
+ network_addresses,
+ fullnode_addresses,
+ false
+ );
+}
+
+
+
+
+
+
+
+
+## Function `initialize_validator_genesis`
+
+Initialize a validator during genesis. Identical to initialize_validator except that the
+consensus key blob is trusted genesis output and therefore carries no operator
+proof-of-possession (mirrors rotate_consensus_key / rotate_consensus_key_genesis).
+
+
+public(friend) fun initialize_validator_genesis(account: &signer, consensus_pubkey: vector<u8>, network_addresses: vector<u8>, fullnode_addresses: vector<u8>)
+
+
+
+
+public(friend) fun initialize_validator_genesis(
+ account: &signer,
+ consensus_pubkey: vector<u8>,
+ network_addresses: vector<u8>,
+ fullnode_addresses: vector<u8>
+) acquires AllowedValidators, ValidatorSet, ValidatorConfig {
+ initialize_validator_internal(
+ account,
+ consensus_pubkey,
+ network_addresses,
+ fullnode_addresses,
+ true
+ );
+}
+
+
+
+
+fun initialize_validator_internal(account: &signer, consensus_pubkey: vector<u8>, network_addresses: vector<u8>, fullnode_addresses: vector<u8>, genesis: bool)
+
+
+
+
+fun initialize_validator_internal(
+ account: &signer,
+ consensus_pubkey: vector<u8>,
+ network_addresses: vector<u8>,
+ fullnode_addresses: vector<u8>,
+ genesis: bool
+) acquires AllowedValidators, ValidatorSet, ValidatorConfig {
+ // Verify the keys (and, for operator submissions, the appended BLS multisig PoP) and keep
+ // only the canonical key bytes (any appended PoP is stripped before storage).
+ let consensus_pubkey = validate_consensus_public_key(consensus_pubkey, genesis);
let account_addr = signer::address_of(account);
assert_consensus_pubkey_unique_in_next_validator_set(
account_addr, &consensus_pubkey
@@ -2741,7 +2829,7 @@ Initialize the validator account and give ownership to the signing account.
-fun validate_consensus_public_key(consensus_pubkey: vector<u8>)
+fun validate_consensus_public_key(consensus_pubkey: vector<u8>, genesis: bool): vector<u8>
@@ -2750,26 +2838,198 @@ Initialize the validator account and give ownership to the signing account.
Implementation
-fun validate_consensus_public_key(consensus_pubkey: vector<u8>) {
- if (std::features::supra_validator_identity_v2_enabled()) {
- // Expect the new format.
- let _valid_public_key =
- validator_public_keys::validator_public_keys_from_bytes(consensus_pubkey);
- } else {
- // Check the old format.
- let maybe_valid_public_key =
+fun validate_consensus_public_key(
+ consensus_pubkey: vector<u8>, genesis: bool
+): vector<u8> {
+ // Legacy format (only acceptable before v2): a bare ed25519 key with no aggregatable BLS
+ // key, hence no PoP. Accept and store it unchanged.
+ if (!std::features::supra_validator_identity_v2_enabled()) {
+ let maybe_legacy =
ed25519::new_validated_public_key_from_bytes(consensus_pubkey);
+ if (option::is_some(&maybe_legacy)) {
+ return consensus_pubkey
+ };
+ };
- if (option::is_none(&maybe_valid_public_key)) {
- // Fall back to the new format. This enables validators to register their keys in
- // the new format before the v2 feature flag is activated, which is safe because the
- // new keys are a superset of the old and the consensus has logic for maintaining
- // backwards compatibility.
- let _valid_public_key =
- validator_public_keys::validator_public_keys_from_bytes(
- consensus_pubkey
- );
- }
+ // New identity format (v2, or a pre-v2 early-migration submission). Operator (non-genesis)
+ // submissions carry an appended BLS multisig PoP; verify and strip it. Genesis is exempt.
+ let (keys_bytes, maybe_pop) =
+ if (genesis) {
+ (consensus_pubkey, option::none<vector<u8>>())
+ } else {
+ let (keys_bytes, pop) =
+ validator_public_keys::split_consensus_key_and_pop(consensus_pubkey);
+ (keys_bytes, option::some(pop))
+ };
+
+ let valid_public_keys =
+ validator_public_keys::validator_public_keys_from_bytes(keys_bytes);
+
+ if (option::is_some(&maybe_pop)) {
+ assert!(
+ validator_public_keys::verify_bls_multisig_pop(
+ &valid_public_keys, option::extract(&mut maybe_pop)
+ ),
+ error::invalid_argument(EINVALID_PROOF_OF_POSSESSION)
+ );
+ };
+
+ assert!(
+ validator_public_keys::validate_static_keys(&valid_public_keys),
+ error::invalid_argument(EINVALID_PUBLIC_KEY)
+ );
+
+ keys_bytes
+}
+
+
+
+
+
self_addr in the next validator set
+(the union of active_validators and pending_active) already uses
+consensus_pubkey as its latest on-chain consensus key.
+
+
+fun assert_consensus_pubkey_unique_in_next_validator_set(self_addr: address, consensus_pubkey: &vector<u8>)
+
+
+
+
+fun assert_consensus_pubkey_unique_in_next_validator_set(
+ self_addr: address, consensus_pubkey: &vector<u8>
+) acquires ValidatorSet, ValidatorConfig {
+ let validator_set = borrow_global<ValidatorSet>(@supra_framework);
+ assert_consensus_pubkey_unique_in_validator_list(
+ self_addr, consensus_pubkey, &validator_set.active_validators
+ );
+ assert_consensus_pubkey_unique_in_validator_list(
+ self_addr, consensus_pubkey, &validator_set.pending_active
+ );
+}
+
+
+
+
+fun assert_consensus_pubkey_unique_in_validator_list(self_addr: address, consensus_pubkey: &vector<u8>, validators: &vector<stake::ValidatorInfo>)
+
+
+
+
+fun assert_consensus_pubkey_unique_in_validator_list(
+ self_addr: address,
+ consensus_pubkey: &vector<u8>,
+ validators: &vector<ValidatorInfo>
+) acquires ValidatorConfig {
+ let len = vector::length(validators);
+ let i = 0;
+ while (i < len) {
+ let other = vector::borrow(validators, i);
+ if (other.addr != self_addr && exists<ValidatorConfig>(other.addr)) {
+ let other_config = borrow_global<ValidatorConfig>(other.addr);
+ assert!(
+ other_config.consensus_pubkey != *consensus_pubkey,
+ error::invalid_argument(EDUPLICATE_CONSENSUS_PUBKEY)
+ );
+ };
+ i = i + 1;
+ };
+}
+
+
+
+
+self_addr in the next validator set
+already uses network_addresses. Empty network_addresses is treated
+as "unset" and skips the check.
+
+
+fun assert_network_addresses_unique_in_next_validator_set(self_addr: address, network_addresses: &vector<u8>)
+
+
+
+
+fun assert_network_addresses_unique_in_next_validator_set(
+ self_addr: address, network_addresses: &vector<u8>
+) acquires ValidatorSet, ValidatorConfig {
+ if (vector::is_empty(network_addresses)) { return };
+ let validator_set = borrow_global<ValidatorSet>(@supra_framework);
+ assert_network_addresses_unique_in_validator_list(
+ self_addr, network_addresses, &validator_set.active_validators
+ );
+ assert_network_addresses_unique_in_validator_list(
+ self_addr, network_addresses, &validator_set.pending_active
+ );
+}
+
+
+
+
+fun assert_network_addresses_unique_in_validator_list(self_addr: address, network_addresses: &vector<u8>, validators: &vector<stake::ValidatorInfo>)
+
+
+
+
+fun assert_network_addresses_unique_in_validator_list(
+ self_addr: address,
+ network_addresses: &vector<u8>,
+ validators: &vector<ValidatorInfo>
+) acquires ValidatorConfig {
+ let len = vector::length(validators);
+ let i = 0;
+ while (i < len) {
+ let other = vector::borrow(validators, i);
+ if (other.addr != self_addr && exists<ValidatorConfig>(other.addr)) {
+ let other_config = borrow_global<ValidatorConfig>(other.addr);
+ assert!(
+ other_config.network_addresses != *network_addresses,
+ error::invalid_argument(EDUPLICATE_NETWORK_ADDRESSES)
+ );
+ };
+ i = i + 1;
};
}
@@ -3426,12 +3686,50 @@ Move amount of coins from pending_inactive to active.
exists<ValidatorConfig>(pool_address),
error::not_found(EVALIDATOR_CONFIG)
);
- validate_consensus_public_key(new_consensus_pubkey);
+ // Verify the keys (and, for operator submissions, the appended BLS multisig PoP) and keep
+ // only the canonical key bytes (any appended PoP is stripped before storage/merge).
+ let new_consensus_pubkey =
+ validate_consensus_public_key(new_consensus_pubkey, genesis);
assert_consensus_pubkey_unique_in_next_validator_set(
pool_address, &new_consensus_pubkey
);
let validator_info = borrow_global_mut<ValidatorConfig>(pool_address);
let old_consensus_pubkey = validator_info.consensus_pubkey;
+
+ // Preserve the DKG-managed BLS threshold keys: an operator rotation must only change the
+ // static keys. We load the current on-chain keys and copy in only the operator-supplied
+ // static keys, leaving the threshold keys (written by `set_dkg_output_keys`) intact.
+ //
+ // The merge is skipped (and the incoming blob stored verbatim, preserving the original
+ // behavior) when there is nothing to preserve or the stored blob cannot be parsed:
+ // - before v2: the stored blob may be a legacy ed25519 key with no threshold keys;
+ // - genesis: no DKG threshold keys exist yet;
+ // - an empty stored key: a validator initialized via `initialize_stake_owner` sets its key
+ // for the first time here, so there is no prior `ValidatorPublicKeys` to merge into
+ // (parsing the empty blob would abort).
+ // Under v2 a non-empty stored blob is guaranteed to be a valid `ValidatorPublicKeys` (the
+ // feature is only enabled once all validators have migrated to the new format), and
+ // `validate_consensus_public_key` has already verified the incoming blob, so both
+ // deserializations are safe.
+ let new_consensus_pubkey =
+ if (!genesis
+ && std::features::supra_validator_identity_v2_enabled()
+ && !vector::is_empty(&old_consensus_pubkey)) {
+ let current_keys =
+ validator_public_keys::validator_public_keys_from_bytes(
+ old_consensus_pubkey
+ );
+ let incoming_keys =
+ validator_public_keys::validator_public_keys_from_bytes(
+ new_consensus_pubkey
+ );
+ validator_public_keys::replace_static_keys(
+ &mut current_keys, &incoming_keys
+ );
+ validator_public_keys::public_key_to_bytes(current_keys)
+ } else {
+ new_consensus_pubkey
+ };
validator_info.consensus_pubkey = new_consensus_pubkey;
if (std::features::module_event_migration_enabled()) {
diff --git a/aptos-move/framework/supra-framework/doc/validator_public_keys.md b/aptos-move/framework/supra-framework/doc/validator_public_keys.md
index 063d42f0467..af5be0c5e11 100644
--- a/aptos-move/framework/supra-framework/doc/validator_public_keys.md
+++ b/aptos-move/framework/supra-framework/doc/validator_public_keys.md
@@ -29,6 +29,13 @@
- [Function `get_supra_bls_multi_sig_pub_key`](#0x1_validator_public_keys_get_supra_bls_multi_sig_pub_key)
- [Function `get_supra_cg_key`](#0x1_validator_public_keys_get_supra_cg_key)
- [Function `get_supra_ed_key`](#0x1_validator_public_keys_get_supra_ed_key)
+- [Function `replace_static_keys`](#0x1_validator_public_keys_replace_static_keys)
+- [Function `validate_static_keys`](#0x1_validator_public_keys_validate_static_keys)
+- [Function `is_valid_ed25519_key`](#0x1_validator_public_keys_is_valid_ed25519_key)
+- [Function `is_valid_bls12381_key`](#0x1_validator_public_keys_is_valid_bls12381_key)
+- [Function `is_valid_cg_key`](#0x1_validator_public_keys_is_valid_cg_key)
+- [Function `split_consensus_key_and_pop`](#0x1_validator_public_keys_split_consensus_key_and_pop)
+- [Function `verify_bls_multisig_pop`](#0x1_validator_public_keys_verify_bls_multisig_pop)
- [Function `rotate_supra_bls_threshold_validity_key`](#0x1_validator_public_keys_rotate_supra_bls_threshold_validity_key)
- [Function `rotate_supra_bls_threshold_quorum_key`](#0x1_validator_public_keys_rotate_supra_bls_threshold_quorum_key)
- [Function `rotate_supra_bls_threshold_unanimous_key`](#0x1_validator_public_keys_rotate_supra_bls_threshold_unanimous_key)
@@ -48,6 +55,7 @@
use 0x1::option;
use 0x1::string;
use 0x1::type_info;
+use 0x1::vector;
@@ -217,6 +225,16 @@ The size of a serialized bls12381 G1 public key, in bytes.
+
+
+The size of a serialized bls12381 proof-of-possession (a G2 signature), in bytes.
+
+
+const BLS12381_POP_NUM_BYTES: u64 = 96;
+
+
+
+
The integer should match the Rust enum value representation in CertificateThresholdType.
@@ -304,6 +322,16 @@ The size of a serialized ed25519 public key, in bytes.
+
+
+Error: A consensus key blob is too short to contain an appended BLS proof-of-possession.
+
+
+const EINVALID_POP_LENGTH: u64 = 2;
+
+
+
+
Error: Unknown certificate threshold type.
@@ -329,7 +357,9 @@ Error: Unknown certificate threshold type.
public fun validity_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_VALIDITY } }
+public fun validity_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_VALIDITY }
+}
@@ -351,7 +381,9 @@ Error: Unknown certificate threshold type.
Implementation
-public fun quorum_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_QUORUM } }
+public fun quorum_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_QUORUM }
+}
@@ -373,7 +405,9 @@ Error: Unknown certificate threshold type.
Implementation
-public fun unanimous_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_UNANIMOUS } }
+public fun unanimous_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_UNANIMOUS }
+}
@@ -395,7 +429,9 @@ Error: Unknown certificate threshold type.
Implementation
-public fun bcft_validity_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_BCFT_VALIDITY } }
+public fun bcft_validity_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_BCFT_VALIDITY }
+}
@@ -417,7 +453,9 @@ Error: Unknown certificate threshold type.
Implementation
-public fun bcft_quorum_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_BCFT_QUORUM } }
+public fun bcft_quorum_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_BCFT_QUORUM }
+}
@@ -439,7 +477,11 @@ Error: Unknown certificate threshold type.
Implementation
-public fun bcft_fallback_view_change_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE } }
+public fun bcft_fallback_view_change_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType {
+ tag: CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE
+ }
+}
@@ -461,7 +503,9 @@ Error: Unknown certificate threshold type.
Implementation
-public fun clan_majority_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_CLAN_MAJORITY } }
+public fun clan_majority_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_CLAN_MAJORITY }
+}
@@ -483,7 +527,9 @@ Error: Unknown certificate threshold type.
Implementation
-public fun is_validity_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_VALIDITY }
+public fun is_validity_certificate_type(t: &CertificateThresholdType): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_VALIDITY
+}
@@ -505,7 +551,9 @@ Error: Unknown certificate threshold type.
Implementation
-public fun is_quorum_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_QUORUM }
+public fun is_quorum_certificate_type(t: &CertificateThresholdType): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_QUORUM
+}
@@ -527,7 +575,11 @@ Error: Unknown certificate threshold type.
Implementation
-public fun is_unanimous_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_UNANIMOUS }
+public fun is_unanimous_certificate_type(
+ t: &CertificateThresholdType
+): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_UNANIMOUS
+}
@@ -549,7 +601,11 @@ Error: Unknown certificate threshold type.
Implementation
-public fun is_bcft_validity_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_BCFT_VALIDITY }
+public fun is_bcft_validity_certificate_type(
+ t: &CertificateThresholdType
+): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_BCFT_VALIDITY
+}
@@ -571,7 +627,11 @@ Error: Unknown certificate threshold type.
Implementation
-public fun is_bcft_quorum_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_BCFT_QUORUM }
+public fun is_bcft_quorum_certificate_type(
+ t: &CertificateThresholdType
+): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_BCFT_QUORUM
+}
@@ -593,7 +653,11 @@ Error: Unknown certificate threshold type.
Implementation
-public fun is_bcft_fallback_view_change_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE }
+public fun is_bcft_fallback_view_change_certificate_type(
+ t: &CertificateThresholdType
+): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE
+}
@@ -615,7 +679,11 @@ Error: Unknown certificate threshold type.
Implementation
-public fun is_clan_majority_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_CLAN_MAJORITY }
+public fun is_clan_majority_certificate_type(
+ t: &CertificateThresholdType
+): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_CLAN_MAJORITY
+}
@@ -639,7 +707,8 @@ Error: Unknown certificate threshold type.
public fun validator_public_keys_from_bytes(bytes: vector<u8>): ValidatorPublicKeys {
// bcs deserialization
- let bytes_serialized = any::new(type_info::type_name<ValidatorPublicKeys>(), bytes);
+ let bytes_serialized =
+ any::new(type_info::type_name<ValidatorPublicKeys>(), bytes);
let validator_public_keys = any::unpack<ValidatorPublicKeys>(bytes_serialized);
validator_public_keys
}
@@ -664,7 +733,7 @@ Error: Unknown certificate threshold type.
Implementation
-public fun public_key_to_bytes(pk: ValidatorPublicKeys): vector<u8>{
+public fun public_key_to_bytes(pk: ValidatorPublicKeys): vector<u8> {
// bcs deserialization
bcs::to_bytes<ValidatorPublicKeys>(&pk)
}
@@ -689,7 +758,7 @@ Error: Unknown certificate threshold type.
Implementation
-public fun get_network_key(pk: &ValidatorPublicKeys): ed25519::ValidatedPublicKey{
+public fun get_network_key(pk: &ValidatorPublicKeys): ed25519::ValidatedPublicKey {
pk.network_key
}
@@ -713,7 +782,7 @@ Error: Unknown certificate threshold type.
Implementation
-public fun get_supra_bls_multi_sig_pub_key(pk: &ValidatorPublicKeys): bls12381::PublicKey{
+public fun get_supra_bls_multi_sig_pub_key(pk: &ValidatorPublicKeys): bls12381::PublicKey {
pk.supra_keys.bls_multisig_key
}
@@ -737,7 +806,7 @@ Error: Unknown certificate threshold type.
Implementation
-public fun get_supra_cg_key(pk: &ValidatorPublicKeys): class_groups::CGPublicKey{
+public fun get_supra_cg_key(pk: &ValidatorPublicKeys): class_groups::CGPublicKey {
pk.supra_keys.class_group_key
}
@@ -761,13 +830,230 @@ Error: Unknown certificate threshold type.
Implementation
-public fun get_supra_ed_key(pk: &ValidatorPublicKeys): ed25519::ValidatedPublicKey{
+public fun get_supra_ed_key(pk: &ValidatorPublicKeys): ed25519::ValidatedPublicKey {
pk.supra_keys.ed25519_key
}
+
target (network_key, bls_multisig_key, class_group_key,
+ed25519_key) with those from source, leaving the DKG-managed BLS threshold key fields of
+target untouched. Used by stake::rotate_consensus_key so that an operator key rotation
+only changes the static keys and cannot clobber the threshold keys written by the DKG via
+stake::set_dkg_output_keys.
+
+
+public fun replace_static_keys(target: &mut validator_public_keys::ValidatorPublicKeys, source: &validator_public_keys::ValidatorPublicKeys)
+
+
+
+
+public fun replace_static_keys(
+ target: &mut ValidatorPublicKeys, source: &ValidatorPublicKeys
+) {
+ target.network_key = source.network_key;
+ target.supra_keys.bls_multisig_key = source.supra_keys.bls_multisig_key;
+ target.supra_keys.class_group_key = source.supra_keys.class_group_key;
+ target.supra_keys.ed25519_key = source.supra_keys.ed25519_key;
+}
+
+
+
+
+validator_public_keys_from_bytes reconstructs each key's raw bytes via BCS without
+running the per-key validation natives, so a registration blob can carry malformed keys
+(the Validated*/PublicKey types are a misnomer after deserialization). This re-runs
+each key's validating constructor on the recovered bytes to reject them.
+
+The DKG-managed BLS threshold key shares are intentionally not checked: they are absent at
+registration and are produced/overwritten by the protocol via stake::set_dkg_output_keys
+(which validates them). Proof-of-possession is not verified here either (no PoP is supplied
+at registration); only key well-formedness / subgroup membership is checked.
+
+
+public fun validate_static_keys(pk: &validator_public_keys::ValidatorPublicKeys): bool
+
+
+
+
+public fun validate_static_keys(pk: &ValidatorPublicKeys): bool {
+ is_valid_ed25519_key(&pk.network_key)
+ && is_valid_bls12381_key(&pk.supra_keys.bls_multisig_key)
+ && is_valid_cg_key(&pk.supra_keys.class_group_key)
+ && is_valid_ed25519_key(&pk.supra_keys.ed25519_key)
+}
+
+
+
+
+fun is_valid_ed25519_key(pk: &ed25519::ValidatedPublicKey): bool
+
+
+
+
+fun is_valid_ed25519_key(pk: &ed25519::ValidatedPublicKey): bool {
+ option::is_some(
+ &ed25519::new_validated_public_key_from_bytes(
+ ed25519::validated_public_key_to_bytes(pk)
+ )
+ )
+}
+
+
+
+
+fun is_valid_bls12381_key(pk: &bls12381::PublicKey): bool
+
+
+
+
+fun is_valid_bls12381_key(pk: &bls12381::PublicKey): bool {
+ option::is_some(
+ &bls12381::public_key_from_bytes(bls12381::public_key_to_bytes(pk))
+ )
+}
+
+
+
+
+fun is_valid_cg_key(pk: &class_groups::CGPublicKey): bool
+
+
+
+
+fun is_valid_cg_key(pk: &class_groups::CGPublicKey): bool {
+ option::is_some(
+ &class_groups::public_key_from_bytes(class_groups::public_key_to_bytes(pk))
+ )
+}
+
+
+
+
+ValidatorPublicKeys bytes (the prefix) and
+the appended BLS12-381 proof-of-possession (the trailing BLS12381_POP_NUM_BYTES bytes).
+Operators submit the PoP appended to the key bytes so that the contract can verify it
+without a dedicated entry-function parameter (Move forbids changing public signatures).
+
+
+public fun split_consensus_key_and_pop(blob: vector<u8>): (vector<u8>, vector<u8>)
+
+
+
+
+public fun split_consensus_key_and_pop(blob: vector<u8>): (vector<u8>, vector<u8>) {
+ let len = vector::length(&blob);
+ assert!(len >= BLS12381_POP_NUM_BYTES, error::invalid_argument(EINVALID_POP_LENGTH));
+ let pop = vector::trim(&mut blob, len - BLS12381_POP_NUM_BYTES);
+ (blob, pop)
+}
+
+
+
+
+pop_bytes is a valid BLS12-381 proof-of-possession for the BLS multisig
+key in pk. The multisig key is the only aggregatable (hence rogue-key-attackable) key an
+operator submits, so it is the only one that requires a PoP; the class-group key carries its
+own ZK PoP (verified by its validate_pubkey_internal native), the ed25519 keys are
+non-aggregated, and the BLS threshold shares are produced by the DKG.
+
+
+public fun verify_bls_multisig_pop(pk: &validator_public_keys::ValidatorPublicKeys, pop_bytes: vector<u8>): bool
+
+
+
+
+public fun verify_bls_multisig_pop(
+ pk: &ValidatorPublicKeys, pop_bytes: vector<u8>
+): bool {
+ let pk_bytes = bls12381::public_key_to_bytes(&pk.supra_keys.bls_multisig_key);
+ let pop = bls12381::proof_of_possession_from_bytes(pop_bytes);
+ option::is_some(&bls12381::public_key_from_bytes_with_pop(pk_bytes, &pop))
+}
+
+
+
+
public fun rotate_supra_bls_threshold_validity_key(pk: &mut ValidatorPublicKeys, new_bls_threshold_validity_key: bls12381::PublicKey) {
- pk.supra_keys.bls_threshold_validity_certificate_key = option::some(new_bls_threshold_validity_key);
+public fun rotate_supra_bls_threshold_validity_key(
+ pk: &mut ValidatorPublicKeys, new_bls_threshold_validity_key: bls12381::PublicKey
+) {
+ pk.supra_keys.bls_threshold_validity_certificate_key = option::some(
+ new_bls_threshold_validity_key
+ );
}
@@ -809,8 +1099,12 @@ Error: Unknown certificate threshold type.
Implementation
-public fun rotate_supra_bls_threshold_quorum_key(pk: &mut ValidatorPublicKeys, new_bls_threshold_quorum_key: bls12381::PublicKey) {
- pk.supra_keys.bls_threshold_quorum_certificate_key = option::some(new_bls_threshold_quorum_key);
+public fun rotate_supra_bls_threshold_quorum_key(
+ pk: &mut ValidatorPublicKeys, new_bls_threshold_quorum_key: bls12381::PublicKey
+) {
+ pk.supra_keys.bls_threshold_quorum_certificate_key = option::some(
+ new_bls_threshold_quorum_key
+ );
}
@@ -833,7 +1127,9 @@ Error: Unknown certificate threshold type.
Implementation
-public fun rotate_supra_bls_threshold_unanimous_key(pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey) {
+public fun rotate_supra_bls_threshold_unanimous_key(
+ pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey
+) {
pk.supra_keys.bls_threshold_unanimous_certificate_key = option::some(new_key);
}
@@ -857,7 +1153,9 @@ Error: Unknown certificate threshold type.
Implementation
-public fun rotate_supra_bls_threshold_bcft_validity_key(pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey) {
+public fun rotate_supra_bls_threshold_bcft_validity_key(
+ pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey
+) {
pk.supra_keys.bls_threshold_bcft_validity_certificate_key = option::some(new_key);
}
@@ -881,7 +1179,9 @@ Error: Unknown certificate threshold type.
Implementation
-public fun rotate_supra_bls_threshold_bcft_quorum_key(pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey) {
+public fun rotate_supra_bls_threshold_bcft_quorum_key(
+ pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey
+) {
pk.supra_keys.bls_threshold_bcft_quorum_certificate_key = option::some(new_key);
}
@@ -905,8 +1205,12 @@ Error: Unknown certificate threshold type.
Implementation
-public fun rotate_supra_bls_threshold_bcft_fallback_view_change_key(pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey) {
- pk.supra_keys.bls_threshold_bcft_fallback_view_change_certificate_key = option::some(new_key);
+public fun rotate_supra_bls_threshold_bcft_fallback_view_change_key(
+ pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey
+) {
+ pk.supra_keys.bls_threshold_bcft_fallback_view_change_certificate_key = option::some(
+ new_key
+ );
}
@@ -929,7 +1233,9 @@ Error: Unknown certificate threshold type.
Implementation
-public fun rotate_supra_bls_threshold_clan_majority_key(pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey) {
+public fun rotate_supra_bls_threshold_clan_majority_key(
+ pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey
+) {
pk.supra_keys.bls_threshold_clan_majority_certificate_key = option::some(new_key);
}
@@ -955,7 +1261,9 @@ threshold_type: 0=validity, 1=quorum, 2=unanimous, 3=bcft_validity, 4=bcft_quoru
Implementation
-public fun rotate_supra_bls_threshold_key_by_type(pk: &mut ValidatorPublicKeys, threshold_type: u8, new_key: bls12381::PublicKey) {
+public fun rotate_supra_bls_threshold_key_by_type(
+ pk: &mut ValidatorPublicKeys, threshold_type: u8, new_key: bls12381::PublicKey
+) {
if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_VALIDITY) {
rotate_supra_bls_threshold_validity_key(pk, new_key);
} else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_QUORUM) {
@@ -966,7 +1274,8 @@ threshold_type: 0=validity, 1=quorum, 2=unanimous, 3=bcft_validity, 4=bcft_quoru
rotate_supra_bls_threshold_bcft_validity_key(pk, new_key);
} else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_BCFT_QUORUM) {
rotate_supra_bls_threshold_bcft_quorum_key(pk, new_key);
- } else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE) {
+ } else if (threshold_type
+ == CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE) {
rotate_supra_bls_threshold_bcft_fallback_view_change_key(pk, new_key);
} else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_CLAN_MAJORITY) {
rotate_supra_bls_threshold_clan_majority_key(pk, new_key);
diff --git a/aptos-move/framework/supra-framework/sources/stake.move b/aptos-move/framework/supra-framework/sources/stake.move
index bc58805b752..94bb1235525 100644
--- a/aptos-move/framework/supra-framework/sources/stake.move
+++ b/aptos-move/framework/supra-framework/sources/stake.move
@@ -95,6 +95,8 @@ module supra_framework::stake {
const EDUPLICATE_CONSENSUS_PUBKEY: u64 = 22;
/// Another validator in the next validator set already uses these network addresses.
const EDUPLICATE_NETWORK_ADDRESSES: u64 = 23;
+ /// The submitted BLS multisig proof-of-possession does not verify against the multisig key.
+ const EINVALID_PROOF_OF_POSSESSION: u64 = 24;
/// Validator status enum. We can switch to proper enum later once Move supports it.
const VALIDATOR_STATUS_PENDING_ACTIVE: u64 = 1;
@@ -703,7 +705,43 @@ module supra_framework::stake {
network_addresses: vector,
fullnode_addresses: vector
) acquires AllowedValidators, ValidatorSet, ValidatorConfig {
- validate_consensus_public_key(consensus_pubkey);
+ initialize_validator_internal(
+ account,
+ consensus_pubkey,
+ network_addresses,
+ fullnode_addresses,
+ false
+ );
+ }
+
+ /// Initialize a validator during genesis. Identical to `initialize_validator` except that the
+ /// consensus key blob is trusted genesis output and therefore carries no operator
+ /// proof-of-possession (mirrors `rotate_consensus_key` / `rotate_consensus_key_genesis`).
+ public(friend) fun initialize_validator_genesis(
+ account: &signer,
+ consensus_pubkey: vector,
+ network_addresses: vector,
+ fullnode_addresses: vector
+ ) acquires AllowedValidators, ValidatorSet, ValidatorConfig {
+ initialize_validator_internal(
+ account,
+ consensus_pubkey,
+ network_addresses,
+ fullnode_addresses,
+ true
+ );
+ }
+
+ fun initialize_validator_internal(
+ account: &signer,
+ consensus_pubkey: vector,
+ network_addresses: vector,
+ fullnode_addresses: vector,
+ genesis: bool
+ ) acquires AllowedValidators, ValidatorSet, ValidatorConfig {
+ // Verify the keys (and, for operator submissions, the appended BLS multisig PoP) and keep
+ // only the canonical key bytes (any appended PoP is stripped before storage).
+ let consensus_pubkey = validate_consensus_public_key(consensus_pubkey, genesis);
let account_addr = signer::address_of(account);
assert_consensus_pubkey_unique_in_next_validator_set(
account_addr, &consensus_pubkey
@@ -723,42 +761,74 @@ module supra_framework::stake {
);
}
- // Checks the public key is valid to prevent rogue-key attacks.
+ // Verifies a submitted consensus key blob and returns the canonical key bytes to store.
//
- // The deserializer for the new format currently doesn't offer a way for us to handle
- // failure, so we expect the new format when the related feature flag has been active
- // (the flag should only be activated after all validators have migrated to the new
- // format) and fall back to the new format when deserialization fails before the flag
- // has been activated.
- fun validate_consensus_public_key(consensus_pubkey: vector) {
- if (std::features::supra_validator_identity_v2_enabled()) {
- // Expect the new format.
- let _valid_public_key =
- validator_public_keys::validator_public_keys_from_bytes(consensus_pubkey);
- } else {
- // Check the old format.
- let maybe_valid_public_key =
+ // A legacy consensus key (a bare ed25519 public key) is only accepted before the v2 identity
+ // feature flag is activated; it has no aggregatable BLS key and so carries no proof-of-possession.
+ //
+ // Every other (new-format `ValidatorPublicKeys`) submission -- whether under v2 or via the
+ // pre-v2 early-migration path -- must carry an appended BLS12-381 proof-of-possession for the
+ // BLS multisig key (operator / non-genesis submissions only; genesis output is trusted and
+ // carries no PoP). The multisig key is the only operator-submitted key exposed to rogue-key
+ // attacks (the class-group key carries its own ZK PoP verified by its native, the ed25519 keys
+ // are non-aggregated, and the BLS threshold shares are DKG-produced). We split off and verify
+ // that PoP (see `validator_public_keys::split_consensus_key_and_pop`) and return the key bytes
+ // with the PoP stripped, so the stored key keeps its canonical format. PoP enforcement is tied
+ // to the key format, not the v2 flag, so a key registered during the pre-v2 migration window
+ // cannot skip it.
+ //
+ // Deserialization (`validator_public_keys_from_bytes`) only reconstructs each key's bytes
+ // without running the per-key validation natives, so we also re-validate every static key via
+ // `validate_static_keys` (on-curve / subgroup / valid-encoding checks).
+ fun validate_consensus_public_key(
+ consensus_pubkey: vector, genesis: bool
+ ): vector {
+ // Legacy format (only acceptable before v2): a bare ed25519 key with no aggregatable BLS
+ // key, hence no PoP. Accept and store it unchanged.
+ if (!std::features::supra_validator_identity_v2_enabled()) {
+ let maybe_legacy =
ed25519::new_validated_public_key_from_bytes(consensus_pubkey);
+ if (option::is_some(&maybe_legacy)) {
+ return consensus_pubkey
+ };
+ };
- if (option::is_none(&maybe_valid_public_key)) {
- // Fall back to the new format. This enables validators to register their keys in
- // the new format before the v2 feature flag is activated, which is safe because the
- // new keys are a superset of the old and the consensus has logic for maintaining
- // backwards compatibility.
- let _valid_public_key =
- validator_public_keys::validator_public_keys_from_bytes(
- consensus_pubkey
- );
- }
+ // New identity format (v2, or a pre-v2 early-migration submission). Operator (non-genesis)
+ // submissions carry an appended BLS multisig PoP; verify and strip it. Genesis is exempt.
+ let (keys_bytes, maybe_pop) =
+ if (genesis) {
+ (consensus_pubkey, option::none>())
+ } else {
+ let (keys_bytes, pop) =
+ validator_public_keys::split_consensus_key_and_pop(consensus_pubkey);
+ (keys_bytes, option::some(pop))
+ };
+
+ let valid_public_keys =
+ validator_public_keys::validator_public_keys_from_bytes(keys_bytes);
+
+ if (option::is_some(&maybe_pop)) {
+ assert!(
+ validator_public_keys::verify_bls_multisig_pop(
+ &valid_public_keys, option::extract(&mut maybe_pop)
+ ),
+ error::invalid_argument(EINVALID_PROOF_OF_POSSESSION)
+ );
};
+
+ assert!(
+ validator_public_keys::validate_static_keys(&valid_public_keys),
+ error::invalid_argument(EINVALID_PUBLIC_KEY)
+ );
+
+ keys_bytes
}
/// Aborts if any validator other than `self_addr` in the next validator set
/// (the union of `active_validators` and `pending_active`) already uses
/// `consensus_pubkey` as its latest on-chain consensus key.
fun assert_consensus_pubkey_unique_in_next_validator_set(
- self_addr: address,
- consensus_pubkey: &vector
+ self_addr: address, consensus_pubkey: &vector
) acquires ValidatorSet, ValidatorConfig {
let validator_set = borrow_global(@supra_framework);
assert_consensus_pubkey_unique_in_validator_list(
@@ -793,8 +863,7 @@ module supra_framework::stake {
/// already uses `network_addresses`. Empty `network_addresses` is treated
/// as "unset" and skips the check.
fun assert_network_addresses_unique_in_next_validator_set(
- self_addr: address,
- network_addresses: &vector
+ self_addr: address, network_addresses: &vector
) acquires ValidatorSet, ValidatorConfig {
if (vector::is_empty(network_addresses)) { return };
let validator_set = borrow_global(@supra_framework);
@@ -1066,12 +1135,50 @@ module supra_framework::stake {
exists(pool_address),
error::not_found(EVALIDATOR_CONFIG)
);
- validate_consensus_public_key(new_consensus_pubkey);
+ // Verify the keys (and, for operator submissions, the appended BLS multisig PoP) and keep
+ // only the canonical key bytes (any appended PoP is stripped before storage/merge).
+ let new_consensus_pubkey =
+ validate_consensus_public_key(new_consensus_pubkey, genesis);
assert_consensus_pubkey_unique_in_next_validator_set(
pool_address, &new_consensus_pubkey
);
let validator_info = borrow_global_mut(pool_address);
let old_consensus_pubkey = validator_info.consensus_pubkey;
+
+ // Preserve the DKG-managed BLS threshold keys: an operator rotation must only change the
+ // static keys. We load the current on-chain keys and copy in only the operator-supplied
+ // static keys, leaving the threshold keys (written by `set_dkg_output_keys`) intact.
+ //
+ // The merge is skipped (and the incoming blob stored verbatim, preserving the original
+ // behavior) when there is nothing to preserve or the stored blob cannot be parsed:
+ // - before v2: the stored blob may be a legacy ed25519 key with no threshold keys;
+ // - genesis: no DKG threshold keys exist yet;
+ // - an empty stored key: a validator initialized via `initialize_stake_owner` sets its key
+ // for the first time here, so there is no prior `ValidatorPublicKeys` to merge into
+ // (parsing the empty blob would abort).
+ // Under v2 a non-empty stored blob is guaranteed to be a valid `ValidatorPublicKeys` (the
+ // feature is only enabled once all validators have migrated to the new format), and
+ // `validate_consensus_public_key` has already verified the incoming blob, so both
+ // deserializations are safe.
+ let new_consensus_pubkey =
+ if (!genesis
+ && std::features::supra_validator_identity_v2_enabled()
+ && !vector::is_empty(&old_consensus_pubkey)) {
+ let current_keys =
+ validator_public_keys::validator_public_keys_from_bytes(
+ old_consensus_pubkey
+ );
+ let incoming_keys =
+ validator_public_keys::validator_public_keys_from_bytes(
+ new_consensus_pubkey
+ );
+ validator_public_keys::replace_static_keys(
+ &mut current_keys, &incoming_keys
+ );
+ validator_public_keys::public_key_to_bytes(current_keys)
+ } else {
+ new_consensus_pubkey
+ };
validator_info.consensus_pubkey = new_consensus_pubkey;
if (std::features::module_event_migration_enabled()) {
@@ -2341,7 +2448,8 @@ module supra_framework::stake {
should_end_epoch: bool
) acquires SupraCoinCapabilities, StakePool, ValidatorConfig, ValidatorPerformance, ValidatorSet, ValidatorFees {
let pk_bytes = validator_public_keys::public_key_to_bytes(*pk);
- rotate_consensus_key(operator, pool_address, pk_bytes);
+ // Test fixture: rotate through the genesis (PoP-exempt) path.
+ rotate_consensus_key_genesis(operator, pool_address, pk_bytes);
join_validator_set(operator, pool_address);
if (should_end_epoch) {
end_epoch();
@@ -2427,7 +2535,9 @@ module supra_framework::stake {
};
let pk_bytes = validator_public_keys::public_key_to_bytes(*public_key);
- initialize_validator(
+ // Test fixtures register through the genesis (PoP-exempt) path; see
+ // `register_test_validator_with_addresses`.
+ initialize_validator_genesis(
validator,
pk_bytes,
vector::empty(),
@@ -2520,13 +2630,13 @@ module supra_framework::stake {
validator_public_keys::generate_keys()
}
- /// Returns the serialized bytes of a freshly generated, unique consensus public key.
- /// Useful for test helpers that previously hardcoded a shared key but now must
- /// satisfy the on-chain uniqueness requirement enforced by `rotate_consensus_key`.
+ /// Returns the serialized bytes of a freshly generated, unique consensus public key with a
+ /// valid BLS multisig proof-of-possession appended, i.e. exactly what an operator submits to
+ /// `rotate_consensus_key` under the v2 identity format (which is enabled in the test VM).
#[test_only]
public fun generate_unique_consensus_pubkey_bytes(): vector {
- let (_sk, pk) = generate_identity();
- validator_public_keys::public_key_to_bytes(pk)
+ let (sk, pk) = generate_identity();
+ validator_public_keys::serialized_keys_with_pop_for_test(&sk, &pk)
}
#[test_only]
@@ -2541,7 +2651,15 @@ module supra_framework::stake {
account::create_account_for_test(validator_address);
};
let pk_bytes = validator_public_keys::public_key_to_bytes(*public_key);
- initialize_validator(validator, pk_bytes, network_addresses, fullnode_addresses);
+ // Test fixtures register through the genesis (PoP-exempt) path: they are trusted setup and
+ // do not carry an operator proof-of-possession. PoP enforcement is covered by the dedicated
+ // `initialize_validator` / `rotate_consensus_key` tests.
+ initialize_validator_genesis(
+ validator,
+ pk_bytes,
+ network_addresses,
+ fullnode_addresses
+ );
}
#[test(supra_framework = @supra_framework, validator_1 = @0x123, validator_2 = @0x234)]
@@ -2620,9 +2738,10 @@ module supra_framework::stake {
supra_framework: &signer, validator_1: &signer, validator_2: &signer
) acquires AllowedValidators, SupraCoinCapabilities, OwnerCapability, StakePool, ValidatorConfig, ValidatorPerformance, ValidatorSet, ValidatorFees {
initialize_for_test(supra_framework);
- let (_sk_1, pk_1) = generate_identity();
+ let (sk_1, pk_1) = generate_identity();
let (_sk_2, pk_2) = generate_identity();
- let pk_1_bytes = validator_public_keys::public_key_to_bytes(pk_1);
+ let pk_1_bytes =
+ validator_public_keys::serialized_keys_with_pop_for_test(&sk_1, &pk_1);
initialize_test_validator(&pk_1, validator_1, 100, true, true);
initialize_test_validator(&pk_2, validator_2, 100, true, true);
// validator_2 tries to rotate to validator_1's consensus key - should abort.
@@ -2636,8 +2755,9 @@ module supra_framework::stake {
initialize_for_test(supra_framework);
let (_sk, pk) = generate_identity();
initialize_test_validator(&pk, validator, 100, true, true);
- let (_sk_new, pk_new) = generate_identity();
- let pk_new_bytes = validator_public_keys::public_key_to_bytes(pk_new);
+ let (sk_new, pk_new) = generate_identity();
+ let pk_new_bytes =
+ validator_public_keys::serialized_keys_with_pop_for_test(&sk_new, &pk_new);
rotate_consensus_key(validator, signer::address_of(validator), pk_new_bytes);
}
@@ -2646,13 +2766,245 @@ module supra_framework::stake {
supra_framework: &signer, validator: &signer
) acquires AllowedValidators, SupraCoinCapabilities, OwnerCapability, StakePool, ValidatorConfig, ValidatorPerformance, ValidatorSet, ValidatorFees {
initialize_for_test(supra_framework);
- let (_sk, pk) = generate_identity();
- let pk_bytes = validator_public_keys::public_key_to_bytes(pk);
+ let (sk, pk) = generate_identity();
+ let pk_bytes = validator_public_keys::serialized_keys_with_pop_for_test(
+ &sk, &pk
+ );
initialize_test_validator(&pk, validator, 100, true, true);
// Rotating to the same key the validator already holds is a no-op and must not abort.
rotate_consensus_key(validator, signer::address_of(validator), pk_bytes);
}
+ #[test(supra_framework = @supra_framework, validator = @0x123)]
+ /// An operator rotation must preserve the DKG-managed BLS threshold keys (written by
+ /// `set_dkg_output_keys`) and only swap the static keys. This holds under the v2 identity format.
+ public entry fun test_rotate_consensus_key_preserves_threshold_keys(
+ supra_framework: &signer, validator: &signer
+ ) acquires AllowedValidators, SupraCoinCapabilities, OwnerCapability, StakePool, ValidatorConfig, ValidatorPerformance, ValidatorSet, ValidatorFees {
+ initialize_for_test(supra_framework);
+ // v2 is enabled by default in the test VM; this makes the dependency explicit.
+ features::change_feature_flags_for_testing(
+ supra_framework,
+ vector[features::get_supra_validator_identity_v2_feature()],
+ vector[]
+ );
+
+ // Seed a DKG-style threshold key on the validator's initial keys. The fixture registers via
+ // the genesis (PoP-exempt) helper, so the seeded keys are stored as-is. Reuse the multisig
+ // key as a stand-in `bls12381::PublicKey` so the test needs no extra crypto imports.
+ let (_sk, pk) = generate_identity();
+ let threshold_key = validator_public_keys::get_supra_bls_multi_sig_pub_key(&pk);
+ validator_public_keys::rotate_supra_bls_threshold_validity_key(
+ &mut pk, threshold_key
+ );
+ initialize_test_validator(&pk, validator, 100, true, true);
+
+ // Rotate to a fresh static identity whose threshold fields are empty (modelling an operator
+ // rotating from a node that has not yet received the latest DKG keys). The operator submits
+ // the keys with an appended BLS multisig proof-of-possession.
+ let (sk_new, pk_new) = generate_identity();
+ let pk_new_bytes =
+ validator_public_keys::serialized_keys_with_pop_for_test(&sk_new, &pk_new);
+ rotate_consensus_key(validator, signer::address_of(validator), pk_new_bytes);
+
+ let (stored_bytes, _net, _fn) =
+ get_validator_config(signer::address_of(validator));
+ let stored =
+ validator_public_keys::validator_public_keys_from_bytes(stored_bytes);
+
+ // The DKG threshold key survived the operator rotation (0 = validity certificate type).
+ let preserved =
+ validator_public_keys::get_supra_bls_threshold_key_by_type(&stored, 0);
+ assert!(option::is_some(&preserved), 3001);
+ assert!(option::extract(&mut preserved) == threshold_key, 3002);
+
+ // The static keys were actually rotated to `pk_new`'s.
+ assert!(
+ validator_public_keys::get_supra_ed_key(&stored)
+ == validator_public_keys::get_supra_ed_key(&pk_new),
+ 3003
+ );
+ assert!(
+ validator_public_keys::get_network_key(&stored)
+ == validator_public_keys::get_network_key(&pk_new),
+ 3004
+ );
+ }
+
+ #[test(supra_framework = @supra_framework, validator = @0x123)]
+ #[expected_failure(abort_code = 0x1000b, location = Self)]
+ /// Registration must reject a consensus public key blob that deserializes but carries a
+ /// malformed static key (here, an empty network key), even when its appended BLS multisig PoP
+ /// is valid. Validated under the v2 identity format.
+ public entry fun test_initialize_validator_rejects_invalid_consensus_pubkey(
+ supra_framework: &signer, validator: &signer
+ ) acquires AllowedValidators, ValidatorSet, ValidatorConfig {
+ initialize_for_test(supra_framework);
+ features::change_feature_flags_for_testing(
+ supra_framework,
+ vector[features::get_supra_validator_identity_v2_feature()],
+ vector[]
+ );
+ let invalid_pubkey =
+ validator_public_keys::serialized_keys_with_invalid_network_key_and_pop_for_test();
+ initialize_validator(validator, invalid_pubkey, b"net", b"fn");
+ }
+
+ #[test(supra_framework = @supra_framework, validator = @0x123)]
+ #[expected_failure(abort_code = 0x10018, location = Self)]
+ /// Under v2, an operator key submission whose appended BLS multisig PoP does not verify must be
+ /// rejected with `EINVALID_PROOF_OF_POSSESSION` (24 = 0x18).
+ public entry fun test_initialize_validator_rejects_invalid_pop(
+ supra_framework: &signer, validator: &signer
+ ) acquires AllowedValidators, ValidatorSet, ValidatorConfig {
+ initialize_for_test(supra_framework);
+ features::change_feature_flags_for_testing(
+ supra_framework,
+ vector[features::get_supra_validator_identity_v2_feature()],
+ vector[]
+ );
+ // Well-formed keys, but the appended PoP is generated from a different (freshly generated)
+ // secret, so it does not match pk's multisig key.
+ let (_sk, pk) = generate_identity();
+ let (other_sk, _other_pk) = generate_identity();
+ let blob =
+ validator_public_keys::serialized_keys_with_pop_for_test(&other_sk, &pk);
+ initialize_validator(validator, blob, b"net", b"fn");
+ }
+
+ #[test(supra_framework = @supra_framework, validator = @0x123)]
+ /// Under v2, a correctly pop-appended operator submission succeeds and the stored key is the
+ /// canonical `ValidatorPublicKeys` with the PoP stripped (parses with no trailing bytes).
+ public entry fun test_rotate_consensus_key_accepts_valid_pop_and_strips_it(
+ supra_framework: &signer, validator: &signer
+ ) acquires AllowedValidators, SupraCoinCapabilities, OwnerCapability, StakePool, ValidatorConfig, ValidatorPerformance, ValidatorSet, ValidatorFees {
+ initialize_for_test(supra_framework);
+ let (_sk, pk) = generate_identity();
+ initialize_test_validator(&pk, validator, 100, true, true);
+
+ features::change_feature_flags_for_testing(
+ supra_framework,
+ vector[features::get_supra_validator_identity_v2_feature()],
+ vector[]
+ );
+
+ let (sk_new, pk_new) = generate_identity();
+ let blob =
+ validator_public_keys::serialized_keys_with_pop_for_test(&sk_new, &pk_new);
+ rotate_consensus_key(validator, signer::address_of(validator), blob);
+
+ // The stored key is the canonical (PoP-stripped) ValidatorPublicKeys for pk_new.
+ let (stored_bytes, _net, _fn) =
+ get_validator_config(signer::address_of(validator));
+ let stored =
+ validator_public_keys::validator_public_keys_from_bytes(stored_bytes);
+ assert!(
+ validator_public_keys::get_supra_ed_key(&stored)
+ == validator_public_keys::get_supra_ed_key(&pk_new),
+ 6001
+ );
+ assert!(
+ stored_bytes == validator_public_keys::public_key_to_bytes(pk_new),
+ 6002
+ );
+ }
+
+ #[test(supra_framework = @supra_framework, validator = @0x123)]
+ /// Genesis registration is PoP-exempt: under v2, `initialize_validator_genesis` accepts a blob
+ /// with no appended PoP and stores it verbatim.
+ public entry fun test_initialize_validator_genesis_skips_pop(
+ supra_framework: &signer, validator: &signer
+ ) acquires AllowedValidators, ValidatorSet, ValidatorConfig {
+ initialize_for_test(supra_framework);
+ features::change_feature_flags_for_testing(
+ supra_framework,
+ vector[features::get_supra_validator_identity_v2_feature()],
+ vector[]
+ );
+ let (_sk, pk) = generate_identity();
+ let pk_bytes = validator_public_keys::public_key_to_bytes(pk);
+ let validator_address = signer::address_of(validator);
+ if (!account::exists_at(validator_address)) {
+ account::create_account_for_test(validator_address);
+ };
+ initialize_validator_genesis(validator, pk_bytes, b"net", b"fn");
+ let (stored_bytes, _net, _fn) = get_validator_config(validator_address);
+ assert!(stored_bytes == pk_bytes, 7001);
+ }
+
+ #[test(supra_framework = @supra_framework, validator = @0x123)]
+ /// Pre-v2 migration fallback: an operator new-format submission must still carry a valid BLS
+ /// multisig PoP, which the contract verifies and strips, storing the canonical key. PoP
+ /// enforcement is tied to the key format, not the v2 flag.
+ public entry fun test_rotate_consensus_key_fallback_verifies_pop(
+ supra_framework: &signer, validator: &signer
+ ) acquires AllowedValidators, SupraCoinCapabilities, OwnerCapability, StakePool, ValidatorConfig, ValidatorPerformance, ValidatorSet, ValidatorFees {
+ initialize_for_test(supra_framework);
+ let (_sk, pk) = generate_identity();
+ initialize_test_validator(&pk, validator, 100, true, true);
+
+ // Disable v2 to exercise the pre-v2 migration fallback.
+ features::change_feature_flags_for_testing(
+ supra_framework,
+ vector[],
+ vector[features::get_supra_validator_identity_v2_feature()]
+ );
+
+ let (sk_new, pk_new) = generate_identity();
+ let blob =
+ validator_public_keys::serialized_keys_with_pop_for_test(&sk_new, &pk_new);
+ rotate_consensus_key(validator, signer::address_of(validator), blob);
+
+ let (stored_bytes, _net, _fn) =
+ get_validator_config(signer::address_of(validator));
+ assert!(stored_bytes == validator_public_keys::public_key_to_bytes(pk_new), 8001);
+ }
+
+ #[test(supra_framework = @supra_framework, validator = @0x123)]
+ #[expected_failure(abort_code = 0x10018, location = Self)]
+ /// Pre-v2 migration fallback: a new-format submission with a non-matching PoP is rejected with
+ /// `EINVALID_PROOF_OF_POSSESSION` (24 = 0x18), exactly as under v2.
+ public entry fun test_rotate_consensus_key_fallback_rejects_invalid_pop(
+ supra_framework: &signer, validator: &signer
+ ) acquires AllowedValidators, SupraCoinCapabilities, OwnerCapability, StakePool, ValidatorConfig, ValidatorPerformance, ValidatorSet, ValidatorFees {
+ initialize_for_test(supra_framework);
+ let (_sk, pk) = generate_identity();
+ initialize_test_validator(&pk, validator, 100, true, true);
+ features::change_feature_flags_for_testing(
+ supra_framework,
+ vector[],
+ vector[features::get_supra_validator_identity_v2_feature()]
+ );
+ // PoP generated from a different secret, so it does not match pk_new's multisig key.
+ let (_sk_new, pk_new) = generate_identity();
+ let (other_sk, _other_pk) = generate_identity();
+ let blob =
+ validator_public_keys::serialized_keys_with_pop_for_test(&other_sk, &pk_new);
+ rotate_consensus_key(validator, signer::address_of(validator), blob);
+ }
+
+ #[test(supra_framework = @supra_framework, validator = @0x123)]
+ /// Pre-v2 migration fallback: a bare 32-byte ed25519 consensus key (legacy format) has no
+ /// aggregatable BLS key, so it is accepted without a PoP and stored unchanged.
+ public entry fun test_rotate_consensus_key_fallback_accepts_legacy_ed25519(
+ supra_framework: &signer, validator: &signer
+ ) acquires AllowedValidators, SupraCoinCapabilities, OwnerCapability, StakePool, ValidatorConfig, ValidatorPerformance, ValidatorSet, ValidatorFees {
+ initialize_for_test(supra_framework);
+ let (_sk, pk) = generate_identity();
+ initialize_test_validator(&pk, validator, 100, true, true);
+ features::change_feature_flags_for_testing(
+ supra_framework,
+ vector[],
+ vector[features::get_supra_validator_identity_v2_feature()]
+ );
+ let (_legacy_sk, legacy_pk) = ed25519::generate_keys();
+ let legacy_bytes = ed25519::validated_public_key_to_bytes(&legacy_pk);
+ rotate_consensus_key(validator, signer::address_of(validator), legacy_bytes);
+ let (stored_bytes, _net, _fn) =
+ get_validator_config(signer::address_of(validator));
+ assert!(stored_bytes == legacy_bytes, 8101);
+ }
+
#[test(supra_framework = @supra_framework, validator_1 = @0x123, validator_2 = @0x234)]
#[expected_failure(abort_code = 0x10017, location = Self)]
public entry fun test_update_network_addresses_rejects_duplicate(
@@ -2668,7 +3020,10 @@ module supra_framework::stake {
// validator_2 registers with empty addresses then tries to claim validator_1's net address.
initialize_test_validator(&pk_2, validator_2, 100, true, true);
update_network_and_fullnode_addresses(
- validator_2, signer::address_of(validator_2), b"net-1", b"fn-2"
+ validator_2,
+ signer::address_of(validator_2),
+ b"net-1",
+ b"fn-2"
);
}
@@ -2689,7 +3044,10 @@ module supra_framework::stake {
join_validator_set(validator_2, validator_2_address);
// Clearing to empty is always allowed (empty == "unset / non-functional").
update_network_and_fullnode_addresses(
- validator_2, validator_2_address, vector::empty(), vector::empty()
+ validator_2,
+ validator_2_address,
+ vector::empty(),
+ vector::empty()
);
}
@@ -2705,7 +3063,10 @@ module supra_framework::stake {
join_validator_set(validator, validator_address);
// Re-setting the same address on self must be a no-op, not an abort.
update_network_and_fullnode_addresses(
- validator, validator_address, b"net-self", b"fn-self"
+ validator,
+ validator_address,
+ b"net-self",
+ b"fn-self"
);
}
@@ -3406,9 +3767,15 @@ module supra_framework::stake {
assert!(validator_config_2.config.validator_index == 1, 5);
// Validator 1 rotates consensus key. Validator 2 leaves. Validator 3 joins.
- let (_sk_1b, pk_1b) = generate_identity();
+ let (sk_1b, pk_1b) = generate_identity();
+ // `pk_1b_bytes` is the canonical (PoP-stripped) form the contract stores; the submission
+ // carries the appended PoP.
let pk_1b_bytes = validator_public_keys::public_key_to_bytes(pk_1b);
- rotate_consensus_key(validator_1, validator_1_address, pk_1b_bytes);
+ rotate_consensus_key(
+ validator_1,
+ validator_1_address,
+ validator_public_keys::serialized_keys_with_pop_for_test(&sk_1b, &pk_1b)
+ );
leave_validator_set(validator_2, validator_2_address);
join_validator_set(validator_3, validator_3_address);
// Validator 2 is not effectively removed until next epoch.
@@ -3512,9 +3879,15 @@ module supra_framework::stake {
assert_validator_state(pool_address, 0, 0, 0, 0, 0);
// Operator can separately rotate consensus key.
- let (_sk_new, pk_new) = generate_identity();
+ let (sk_new, pk_new) = generate_identity();
+ // `pk_new_bytes` is the canonical (PoP-stripped) form the contract stores; the submission
+ // carries the appended PoP.
let pk_new_bytes = validator_public_keys::public_key_to_bytes(pk_new);
- rotate_consensus_key(validator, pool_address, pk_new_bytes);
+ rotate_consensus_key(
+ validator,
+ pool_address,
+ validator_public_keys::serialized_keys_with_pop_for_test(&sk_new, &pk_new)
+ );
let validator_config = borrow_global(pool_address);
assert!(validator_config.consensus_pubkey == pk_new_bytes, 2);
@@ -3958,8 +4331,9 @@ module supra_framework::stake {
// Initialize validator config.
let validator_address = signer::address_of(validator);
- let (_sk_new, pk_new) = generate_identity();
- let pk_new_bytes = validator_public_keys::public_key_to_bytes(pk_new);
+ let (sk_new, pk_new) = generate_identity();
+ let pk_new_bytes =
+ validator_public_keys::serialized_keys_with_pop_for_test(&sk_new, &pk_new);
rotate_consensus_key(validator, validator_address, pk_new_bytes);
// Join the validator set with enough stake. This now wouldn't fail since the validator config already exists.
diff --git a/aptos-move/framework/supra-framework/sources/validator_public_keys.move b/aptos-move/framework/supra-framework/sources/validator_public_keys.move
index f6dba388b6b..d22e1f1a562 100644
--- a/aptos-move/framework/supra-framework/sources/validator_public_keys.move
+++ b/aptos-move/framework/supra-framework/sources/validator_public_keys.move
@@ -8,6 +8,7 @@ module supra_framework::validator_public_keys {
use aptos_std::ed25519;
use aptos_std::type_info;
use supra_std::class_groups;
+ use std::vector;
#[test_only]
use aptos_std::bls12381::public_key_with_pop_to_normal;
#[test_only]
@@ -37,30 +38,88 @@ module supra_framework::validator_public_keys {
/// Error: Unknown certificate threshold type.
const EUNKNOWN_THRESHOLD_TYPE: u64 = 1;
+ /// Error: A consensus key blob is too short to contain an appended BLS proof-of-possession.
+ const EINVALID_POP_LENGTH: u64 = 2;
/// Internal tag wrapper
- struct CertificateThresholdType has copy, drop, store { tag: u8 }
-
- public fun validity_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_VALIDITY } }
- public fun quorum_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_QUORUM } }
- public fun unanimous_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_UNANIMOUS } }
- public fun bcft_validity_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_BCFT_VALIDITY } }
- public fun bcft_quorum_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_BCFT_QUORUM } }
- public fun bcft_fallback_view_change_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE } }
- public fun clan_majority_certificate_type(): CertificateThresholdType { CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_CLAN_MAJORITY } }
-
- public fun is_validity_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_VALIDITY }
- public fun is_quorum_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_QUORUM }
- public fun is_unanimous_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_UNANIMOUS }
- public fun is_bcft_validity_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_BCFT_VALIDITY }
- public fun is_bcft_quorum_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_BCFT_QUORUM }
- public fun is_bcft_fallback_view_change_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE }
- public fun is_clan_majority_certificate_type(t: &CertificateThresholdType): bool { t.tag == CERTIFICATE_THRESHOLD_TYPE_CLAN_MAJORITY }
+ struct CertificateThresholdType has copy, drop, store {
+ tag: u8
+ }
+
+ public fun validity_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_VALIDITY }
+ }
+
+ public fun quorum_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_QUORUM }
+ }
+
+ public fun unanimous_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_UNANIMOUS }
+ }
+
+ public fun bcft_validity_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_BCFT_VALIDITY }
+ }
+
+ public fun bcft_quorum_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_BCFT_QUORUM }
+ }
+
+ public fun bcft_fallback_view_change_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType {
+ tag: CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE
+ }
+ }
+
+ public fun clan_majority_certificate_type(): CertificateThresholdType {
+ CertificateThresholdType { tag: CERTIFICATE_THRESHOLD_TYPE_CLAN_MAJORITY }
+ }
+
+ public fun is_validity_certificate_type(t: &CertificateThresholdType): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_VALIDITY
+ }
+
+ public fun is_quorum_certificate_type(t: &CertificateThresholdType): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_QUORUM
+ }
+
+ public fun is_unanimous_certificate_type(
+ t: &CertificateThresholdType
+ ): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_UNANIMOUS
+ }
+
+ public fun is_bcft_validity_certificate_type(
+ t: &CertificateThresholdType
+ ): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_BCFT_VALIDITY
+ }
+
+ public fun is_bcft_quorum_certificate_type(
+ t: &CertificateThresholdType
+ ): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_BCFT_QUORUM
+ }
+
+ public fun is_bcft_fallback_view_change_certificate_type(
+ t: &CertificateThresholdType
+ ): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE
+ }
+
+ public fun is_clan_majority_certificate_type(
+ t: &CertificateThresholdType
+ ): bool {
+ t.tag == CERTIFICATE_THRESHOLD_TYPE_CLAN_MAJORITY
+ }
/// The size of a serialized ed25519 public key, in bytes.
const ED25519_PUBLIC_KEY_NUM_BYTES: u64 = 32;
/// The size of a serialized bls12381 G1 public key, in bytes.
const BLS12381_G1_PUBLIC_KEY_NUM_BYTES: u64 = 48;
+ /// The size of a serialized bls12381 proof-of-possession (a G2 signature), in bytes.
+ const BLS12381_POP_NUM_BYTES: u64 = 96;
/// InternalPublicKeys consists of:
/// 1. bls multisig key
@@ -77,7 +136,7 @@ module supra_framework::validator_public_keys {
bls_threshold_bcft_fallback_view_change_certificate_key: option::Option,
bls_threshold_clan_majority_certificate_key: option::Option,
class_group_key: class_groups::CGPublicKey,
- ed25519_key: ed25519::ValidatedPublicKey,
+ ed25519_key: ed25519::ValidatedPublicKey
}
/// ValidatorPublicKeys consists of:
@@ -85,7 +144,7 @@ module supra_framework::validator_public_keys {
/// 2. supra's internal keys
struct ValidatorPublicKeys has copy, drop, store {
network_key: ed25519::ValidatedPublicKey,
- supra_keys: InternalPublicKeys,
+ supra_keys: InternalPublicKeys
}
#[test_only]
@@ -101,68 +160,194 @@ module supra_framework::validator_public_keys {
supra_bls_threshold_bcft_fallback_view_change_key: option::Option,
supra_bls_threshold_clan_majority_key: option::Option,
cg_key: class_groups::SecretKey,
- supra_ed_key: ed25519::SecretKey,
+ supra_ed_key: ed25519::SecretKey
}
public fun validator_public_keys_from_bytes(bytes: vector): ValidatorPublicKeys {
// bcs deserialization
- let bytes_serialized = any::new(type_info::type_name(), bytes);
+ let bytes_serialized =
+ any::new(type_info::type_name(), bytes);
let validator_public_keys = any::unpack(bytes_serialized);
validator_public_keys
}
- public fun public_key_to_bytes(pk: ValidatorPublicKeys): vector{
+ public fun public_key_to_bytes(pk: ValidatorPublicKeys): vector {
// bcs deserialization
bcs::to_bytes(&pk)
}
- public fun get_network_key(pk: &ValidatorPublicKeys): ed25519::ValidatedPublicKey{
+ public fun get_network_key(pk: &ValidatorPublicKeys): ed25519::ValidatedPublicKey {
pk.network_key
}
- public fun get_supra_bls_multi_sig_pub_key(pk: &ValidatorPublicKeys): bls12381::PublicKey{
+ public fun get_supra_bls_multi_sig_pub_key(pk: &ValidatorPublicKeys): bls12381::PublicKey {
pk.supra_keys.bls_multisig_key
}
- public fun get_supra_cg_key(pk: &ValidatorPublicKeys): class_groups::CGPublicKey{
+ public fun get_supra_cg_key(pk: &ValidatorPublicKeys): class_groups::CGPublicKey {
pk.supra_keys.class_group_key
}
- public fun get_supra_ed_key(pk: &ValidatorPublicKeys): ed25519::ValidatedPublicKey{
+ public fun get_supra_ed_key(pk: &ValidatorPublicKeys): ed25519::ValidatedPublicKey {
pk.supra_keys.ed25519_key
}
- public fun rotate_supra_bls_threshold_validity_key(pk: &mut ValidatorPublicKeys, new_bls_threshold_validity_key: bls12381::PublicKey) {
- pk.supra_keys.bls_threshold_validity_certificate_key = option::some(new_bls_threshold_validity_key);
+ /// Overwrites the four static keys of `target` (network_key, bls_multisig_key, class_group_key,
+ /// ed25519_key) with those from `source`, leaving the DKG-managed BLS threshold key fields of
+ /// `target` untouched. Used by `stake::rotate_consensus_key` so that an operator key rotation
+ /// only changes the static keys and cannot clobber the threshold keys written by the DKG via
+ /// `stake::set_dkg_output_keys`.
+ public fun replace_static_keys(
+ target: &mut ValidatorPublicKeys, source: &ValidatorPublicKeys
+ ) {
+ target.network_key = source.network_key;
+ target.supra_keys.bls_multisig_key = source.supra_keys.bls_multisig_key;
+ target.supra_keys.class_group_key = source.supra_keys.class_group_key;
+ target.supra_keys.ed25519_key = source.supra_keys.ed25519_key;
+ }
+
+ /// Returns true iff the four static (non-DKG) public keys decode to cryptographically valid
+ /// keys: the network key, the BLS multisig key, the class-group key and the ed25519 key.
+ ///
+ /// `validator_public_keys_from_bytes` reconstructs each key's raw bytes via BCS without
+ /// running the per-key validation natives, so a registration blob can carry malformed keys
+ /// (the `Validated*`/`PublicKey` types are a misnomer after deserialization). This re-runs
+ /// each key's validating constructor on the recovered bytes to reject them.
+ ///
+ /// The DKG-managed BLS threshold key shares are intentionally not checked: they are absent at
+ /// registration and are produced/overwritten by the protocol via `stake::set_dkg_output_keys`
+ /// (which validates them). Proof-of-possession is not verified here either (no PoP is supplied
+ /// at registration); only key well-formedness / subgroup membership is checked.
+ public fun validate_static_keys(pk: &ValidatorPublicKeys): bool {
+ is_valid_ed25519_key(&pk.network_key)
+ && is_valid_bls12381_key(&pk.supra_keys.bls_multisig_key)
+ && is_valid_cg_key(&pk.supra_keys.class_group_key)
+ && is_valid_ed25519_key(&pk.supra_keys.ed25519_key)
+ }
+
+ fun is_valid_ed25519_key(pk: &ed25519::ValidatedPublicKey): bool {
+ option::is_some(
+ &ed25519::new_validated_public_key_from_bytes(
+ ed25519::validated_public_key_to_bytes(pk)
+ )
+ )
+ }
+
+ fun is_valid_bls12381_key(pk: &bls12381::PublicKey): bool {
+ option::is_some(
+ &bls12381::public_key_from_bytes(bls12381::public_key_to_bytes(pk))
+ )
+ }
+
+ fun is_valid_cg_key(pk: &class_groups::CGPublicKey): bool {
+ option::is_some(
+ &class_groups::public_key_from_bytes(class_groups::public_key_to_bytes(pk))
+ )
+ }
+
+ /// Splits a submitted consensus key blob into its `ValidatorPublicKeys` bytes (the prefix) and
+ /// the appended BLS12-381 proof-of-possession (the trailing `BLS12381_POP_NUM_BYTES` bytes).
+ /// Operators submit the PoP appended to the key bytes so that the contract can verify it
+ /// without a dedicated entry-function parameter (Move forbids changing public signatures).
+ public fun split_consensus_key_and_pop(blob: vector): (vector, vector) {
+ let len = vector::length(&blob);
+ assert!(len >= BLS12381_POP_NUM_BYTES, error::invalid_argument(EINVALID_POP_LENGTH));
+ let pop = vector::trim(&mut blob, len - BLS12381_POP_NUM_BYTES);
+ (blob, pop)
+ }
+
+ /// Returns true iff `pop_bytes` is a valid BLS12-381 proof-of-possession for the BLS multisig
+ /// key in `pk`. The multisig key is the only aggregatable (hence rogue-key-attackable) key an
+ /// operator submits, so it is the only one that requires a PoP; the class-group key carries its
+ /// own ZK PoP (verified by its `validate_pubkey_internal` native), the ed25519 keys are
+ /// non-aggregated, and the BLS threshold shares are produced by the DKG.
+ public fun verify_bls_multisig_pop(
+ pk: &ValidatorPublicKeys, pop_bytes: vector
+ ): bool {
+ let pk_bytes = bls12381::public_key_to_bytes(&pk.supra_keys.bls_multisig_key);
+ let pop = bls12381::proof_of_possession_from_bytes(pop_bytes);
+ option::is_some(&bls12381::public_key_from_bytes_with_pop(pk_bytes, &pop))
}
- public fun rotate_supra_bls_threshold_quorum_key(pk: &mut ValidatorPublicKeys, new_bls_threshold_quorum_key: bls12381::PublicKey) {
- pk.supra_keys.bls_threshold_quorum_certificate_key = option::some(new_bls_threshold_quorum_key);
+ #[test_only]
+ /// Returns the BLS threshold key for the given threshold type, used by tests to assert that
+ /// the DKG-managed threshold keys survive an operator key rotation.
+ public fun get_supra_bls_threshold_key_by_type(
+ pk: &ValidatorPublicKeys, threshold_type: u8
+ ): option::Option {
+ if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_VALIDITY) {
+ pk.supra_keys.bls_threshold_validity_certificate_key
+ } else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_QUORUM) {
+ pk.supra_keys.bls_threshold_quorum_certificate_key
+ } else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_UNANIMOUS) {
+ pk.supra_keys.bls_threshold_unanimous_certificate_key
+ } else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_BCFT_VALIDITY) {
+ pk.supra_keys.bls_threshold_bcft_validity_certificate_key
+ } else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_BCFT_QUORUM) {
+ pk.supra_keys.bls_threshold_bcft_quorum_certificate_key
+ } else if (threshold_type
+ == CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE) {
+ pk.supra_keys.bls_threshold_bcft_fallback_view_change_certificate_key
+ } else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_CLAN_MAJORITY) {
+ pk.supra_keys.bls_threshold_clan_majority_certificate_key
+ } else {
+ abort error::invalid_argument(EUNKNOWN_THRESHOLD_TYPE)
+ }
+ }
+
+ public fun rotate_supra_bls_threshold_validity_key(
+ pk: &mut ValidatorPublicKeys, new_bls_threshold_validity_key: bls12381::PublicKey
+ ) {
+ pk.supra_keys.bls_threshold_validity_certificate_key = option::some(
+ new_bls_threshold_validity_key
+ );
}
- public fun rotate_supra_bls_threshold_unanimous_key(pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey) {
+ public fun rotate_supra_bls_threshold_quorum_key(
+ pk: &mut ValidatorPublicKeys, new_bls_threshold_quorum_key: bls12381::PublicKey
+ ) {
+ pk.supra_keys.bls_threshold_quorum_certificate_key = option::some(
+ new_bls_threshold_quorum_key
+ );
+ }
+
+ public fun rotate_supra_bls_threshold_unanimous_key(
+ pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey
+ ) {
pk.supra_keys.bls_threshold_unanimous_certificate_key = option::some(new_key);
}
- public fun rotate_supra_bls_threshold_bcft_validity_key(pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey) {
+ public fun rotate_supra_bls_threshold_bcft_validity_key(
+ pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey
+ ) {
pk.supra_keys.bls_threshold_bcft_validity_certificate_key = option::some(new_key);
}
- public fun rotate_supra_bls_threshold_bcft_quorum_key(pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey) {
+ public fun rotate_supra_bls_threshold_bcft_quorum_key(
+ pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey
+ ) {
pk.supra_keys.bls_threshold_bcft_quorum_certificate_key = option::some(new_key);
}
- public fun rotate_supra_bls_threshold_bcft_fallback_view_change_key(pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey) {
- pk.supra_keys.bls_threshold_bcft_fallback_view_change_certificate_key = option::some(new_key);
+ public fun rotate_supra_bls_threshold_bcft_fallback_view_change_key(
+ pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey
+ ) {
+ pk.supra_keys.bls_threshold_bcft_fallback_view_change_certificate_key = option::some(
+ new_key
+ );
}
- public fun rotate_supra_bls_threshold_clan_majority_key(pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey) {
+ public fun rotate_supra_bls_threshold_clan_majority_key(
+ pk: &mut ValidatorPublicKeys, new_key: bls12381::PublicKey
+ ) {
pk.supra_keys.bls_threshold_clan_majority_certificate_key = option::some(new_key);
}
/// Rotate a threshold key based on the threshold type tag.
/// threshold_type: 0=validity, 1=quorum, 2=unanimous, 3=bcft_validity, 4=bcft_quorum, 5=bcft_fallback_view_change, 6=clan_majority
- public fun rotate_supra_bls_threshold_key_by_type(pk: &mut ValidatorPublicKeys, threshold_type: u8, new_key: bls12381::PublicKey) {
+ public fun rotate_supra_bls_threshold_key_by_type(
+ pk: &mut ValidatorPublicKeys, threshold_type: u8, new_key: bls12381::PublicKey
+ ) {
if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_VALIDITY) {
rotate_supra_bls_threshold_validity_key(pk, new_key);
} else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_QUORUM) {
@@ -173,7 +358,8 @@ module supra_framework::validator_public_keys {
rotate_supra_bls_threshold_bcft_validity_key(pk, new_key);
} else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_BCFT_QUORUM) {
rotate_supra_bls_threshold_bcft_quorum_key(pk, new_key);
- } else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE) {
+ } else if (threshold_type
+ == CERTIFICATE_THRESHOLD_TYPE_BCFT_FALLBACK_VIEW_CHANGE) {
rotate_supra_bls_threshold_bcft_fallback_view_change_key(pk, new_key);
} else if (threshold_type == CERTIFICATE_THRESHOLD_TYPE_CLAN_MAJORITY) {
rotate_supra_bls_threshold_clan_majority_key(pk, new_key);
@@ -186,11 +372,12 @@ module supra_framework::validator_public_keys {
/// Generates validator key pair for testing.
public fun generate_keys(): (ValidatorSecretKeys, ValidatorPublicKeys) {
let (network_key_sk, network_key_pk) = ed25519::generate_keys();
- let (supra_bls12381_multi_sig_sk, supra_bls12381_multi_sig_pk) = bls12381::generate_keys();
+ let (supra_bls12381_multi_sig_sk, supra_bls12381_multi_sig_pk) =
+ bls12381::generate_keys();
let (supra_cg_sk, supra_cg_pk) = class_groups::generate_keys();
let (supra_ed_key_sk, supra_ed_key_pk) = ed25519::generate_keys();
- let sk = ValidatorSecretKeys{
+ let sk = ValidatorSecretKeys {
network_key: network_key_sk,
supra_bls_multi_sig_bls_key: supra_bls12381_multi_sig_sk,
supra_bls_threshold_validity_key: option::none(),
@@ -201,13 +388,15 @@ module supra_framework::validator_public_keys {
supra_bls_threshold_bcft_fallback_view_change_key: option::none(),
supra_bls_threshold_clan_majority_key: option::none(),
cg_key: supra_cg_sk,
- supra_ed_key: supra_ed_key_sk,
+ supra_ed_key: supra_ed_key_sk
};
let pk = ValidatorPublicKeys {
network_key: network_key_pk,
- supra_keys: InternalPublicKeys{
- bls_multisig_key: public_key_with_pop_to_normal(&supra_bls12381_multi_sig_pk),
+ supra_keys: InternalPublicKeys {
+ bls_multisig_key: public_key_with_pop_to_normal(
+ &supra_bls12381_multi_sig_pk
+ ),
bls_threshold_validity_certificate_key: option::none(),
bls_threshold_quorum_certificate_key: option::none(),
bls_threshold_unanimous_certificate_key: option::none(),
@@ -216,13 +405,57 @@ module supra_framework::validator_public_keys {
bls_threshold_bcft_fallback_view_change_certificate_key: option::none(),
bls_threshold_clan_majority_certificate_key: option::none(),
class_group_key: supra_cg_pk,
- ed25519_key: supra_ed_key_pk,
- },
+ ed25519_key: supra_ed_key_pk
+ }
};
(sk, pk)
}
+ #[test_only]
+ /// Serializes `pk` and appends a valid BLS multisig proof-of-possession generated from `sk`,
+ /// producing the exact blob an operator submits to `stake::rotate_consensus_key` under the v2
+ /// identity format.
+ public fun serialized_keys_with_pop_for_test(
+ sk: &ValidatorSecretKeys, pk: &ValidatorPublicKeys
+ ): vector {
+ let pop = bls12381::generate_proof_of_possession(&sk.supra_bls_multi_sig_bls_key);
+ let blob = public_key_to_bytes(*pk);
+ vector::append(&mut blob, bls12381::proof_of_possession_to_bytes(&pop));
+ blob
+ }
+
+ #[test]
+ fun test_verify_bls_multisig_pop() {
+ let (sk, pk) = validator_public_keys::generate_keys();
+ // A PoP generated from the matching secret verifies.
+ let pop = bls12381::generate_proof_of_possession(&sk.supra_bls_multi_sig_bls_key);
+ assert!(
+ verify_bls_multisig_pop(&pk, bls12381::proof_of_possession_to_bytes(&pop)),
+ 5001
+ );
+ // A PoP for a different key does not.
+ let (other_sk, _other_pk) = validator_public_keys::generate_keys();
+ let other_pop =
+ bls12381::generate_proof_of_possession(&other_sk.supra_bls_multi_sig_bls_key);
+ assert!(
+ !verify_bls_multisig_pop(&pk, bls12381::proof_of_possession_to_bytes(&other_pop)),
+ 5002
+ );
+ }
+
+ #[test]
+ fun test_split_consensus_key_and_pop_roundtrip() {
+ let (sk, pk) = validator_public_keys::generate_keys();
+ let keys_bytes = public_key_to_bytes(pk);
+ let blob = serialized_keys_with_pop_for_test(&sk, &pk);
+ let (split_keys, split_pop) = split_consensus_key_and_pop(blob);
+ // The prefix is exactly the original key bytes...
+ assert!(split_keys == keys_bytes, 5101);
+ // ...and the suffix is a PoP that verifies against the multisig key.
+ assert!(verify_bls_multisig_pop(&pk, split_pop), 5102);
+ }
+
#[test]
fun test_serde_roundtrip() {
// Generate full keypair
@@ -254,4 +487,87 @@ module supra_framework::validator_public_keys {
let ed1 = get_supra_ed_key(&parsed);
assert!(ed0 == ed1, 1004);
}
+
+ #[test]
+ fun test_replace_static_keys_preserves_threshold_keys() {
+ // `target` carries a DKG-style threshold key; `source` carries only static keys (its
+ // threshold fields are `none`, modelling an operator's rotation payload).
+ let (_sk_a, pk_a) = validator_public_keys::generate_keys();
+ let (_sk_b, pk_b) = validator_public_keys::generate_keys();
+
+ let (_t_sk, t_pop_pk) = bls12381::generate_keys();
+ let threshold_key = public_key_with_pop_to_normal(&t_pop_pk);
+ rotate_supra_bls_threshold_validity_key(&mut pk_a, threshold_key);
+
+ replace_static_keys(&mut pk_a, &pk_b);
+
+ // The four static keys now equal those of `source`.
+ assert!(get_network_key(&pk_a) == get_network_key(&pk_b), 2001);
+ assert!(
+ get_supra_bls_multi_sig_pub_key(&pk_a)
+ == get_supra_bls_multi_sig_pub_key(&pk_b),
+ 2002
+ );
+ assert!(get_supra_cg_key(&pk_a) == get_supra_cg_key(&pk_b), 2003);
+ assert!(get_supra_ed_key(&pk_a) == get_supra_ed_key(&pk_b), 2004);
+
+ // The DKG-managed threshold key was preserved (it was `none` in `source`).
+ let preserved =
+ get_supra_bls_threshold_key_by_type(
+ &pk_a, CERTIFICATE_THRESHOLD_TYPE_VALIDITY
+ );
+ assert!(option::is_some(&preserved), 2005);
+ assert!(option::extract(&mut preserved) == threshold_key, 2006);
+ }
+
+ #[test_only]
+ /// Rewrites a serialized `ValidatorPublicKeys` blob so its network key is malformed (replaced
+ /// with an empty byte vector) while keeping the BCS framing intact, so it still deserializes via
+ /// `validator_public_keys_from_bytes` yet fails `validate_static_keys`.
+ fun with_empty_network_key(bytes: vector): vector {
+ // The network key is the first BCS field: a 32-byte vector encoded as `0x20 ++ 32 bytes`.
+ // Replace it with an empty vector (`0x00`) and keep the remaining fields untouched.
+ let corrupt = vector::empty();
+ vector::push_back(&mut corrupt, 0);
+ let i = 33; // skip the original length byte (0x20) plus the 32 key bytes.
+ let len = vector::length(&bytes);
+ while (i < len) {
+ vector::push_back(&mut corrupt, *vector::borrow(&bytes, i));
+ i = i + 1;
+ };
+ corrupt
+ }
+
+ #[test_only]
+ /// Returns a malformed (empty network key) `ValidatorPublicKeys` blob with no appended PoP. Used
+ /// to exercise `validate_static_keys` directly.
+ public fun serialized_keys_with_invalid_network_key_for_test(): vector {
+ let (_sk, pk) = validator_public_keys::generate_keys();
+ with_empty_network_key(public_key_to_bytes(pk))
+ }
+
+ #[test_only]
+ /// Returns a malformed (empty network key) blob with a *valid* BLS multisig PoP appended -- the
+ /// shape an operator submits under v2. PoP verification passes (the multisig key is untouched),
+ /// so `stake::validate_consensus_public_key` reaches and fails the static-key check, exercising
+ /// the full registration path.
+ public fun serialized_keys_with_invalid_network_key_and_pop_for_test(): vector {
+ let (sk, pk) = validator_public_keys::generate_keys();
+ let corrupt = with_empty_network_key(public_key_to_bytes(pk));
+ let pop = bls12381::generate_proof_of_possession(&sk.supra_bls_multi_sig_bls_key);
+ vector::append(&mut corrupt, bls12381::proof_of_possession_to_bytes(&pop));
+ corrupt
+ }
+
+ #[test]
+ fun test_validate_static_keys_rejects_malformed_key() {
+ // Freshly generated keys are valid.
+ let (_sk, pk) = validator_public_keys::generate_keys();
+ assert!(validate_static_keys(&pk), 4001);
+
+ // A blob carrying a malformed (empty) network key deserializes but fails validation.
+ let corrupt = serialized_keys_with_invalid_network_key_for_test();
+ let bad = validator_public_keys_from_bytes(corrupt);
+ assert!(!validate_static_keys(&bad), 4002);
+ }
}
diff --git a/aptos-move/framework/supra-framework/tests/delegation_pool_integration_tests.move b/aptos-move/framework/supra-framework/tests/delegation_pool_integration_tests.move
index 7bc68221d48..9fec6ac9f86 100644
--- a/aptos-move/framework/supra-framework/tests/delegation_pool_integration_tests.move
+++ b/aptos-move/framework/supra-framework/tests/delegation_pool_integration_tests.move
@@ -14,6 +14,7 @@ module supra_framework::delegation_pool_integration_tests {
use supra_framework::reconfiguration;
use supra_framework::pbo_delegation_pool as dp;
use supra_framework::timestamp;
+ use supra_framework::validator_public_keys;
#[test_only]
const EPOCH_DURATION: u64 = 60;
@@ -36,9 +37,6 @@ module supra_framework::delegation_pool_integration_tests {
#[test_only]
const MODULE_EVENT: u64 = 26;
- #[test_only]
- const CONSENSUS_KEY_1: vector = x"20ed28c35dc60a6c9629a7eb0a8dfa815c85c6cf2cedc9e43314cc26f02d741dc430b40c7239278dae17d68d06950d51ce61c2c16c032ceea62c9715611be7f577f48525ad610432bb5d13fa5826fc07c6650000000000000080090192000000000000007bb0b330cbdcec145d2671ef2532bb7a856a5155c66d7ad89ef2cb65e854fc08a5d43cd159a7abc77adfee05e764ec9abdfc133db8e8621398dcbe53db1069de56405f02a1347d5399502fe849b44cfa673ad95bdbf33b417f3f8e4fa854b87b4a60eff6426b277d6036ffc0cfcdd25c971d53c2b612ed1df66f33eff839c0064ac16d7e04650d13984b08b2f9e7bb240fe3ff920000000000000014a973f3c74f41cecc3c596a125835264d72803ba1ef9c9270fa2228e8f8442889f20ddfffc60bc073758c3fd13a29219ad4532c512d2a97c3a8421dedfa41e4d8d9ffaa3581049a65a64f57ea533ab831c88527bb3d7ce46b6eebd6fef6d3303cf7716455381c74da2bcbfc3e02de2e5fadb014ed4f7ecf6b62a4ce6c0db90b91f084512a18be5cb8892b1615a7bb7ef35d0192000000000000008c49908494889c8d07e0b6f0cb6871aaf7e97506476d0a63530ce06b58928225a4292dff8ffaccd4f217fd65c2afbac848ac5d5957d15aa83fb8d2f261d85ddf4f16feee49d07f274dd421f84862bd73634925c41d2b470066d751732cc4f9630e886df15e63f962959c8f765b48434b6124fe051c2a02ba91bd94a7d8fc3cf7240ffba508ff73d8bb95a8ba661fa59ed9c9b8030000000000000192000000000000007bb0b330cbdcec145d2671ef2532bb7a856a5155c66d7ad89ef2cb65e854fc08a5d43cd159a7abc77adfee05e764ec9abdfc133db8e8621398dcbe53db1069de56405f02a1347d5399502fe849b44cfa673ad95bdbf33b417f3f8e4fa854b87b4a60eff6426b277d6036ffc0cfcdd25c971d53c2b612ed1df66f33eff839c0064ac16d7e04650d13984b08b2f9e7bb240fe3ff920000000000000014a973f3c74f41cecc3c596a125835264d72803ba1ef9c9270fa2228e8f8442889f20ddfffc60bc073758c3fd13a29219ad4532c512d2a97c3a8421dedfa41e4d8d9ffaa3581049a65a64f57ea533ab831c88527bb3d7ce46b6eebd6fef6d3303cf7716455381c74da2bcbfc3e02de2e5fadb014ed4f7ecf6b62a4ce6c0db90b91f084512a18be5cb8892b1615a7bb7ef35d0192000000000000008c49908494889c8d07e0b6f0cb6871aaf7e97506476d0a63530ce06b58928225a4292dff8ffaccd4f217fd65c2afbac848ac5d5957d15aa83fb8d2f261d85ddf4f16feee49d07f274dd421f84862bd73634925c41d2b470066d751732cc4f9630e886df15e63f962959c8f765b48434b6124fe051c2a02ba91bd94a7d8fc3cf7240ffba508ff73d8bb95a8ba661fa59ed9c9b8030000000000000120000000000000000da685763a376d42546959f3e2d96ff46256cd3e539dd458b800c9d0daf4d981019c00000000000000a529edcfd13d9f5014b4e2fab134f833d266876a0a0fbcf3577ef87ced2656924e235d4a6f48169d0c6b1fd4d77b27f36b0eda1e78fb1691894e077333e7f278444a3e6de72685de03564bcf61466567e34c79279fcdccc42c2354e4c0a836e785f909656badb5950ebcf77a1ef9a3680818abe674760bc09888a446b96d9765779222970aaeb804abaa939c4225c474c2bc925be1d24648c6298d69208e695ae5888320393ef041be5f42d0f2eaac6036daffb8373eb80ad275fcc2b9";
-
#[test_only]
public fun initialize_for_test(supra_framework: &signer) {
initialize_for_test_custom(
@@ -810,8 +808,11 @@ module supra_framework::delegation_pool_integration_tests {
stake::assert_validator_state(validator_2_address, 100 * ONE_SUPRA, 0, 0, 0, 1);
// Validator 1 rotates consensus key. Validator 2 leaves. Validator 3 joins.
- let (_sk_1b, _, _) = generate_identity();
- stake::rotate_consensus_key(validator_1, validator_1_address, CONSENSUS_KEY_1);
+ stake::rotate_consensus_key(
+ validator_1,
+ validator_1_address,
+ stake::generate_unique_consensus_pubkey_bytes()
+ );
stake::leave_validator_set(validator_2, validator_2_address);
stake::join_validator_set(validator_3, validator_3_address);
// Validator 2 is not effectively removed until next epoch.
@@ -878,11 +879,14 @@ module supra_framework::delegation_pool_integration_tests {
assert!(coin::balance(signer::address_of(validator)) == 101 * ONE_SUPRA, 1);
stake::assert_validator_state(pool_address, 0, 0, 0, 0, 0);
- // Operator can separately rotate consensus key.
- let (_sk_new, _, _) = generate_identity();
- stake::rotate_consensus_key(validator, pool_address, CONSENSUS_KEY_1);
+ // Operator can separately rotate consensus key. The submission carries an appended BLS
+ // multisig PoP, which the contract verifies and strips before storing the canonical key.
+ let new_consensus_key = stake::generate_unique_consensus_pubkey_bytes();
+ stake::rotate_consensus_key(validator, pool_address, new_consensus_key);
let (consensus_pubkey, _, _) = stake::get_validator_config(pool_address);
- assert!(consensus_pubkey == CONSENSUS_KEY_1, 2);
+ let (expected_stored, _pop) =
+ validator_public_keys::split_consensus_key_and_pop(new_consensus_key);
+ assert!(consensus_pubkey == expected_stored, 2);
// Operator can update network and fullnode addresses.
stake::update_network_and_fullnode_addresses(validator, pool_address, b"1", b"2");
diff --git a/crates/aptos-telemetry-service/src/validator_cache.rs b/crates/aptos-telemetry-service/src/validator_cache.rs
index 53afae31d1a..a4fbcd7dc4d 100644
--- a/crates/aptos-telemetry-service/src/validator_cache.rs
+++ b/crates/aptos-telemetry-service/src/validator_cache.rs
@@ -199,7 +199,7 @@ mod tests {
let validator_info = ValidatorInfo::new(
PeerId::random(),
10,
- ValidatorConfig::new(keypair.public_key, vec![0, 0], vec![0, 0], 2),
+ ValidatorConfig::legacy_new_do_not_use(keypair.public_key, vec![0, 0], vec![0, 0], 2),
);
let validator_set = ValidatorSet::new(vec![validator_info]);
@@ -242,7 +242,7 @@ mod tests {
let validator_info = ValidatorInfo::new(
PeerId::random(),
10,
- ValidatorConfig::new(
+ ValidatorConfig::legacy_new_do_not_use(
keypair.public_key,
bcs::to_bytes(&vec![NetworkAddress::from_str("/dns/a5f3d921730874389bb2f66275f163a5-8f14ad5b5e992c1c.elb.ap-southeast-1.amazonaws.com/tcp/6180/noise-ik/0xc5edf62233096df793b554e1013b07c83d01b3cf50c14ac83a0a7e0cfe340426/handshake/0").unwrap()]).unwrap(),
bcs::to_bytes(&vec![NetworkAddress::from_str("/dns/fullnode0.testnet.aptoslabs.com/tcp/6182/noise-ik/0xea19ab47ed9191865f15d85d751ed0663205c0b2f0f465714b1947c023715973/handshake/0").unwrap()]).unwrap(),
diff --git a/crates/aptos/src/node/mod.rs b/crates/aptos/src/node/mod.rs
index 1aaa6916a00..1ca112de70e 100644
--- a/crates/aptos/src/node/mod.rs
+++ b/crates/aptos/src/node/mod.rs
@@ -902,7 +902,7 @@ impl TryFrom<&ValidatorInfo> for ValidatorInfoSummary {
fn try_from(info: &ValidatorInfo) -> Result {
let config = info.config();
let config = ValidatorConfig {
- consensus_public_key: config.consensus_public_key().to_bytes().to_vec(),
+ consensus_public_key: config.legacy_consensus_public_key_do_not_use().to_bytes().to_vec(),
validator_network_addresses: config.validator_network_addresses.clone(),
fullnode_network_addresses: config.fullnode_network_addresses.clone(),
validator_index: config.validator_index,
@@ -921,7 +921,7 @@ impl From<&ValidatorInfoSummary> for ValidatorInfo {
ValidatorInfo::new(
summary.account_address,
summary.consensus_voting_power,
- aptos_types::validator_config::ValidatorConfig::new(
+ aptos_types::validator_config::ValidatorConfig::legacy_new_do_not_use(
PublicKey::from_encoded_string(&config.consensus_public_key).unwrap(),
bcs::to_bytes(&config.validator_network_addresses).unwrap(),
bcs::to_bytes(&config.fullnode_network_addresses).unwrap(),
diff --git a/execution/executor/tests/storage_integration_test.rs b/execution/executor/tests/storage_integration_test.rs
index 1e413c1d056..3c68b4f7c29 100644
--- a/execution/executor/tests/storage_integration_test.rs
+++ b/execution/executor/tests/storage_integration_test.rs
@@ -97,11 +97,11 @@ fn test_reconfiguration() {
.payload()
.next()
.unwrap()
- .consensus_public_key(),
- &ValidatorConfig::fetch_move_resource(&db_state_view, &validator_account)
+ .legacy_consensus_public_key_do_not_use(),
+ ValidatorConfig::fetch_move_resource(&db_state_view, &validator_account)
.unwrap()
.unwrap()
- .consensus_public_key
+ .legacy_consensus_public_key_do_not_use()
);
// txn1 = give the validator some money so they can send a tx
diff --git a/network/discovery/src/validator_set.rs b/network/discovery/src/validator_set.rs
index 6f8305950ae..31507209bea 100644
--- a/network/discovery/src/validator_set.rs
+++ b/network/discovery/src/validator_set.rs
@@ -252,7 +252,7 @@ mod tests {
let validator = ValidatorInfo::new(
peer_id,
0,
- ValidatorConfig::new(
+ ValidatorConfig::legacy_new_do_not_use(
consensus_pubkey,
validator_encoded_addresses,
fullnode_encoded_addresses,
diff --git a/types/src/validator_config.rs b/types/src/validator_config.rs
index 65baf9ed0ea..cc79c18418c 100644
--- a/types/src/validator_config.rs
+++ b/types/src/validator_config.rs
@@ -35,6 +35,8 @@ impl MoveResource for ValidatorOperatorConfigResource {}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[cfg_attr(any(test, feature = "fuzzing"), derive(Arbitrary))]
pub struct ValidatorConfig {
+ /// This is a BCS serialized `ValidatorPublicKeys` (see the smr-moonshot type).
+ /// We haven't renamed the field because it is exposed to users.
consensus_public_key: Vec,
/// This is an bcs serialized `Vec`
pub validator_network_addresses: Vec,
@@ -44,7 +46,9 @@ pub struct ValidatorConfig {
}
impl ValidatorConfig {
- pub fn new(
+ /// Deprecated function kept to avoid making changes to aptos-core. Must not be used in smr-moonshot.
+ /// TODO: Remove when unused aptos-core code is removed.
+ pub fn legacy_new_do_not_use(
consensus_public_key: ed25519::PublicKey,
validator_network_addresses: Vec,
fullnode_network_addresses: Vec,
@@ -58,11 +62,13 @@ impl ValidatorConfig {
}
}
- pub fn consensus_key_raw(&self) -> Vec {
- self.consensus_public_key.clone()
+ pub fn public_keys(&self) -> &Vec {
+ &self.consensus_public_key
}
- pub fn consensus_public_key(&self) -> Ed25519PublicKey {
+ /// Deprecated function kept to avoid making changes to aptos-core. Must not be used in smr-moonshot.
+ /// TODO: Remove when unused aptos-core code is removed.
+ pub fn legacy_consensus_public_key_do_not_use(&self) -> Ed25519PublicKey {
let keys = bcs::from_bytes::(&self.consensus_public_key);
if let Ok(keys) = keys {
let ed_key = Ed25519PublicKey::try_from(keys.supra_keys().ed25519_key().as_slice())
diff --git a/types/src/validator_info.rs b/types/src/validator_info.rs
index 999ec011df9..2e9723be959 100644
--- a/types/src/validator_info.rs
+++ b/types/src/validator_info.rs
@@ -60,7 +60,7 @@ impl ValidatorInfo {
validator_index: u64,
) -> Self {
let addr = NetworkAddress::mock();
- let config = ValidatorConfig::new(
+ let config = ValidatorConfig::legacy_new_do_not_use(
consensus_public_key,
bcs::to_bytes(&vec![addr.clone()]).unwrap(),
bcs::to_bytes(&vec![addr]).unwrap(),
@@ -80,14 +80,10 @@ impl ValidatorInfo {
&self.account_address
}
- /// Returns the key for validating signed messages from this validator
- pub fn consensus_public_key(&self) -> ed25519::PublicKey {
- self.config.consensus_public_key()
- }
-
- /// Returns the key for validating signed messages from this validator
- pub fn consensus_key_bytes(&self) -> ed25519::PublicKey {
- self.config.consensus_public_key()
+ /// Deprecated function kept to avoid making changes to aptos-core. Must not be used in smr-moonshot.
+ /// TODO: Remove when unused aptos-core code is removed.
+ pub fn legacy_consensus_public_key_do_not_use(&self) -> ed25519::PublicKey {
+ self.config.legacy_consensus_public_key_do_not_use()
}
/// Returns the voting power for this validator
diff --git a/types/src/validator_verifier.rs b/types/src/validator_verifier.rs
index a13f7b69575..9672b98092b 100644
--- a/types/src/validator_verifier.rs
+++ b/types/src/validator_verifier.rs
@@ -476,7 +476,7 @@ impl From<&ValidatorSet> for ValidatorVerifier {
info.config().validator_index,
ValidatorConsensusInfo::new(
info.account_address,
- info.consensus_public_key().clone(),
+ info.legacy_consensus_public_key_do_not_use().clone(),
info.consensus_voting_power(),
),
)