From 864e7b4b23633423ec9b51c0f8b2e9e1b064d883 Mon Sep 17 00:00:00 2001 From: Houmaan Chamani Date: Wed, 4 Sep 2024 12:19:47 -0400 Subject: [PATCH] feat(minor-service-registry)!: verifier details query (#591) --- Cargo.lock | 1 + contracts/service-registry/Cargo.toml | 1 + contracts/service-registry/src/contract.rs | 106 +++++++++++++++++- .../service-registry/src/contract/query.rs | 61 ++++++---- contracts/service-registry/src/msg.rs | 13 ++- contracts/service-registry/src/state.rs | 52 +++++++-- 6 files changed, 199 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f199f40c..e8262480a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7538,6 +7538,7 @@ dependencies = [ "cw2 1.1.2", "error-stack", "integration-tests", + "itertools 0.11.0", "msgs-derive", "report", "router-api", diff --git a/contracts/service-registry/Cargo.toml b/contracts/service-registry/Cargo.toml index b6c9861eb..14262c47e 100644 --- a/contracts/service-registry/Cargo.toml +++ b/contracts/service-registry/Cargo.toml @@ -39,6 +39,7 @@ cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } cw2 = { workspace = true } error-stack = { workspace = true } +itertools = { workspace = true } msgs-derive = { workspace = true } report = { workspace = true } router-api = { workspace = true } diff --git a/contracts/service-registry/src/contract.rs b/contracts/service-registry/src/contract.rs index 3cd025cec..1c401a793 100644 --- a/contracts/service-registry/src/contract.rs +++ b/contracts/service-registry/src/contract.rs @@ -2,14 +2,14 @@ use axelar_wasm_std::{address, permission_control, FnExt}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_json_binary, Addr, BankMsg, Binary, Coin, Deps, DepsMut, Empty, Env, MessageInfo, Order, + to_json_binary, Addr, BankMsg, Binary, Coin, Deps, DepsMut, Empty, Env, MessageInfo, QueryRequest, Response, Storage, WasmQuery, }; use error_stack::{bail, Report, ResultExt}; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::state::{AuthorizationState, BondingState, Service, Verifier, SERVICES, VERIFIERS}; +use crate::state::{AuthorizationState, BondingState, Service, SERVICES, VERIFIERS}; mod execute; mod migrations; @@ -182,6 +182,7 @@ pub fn migrate( #[cfg(test)] mod test { + use std::collections::HashSet; use std::str::FromStr; use axelar_wasm_std::error::err_contains; @@ -193,7 +194,8 @@ mod test { use router_api::ChainName; use super::*; - use crate::state::{WeightedVerifier, VERIFIER_WEIGHT}; + use crate::msg::VerifierDetails; + use crate::state::{Verifier, WeightedVerifier, VERIFIER_WEIGHT}; const GOVERNANCE_ADDRESS: &str = "governance"; const UNAUTHORIZED_ADDRESS: &str = "unauthorized"; @@ -2055,7 +2057,7 @@ mod test { }, ); assert!(res.is_ok()); - let verifier: Verifier = from_json( + let verifier2_details: VerifierDetails = from_json( query( deps.as_ref(), mock_env(), @@ -2067,6 +2069,7 @@ mod test { .unwrap(), ) .unwrap(); + let verifier = verifier2_details.verifier; assert_eq!( verifier.bonding_state, @@ -2111,4 +2114,99 @@ mod test { ContractError::VerifierJailed )); } + + #[test] + fn get_single_verifier_details() { + let mut deps = setup(); + + let service_name = "validators"; + let min_verifier_bond: nonempty::Uint128 = Uint128::new(100).try_into().unwrap(); + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(GOVERNANCE_ADDRESS, &[]), + ExecuteMsg::RegisterService { + service_name: service_name.into(), + coordinator_contract: Addr::unchecked(COORDINATOR_ADDRESS), + min_num_verifiers: 0, + max_num_verifiers: Some(100), + min_verifier_bond, + bond_denom: AXL_DENOMINATION.into(), + unbonding_period_days: 10, + description: "Some service".into(), + }, + ); + assert!(res.is_ok()); + + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(GOVERNANCE_ADDRESS, &[]), + ExecuteMsg::AuthorizeVerifiers { + verifiers: vec![VERIFIER_ADDRESS.into()], + service_name: service_name.into(), + }, + ); + assert!(res.is_ok()); + + let res = execute( + deps.as_mut(), + mock_env(), + mock_info( + VERIFIER_ADDRESS, + &coins(min_verifier_bond.into_inner().u128(), AXL_DENOMINATION), + ), + ExecuteMsg::BondVerifier { + service_name: service_name.into(), + }, + ); + assert!(res.is_ok()); + + let chains = vec![ + ChainName::from_str("ethereum").unwrap(), + ChainName::from_str("binance").unwrap(), + ChainName::from_str("avalanche").unwrap(), + ]; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(VERIFIER_ADDRESS, &[]), + ExecuteMsg::RegisterChainSupport { + service_name: service_name.into(), + chains: chains.clone(), + }, + ); + assert!(res.is_ok()); + + let verifier_details: VerifierDetails = from_json( + query( + deps.as_ref(), + mock_env(), + QueryMsg::Verifier { + service_name: service_name.into(), + verifier: VERIFIER_ADDRESS.into(), + }, + ) + .unwrap(), + ) + .unwrap(); + + assert_eq!( + verifier_details.verifier, + Verifier { + address: Addr::unchecked(VERIFIER_ADDRESS), + bonding_state: BondingState::Bonded { + amount: min_verifier_bond + }, + authorization_state: AuthorizationState::Authorized, + service_name: service_name.into() + } + ); + assert_eq!(verifier_details.weight, VERIFIER_WEIGHT); + + let expected_chains: HashSet = chains.into_iter().collect(); + let actual_chains: HashSet = + verifier_details.supported_chains.into_iter().collect(); + assert_eq!(expected_chains, actual_chains); + } } diff --git a/contracts/service-registry/src/contract/query.rs b/contracts/service-registry/src/contract/query.rs index 8e48a3b5a..6f5127107 100644 --- a/contracts/service-registry/src/contract/query.rs +++ b/contracts/service-registry/src/contract/query.rs @@ -1,6 +1,9 @@ +use cosmwasm_std::Order; +use itertools::Itertools; use router_api::ChainName; use super::*; +use crate::msg::VerifierDetails; use crate::state::{WeightedVerifier, VERIFIERS, VERIFIERS_PER_CHAIN, VERIFIER_WEIGHT}; pub fn active_verifiers( @@ -13,21 +16,26 @@ pub fn active_verifiers( .ok_or(ContractError::ServiceNotFound)?; let verifiers: Vec<_> = VERIFIERS_PER_CHAIN - .prefix((&service_name, &chain_name)) - .range(deps.storage, None, None, Order::Ascending) - .map(|res| res.and_then(|(addr, _)| VERIFIERS.load(deps.storage, (&service_name, &addr)))) - .collect::, _>>()? - .into_iter() - .filter(|verifier| match verifier.bonding_state { - BondingState::Bonded { amount } => amount >= service.min_verifier_bond, - _ => false, + .prefix((service_name.clone(), chain_name.clone())) + .keys(deps.storage, None, None, Order::Ascending) + .filter_map_ok(|verifier_addr| { + VERIFIERS + .may_load(deps.storage, (&service_name, &verifier_addr)) + .ok() + .flatten() }) - .filter(|verifier| verifier.authorization_state == AuthorizationState::Authorized) - .map(|verifier| WeightedVerifier { + .filter_ok(|verifier| { + matches!( + verifier.bonding_state, + BondingState::Bonded { amount } if amount >= service.min_verifier_bond + ) + }) + .filter_ok(|verifier| verifier.authorization_state == AuthorizationState::Authorized) + .map_ok(|verifier| WeightedVerifier { verifier_info: verifier, weight: VERIFIER_WEIGHT, // all verifiers have an identical const weight for now }) - .collect(); + .try_collect()?; if verifiers.len() < service.min_num_verifiers.into() { Err(ContractError::NotEnoughVerifiers) @@ -40,17 +48,26 @@ pub fn verifier( deps: Deps, service_name: String, verifier: String, -) -> Result { - VERIFIERS - .may_load( - deps.storage, - ( - &service_name, - &address::validate_cosmwasm_address(deps.api, &verifier)?, - ), - )? - .ok_or(ContractError::VerifierNotFound)? - .then(Ok) +) -> Result { + let verifier_addr = address::validate_cosmwasm_address(deps.api, &verifier)?; + + let verifier = VERIFIERS + .may_load(deps.storage, (&service_name, &verifier_addr))? + .ok_or(ContractError::VerifierNotFound)?; + + let supported_chains = VERIFIERS_PER_CHAIN + .idx + .verifier_address + .prefix((service_name, verifier_addr.clone())) + .keys(deps.storage, None, None, Order::Ascending) + .map_ok(|(_, chain, _)| chain) + .try_collect()?; + + Ok(VerifierDetails { + verifier, + weight: VERIFIER_WEIGHT, + supported_chains, + }) } pub fn service(deps: Deps, service_name: String) -> Result { diff --git a/contracts/service-registry/src/msg.rs b/contracts/service-registry/src/msg.rs index e3c6b2ed6..b6a289cba 100644 --- a/contracts/service-registry/src/msg.rs +++ b/contracts/service-registry/src/msg.rs @@ -3,6 +3,10 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Addr; use msgs_derive::EnsurePermissions; use router_api::ChainName; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::Verifier; #[cw_serde] pub struct InstantiateMsg { @@ -79,13 +83,20 @@ pub enum QueryMsg { #[returns(crate::state::Service)] Service { service_name: String }, - #[returns(crate::state::Verifier)] + #[returns(VerifierDetails)] Verifier { service_name: String, verifier: String, }, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct VerifierDetails { + pub verifier: Verifier, + pub weight: nonempty::Uint128, + pub supported_chains: Vec, +} + #[cw_serde] pub struct MigrateMsg { pub coordinator_contract: Addr, diff --git a/contracts/service-registry/src/state.rs b/contracts/service-registry/src/state.rs index cf1de2cd4..d824dfb3b 100644 --- a/contracts/service-registry/src/state.rs +++ b/contracts/service-registry/src/state.rs @@ -2,7 +2,7 @@ use axelar_wasm_std::nonempty; use axelar_wasm_std::snapshot::Participant; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Storage, Timestamp, Uint128}; -use cw_storage_plus::Map; +use cw_storage_plus::{Index, IndexList, IndexedMap, KeyDeserialize, Map, MultiIndex}; use router_api::ChainName; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -143,12 +143,46 @@ pub enum AuthorizationState { Jailed, } -type ServiceName = str; +pub struct VerifierPerChainIndexes<'a> { + pub verifier_address: MultiIndex< + 'a, + (ServiceName, VerifierAddress), + (), + (ServiceName, ChainName, VerifierAddress), + >, +} + +impl<'a> IndexList<()> for VerifierPerChainIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index<()>> = vec![&self.verifier_address]; + Box::new(v.into_iter()) + } +} + +pub const VERIFIERS_PER_CHAIN: IndexedMap< + (ServiceName, ChainName, VerifierAddress), + (), + VerifierPerChainIndexes, +> = IndexedMap::new( + "verifiers_per_chain", + VerifierPerChainIndexes { + verifier_address: MultiIndex::new( + |pk: &[u8], _: &()| { + let (service_name, _, verifier) = + <(ServiceName, ChainName, VerifierAddress)>::from_slice(pk) + .expect("invalid primary key"); + (service_name, verifier) + }, + "verifiers_per_chain", + "verifiers_per_chain__address", + ), + }, +); + +type ServiceName = String; type VerifierAddress = Addr; pub const SERVICES: Map<&ServiceName, Service> = Map::new("services"); -pub const VERIFIERS_PER_CHAIN: Map<(&ServiceName, &ChainName, &VerifierAddress), ()> = - Map::new("verifiers_per_chain"); pub const VERIFIERS: Map<(&ServiceName, &VerifierAddress), Verifier> = Map::new("verifiers"); pub fn register_chains_support( @@ -158,9 +192,12 @@ pub fn register_chains_support( verifier: VerifierAddress, ) -> Result<(), ContractError> { for chain in chains.iter() { - VERIFIERS_PER_CHAIN.save(storage, (&service_name, chain, &verifier), &())?; + VERIFIERS_PER_CHAIN.save( + storage, + (service_name.clone(), chain.clone(), verifier.clone()), + &(), + )?; } - Ok(()) } @@ -171,9 +208,8 @@ pub fn deregister_chains_support( verifier: VerifierAddress, ) -> Result<(), ContractError> { for chain in chains { - VERIFIERS_PER_CHAIN.remove(storage, (&service_name, &chain, &verifier)); + VERIFIERS_PER_CHAIN.remove(storage, (service_name.clone(), chain, verifier.clone()))?; } - Ok(()) }