Skip to content

Commit

Permalink
feat(cat-voices): cip36 registrations consolidated endpoint (#1494)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
cong-or authored Jan 14, 2025
1 parent 5810331 commit eb15954
Show file tree
Hide file tree
Showing 24 changed files with 738 additions and 679 deletions.
2 changes: 2 additions & 0 deletions .config/dictionaries/project.dic
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,5 @@ xpub
xpublic
xvfb
yoroi
stake1u94ullc9nj9gawc08990nx8hwgw80l9zpmr8re44kydqy9cdjq6rq
invalid1u9nlq5nmuzthw3vhgakfpxyq4r0zl2c0p8uqy24gpyjsa6c3df4h6
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SELECT
stake_address,vote_key
FROM cip36_registration;
13 changes: 12 additions & 1 deletion catalyst-gateway/bin/src/db/index/queries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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?,
})
}

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<u8>,
/// Voting Public Key
pub vote_key: Vec<u8>,
}

impl GetAllStakesAndVoteKeysQuery {
/// Prepares get all `stake_addr` paired with vote keys [(`stake_addr,vote_key`)]
pub(crate) async fn prepare(session: Arc<Session>) -> anyhow::Result<PreparedStatement> {
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<TypedRowStream<GetAllStakesAndVoteKeysQuery>> {
let iter = session
.execute_iter(PreparedSelectQuery::GetAllStakesAndVoteKeys, params)
.await?
.rows_stream::<GetAllStakesAndVoteKeysQuery>()?;

Ok(iter)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ pub(crate) struct GetRegistrationParams {
pub stake_address: Vec<u8>,
}

impl From<&ed25519_dalek::VerifyingKey> for GetRegistrationParams {
fn from(value: &ed25519_dalek::VerifyingKey) -> Self {
impl From<Vec<u8>> for GetRegistrationParams {
fn from(value: Vec<u8>) -> Self {
GetRegistrationParams {
stake_address: value.as_bytes().to_vec(),
stake_address: value,
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -28,12 +31,10 @@ pub(crate) struct GetInvalidRegistrationParams {

impl GetInvalidRegistrationParams {
/// Create a new instance of [`GetInvalidRegistrationParams`]
pub(crate) fn new(
stake_address: Vec<u8>, slot_no: num_bigint::BigInt,
) -> GetInvalidRegistrationParams {
pub(crate) fn new(stake_address: Vec<u8>, slot_no: SlotNo) -> GetInvalidRegistrationParams {
Self {
stake_address,
slot_no,
slot_no: slot_no.into(),
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
23 changes: 13 additions & 10 deletions catalyst-gateway/bin/src/db/index/tests/scylla_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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();
Expand Down
72 changes: 60 additions & 12 deletions catalyst-gateway/bin/src/service/api/cardano/cip36/endpoint.rs
Original file line number Diff line number Diff line change
@@ -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<cardano::query::stake_or_voter::StakeOrVoter>, _asat: Option<SlotNo>,
lookup: Option<cardano::query::stake_or_voter::StakeOrVoter>, asat: Option<SlotNo>,
_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.
Expand Down
Loading

0 comments on commit eb15954

Please sign in to comment.