From eb15954d02ec74a8aca007f75aa28a3f0b0e0328 Mon Sep 17 00:00:00 2001 From: cong-or Date: Tue, 14 Jan 2025 10:19:21 +0000 Subject: [PATCH] feat(cat-voices): cip36 registrations consolidated endpoint (#1494) * refactor(ignore): wip * refactor(cip36): remove legacy endpoints * refactor(stake addr): get registration by latest or slot no * refactor(fix types): incoming stake pub key conversion * refactor(invalid registrations): fix * refactor(fix as at time from query string parameter ): asat * refactor(voting key): cross reference activity with stake addr Stake addresses need to be individually checked to make sure they are still actively associated with the voting key, and have not been registered to another voting key. * refactor(voting key): cross reference activity with stake addr Stake addresses need to be individually checked to make sure they are still actively associated with the voting key, and have not been registered to another voting key. * refactor(slot no to big int): into * refactor(snapshot): auth * refactor(snapshot): auth * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(query all): snapshot * refactor(housekeeping): local * refactor(housekeeping): local * refactor(housekeeping): spellcheck * refactor(no auth): rm auth * refactor(improved error handling): bubble up rather than log * refactor(improved error handling): bubble up rather than log * refactor(improved error handling): bubble up rather than log * refactor(add test docs): cardano addresses context * refactor(add test docs): cardano addresses context * refactor(not found): response code --- .config/dictionaries/project.dic | 2 + .../index/queries/cql/get_all_stake_addrs.cql | 3 + .../bin/src/db/index/queries/mod.rs | 13 +- .../get_all_stakes_and_vote_keys.rs | 61 +++ .../registrations/get_from_stake_addr.rs | 6 +- .../queries/registrations/get_invalid.rs | 15 +- .../src/db/index/queries/registrations/mod.rs | 1 + .../cql/cip36_registration_for_vote_key.cql | 2 +- .../bin/src/db/index/tests/scylla_queries.rs | 23 +- .../src/service/api/cardano/cip36/endpoint.rs | 72 ++- .../src/service/api/cardano/cip36/filter.rs | 458 ++++++++++++++++++ .../bin/src/service/api/cardano/cip36/mod.rs | 74 +-- .../service/api/cardano/cip36/old_endpoint.rs | 412 ---------------- .../src/service/api/cardano/cip36/response.rs | 13 +- .../service/common/objects/cardano/cip36.rs | 142 ------ .../src/service/common/objects/cardano/mod.rs | 1 - .../types/cardano/cip19_stake_address.rs | 20 + .../src/service/common/types/cardano/nonce.rs | 2 +- .../common/types/cardano/query/as_at.rs | 16 +- .../service/common/types/cardano/query/mod.rs | 1 + .../types/cardano/query/stake_or_voter.rs | 46 +- .../service/common/types/cardano/slot_no.rs | 10 +- .../service/common/types/cardano/txn_index.rs | 2 +- .../types/generic/ed25519_public_key.rs | 22 + 24 files changed, 738 insertions(+), 679 deletions(-) create mode 100644 catalyst-gateway/bin/src/db/index/queries/cql/get_all_stake_addrs.cql create mode 100644 catalyst-gateway/bin/src/db/index/queries/registrations/get_all_stakes_and_vote_keys.rs create mode 100644 catalyst-gateway/bin/src/service/api/cardano/cip36/filter.rs delete mode 100644 catalyst-gateway/bin/src/service/api/cardano/cip36/old_endpoint.rs delete mode 100644 catalyst-gateway/bin/src/service/common/objects/cardano/cip36.rs diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 9c6c3354447..52e4d0d526f 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -351,3 +351,5 @@ xpub xpublic xvfb yoroi +stake1u94ullc9nj9gawc08990nx8hwgw80l9zpmr8re44kydqy9cdjq6rq +invalid1u9nlq5nmuzthw3vhgakfpxyq4r0zl2c0p8uqy24gpyjsa6c3df4h6 \ No newline at end of file diff --git a/catalyst-gateway/bin/src/db/index/queries/cql/get_all_stake_addrs.cql b/catalyst-gateway/bin/src/db/index/queries/cql/get_all_stake_addrs.cql new file mode 100644 index 00000000000..717a0cca59f --- /dev/null +++ b/catalyst-gateway/bin/src/db/index/queries/cql/get_all_stake_addrs.cql @@ -0,0 +1,3 @@ +SELECT + stake_address,vote_key +FROM cip36_registration; \ No newline at end of file diff --git a/catalyst-gateway/bin/src/db/index/queries/mod.rs b/catalyst-gateway/bin/src/db/index/queries/mod.rs index a759fb5a27e..3496f9338a0 100644 --- a/catalyst-gateway/bin/src/db/index/queries/mod.rs +++ b/catalyst-gateway/bin/src/db/index/queries/mod.rs @@ -16,6 +16,7 @@ use rbac::{ get_role0_chain_root::GetRole0ChainRootQuery, }; use registrations::{ + get_all_stakes_and_vote_keys::GetAllStakesAndVoteKeysQuery, get_from_stake_addr::GetRegistrationQuery, get_from_stake_hash::GetStakeAddrQuery, get_from_vote_key::GetStakeAddrFromVoteKeyQuery, get_invalid::GetInvalidRegistrationQuery, }; @@ -96,6 +97,8 @@ pub(crate) enum PreparedSelectQuery { RegistrationsByChainRoot, /// Get chain root by role0 key ChainRootByRole0Key, + /// Get all stake and vote keys for snapshot (`stake_pub_key,vote_key`) + GetAllStakesAndVoteKeys, } /// All prepared UPSERT query statements (inserts/updates a single value of data). @@ -157,6 +160,8 @@ pub(crate) struct PreparedQueries { registrations_by_chain_root_query: PreparedStatement, /// Get chain root by role0 key chain_root_by_role0_key_query: PreparedStatement, + /// Get all stake and vote keys (`stake_key,vote_key`) for snapshot + get_all_stakes_and_vote_keys_query: PreparedStatement, } /// An individual query response that can fail @@ -193,7 +198,9 @@ impl PreparedQueries { let chain_root_by_stake_address = GetChainRootQuery::prepare(session.clone()).await; let registrations_by_chain_root = GetRegistrationsByChainRootQuery::prepare(session.clone()).await; - let chain_root_by_role0_key = GetRole0ChainRootQuery::prepare(session).await; + let chain_root_by_role0_key = GetRole0ChainRootQuery::prepare(session.clone()).await; + let get_all_stakes_and_vote_keys_query = + GetAllStakesAndVoteKeysQuery::prepare(session).await; let ( txo_insert_queries, @@ -241,6 +248,7 @@ impl PreparedQueries { chain_root_by_stake_address_query: chain_root_by_stake_address?, registrations_by_chain_root_query: registrations_by_chain_root?, chain_root_by_role0_key_query: chain_root_by_role0_key?, + get_all_stakes_and_vote_keys_query: get_all_stakes_and_vote_keys_query?, }) } @@ -331,6 +339,9 @@ impl PreparedQueries { &self.registrations_by_chain_root_query }, PreparedSelectQuery::ChainRootByRole0Key => &self.chain_root_by_role0_key_query, + PreparedSelectQuery::GetAllStakesAndVoteKeys => { + &self.get_all_stakes_and_vote_keys_query + }, }; session diff --git a/catalyst-gateway/bin/src/db/index/queries/registrations/get_all_stakes_and_vote_keys.rs b/catalyst-gateway/bin/src/db/index/queries/registrations/get_all_stakes_and_vote_keys.rs new file mode 100644 index 00000000000..2143cdedd48 --- /dev/null +++ b/catalyst-gateway/bin/src/db/index/queries/registrations/get_all_stakes_and_vote_keys.rs @@ -0,0 +1,61 @@ +//! Get all stake and vote keys (`stake_pub_key,vote_key`) +//! Result is used to compose various query registrations for snapshot. + +use std::sync::Arc; + +use scylla::{ + prepared_statement::PreparedStatement, transport::iterator::TypedRowStream, DeserializeRow, + SerializeRow, Session, +}; +use tracing::error; + +use crate::db::index::{ + queries::{PreparedQueries, PreparedSelectQuery}, + session::CassandraSession, +}; + +/// Get all (`stake_addr,vote` keys) +/// [(`stake_addr,vote_key`)] +const GET_ALL_STAKES_AND_VOTE_KEYS: &str = include_str!("../cql/get_all_stake_addrs.cql"); + +/// Get all stake and vote keys from cip36 registration +#[derive(SerializeRow)] +pub(crate) struct GetAllStakesAndVoteKeysParams {} + +/// Get stakes and vote keys for snapshot. +#[derive(DeserializeRow)] +pub(crate) struct GetAllStakesAndVoteKeysQuery { + /// Full Stake Address (not hashed, 32 byte ED25519 Public key). + pub stake_address: Vec, + /// Voting Public Key + pub vote_key: Vec, +} + +impl GetAllStakesAndVoteKeysQuery { + /// Prepares get all `stake_addr` paired with vote keys [(`stake_addr,vote_key`)] + pub(crate) async fn prepare(session: Arc) -> anyhow::Result { + let get_all_stake_and_vote_keys = PreparedQueries::prepare( + session, + GET_ALL_STAKES_AND_VOTE_KEYS, + scylla::statement::Consistency::All, + true, + ) + .await; + + get_all_stake_and_vote_keys.inspect_err( + |error| error!(error=%error, "Failed to prepare get all (stake addrs, vote_keys)"), + ) + } + + /// Executes get all `stake_addr` paired with vote keys [(`stake_addr,vote_key`)] + pub(crate) async fn execute( + session: &CassandraSession, params: GetAllStakesAndVoteKeysParams, + ) -> anyhow::Result> { + let iter = session + .execute_iter(PreparedSelectQuery::GetAllStakesAndVoteKeys, params) + .await? + .rows_stream::()?; + + Ok(iter) + } +} diff --git a/catalyst-gateway/bin/src/db/index/queries/registrations/get_from_stake_addr.rs b/catalyst-gateway/bin/src/db/index/queries/registrations/get_from_stake_addr.rs index c0ff611937a..5c6e0d7362f 100644 --- a/catalyst-gateway/bin/src/db/index/queries/registrations/get_from_stake_addr.rs +++ b/catalyst-gateway/bin/src/db/index/queries/registrations/get_from_stake_addr.rs @@ -24,10 +24,10 @@ pub(crate) struct GetRegistrationParams { pub stake_address: Vec, } -impl From<&ed25519_dalek::VerifyingKey> for GetRegistrationParams { - fn from(value: &ed25519_dalek::VerifyingKey) -> Self { +impl From> for GetRegistrationParams { + fn from(value: Vec) -> Self { GetRegistrationParams { - stake_address: value.as_bytes().to_vec(), + stake_address: value, } } } diff --git a/catalyst-gateway/bin/src/db/index/queries/registrations/get_invalid.rs b/catalyst-gateway/bin/src/db/index/queries/registrations/get_invalid.rs index ada25c54937..130f6bcb2d8 100644 --- a/catalyst-gateway/bin/src/db/index/queries/registrations/get_invalid.rs +++ b/catalyst-gateway/bin/src/db/index/queries/registrations/get_invalid.rs @@ -8,9 +8,12 @@ use scylla::{ }; use tracing::error; -use crate::db::index::{ - queries::{PreparedQueries, PreparedSelectQuery}, - session::CassandraSession, +use crate::{ + db::index::{ + queries::{PreparedQueries, PreparedSelectQuery}, + session::CassandraSession, + }, + service::common::types::cardano::slot_no::SlotNo, }; /// Get invalid registrations from stake addr query. @@ -28,12 +31,10 @@ pub(crate) struct GetInvalidRegistrationParams { impl GetInvalidRegistrationParams { /// Create a new instance of [`GetInvalidRegistrationParams`] - pub(crate) fn new( - stake_address: Vec, slot_no: num_bigint::BigInt, - ) -> GetInvalidRegistrationParams { + pub(crate) fn new(stake_address: Vec, slot_no: SlotNo) -> GetInvalidRegistrationParams { Self { stake_address, - slot_no, + slot_no: slot_no.into(), } } } diff --git a/catalyst-gateway/bin/src/db/index/queries/registrations/mod.rs b/catalyst-gateway/bin/src/db/index/queries/registrations/mod.rs index b0f2edfa4e4..c4e66038a79 100644 --- a/catalyst-gateway/bin/src/db/index/queries/registrations/mod.rs +++ b/catalyst-gateway/bin/src/db/index/queries/registrations/mod.rs @@ -1,4 +1,5 @@ //! Registration related queries. +pub(crate) mod get_all_stakes_and_vote_keys; pub(crate) mod get_from_stake_addr; pub(crate) mod get_from_stake_hash; pub(crate) mod get_from_vote_key; diff --git a/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration_for_vote_key.cql b/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration_for_vote_key.cql index c3ba5f6dfce..7ec1aee35c2 100644 --- a/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration_for_vote_key.cql +++ b/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration_for_vote_key.cql @@ -1,5 +1,5 @@ -- Index of CIP-36 registrations searchable by Stake Address. --- Full registration data needs to be queried from the man cip36 registration tables. +-- Full registration data needs to be queried from the main cip36 registration tables. -- Includes both Valid and Invalid registrations. CREATE TABLE IF NOT EXISTS cip36_registration_for_vote_key ( -- Primary Key Data diff --git a/catalyst-gateway/bin/src/db/index/tests/scylla_queries.rs b/catalyst-gateway/bin/src/db/index/tests/scylla_queries.rs index 4c83e087e85..c233b9d02df 100644 --- a/catalyst-gateway/bin/src/db/index/tests/scylla_queries.rs +++ b/catalyst-gateway/bin/src/db/index/tests/scylla_queries.rs @@ -4,16 +4,19 @@ use futures::StreamExt; use super::*; -use crate::db::index::queries::{ - rbac::{get_chain_root::*, get_registrations::*, get_role0_chain_root::*}, - registrations::{ - get_from_stake_addr::*, get_from_stake_hash::*, get_from_vote_key::*, get_invalid::*, +use crate::{ + db::index::queries::{ + rbac::{get_chain_root::*, get_registrations::*, get_role0_chain_root::*}, + registrations::{ + get_from_stake_addr::*, get_from_stake_hash::*, get_from_vote_key::*, get_invalid::*, + }, + staked_ada::{ + get_assets_by_stake_address::*, get_txi_by_txn_hash::*, get_txo_by_stake_address::*, + update_txo_spent::*, + }, + sync_status::update::*, }, - staked_ada::{ - get_assets_by_stake_address::*, get_txi_by_txn_hash::*, get_txo_by_stake_address::*, - update_txo_spent::*, - }, - sync_status::update::*, + service::common::types::cardano::slot_no::SlotNo, }; #[ignore = "An integration test which requires a running Scylla node instance, disabled from `testunit` CI run"] @@ -58,7 +61,7 @@ async fn test_get_invalid_registration_w_stake_addr() { let mut row_stream = GetInvalidRegistrationQuery::execute( &session, - GetInvalidRegistrationParams::new(vec![], num_bigint::BigInt::from(i64::MAX)), + GetInvalidRegistrationParams::new(vec![], SlotNo::from(u64::MAX)), ) .await .unwrap(); diff --git a/catalyst-gateway/bin/src/service/api/cardano/cip36/endpoint.rs b/catalyst-gateway/bin/src/service/api/cardano/cip36/endpoint.rs index cfd5d5f2a51..8ad44ace7fb 100644 --- a/catalyst-gateway/bin/src/service/api/cardano/cip36/endpoint.rs +++ b/catalyst-gateway/bin/src/service/api/cardano/cip36/endpoint.rs @@ -1,29 +1,77 @@ //! Implementation of the GET `/cardano/cip36` endpoint -use std::time::Duration; - use poem::http::HeaderMap; -use tokio::time::sleep; +use tracing::error; +use self::cardano::{hash28::HexEncodedHash28, query::stake_or_voter::StakeAddressOrPublicKey}; use super::{ cardano::{self}, - response, NoneOrRBAC, SlotNo, + filter::{get_registration_given_stake_key_hash, get_registration_given_vote_key, snapshot}, + response, SlotNo, +}; +use crate::{ + db::index::session::CassandraSession, + service::{ + api::cardano::cip36::response::AllRegistration, + common::{self, types::headers::retry_after::RetryAfterOption}, + }, }; -use crate::service::common::{self}; /// Process the endpoint operation pub(crate) async fn cip36_registrations( - _lookup: Option, _asat: Option, + lookup: Option, asat: Option, _page: common::types::generic::query::pagination::Page, - _limit: common::types::generic::query::pagination::Limit, _auth: NoneOrRBAC, - _headers: &HeaderMap, + _limit: common::types::generic::query::pagination::Limit, _headers: &HeaderMap, ) -> response::AllRegistration { - // Dummy sleep, remove it - sleep(Duration::from_millis(1)).await; + let Some(session) = CassandraSession::get(true) else { + error!("Failed to acquire db session"); + return AllRegistration::service_unavailable( + &anyhow::anyhow!("Failed to acquire db session"), + RetryAfterOption::Default, + ); + }; + + if let Some(stake_or_voter) = lookup { + match StakeAddressOrPublicKey::from(stake_or_voter) { + StakeAddressOrPublicKey::Address(cip19_stake_address) => { + // Typically, a stake address will start with 'stake1', + // We need to convert this to a stake hash as per our data model to then find the, + // Full Stake Public Key (32 byte Ed25519 Public key, not hashed). + // We then get the latest registration or from a specific time as optionally + // specified in the query parameter. This can be represented as either + // the blockchains slot number or a unix timestamp. + let stake_hash: HexEncodedHash28 = match cip19_stake_address.try_into() { + Ok(stake_hash) => stake_hash, + Err(err) => { + return AllRegistration::handle_error(&anyhow::anyhow!( + "Given stake pub key is corrupt {:?}", + err + )); + }, + }; - // Todo: refactor the below into a single operation here. + return get_registration_given_stake_key_hash(stake_hash, session, asat).await; + }, + StakeAddressOrPublicKey::PublicKey(ed25519_hex_encoded_public_key) => { + // As above... + // Except using a voting key. + return get_registration_given_vote_key( + ed25519_hex_encoded_public_key, + session, + asat, + ) + .await; + }, + StakeAddressOrPublicKey::All => + // As above... + // Snapshot replacement, returns all registrations or returns a + // subset of registrations if constrained by a given time. + { + return snapshot(session, asat).await + }, + }; + }; - // If _asat is None, then get the latest slot number from the chain follower and use that. // If _for is not defined, use the stake addresses defined for Role0 in the _auth // parameter. _auth not yet implemented, so put placeholder for that, and return not // found until _auth is implemented. diff --git a/catalyst-gateway/bin/src/service/api/cardano/cip36/filter.rs b/catalyst-gateway/bin/src/service/api/cardano/cip36/filter.rs new file mode 100644 index 00000000000..53c8cc3de33 --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/cardano/cip36/filter.rs @@ -0,0 +1,458 @@ +//! Implementation of the GET `/cardano/cip36` endpoint + +use std::{cmp::Reverse, sync::Arc}; + +use futures::StreamExt; + +use super::{ + cardano::{ + cip19_shelley_address::Cip19ShelleyAddress, hash28::HexEncodedHash28, nonce::Nonce, + txn_index::TxnIndex, + }, + common::types::generic::error_msg::ErrorMessage, + response::{ + AllRegistration, Cip36Details, Cip36Registration, Cip36RegistrationList, + Cip36RegistrationsForVotingPublicKey, + }, + Ed25519HexEncodedPublicKey, SlotNo, +}; +use crate::db::index::{ + queries::registrations::{ + get_all_stakes_and_vote_keys::{ + GetAllStakesAndVoteKeysParams, GetAllStakesAndVoteKeysQuery, + }, + get_from_stake_addr::{GetRegistrationParams, GetRegistrationQuery}, + get_from_stake_hash::{GetStakeAddrParams, GetStakeAddrQuery}, + get_from_vote_key::{GetStakeAddrFromVoteKeyParams, GetStakeAddrFromVoteKeyQuery}, + get_invalid::{GetInvalidRegistrationParams, GetInvalidRegistrationQuery}, + }, + session::CassandraSession, +}; + +/// Get registration given a stake key hash, it can be time specific based on asat param, +/// or the latest registration returned if no asat given. +pub(crate) async fn get_registration_given_stake_key_hash( + stake_hash: HexEncodedHash28, session: Arc, asat: Option, +) -> AllRegistration { + // Get stake addr associated with given stake hash. + let mut stake_addr_iter = match GetStakeAddrQuery::execute( + &session, + GetStakeAddrParams::new(stake_hash.into()), + ) + .await + { + Ok(stake_addr) => stake_addr, + Err(err) => { + return AllRegistration::handle_error(&anyhow::anyhow!( + "Failed to query stake addr from stake hash {err:?}", + )); + }, + }; + + if let Some(row_stake_addr) = stake_addr_iter.next().await { + let row = match row_stake_addr { + Ok(r) => r, + Err(err) => { + return AllRegistration::handle_error(&anyhow::anyhow!( + "Failed to query stake addr from stake hash {err:?}", + )); + }, + }; + + // Stake hash successfully converted into associated stake pub key which we use to lookup + // registrations. + let stake_pub_key = match Ed25519HexEncodedPublicKey::try_from(row.stake_address.clone()) { + Ok(key) => key, + Err(err) => { + return AllRegistration::internal_error(&anyhow::anyhow!( + "Failed to type stake address {err:?}", + )); + }, + }; + + return get_registration_from_stake_addr(stake_pub_key, asat, session, None).await; + } + + AllRegistration::With(Cip36Registration::NotFound) +} + +/// Get registration from stake addr +pub async fn get_registration_from_stake_addr( + stake_pub_key: Ed25519HexEncodedPublicKey, asat: Option, + session: Arc, vote_key: Option, +) -> AllRegistration { + // Get all registrations from given stake pub key. + let mut registrations = + match get_all_registrations_from_stake_pub_key(&session.clone(), stake_pub_key.clone()) + .await + { + Ok(registration) => registration, + Err(err) => { + return AllRegistration::handle_error(&anyhow::anyhow!( + "Failed to query stake stake pub key {err:?}", + )); + }, + }; + + // check registrations are still actively associated with the voting key, + // and have not been registered to another voting key. + if let Some(vote_key) = vote_key { + registrations = check_stake_addr_voting_key_association(registrations, &vote_key); + } + + // Query requires the registration to be bound by time. + let registration = if let Some(slot_no) = asat { + match get_registration_given_slot_no(registrations, &slot_no) { + Ok(registration) => registration, + Err(err) => { + return AllRegistration::internal_error(&anyhow::anyhow!( + "Failed to get registration given slot no {err:?}", + )); + }, + } + } else { + // Query not bound by time, return latest registration. + match sort_latest_registration(registrations) { + Ok(registration) => registration, + Err(err) => { + return AllRegistration::internal_error(&anyhow::anyhow!( + "Failed to sort latest registration {err:?}", + )); + }, + } + }; + + // Registration found, now find invalids. + let slot_no = registration.clone().slot_no; + let Some(stake_pub_key) = registration.clone().stake_pub_key else { + return AllRegistration::internal_error(&anyhow::anyhow!( + "Stake pub key not in registration {stake_pub_key:?}", + )); + }; + + let Some(vote_pub_key) = registration.clone().vote_pub_key else { + return AllRegistration::internal_error(&anyhow::anyhow!( + "Vote pub key not in registration {stake_pub_key:?}", + )); + }; + + // include any erroneous registrations which occur AFTER the slot# of the last valid + // registration + let invalids_report = + match get_invalid_registrations(stake_pub_key, Some(slot_no.clone()), session).await { + Ok(invalids) => invalids, + Err(err) => { + return AllRegistration::handle_error(&anyhow::anyhow!( + "Failed to obtain invalid registrations for given stake pub key {err:?}", + )); + }, + }; + + AllRegistration::With(Cip36Registration::Ok(poem_openapi::payload::Json( + Cip36RegistrationList { + slot: slot_no, + voting_key: vec![Cip36RegistrationsForVotingPublicKey { + vote_pub_key, + registrations: vec![registration.clone()], + }], + invalid: invalids_report, + page: None, + }, + ))) +} + +/// Stake addresses need to be individually checked to make sure they are still actively +/// associated with the voting key, and have not been registered to another voting key. +fn check_stake_addr_voting_key_association( + registrations: Vec, associated_voting_key: &Ed25519HexEncodedPublicKey, +) -> Vec { + registrations + .into_iter() + .filter(|registration| cross_reference_key(associated_voting_key, registration)) + .collect() +} + +/// Check associated voting key matches registration voting key +fn cross_reference_key( + associated_voting_key: &Ed25519HexEncodedPublicKey, r: &Cip36Details, +) -> bool { + r.vote_pub_key + .clone() + .map(|key| key == *associated_voting_key) + .is_some() +} + +/// Get all cip36 registrations for a given stake address. +async fn get_all_registrations_from_stake_pub_key( + session: &Arc, stake_pub_key: Ed25519HexEncodedPublicKey, +) -> Result, anyhow::Error> { + let mut registrations_iter = GetRegistrationQuery::execute(session, GetRegistrationParams { + stake_address: stake_pub_key.try_into()?, + }) + .await?; + let mut registrations = Vec::new(); + while let Some(row) = registrations_iter.next().await { + let row = row?; + + let nonce = if let Some(nonce) = row.nonce.into_parts().1.to_u64_digits().first() { + *nonce + } else { + continue; + }; + + let slot_no = if let Some(slot_no) = row.slot_no.into_parts().1.to_u64_digits().first() { + *slot_no + } else { + continue; + }; + + let cip36 = Cip36Details { + slot_no: SlotNo::from(slot_no), + stake_pub_key: Some(Ed25519HexEncodedPublicKey::try_from(row.stake_address)?), + vote_pub_key: Some(Ed25519HexEncodedPublicKey::try_from(row.vote_key)?), + nonce: Some(Nonce::from(nonce)), + txn: Some(TxnIndex::try_from(row.txn)?), + payment_address: Some(Cip19ShelleyAddress::new(hex::encode(row.payment_address))), + is_payable: row.is_payable, + cip15: !row.cip36, + errors: vec![], + }; + + registrations.push(cip36); + } + Ok(registrations) +} + +/// Sort latest registrations for a given stake address, sort by slot no and return +/// latest. +fn sort_latest_registration(mut registrations: Vec) -> anyhow::Result { + registrations.sort_by_key(|registration| Reverse(registration.slot_no.clone())); + registrations.into_iter().next().ok_or(anyhow::anyhow!( + "Can't sort latest registrations by slot no" + )) +} + +/// Get registration given slot# +fn get_registration_given_slot_no( + registrations: Vec, slot_no: &SlotNo, +) -> anyhow::Result { + registrations + .into_iter() + .find(|registration| registration.slot_no == *slot_no) + .ok_or(anyhow::anyhow!("Unable to get registration given slot no")) +} + +/// Get invalid registrations for stake addr after given slot# +async fn get_invalid_registrations( + stake_pub_key: Ed25519HexEncodedPublicKey, slot_no: Option, + session: Arc, +) -> anyhow::Result> { + // include any erroneous registrations which occur AFTER the slot# of the last valid + // registration or return all invalids if NO slot# declared. + let slot_no = if let Some(slot_no) = slot_no { + slot_no + } else { + SlotNo::from(0) + }; + + let mut invalid_registrations_iter = GetInvalidRegistrationQuery::execute( + &session, + GetInvalidRegistrationParams::new(stake_pub_key.try_into()?, slot_no.clone()), + ) + .await?; + let mut invalid_registrations = Vec::new(); + while let Some(row) = invalid_registrations_iter.next().await { + let row = row?; + + invalid_registrations.push(Cip36Details { + slot_no: slot_no.clone(), + stake_pub_key: Some(Ed25519HexEncodedPublicKey::try_from(row.stake_address)?), + vote_pub_key: Some(Ed25519HexEncodedPublicKey::try_from(row.vote_key)?), + nonce: None, + txn: None, + payment_address: Some(Cip19ShelleyAddress::new(hex::encode(row.payment_address))), + is_payable: row.is_payable, + cip15: !row.cip36, + errors: row + .error_report + .iter() + .map(|e| ErrorMessage::from(e.to_string())) + .collect(), + }); + } + + Ok(invalid_registrations) +} + +/// Get registration given a vote key, time specific based on asat param, +/// or latest registration returned if no asat given. +pub(crate) async fn get_registration_given_vote_key( + vote_key: Ed25519HexEncodedPublicKey, session: Arc, asat: Option, +) -> AllRegistration { + let voting_key: Vec = match vote_key.clone().try_into() { + Ok(vote_key) => vote_key, + Err(err) => { + return AllRegistration::internal_error(&anyhow::anyhow!( + "Failed to convert vote key to bytes {err:?}", + )); + }, + }; + + // Get stake addr associated voting key. + let mut stake_addr_iter = match GetStakeAddrFromVoteKeyQuery::execute( + &session, + GetStakeAddrFromVoteKeyParams::new(voting_key), + ) + .await + { + Ok(stake_addr) => stake_addr, + Err(err) => { + return AllRegistration::handle_error(&anyhow::anyhow!( + "Failed to query stake addr from vote key {err:?}", + )); + }, + }; + + if let Some(row_stake_addr) = stake_addr_iter.next().await { + let row = match row_stake_addr { + Ok(r) => r, + Err(err) => { + return AllRegistration::handle_error(&anyhow::anyhow!( + "Failed to query stake addr from vote key {err:?}", + )); + }, + }; + + // Stake hash successfully converted into associated stake pub key which we use to lookup + // registrations. + let stake_pub_key = match Ed25519HexEncodedPublicKey::try_from(row.stake_address.clone()) { + Ok(key) => key, + Err(err) => { + return AllRegistration::internal_error(&anyhow::anyhow!( + "Failed to type stake address {err:?}", + )); + }, + }; + + return get_registration_from_stake_addr(stake_pub_key, asat, session, Some(vote_key)) + .await; + } + + AllRegistration::With(Cip36Registration::NotFound) +} + +/// ALL +/// Get all registrations or constrain if slot# given. +pub async fn snapshot(session: Arc, slot_no: Option) -> AllRegistration { + let all_stakes_and_vote_keys = match get_all_stake_addrs_and_vote_keys(&session.clone()).await { + Ok(key_pairs) => key_pairs, + Err(err) => { + return AllRegistration::handle_error(&anyhow::anyhow!("Failed to query ALL {err:?}",)); + }, + }; + + let mut all_registrations_after_filtering = Vec::new(); + let mut all_invalids_after_filtering = Vec::new(); + + // We now have all stake pub keys and vote keys for cip36 registrations. + // Iterate through them and individually get all valid and invalid registrations. + // Compose the result into a snapshot. + // TODO: Optimize: Can be done parallel. + for (stake_public_key, vote_pub_key) in &all_stakes_and_vote_keys { + let mut registrations_for_given_stake_pub_key = + match get_all_registrations_from_stake_pub_key(&session, stake_public_key.clone()).await + { + Ok(registrations) => registrations, + Err(err) => { + return AllRegistration::handle_error(&anyhow::anyhow!( + "Failed to query ALL {err:?}", + )); + }, + }; + + // check the registrations stake pub key are still actively associated with the voting + // key, and have not been registered to another voting key. + registrations_for_given_stake_pub_key = check_stake_addr_voting_key_association( + registrations_for_given_stake_pub_key, + vote_pub_key, + ); + + // ALL: Snapshot can be constrained into a subset with a time constraint or NOT. + if let Some(ref slot_no) = slot_no { + // Any registrations that occurred after this Slot are not included in the list. + let filtered_registrations = + slot_filter(registrations_for_given_stake_pub_key, slot_no); + + if filtered_registrations.is_empty() { + continue; + } + + all_registrations_after_filtering.push(Cip36RegistrationsForVotingPublicKey { + vote_pub_key: vote_pub_key.clone(), + registrations: filtered_registrations, + }); + } else { + // No slot filtering, return ALL registrations without constraints. + all_registrations_after_filtering.push(Cip36RegistrationsForVotingPublicKey { + vote_pub_key: vote_pub_key.clone(), + registrations: registrations_for_given_stake_pub_key, + }); + } + + // include any erroneous registrations which occur AFTER the slot# of the last valid + // registration or return all if NO slot# declared. + let invalids_report = match get_invalid_registrations( + stake_public_key.clone(), + slot_no.clone(), + session.clone(), + ) + .await + { + Ok(invalids) => invalids, + Err(err) => { + return AllRegistration::handle_error(&anyhow::anyhow!( + "Failed to obtain invalid registrations for given stake addr {err:?} in snapshot", + )); + }, + }; + all_invalids_after_filtering.push(invalids_report); + } + + AllRegistration::With(Cip36Registration::Ok(poem_openapi::payload::Json( + Cip36RegistrationList { + slot: slot_no.unwrap_or(SlotNo::from(0)), + voting_key: all_registrations_after_filtering, + invalid: all_invalids_after_filtering.into_iter().flatten().collect(), + page: None, + }, + ))) +} + +/// Filter out any registrations that occurred after this Slot no +fn slot_filter(registrations: Vec, slot_no: &SlotNo) -> Vec { + registrations + .into_iter() + .filter(|registration| registration.slot_no < *slot_no) + .collect() +} + +/// Get all `stake_addr` paired with vote keys [(`stake_addr,vote_key`)] from cip36 +/// registrations. +pub async fn get_all_stake_addrs_and_vote_keys( + session: &Arc, +) -> Result, anyhow::Error> { + let mut stake_addr_iter = + GetAllStakesAndVoteKeysQuery::execute(session, GetAllStakesAndVoteKeysParams {}).await?; + + let mut vote_key_stake_addr_pair = Vec::new(); + + while let Some(row) = stake_addr_iter.next().await { + let row = row?; + + let stake_addr = Ed25519HexEncodedPublicKey::try_from(row.stake_address)?; + let vote_key = Ed25519HexEncodedPublicKey::try_from(row.vote_key)?; + + vote_key_stake_addr_pair.push((stake_addr, vote_key)); + } + Ok(vote_key_stake_addr_pair) +} diff --git a/catalyst-gateway/bin/src/service/api/cardano/cip36/mod.rs b/catalyst-gateway/bin/src/service/api/cardano/cip36/mod.rs index 549dc776443..b404f52d3e4 100644 --- a/catalyst-gateway/bin/src/service/api/cardano/cip36/mod.rs +++ b/catalyst-gateway/bin/src/service/api/cardano/cip36/mod.rs @@ -1,20 +1,20 @@ //! CIP36 Registration Endpoints -use ed25519_dalek::VerifyingKey; use poem::http::{HeaderMap, StatusCode}; use poem_openapi::{param::Query, OpenApi}; -use self::cardano::slot_no::SlotNo; +use self::{cardano::slot_no::SlotNo, common::auth::none::NoAuthorization}; use super::Ed25519HexEncodedPublicKey; use crate::service::common::{ self, - auth::none_or_rbac::NoneOrRBAC, tags::ApiTags, types::cardano::{self}, }; pub(crate) mod endpoint; -pub(crate) mod old_endpoint; + +pub(crate) mod filter; + pub(crate) mod response; /// Cardano Staking API Endpoints @@ -47,11 +47,10 @@ impl Api { asat: Query>, page: Query>, limit: Query>, - /// No Authorization required, but Token permitted. - auth: NoneOrRBAC, /// Headers, used if the query is requesting ALL to determine if the secret API /// Key is also defined. headers: &HeaderMap, + _auth: NoAuthorization, ) -> response::AllRegistration { // Special validation for the `lookup` parameter. // If the parameter is ALL, BUT we do not have a valid API Key, just report the parameter @@ -60,7 +59,7 @@ impl Api { if lookup.is_all(headers).is_err() { return response::AllRegistration::unprocessable_content(vec![ poem::Error::from_string( - "Invalid Stake Address or Voter Key", + "Invalid Stake Address or Voter key", StatusCode::UNPROCESSABLE_ENTITY, ), ]); @@ -72,69 +71,8 @@ impl Api { SlotNo::into_option(asat.0), page.0.unwrap_or_default(), limit.0.unwrap_or_default(), - auth, headers, ) .await } - - /// Get latest CIP36 registrations from stake address. - /// - /// This endpoint gets the latest registration given a stake address. - #[oai( - path = "/draft/cardano/cip36/latest_registration/stake_addr", - method = "get", - operation_id = "latestRegistrationGivenStakeAddr" - )] - async fn latest_registration_cip36_given_stake_addr( - &self, - /// Stake Public Key to find the latest registration for. - stake_pub_key: Query, // Validation provided by type. - /// No Authorization required, but Token permitted. - _auth: NoneOrRBAC, - ) -> old_endpoint::SingleRegistrationResponse { - let hex_key = stake_pub_key.0; - let pub_key: VerifyingKey = hex_key.into(); - - old_endpoint::get_latest_registration_from_stake_addr(&pub_key, true).await - } - - /// Get latest CIP36 registrations from a stake key hash. - /// - /// This endpoint gets the latest registration given a stake key hash. - #[oai( - path = "/draft/cardano/cip36/latest_registration/stake_key_hash", - method = "get", - operation_id = "latestRegistrationGivenStakeHash" - )] - async fn latest_registration_cip36_given_stake_key_hash( - &self, - /// Stake Key Hash to find the latest registration for. - #[oai(validator(max_length = 66, min_length = 0, pattern = "[0-9a-f]"))] - stake_key_hash: Query, - /// No Authorization required, but Token permitted. - _auth: NoneOrRBAC, - ) -> old_endpoint::SingleRegistrationResponse { - old_endpoint::get_latest_registration_from_stake_key_hash(stake_key_hash.0, true).await - } - - /// Get latest CIP36 registrations from voting key. - /// - /// This endpoint returns the list of stake address registrations currently associated - /// with a given voting key. - #[oai( - path = "/draft/cardano/cip36/latest_registration/vote_key", - method = "get", - operation_id = "latestRegistrationGivenVoteKey" - )] - async fn latest_registration_cip36_given_vote_key( - &self, - /// Voting Key to find CIP36 registrations for. - #[oai(validator(max_length = 66, min_length = 66, pattern = "0x[0-9a-f]"))] - vote_key: Query, - /// No Authorization required, but Token permitted. - _auth: NoneOrRBAC, - ) -> old_endpoint::MultipleRegistrationResponse { - old_endpoint::get_associated_vote_key_registrations(vote_key.0, true).await - } } diff --git a/catalyst-gateway/bin/src/service/api/cardano/cip36/old_endpoint.rs b/catalyst-gateway/bin/src/service/api/cardano/cip36/old_endpoint.rs deleted file mode 100644 index d004ddf70c1..00000000000 --- a/catalyst-gateway/bin/src/service/api/cardano/cip36/old_endpoint.rs +++ /dev/null @@ -1,412 +0,0 @@ -//! Implementation of the GET `/cardano/cip36` endpoint - -use std::{cmp::Reverse, sync::Arc}; - -use anyhow::anyhow; -use futures::StreamExt; -use poem_openapi::{payload::Json, ApiResponse}; -use tracing::error; - -use crate::{ - db::index::{ - queries::registrations::{ - get_from_stake_addr::GetRegistrationQuery, - get_from_stake_hash::{GetStakeAddrParams, GetStakeAddrQuery}, - get_from_vote_key::{GetStakeAddrFromVoteKeyParams, GetStakeAddrFromVoteKeyQuery}, - get_invalid::{GetInvalidRegistrationParams, GetInvalidRegistrationQuery}, - }, - session::CassandraSession, - }, - service::common::{ - objects::cardano::cip36::{ - Cip36Info, Cip36Reporting, Cip36ReportingList, InvalidRegistrationsReport, - }, - responses::WithErrorResponses, - types::headers::retry_after::RetryAfterOption, - }, - utils::ed25519, -}; - -/// Endpoint responses. -#[derive(ApiResponse)] -pub(crate) enum ResponseSingleRegistration { - /// A CIP36 registration report. - #[oai(status = 200)] - Ok(Json), - /// No valid registration. - #[oai(status = 404)] - NotFound, -} - -/// Endpoint responses. -#[derive(ApiResponse)] -pub(crate) enum ResponseMultipleRegistrations { - /// All CIP36 registrations associated with the same Voting Key. - #[oai(status = 200)] - Ok(Json), - /// No valid registration. - #[oai(status = 404)] - NotFound, -} - -/// Single registration response -pub(crate) type SingleRegistrationResponse = WithErrorResponses; -/// All responses voting key -pub(crate) type MultipleRegistrationResponse = WithErrorResponses; - -/// Get latest registration given a stake public key -pub(crate) async fn get_latest_registration_from_stake_addr( - stake_pub_key: &ed25519_dalek::VerifyingKey, persistent: bool, -) -> SingleRegistrationResponse { - let Some(session) = CassandraSession::get(persistent) else { - error!( - id = "get_latest_registration_from_stake_addr", - "Failed to acquire db session" - ); - return ResponseSingleRegistration::NotFound.into(); - }; - - let registration = - match latest_registration_from_stake_addr(stake_pub_key, session.clone()).await { - Ok(registrations) => registrations, - Err(err) => { - error!( - id="get_latest_registration_from_stake_addr", - error=?err, - "Failed to obtain registrations for given stake addr", - ); - return ResponseSingleRegistration::NotFound.into(); - }, - }; - - let raw_invalids = - get_invalid_registrations(stake_pub_key, registration.slot_no.into(), session).await; - - let invalids_report = match raw_invalids { - Ok(invalids) => invalids, - Err(err) => { - error!( - id="get_latest_registration_from_stake_addr", - error=?err, - "Failed to obtain invalid registrations for given stake addr", - ); - return ResponseSingleRegistration::NotFound.into(); - }, - }; - - let report = Cip36Reporting::new(vec![registration], invalids_report); - - ResponseSingleRegistration::Ok(Json(report)).into() -} - -/// Get latest registration given a stake addr -async fn latest_registration_from_stake_addr( - stake_pub_key: &ed25519_dalek::VerifyingKey, session: Arc, -) -> anyhow::Result { - sort_latest_registration( - get_all_registrations_from_stake_pub_key(session, stake_pub_key).await?, - ) -} - -/// Get all cip36 registrations for a given stake address. -async fn get_all_registrations_from_stake_pub_key( - session: Arc, stake_pub_key: &ed25519_dalek::VerifyingKey, -) -> Result, anyhow::Error> { - let mut registrations_iter = - GetRegistrationQuery::execute(&session, stake_pub_key.into()).await?; - let mut registrations = Vec::new(); - while let Some(row) = registrations_iter.next().await { - let row = row?; - - let nonce = if let Some(nonce) = row.nonce.into_parts().1.to_u64_digits().first() { - *nonce - } else { - continue; - }; - - let slot_no = if let Some(slot_no) = row.slot_no.into_parts().1.to_u64_digits().first() { - *slot_no - } else { - continue; - }; - - let cip36 = Cip36Info { - stake_pub_key: row.stake_address.try_into()?, - nonce, - slot_no, - txn: row.txn, - vote_key: hex::encode(row.vote_key), - payment_address: hex::encode(row.payment_address), - is_payable: row.is_payable, - cip36: row.cip36, - }; - - registrations.push(cip36); - } - Ok(registrations) -} - -/// Sort latest registrations for a given stake address sorting by slot no -fn sort_latest_registration(mut registrations: Vec) -> anyhow::Result { - registrations.sort_by_key(|k| Reverse(k.slot_no)); - registrations.into_iter().next().ok_or(anyhow::anyhow!( - "Can't sort latest registrations by slot no" - )) -} - -/// Get invalid registrations for stake addr after given slot no -async fn get_invalid_registrations( - stake_pub_key: &ed25519_dalek::VerifyingKey, slot_no: num_bigint::BigInt, - session: Arc, -) -> anyhow::Result> { - let mut invalid_registrations_iter = GetInvalidRegistrationQuery::execute( - &session, - GetInvalidRegistrationParams::new(stake_pub_key.as_bytes().to_vec(), slot_no), - ) - .await?; - let mut invalid_registrations = Vec::new(); - while let Some(row) = invalid_registrations_iter.next().await { - let row = row?; - - invalid_registrations.push(InvalidRegistrationsReport { - error_report: row.error_report, - stake_address: row.stake_address.try_into()?, - vote_key: hex::encode(row.vote_key), - payment_address: hex::encode(row.payment_address), - is_payable: row.is_payable, - cip36: row.cip36, - }); - } - - Ok(invalid_registrations) -} - -/// Stake addresses need to be individually checked to make sure they are still actively -/// associated with the voting key, and have not been registered to another voting key. -fn check_stake_addr_voting_key_association( - registrations: Vec, associated_voting_key: &str, -) -> Vec { - registrations - .into_iter() - .filter(|r| r.vote_key == associated_voting_key) - .collect() -} - -/// Get latest registration given a stake key hash. -pub(crate) async fn get_latest_registration_from_stake_key_hash( - stake_hash: String, persistent: bool, -) -> SingleRegistrationResponse { - let stake_hash = match hex::decode(stake_hash) { - Ok(stake_hash) => stake_hash, - Err(err) => { - error!(id="get_latest_registration_from_stake_key_hash_stake_hash",error=?err, "Failed to decode stake hash"); - return ResponseSingleRegistration::NotFound.into(); - }, - }; - - let Some(session) = CassandraSession::get(persistent) else { - error!("Failed to acquire db session"); - let err = anyhow::anyhow!("Failed to acquire db session"); - return SingleRegistrationResponse::service_unavailable(&err, RetryAfterOption::Default); - }; - - // Get stake addr associated with give stake hash - let mut stake_addr_iter = - match GetStakeAddrQuery::execute(&session, GetStakeAddrParams::new(stake_hash)).await { - Ok(latest) => latest, - Err(err) => { - error!( - id="get_latest_registration_from_stake_key_hash_query_stake_addr", - error=?err, - "Failed to query stake addr from stake hash" - ); - return ResponseSingleRegistration::NotFound.into(); - }, - }; - - if let Some(row_stake_addr) = stake_addr_iter.next().await { - let row = match row_stake_addr { - Ok(r) => r, - Err(err) => { - error!( - id="get_latest_registration_from_stake_key_hash_latest_registration", - error=?err, - "Failed to get latest registration" - ); - return ResponseSingleRegistration::NotFound.into(); - }, - }; - - let stake_pub_key = match ed25519::verifying_key_from_vec(&row.stake_address) { - Ok(v) => v, - Err(err) => { - error!(error=?err, "Invalid Stake Public Key in database."); - let err = anyhow!(err); - return SingleRegistrationResponse::internal_error(&err); - }, - }; - - let registration = match latest_registration_from_stake_addr( - &stake_pub_key, - session.clone(), - ) - .await - { - Ok(registration) => registration, - Err(err) => { - error!( - id="get_latest_registration_from_stake_key_hash_registration_for_stake_addr", - error=?err, - "Failed to obtain registration for given stake addr", - ); - return ResponseSingleRegistration::NotFound.into(); - }, - }; - - // include any erroneous registrations which occur AFTER the slot# of the last valid - // registration - let invalids_report = match get_invalid_registrations( - &stake_pub_key, - registration.slot_no.into(), - session, - ) - .await - { - Ok(invalids) => invalids, - Err(err) => { - error!( - id="get_latest_registration_from_stake_key_hash_invalid_registrations_for_stake_addr", - error=?err, - "Failed to obtain invalid registrations for given stake addr", - ); - return ResponseSingleRegistration::NotFound.into(); - }, - }; - - let report = Cip36Reporting::new(vec![registration], invalids_report); - - return ResponseSingleRegistration::Ok(Json(report)).into(); - } - - ResponseSingleRegistration::NotFound.into() -} - -/// Returns the list of stake address registrations currently associated with a given -/// voting key and returns any erroneous registrations which occur AFTER the slot# of the -/// last valid registration. -pub(crate) async fn get_associated_vote_key_registrations( - vote_key: String, persistent: bool, -) -> MultipleRegistrationResponse { - let vote_key = match hex::decode(vote_key) { - Ok(vote_key) => vote_key, - Err(err) => { - error!( - id="get_associated_vote_key_registrations_vote_key", - error=?err, - "Failed to decode vote key" - ); - return ResponseMultipleRegistrations::NotFound.into(); - }, - }; - - let Some(session) = CassandraSession::get(persistent) else { - error!( - id = "get_associated_vote_key_registrations_db_session", - "Failed to acquire db session" - ); - return ResponseMultipleRegistrations::NotFound.into(); - }; - - let mut stake_addr_iter = match GetStakeAddrFromVoteKeyQuery::execute( - &session, - GetStakeAddrFromVoteKeyParams::new(vote_key.clone()), - ) - .await - { - Ok(latest) => latest, - Err(err) => { - error!( - id="get_associated_vote_key_registrations_query_stake_addr_from_vote_key", - error=?err, - "Failed to query stake addr from vote key" - ); - return ResponseMultipleRegistrations::NotFound.into(); - }, - }; - - if let Some(row_stake_addr) = stake_addr_iter.next().await { - let row = match row_stake_addr { - Ok(r) => r, - Err(err) => { - error!( - id="get_associated_vote_key_registrations_latest_registration", - error=?err, - "Failed to get latest registration" - ); - return ResponseMultipleRegistrations::NotFound.into(); - }, - }; - - let stake_pub_key = match ed25519::verifying_key_from_vec(&row.stake_address) { - Ok(k) => k, - Err(err) => { - error!( - id="get_associated_vote_key_registrations_latest_registration", - error=?err, - "Not a valid staking public key" - ); - return ResponseMultipleRegistrations::NotFound.into(); - }, - }; - - // We have the stake addr associated with vote key, now get all registrations with the - // stake addr. - let registrations = - match get_all_registrations_from_stake_pub_key(session.clone(), &stake_pub_key).await { - Ok(registration) => registration, - Err(err) => { - error!( - id="get_associated_vote_key_registrations_get_registrations_for_stake_addr", - error=?err, - "Failed to obtain registrations for given stake addr", - ); - return ResponseMultipleRegistrations::NotFound.into(); - }, - }; - - // check registrations (stake addrs) are still actively associated with the voting key, - // and have not been registered to another voting key. - let redacted_registrations = - check_stake_addr_voting_key_association(registrations, &hex::encode(vote_key)); - - // Report includes registration info and any erroneous registrations which occur AFTER - // the slot# of the last valid registration - let mut reports = Cip36ReportingList::new(); - - for registration in redacted_registrations { - let invalids_report = match get_invalid_registrations( - &stake_pub_key, - registration.slot_no.into(), - session.clone(), - ) - .await - { - Ok(invalids) => invalids, - Err(err) => { - error!( - id="get_associated_vote_key_registrations_invalid_registrations_for_stake_addr", - error=?err, - "Failed to obtain invalid registrations for given stake addr", - ); - continue; - }, - }; - - reports.add(Cip36Reporting::new(vec![registration], invalids_report)); - } - - return ResponseMultipleRegistrations::Ok(Json(reports)).into(); - } - - ResponseMultipleRegistrations::NotFound.into() -} diff --git a/catalyst-gateway/bin/src/service/api/cardano/cip36/response.rs b/catalyst-gateway/bin/src/service/api/cardano/cip36/response.rs index fc9d0b5411a..67b8e5b3082 100644 --- a/catalyst-gateway/bin/src/service/api/cardano/cip36/response.rs +++ b/catalyst-gateway/bin/src/service/api/cardano/cip36/response.rs @@ -8,7 +8,6 @@ use crate::service::common; /// Endpoint responses. #[derive(ApiResponse)] -#[allow(dead_code)] // TODO: Remove once endpoint fully implemented pub(crate) enum Cip36Registration { /// All CIP36 registrations associated with the same Voting Key. #[oai(status = 200)] @@ -31,15 +30,15 @@ pub(crate) struct Cip36RegistrationList { /// Errors are reported only if they fall between the last valid registration and this /// slot number. /// Earlier errors are never reported. - slot: common::types::cardano::slot_no::SlotNo, + pub slot: common::types::cardano::slot_no::SlotNo, /// List of registrations associated with the query. #[oai(validator(max_items = "100"))] - voting_key: Vec, + pub voting_key: Vec, /// List of latest invalid registrations that were found, for the requested filter. #[oai(skip_serializing_if_is_empty, validator(max_items = "10"))] - invalid: Vec, + pub invalid: Vec, /// Current Page - page: common::objects::generic::pagination::CurrentPage, + pub page: Option, } impl Example for Cip36RegistrationList { @@ -48,7 +47,7 @@ impl Example for Cip36RegistrationList { slot: (common::types::cardano::slot_no::EXAMPLE + 635).into(), voting_key: vec![Cip36RegistrationsForVotingPublicKey::example()], invalid: vec![Cip36Details::invalid_example()], - page: common::objects::generic::pagination::CurrentPage::example(), + page: Some(common::objects::generic::pagination::CurrentPage::example()), } } } @@ -75,7 +74,7 @@ impl Example for Cip36RegistrationsForVotingPublicKey { } /// CIP36 Registration Data as found on-chain. -#[derive(Object)] +#[derive(Object, Clone)] #[oai(example = true)] pub(crate) struct Cip36Details { /// Blocks Slot Number that the registration certificate is in. diff --git a/catalyst-gateway/bin/src/service/common/objects/cardano/cip36.rs b/catalyst-gateway/bin/src/service/common/objects/cardano/cip36.rs deleted file mode 100644 index 707d1e5631b..00000000000 --- a/catalyst-gateway/bin/src/service/common/objects/cardano/cip36.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! CIP36 object - -// TODO: This is NOT common, remove it once the rationalized endpoint is implemented. -// Retained to keep the existing code from breaking only. - -use poem_openapi::{types::Example, Object}; - -use crate::service::common::types::generic::ed25519_public_key::Ed25519HexEncodedPublicKey; - -/// List of CIP36 Registration Data as found on-chain. -#[derive(Object, Default)] -#[oai(example = true)] -pub(crate) struct Cip36ReportingList { - /// List of registrations associated with the same Voting Key - #[oai(validator(max_items = "100000"))] - cip36: Vec, -} - -impl Cip36ReportingList { - /// Create a new instance of `Cip36ReportingList`. - pub(crate) fn new() -> Self { - Self { cip36: vec![] } - } - - /// Add a new `Cip36Reporting` to the list. - pub(crate) fn add(&mut self, cip36: Cip36Reporting) { - self.cip36.push(cip36); - } -} - -impl Example for Cip36ReportingList { - fn example() -> Self { - Self { - cip36: vec![Cip36Reporting::example()], - } - } -} - -/// CIP36 info + invalid reporting. -#[derive(Object, Default)] -#[oai(example = true)] -pub(crate) struct Cip36Reporting { - /// List of registrations. - #[oai(validator(max_items = "100000"))] - cip36: Vec, - /// Invalid registration reporting. - #[oai(validator(max_items = "100000"))] - invalids: Vec, -} - -impl Cip36Reporting { - /// Create a new instance of `Cip36Reporting`. - pub(crate) fn new(cip36: Vec, invalids: Vec) -> Self { - Self { cip36, invalids } - } -} - -impl Example for Cip36Reporting { - fn example() -> Self { - Self { - cip36: vec![Cip36Info::example()], - invalids: vec![InvalidRegistrationsReport::example()], - } - } -} - -/// CIP36 Registration Data as found on-chain. -#[derive(Object)] -#[oai(example = true)] -pub(crate) struct Cip36Info { - /// Full Stake Address (not hashed, 32 byte ED25519 Public key). - pub stake_pub_key: Ed25519HexEncodedPublicKey, // Validation provided by type - /// Nonce value after normalization. - #[oai(validator(minimum(value = "0"), maximum(value = "9223372036854775807")))] - pub nonce: u64, - /// Slot Number the cert is in. - #[oai(validator(minimum(value = "0"), maximum(value = "9223372036854775807")))] - pub slot_no: u64, - /// Transaction Index. - #[oai(validator(minimum(value = "0"), maximum(value = "9223372036854775807")))] - pub txn: i16, - /// Voting Public Key - #[oai(validator(max_length = 66, min_length = 66, pattern = "0x[0-9a-f]{64}"))] - pub vote_key: String, - /// Full Payment Address (not hashed, 32 byte ED25519 Public key). - #[oai(validator(max_length = 116, min_length = 66, pattern = "0x[0-9a-f]{64}"))] - pub payment_address: String, - /// Is the stake address a script or not. - pub is_payable: bool, - /// Is the Registration CIP36 format, or CIP15 - pub cip36: bool, -} - -impl Example for Cip36Info { - fn example() -> Self { - Self { - stake_pub_key: Ed25519HexEncodedPublicKey::example(), - nonce: 0, - slot_no: 12345, - txn: 0, - vote_key: "0xa6a3c0447aeb9cc54cf6422ba32b294e5e1c3ef6d782f2acff4a70694c4d1663" - .to_string(), - payment_address: "0x00588e8e1d18cba576a4d35758069fe94e53f638b6faf7c07b8abd2bc5c5cdee47b60edc7772855324c85033c638364214cbfc6627889f81c4".to_string(), - is_payable: false, - cip36: true, - } - } -} - -/// Invalid registration error reporting. -#[derive(Object)] -#[oai(example = true)] -pub(crate) struct InvalidRegistrationsReport { - /// Error report - #[oai(validator(max_items = "100000", max_length = "100", pattern = ".*"))] - pub error_report: Vec, - /// Full Stake Public Key (32 byte Ed25519 Public key, not hashed). - pub stake_address: Ed25519HexEncodedPublicKey, // Validation provided by the type. - /// Voting Public Key - #[oai(validator(max_length = 66, min_length = 0, pattern = "[0-9a-f]"))] - pub vote_key: String, - /// Full Payment Address (not hashed, 32 byte ED25519 Public key). - #[oai(validator(max_length = 116, min_length = 0, pattern = "[0-9a-f]"))] - pub payment_address: String, - /// Is the stake address a script or not. - pub is_payable: bool, - /// Is the Registration CIP36 format, or CIP15 - pub cip36: bool, -} - -impl Example for InvalidRegistrationsReport { - fn example() -> Self { - Self { - error_report: vec!["Invalid registration".to_string()], - stake_address: Ed25519HexEncodedPublicKey::example(), - vote_key: "0xa6a3c0447aeb9cc54cf6422ba32b294e5e1c3ef6d782f2acff4a70694c4d1663".to_string(), - payment_address: "0x00588e8e1d18cba576a4d35758069fe94e53f638b6faf7c07b8abd2bc5c5cdee47b60edc7772855324c85033c638364214cbfc6627889f81c4".to_string(), - is_payable: false, - cip36: true, - } - } -} diff --git a/catalyst-gateway/bin/src/service/common/objects/cardano/mod.rs b/catalyst-gateway/bin/src/service/common/objects/cardano/mod.rs index 391e5e16e4b..e541c9b1164 100644 --- a/catalyst-gateway/bin/src/service/common/objects/cardano/mod.rs +++ b/catalyst-gateway/bin/src/service/common/objects/cardano/mod.rs @@ -4,7 +4,6 @@ //! common. They should not be simple types. but actual objects. //! Simple types belong in `common/types`. -pub(crate) mod cip36; // TODO: Not common, to be removed once code refactored. pub(crate) mod hash; pub(crate) mod network; pub(crate) mod registration_info; diff --git a/catalyst-gateway/bin/src/service/common/types/cardano/cip19_stake_address.rs b/catalyst-gateway/bin/src/service/common/types/cardano/cip19_stake_address.rs index 83f01951003..979f4d67d4f 100644 --- a/catalyst-gateway/bin/src/service/common/types/cardano/cip19_stake_address.rs +++ b/catalyst-gateway/bin/src/service/common/types/cardano/cip19_stake_address.rs @@ -17,6 +17,7 @@ use poem_openapi::{ }; use serde_json::Value; +use super::hash28::HexEncodedHash28; use crate::service::common::types::string_types::impl_string_types; /// Stake address title. @@ -145,6 +146,15 @@ impl TryInto for Cip19StakeAddress { } } +impl TryInto for Cip19StakeAddress { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + let stake_addr: StakeAddress = self.try_into()?; + HexEncodedHash28::try_from(stake_addr.payload().as_hash().to_vec()) + } +} + impl Example for Cip19StakeAddress { fn example() -> Self { Self(EXAMPLE.to_owned()) @@ -180,4 +190,14 @@ mod tests { let stake_address = Cip19StakeAddress::try_from(INVALID_STAKE_ADDRESS.to_string()); assert!(stake_address.is_err()); } + + #[test] + fn test_stake_address_to_stake_hash() { + let stake_address_prod = + Cip19StakeAddress::try_from(VALID_PROD_STAKE_ADDRESS.to_string()).unwrap(); + + let stake_addr: StakeAddress = stake_address_prod.try_into().unwrap(); + + assert!(stake_addr.payload().as_hash().len() == 28); + } } diff --git a/catalyst-gateway/bin/src/service/common/types/cardano/nonce.rs b/catalyst-gateway/bin/src/service/common/types/cardano/nonce.rs index e99679b7d7f..18639402487 100644 --- a/catalyst-gateway/bin/src/service/common/types/cardano/nonce.rs +++ b/catalyst-gateway/bin/src/service/common/types/cardano/nonce.rs @@ -39,7 +39,7 @@ static SCHEMA: LazyLock = LazyLock::new(|| { }); /// Value of a Nonce. -#[derive(Debug, Eq, PartialEq, Hash)] +#[derive(Debug, Eq, PartialEq, Hash, Clone)] pub(crate) struct Nonce(u64); /// Is the Nonce valid? diff --git a/catalyst-gateway/bin/src/service/common/types/cardano/query/as_at.rs b/catalyst-gateway/bin/src/service/common/types/cardano/query/as_at.rs index f95fbb607f6..25645b57b30 100644 --- a/catalyst-gateway/bin/src/service/common/types/cardano/query/as_at.rs +++ b/catalyst-gateway/bin/src/service/common/types/cardano/query/as_at.rs @@ -1,4 +1,4 @@ -//! Query Parameter that can take either a Blockchain slot Number of Unix Epoch timestamp. +//! Query Parameter that can take either a Blockchain slot Number or Unix Epoch timestamp. //! //! Allows better specifying of times that restrict a GET endpoints response. @@ -167,3 +167,17 @@ impl Display for AsAt { write!(f, "{}:{}", self.0 .0, self.0 .1) } } + +#[cfg(test)] +mod tests { + use super::parse_parameter; + + #[test] + fn test_string_to_slot_no() { + let slot_no = "SLOT:12396302"; + assert!(parse_parameter(slot_no).is_ok()); + + let unix_timestamp = "TIME:1736164751"; + assert!(parse_parameter(unix_timestamp).is_ok()); + } +} diff --git a/catalyst-gateway/bin/src/service/common/types/cardano/query/mod.rs b/catalyst-gateway/bin/src/service/common/types/cardano/query/mod.rs index 2583fded07c..00d75839075 100644 --- a/catalyst-gateway/bin/src/service/common/types/cardano/query/mod.rs +++ b/catalyst-gateway/bin/src/service/common/types/cardano/query/mod.rs @@ -4,6 +4,7 @@ //! `OpenAPI` pub(crate) mod as_at; + pub(crate) mod stake_or_voter; pub(crate) use as_at::AsAt; diff --git a/catalyst-gateway/bin/src/service/common/types/cardano/query/stake_or_voter.rs b/catalyst-gateway/bin/src/service/common/types/cardano/query/stake_or_voter.rs index 63e2aaf7a84..b1fd32b2517 100644 --- a/catalyst-gateway/bin/src/service/common/types/cardano/query/stake_or_voter.rs +++ b/catalyst-gateway/bin/src/service/common/types/cardano/query/stake_or_voter.rs @@ -18,10 +18,17 @@ use poem_openapi::{ use regex::Regex; use serde_json::Value; -use crate::service::common::{self, auth::api_key::check_api_key}; +use crate::service::common::{ + self, + auth::api_key::check_api_key, + types::{ + cardano::cip19_stake_address::Cip19StakeAddress, + generic::ed25519_public_key::Ed25519HexEncodedPublicKey, + }, +}; /// A Query Parameter that can take a CIP-19 stake address, or a public key. -/// Defining these are mutually exclusive, ao a single parameter is required to be used. +/// Defining these are mutually exclusive, as a single parameter is required to be used. #[derive(Clone)] pub(crate) enum StakeAddressOrPublicKey { /// A CIP-19 stake address @@ -45,22 +52,20 @@ impl TryFrom<&str> for StakeAddressOrPublicKey { fn try_from(value: &str) -> std::result::Result { /// Regex to parse the parameter #[allow(clippy::unwrap_used)] // Safe because the Regex is constant. Can never panic in prod. - static RE: LazyLock = LazyLock::new(|| Regex::new(PATTERN).unwrap()); + static _RE: LazyLock = LazyLock::new(|| Regex::new(PATTERN).unwrap()); // First check it is the special "ALL" parameter. if value == "ALL" { return Ok(Self::All); } - // Otherwise, work out use the regex to work out what it is, and validate it. - if let Some(results) = RE.captures(value) { - if let Some(stake_addr) = results.get(1) { - return Ok(Self::Address(stake_addr.as_str().try_into()?)); - } else if let Some(public_key) = results.get(2) { - return Ok(Self::PublicKey(public_key.as_str().try_into()?)); - } + if let Ok(pub_key) = Ed25519HexEncodedPublicKey::try_from(value) { + Ok(Self::PublicKey(pub_key)) + } else if let Ok(stake_addr) = Cip19StakeAddress::try_from(value) { + Ok(Self::Address(stake_addr)) + } else { + bail!("Not a valid \"Stake or Public Key\" parameter."); } - bail!("Not a valid \"Stake or Public Key\" parameter."); } } @@ -198,3 +203,22 @@ impl TryInto = LazyLock::new(|| { }); /// Slot number -#[derive(Debug, Eq, PartialEq, Hash)] +#[derive(Debug, Eq, PartialEq, Hash, Clone, PartialOrd, Ord)] + pub(crate) struct SlotNo(u64); /// Is the Slot Number valid? @@ -93,6 +95,12 @@ impl ParseFromJSON for SlotNo { } } +impl From for BigInt { + fn from(val: SlotNo) -> Self { + BigInt::from(val.0) + } +} + impl ToJSON for SlotNo { fn to_json(&self) -> Option { Some(self.0.into()) diff --git a/catalyst-gateway/bin/src/service/common/types/cardano/txn_index.rs b/catalyst-gateway/bin/src/service/common/types/cardano/txn_index.rs index 7a48bd1c344..d37b7008962 100644 --- a/catalyst-gateway/bin/src/service/common/types/cardano/txn_index.rs +++ b/catalyst-gateway/bin/src/service/common/types/cardano/txn_index.rs @@ -36,7 +36,7 @@ static SCHEMA: LazyLock = LazyLock::new(|| { }); /// Transaction Index within a block. -#[derive(Debug, Eq, PartialEq, Hash)] +#[derive(Debug, Eq, PartialEq, Hash, Clone)] pub(crate) struct TxnIndex(u16); /// Is the Slot Number valid? diff --git a/catalyst-gateway/bin/src/service/common/types/generic/ed25519_public_key.rs b/catalyst-gateway/bin/src/service/common/types/generic/ed25519_public_key.rs index eb0954f4e61..c771bf657c7 100644 --- a/catalyst-gateway/bin/src/service/common/types/generic/ed25519_public_key.rs +++ b/catalyst-gateway/bin/src/service/common/types/generic/ed25519_public_key.rs @@ -133,3 +133,25 @@ impl From for ed25519_dalek::VerifyingKey { .expect("This can only fail if the type was invalidly constructed.") } } + +impl TryInto> for Ed25519HexEncodedPublicKey { + type Error = anyhow::Error; + + fn try_into(self) -> Result, Self::Error> { + Ok(hex::decode(self.0.trim_start_matches("0x"))?) + } +} + +#[cfg(test)] +mod tests { + use super::Ed25519HexEncodedPublicKey; + + #[test] + fn hex_to_pub_key() { + // https://cexplorer.io/article/understanding-cardano-addresses + assert!(Ed25519HexEncodedPublicKey::try_from( + "0x76e7ac0e460b6cdecea4be70479dab13c4adbd117421259a9b36caac007394de".to_string(), + ) + .is_ok()); + } +}