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()); + } +}