Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions packages/contracts/mirror/service-manager/src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,23 +229,21 @@ fn validate_quorum(
.may_load_at_height(deps.storage, reference_block)?
.ok_or_else(|| StdError::msg("quorum denominator missing at reference_block"))?;

// Calculate threshold weight: (total_weight * numerator) / denominator
let threshold_weight =
total_weight.full_mul(numerator) / cosmwasm_std::Uint512::from(denominator);
// Convert threshold_weight from Uint512 to Uint256 (safely)
let threshold_weight = threshold_weight
.try_into()
.unwrap_or(cosmwasm_std::Uint256::MAX);

// Avoid 0 weight ever passing this check
if total_weight.is_zero() {
return Ok(WavsValidateResult::Err(
WavsValidateError::InsufficientQuorumZero,
));
}

// Check if signed_weight >= threshold_weight
if signed_weight < threshold_weight {
// Quorum check via cross multiplication to avoid floor-rounding the
// threshold. Integer division would allow under-quorum signatures for
// non-divisible totals, e.g. 1 of 2 passing a 2/3 threshold.
let lhs = cosmwasm_std::Uint512::from(signed_weight) * cosmwasm_std::Uint512::from(denominator);
let rhs = cosmwasm_std::Uint512::from(total_weight) * cosmwasm_std::Uint512::from(numerator);
if lhs < rhs {
let threshold = total_weight.full_mul(numerator) / cosmwasm_std::Uint512::from(denominator);
let threshold_weight = threshold.try_into().unwrap_or(cosmwasm_std::Uint256::MAX);
return Ok(WavsValidateResult::Err(
WavsValidateError::InsufficientQuorum {
signer_weight: signed_weight,
Expand Down
21 changes: 8 additions & 13 deletions packages/contracts/mirror/stake-registry/src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub fn instantiate(
}
}
OWNER.save(deps.storage, &info.sender)?;
TOTAL_WEIGHT.save(deps.storage, &Uint256::zero())?;
TOTAL_WEIGHT.save(deps.storage, &Uint256::zero(), env.block.height)?;

Ok(Response::new()
.add_attribute("method", "instantiate")
Expand Down Expand Up @@ -223,7 +223,7 @@ fn set_operator_details_at(
.map_err(|e| ContractError::Std(StdError::msg(format!("total weight underflow: {e}"))))?
.checked_add(weight)
.map_err(|e| ContractError::Std(StdError::msg(format!("total weight overflow: {e}"))))?;
TOTAL_WEIGHT.save(deps.storage, &new_total_weight)?;
TOTAL_WEIGHT.save(deps.storage, &new_total_weight, snapshot_height)?;

// Update signing key mappings
let current_signing_key =
Expand Down Expand Up @@ -277,7 +277,10 @@ fn query_validate_signature(
envelope: WavsEnvelope,
signature_data: WavsSignatureData,
) -> StdResult<ValidationResult> {
let total_weight = TOTAL_WEIGHT.load(deps.storage)?;
let reference_block = signature_data.reference_block as u64;
let total_weight = TOTAL_WEIGHT
.may_load_at_height(deps.storage, reference_block)?
.unwrap_or_default();
let mut voting_power_signed = Uint256::zero();

// Basic sanity checks to avoid panics and invalid data
Expand Down Expand Up @@ -318,11 +321,7 @@ fn query_validate_signature(
// operator set.
let signing_key_str = signing_key.to_string();
let operator = SIGNING_KEY_TO_OPERATOR
.may_load_at_height(
deps.storage,
signing_key_str.clone(),
signature_data.reference_block as u64,
)?
.may_load_at_height(deps.storage, signing_key_str.clone(), reference_block)?
.ok_or_else(|| {
StdError::msg(format!(
"Signer not registered at reference_block: {signing_key}"
Expand All @@ -344,11 +343,7 @@ fn query_validate_signature(
// post-snapshot weight to a historical envelope.
let operator_key = operator.to_string();
let operator_weight = OPERATOR_WEIGHTS
.may_load_at_height(
deps.storage,
operator_key,
signature_data.reference_block as u64,
)?
.may_load_at_height(deps.storage, operator_key, reference_block)?
.unwrap_or_default();
if operator_weight.is_zero() {
return Err(StdError::msg(format!(
Expand Down
11 changes: 8 additions & 3 deletions packages/contracts/mirror/stake-registry/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Uint256};
use cw_storage_plus::{Item, Map, SnapshotMap, Strategy};
use cw_storage_plus::{Item, Map, SnapshotItem, SnapshotMap, Strategy};
use layer_climb_address::EvmAddr;

// Contract configuration
Expand Down Expand Up @@ -28,8 +28,13 @@ pub const SIGNING_KEY_TO_OPERATOR: SnapshotMap<String, EvmAddr> = SnapshotMap::n
);
pub const OPERATOR_REGISTERED: Map<String, bool> = Map::new("operator_registered");

// Weight tracking
pub const TOTAL_WEIGHT: Item<Uint256> = Item::new("total_weight");
// Weight tracking with historical checkpoints for validating historical signature sets.
pub const TOTAL_WEIGHT: SnapshotItem<Uint256> = SnapshotItem::new(
"total_weight",
"total_weight__checkpoints",
"total_weight__changelog",
Strategy::EveryBlock,
);

#[cw_serde]
pub struct Config {
Expand Down
65 changes: 65 additions & 0 deletions packages/tests/off-chain/tests/contracts_mirror.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#![recursion_limit = "512"]

use cosmwasm_std::{HexBinary, Uint256};
use layer_climb_address::EvmAddr;
use off_chain_tests::client::{
mirror::MirrorTestClient, trigger::SimpleTriggerTestClient, ContractTestClient,
};
Expand Down Expand Up @@ -183,3 +185,66 @@ async fn mirror_service_manager_validate_query_is_result() {
WavsValidateResult::Err(WavsValidateError::InvalidSignature(_))
));
}

#[tokio::test]
async fn mirror_validate_signature_uses_snapshot_total_weight_after_later_weight_changes() {
tracing_tests_init();

let client = ContractTestClient::new("admin");
let app = client.app();
let mirror_client = MirrorTestClient::new(client);

let (signing_key, signing_addr) = mirror_stake_registry::create_signing_key_and_address();
let operator = EvmAddr::new([0x21; 20]);

app.borrow_mut().update_block(|block| block.height += 1);
mirror_client
.stake_registry_executor
.set_operator_details(
operator.clone(),
signing_addr.clone(),
Uint256::from(100u128),
)
.await
.unwrap();
// cw-storage-plus snapshots are queried at the height after the checkpoint.
let reference_block = (app.borrow().block_info().height + 1) as u32;

let message = b"snapshot total weight regression";
let digest = mirror_stake_registry::create_eip191_hash(message);
let signature = mirror_stake_registry::sign_message_hash(&signing_key, digest.as_slice());

app.borrow_mut().update_block(|block| block.height += 2);
mirror_client
.stake_registry_executor
.set_operator_details(
EvmAddr::new([0x22; 20]),
EvmAddr::new([0x23; 20]),
Uint256::from(900u128),
)
.await
.unwrap();

app.borrow_mut().update_block(|block| block.height += 1);
mirror_client
.stake_registry_executor
.set_operator_details(operator, signing_addr.clone(), Uint256::from(1u128))
.await
.unwrap();

let result = mirror_client
.stake_registry_querier
.validate_signature(
WavsEnvelope::new_raw(message.to_vec()),
WavsSignatureData {
signers: vec![signing_addr],
signatures: vec![HexBinary::from(signature)],
reference_block,
},
)
.await
.unwrap();

assert_eq!(result.total_voting_power, Uint256::from(100u128));
assert_eq!(result.voting_power_signed, Uint256::from(100u128));
}
6 changes: 3 additions & 3 deletions packages/tests/shared/src/mirror_stake_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ use layer_climb_address::EvmAddr;
use rand::thread_rng;
use wavs_types::contracts::cosmwasm::service_handler::{WavsEnvelope, WavsSignatureData};

fn create_eip191_hash(message: &[u8]) -> B256 {
pub fn create_eip191_hash(message: &[u8]) -> B256 {
eip191_hash_message(alloy_keccak256(message))
}

fn create_signing_key_and_address() -> (SigningKey, EvmAddr) {
pub fn create_signing_key_and_address() -> (SigningKey, EvmAddr) {
let signing_key = SigningKey::random(&mut thread_rng());
let eth_address = derive_eth_address_from_signing_key(&signing_key);
(signing_key, eth_address)
Expand All @@ -34,7 +34,7 @@ fn derive_eth_address_from_signing_key(signing_key: &SigningKey) -> EvmAddr {
EvmAddr::new(addr_bytes)
}

fn sign_message_hash(signing_key: &SigningKey, message_hash: &[u8]) -> Vec<u8> {
pub fn sign_message_hash(signing_key: &SigningKey, message_hash: &[u8]) -> Vec<u8> {
let signature: Signature = signing_key.sign_prehash(message_hash).unwrap();
let (r, s) = signature.split_bytes();

Expand Down
Loading