mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-07 19:59:23 +00:00
Compare commits
8 Commits
d219b77bd0
...
6100c3ca90
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6100c3ca90 | ||
|
|
fa0ed4b180 | ||
|
|
0ea16f9e01 | ||
|
|
7a314baa9f | ||
|
|
9891ccade8 | ||
|
|
f1f166c168 | ||
|
|
df4aee2d59 | ||
|
|
302a43653f |
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -107,8 +107,8 @@ jobs:
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-serai
|
||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-bitcoin
|
||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-ethereum
|
||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-monero
|
||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-serai
|
||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client
|
||||
|
||||
1452
Cargo.lock
generated
1452
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -93,10 +93,10 @@ members = [
|
||||
"substrate/runtime",
|
||||
"substrate/node",
|
||||
|
||||
"substrate/client/serai",
|
||||
"substrate/client/bitcoin",
|
||||
"substrate/client/ethereum",
|
||||
"substrate/client/monero",
|
||||
"substrate/client/serai",
|
||||
"substrate/client",
|
||||
|
||||
"orchestration",
|
||||
@@ -165,19 +165,19 @@ panic = "unwind"
|
||||
overflow-checks = true
|
||||
|
||||
[patch.crates-io]
|
||||
# Point to empty crates for unused crates in our tree
|
||||
# Point to empty crates for crates unused within in our tree
|
||||
ark-ff-3 = { package = "ark-ff", path = "patches/ethereum/ark-ff-0.3" }
|
||||
ark-ff-4 = { package = "ark-ff", path = "patches/ethereum/ark-ff-0.4" }
|
||||
c-kzg = { path = "patches/ethereum/c-kzg" }
|
||||
secp256k1-30 = { package = "secp256k1", path = "patches/ethereum/secp256k1-30" }
|
||||
|
||||
# Dependencies from monero-oxide which originate from within our own tree
|
||||
# Dependencies from monero-oxide which originate from within our own tree, potentially shimmed to account for deviations since publishing
|
||||
std-shims = { path = "patches/std-shims" }
|
||||
simple-request = { path = "patches/simple-request" }
|
||||
multiexp = { path = "crypto/multiexp" }
|
||||
flexible-transcript = { path = "crypto/transcript" }
|
||||
ciphersuite = { path = "patches/ciphersuite" }
|
||||
dalek-ff-group = { path = "crypto/dalek-ff-group" }
|
||||
dalek-ff-group = { path = "patches/dalek-ff-group" }
|
||||
minimal-ed448 = { path = "crypto/ed448" }
|
||||
modular-frost = { path = "crypto/frost" }
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ messages = { package = "serai-processor-messages", path = "../processor/messages
|
||||
message-queue = { package = "serai-message-queue", path = "../message-queue" }
|
||||
tributary-sdk = { path = "./tributary-sdk" }
|
||||
|
||||
serai-client = { path = "../substrate/client", default-features = false, features = ["serai"] }
|
||||
serai-client-serai = { path = "../substrate/client/serai", default-features = false }
|
||||
|
||||
log = { version = "0.4", default-features = false, features = ["std"] }
|
||||
env_logger = { version = "0.10", default-features = false, features = ["humantime"] }
|
||||
|
||||
@@ -21,7 +21,6 @@ workspace = true
|
||||
blake2 = { version = "0.11.0-rc.0", default-features = false, features = ["alloc"] }
|
||||
|
||||
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
|
||||
serai-abi = { path = "../../substrate/abi", default-features = false, features = ["std"] }
|
||||
serai-client-serai = { path = "../../substrate/client/serai", default-features = false }
|
||||
|
||||
log = { version = "0.4", default-features = false, features = ["std"] }
|
||||
|
||||
@@ -3,11 +3,13 @@ use std::{sync::Arc, collections::HashMap};
|
||||
|
||||
use blake2::{Digest, Blake2b256};
|
||||
|
||||
use serai_abi::primitives::{
|
||||
balance::Amount, validator_sets::ExternalValidatorSet, address::SeraiAddress,
|
||||
merkle::IncrementalUnbalancedMerkleTree,
|
||||
use serai_client_serai::{
|
||||
abi::primitives::{
|
||||
balance::Amount, validator_sets::ExternalValidatorSet, address::SeraiAddress,
|
||||
merkle::IncrementalUnbalancedMerkleTree,
|
||||
},
|
||||
Serai,
|
||||
};
|
||||
use serai_client_serai::Serai;
|
||||
|
||||
use serai_db::*;
|
||||
use serai_task::ContinuallyRan;
|
||||
@@ -85,7 +87,7 @@ impl<D: Db> ContinuallyRan for CosignIntendTask<D> {
|
||||
|
||||
// Check we are indexing a linear chain
|
||||
if block.header.builds_upon() !=
|
||||
builds_upon.clone().calculate(serai_abi::BLOCK_HEADER_BRANCH_TAG)
|
||||
builds_upon.clone().calculate(serai_client_serai::abi::BLOCK_HEADER_BRANCH_TAG)
|
||||
{
|
||||
Err(format!(
|
||||
"node's block #{block_number} doesn't build upon the block #{} prior indexed",
|
||||
@@ -95,8 +97,8 @@ impl<D: Db> ContinuallyRan for CosignIntendTask<D> {
|
||||
let block_hash = block.header.hash();
|
||||
SubstrateBlockHash::set(&mut txn, block_number, &block_hash);
|
||||
builds_upon.append(
|
||||
serai_abi::BLOCK_HEADER_BRANCH_TAG,
|
||||
Blake2b256::new_with_prefix([serai_abi::BLOCK_HEADER_LEAF_TAG])
|
||||
serai_client_serai::abi::BLOCK_HEADER_BRANCH_TAG,
|
||||
Blake2b256::new_with_prefix([serai_client_serai::abi::BLOCK_HEADER_LEAF_TAG])
|
||||
.chain_update(block_hash.0)
|
||||
.finalize()
|
||||
.into(),
|
||||
|
||||
@@ -9,22 +9,24 @@ use blake2::{Digest, Blake2s256};
|
||||
|
||||
use borsh::{BorshSerialize, BorshDeserialize};
|
||||
|
||||
use serai_abi::{
|
||||
primitives::{
|
||||
BlockHash,
|
||||
crypto::{Public, KeyPair},
|
||||
network_id::ExternalNetworkId,
|
||||
validator_sets::{Session, ExternalValidatorSet},
|
||||
address::SeraiAddress,
|
||||
use serai_client_serai::{
|
||||
abi::{
|
||||
primitives::{
|
||||
BlockHash,
|
||||
crypto::{Public, KeyPair},
|
||||
network_id::ExternalNetworkId,
|
||||
validator_sets::{Session, ExternalValidatorSet},
|
||||
address::SeraiAddress,
|
||||
},
|
||||
Block,
|
||||
},
|
||||
Block,
|
||||
Serai, TemporalSerai,
|
||||
};
|
||||
use serai_client_serai::{Serai, TemporalSerai};
|
||||
|
||||
use serai_db::*;
|
||||
use serai_task::*;
|
||||
|
||||
use serai_cosign_types::*;
|
||||
pub use serai_cosign_types::*;
|
||||
|
||||
/// The cosigns which are intended to be performed.
|
||||
mod intend;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![deny(missing_docs)]
|
||||
//! Types used when cosigning Serai. For more info, please see `serai-cosign`.
|
||||
use borsh::{BorshSerialize, BorshDeserialize};
|
||||
|
||||
@@ -29,7 +29,7 @@ schnorrkel = { version = "0.11", default-features = false, features = ["std"] }
|
||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
||||
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
|
||||
|
||||
serai-client = { path = "../../../substrate/client", default-features = false, features = ["serai"] }
|
||||
serai-client-serai = { path = "../../../substrate/client/serai", default-features = false }
|
||||
serai-cosign = { path = "../../cosign" }
|
||||
tributary-sdk = { path = "../../tributary-sdk" }
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use rand_core::{RngCore, OsRng};
|
||||
use blake2::{Digest, Blake2s256};
|
||||
use schnorrkel::{Keypair, PublicKey, Signature};
|
||||
|
||||
use serai_client::primitives::PublicKey as Public;
|
||||
use serai_client_serai::abi::primitives::crypto::Public;
|
||||
|
||||
use futures_util::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use libp2p::{
|
||||
@@ -104,7 +104,7 @@ impl OnlyValidators {
|
||||
.verify_simple(PROTOCOL.as_bytes(), &msg, &sig)
|
||||
.map_err(|_| io::Error::other("invalid signature"))?;
|
||||
|
||||
Ok(peer_id_from_public(Public::from_raw(public_key.to_bytes())))
|
||||
Ok(peer_id_from_public(Public(public_key.to_bytes())))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use core::future::Future;
|
||||
use core::{future::Future, str::FromStr};
|
||||
use std::{sync::Arc, collections::HashSet};
|
||||
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use serai_client::{SeraiError, Serai};
|
||||
use serai_client_serai::{RpcError, Serai};
|
||||
|
||||
use libp2p::{
|
||||
core::multiaddr::{Protocol, Multiaddr},
|
||||
@@ -50,7 +50,7 @@ impl ContinuallyRan for DialTask {
|
||||
const DELAY_BETWEEN_ITERATIONS: u64 = 5 * 60;
|
||||
const MAX_DELAY_BETWEEN_ITERATIONS: u64 = 10 * 60;
|
||||
|
||||
type Error = SeraiError;
|
||||
type Error = RpcError;
|
||||
|
||||
fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, Self::Error>> {
|
||||
async move {
|
||||
@@ -94,6 +94,13 @@ impl ContinuallyRan for DialTask {
|
||||
usize::try_from(OsRng.next_u64() % u64::try_from(potential_peers.len()).unwrap())
|
||||
.unwrap();
|
||||
let randomly_selected_peer = potential_peers.swap_remove(index_to_dial);
|
||||
let Ok(randomly_selected_peer) = libp2p::Multiaddr::from_str(&randomly_selected_peer)
|
||||
else {
|
||||
log::error!(
|
||||
"peer from substrate wasn't a valid `Multiaddr`: {randomly_selected_peer}"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
log::info!("found peer from substrate: {randomly_selected_peer}");
|
||||
|
||||
|
||||
@@ -13,9 +13,10 @@ use rand_core::{RngCore, OsRng};
|
||||
use zeroize::Zeroizing;
|
||||
use schnorrkel::Keypair;
|
||||
|
||||
use serai_client::{
|
||||
primitives::{ExternalNetworkId, PublicKey},
|
||||
validator_sets::primitives::ExternalValidatorSet,
|
||||
use serai_client_serai::{
|
||||
abi::primitives::{
|
||||
crypto::Public, network_id::ExternalNetworkId, validator_sets::ExternalValidatorSet,
|
||||
},
|
||||
Serai,
|
||||
};
|
||||
|
||||
@@ -66,7 +67,7 @@ use dial::DialTask;
|
||||
|
||||
const PORT: u16 = 30563; // 5132 ^ (('c' << 8) | 'o')
|
||||
|
||||
fn peer_id_from_public(public: PublicKey) -> PeerId {
|
||||
fn peer_id_from_public(public: Public) -> PeerId {
|
||||
// 0 represents the identity Multihash, that no hash was performed
|
||||
// It's an internal constant so we can't refer to the constant inside libp2p
|
||||
PeerId::from_multihash(Multihash::wrap(0, &public.0).unwrap()).unwrap()
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
|
||||
use borsh::BorshDeserialize;
|
||||
|
||||
use serai_client::validator_sets::primitives::ExternalValidatorSet;
|
||||
use serai_client_serai::abi::primitives::validator_sets::ExternalValidatorSet;
|
||||
|
||||
use tokio::sync::{mpsc, oneshot, RwLock};
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ use std::{
|
||||
collections::{HashSet, HashMap},
|
||||
};
|
||||
|
||||
use serai_client::{
|
||||
primitives::ExternalNetworkId, validator_sets::primitives::Session, SeraiError, Serai,
|
||||
};
|
||||
use serai_client_serai::abi::primitives::{network_id::ExternalNetworkId, validator_sets::Session};
|
||||
use serai_client_serai::{RpcError, Serai};
|
||||
|
||||
use serai_task::{Task, ContinuallyRan};
|
||||
|
||||
@@ -52,7 +51,7 @@ impl Validators {
|
||||
async fn session_changes(
|
||||
serai: impl Borrow<Serai>,
|
||||
sessions: impl Borrow<HashMap<ExternalNetworkId, Session>>,
|
||||
) -> Result<Vec<(ExternalNetworkId, Session, HashSet<PeerId>)>, SeraiError> {
|
||||
) -> Result<Vec<(ExternalNetworkId, Session, HashSet<PeerId>)>, RpcError> {
|
||||
/*
|
||||
This uses the latest finalized block, not the latest cosigned block, which should be fine as
|
||||
in the worst case, we'd connect to unexpected validators. They still shouldn't be able to
|
||||
@@ -69,10 +68,11 @@ impl Validators {
|
||||
// FuturesUnordered can be bad practice as it'll cause timeouts if infrequently polled, but
|
||||
// we poll it till it yields all futures with the most minimal processing possible
|
||||
let mut futures = FuturesUnordered::new();
|
||||
for network in serai_client::primitives::EXTERNAL_NETWORKS {
|
||||
for network in ExternalNetworkId::all() {
|
||||
let sessions = sessions.borrow();
|
||||
let temporal_serai = temporal_serai.borrow();
|
||||
futures.push(async move {
|
||||
let session = match temporal_serai.session(network.into()).await {
|
||||
let session = match temporal_serai.current_session(network.into()).await {
|
||||
Ok(Some(session)) => session,
|
||||
Ok(None) => return Ok(None),
|
||||
Err(e) => return Err(e),
|
||||
@@ -81,12 +81,16 @@ impl Validators {
|
||||
if sessions.get(&network) == Some(&session) {
|
||||
Ok(None)
|
||||
} else {
|
||||
match temporal_serai.active_network_validators(network.into()).await {
|
||||
Ok(validators) => Ok(Some((
|
||||
match temporal_serai.current_validators(network.into()).await {
|
||||
Ok(Some(validators)) => Ok(Some((
|
||||
network,
|
||||
session,
|
||||
validators.into_iter().map(peer_id_from_public).collect(),
|
||||
validators
|
||||
.into_iter()
|
||||
.map(|validator| peer_id_from_public(validator.into()))
|
||||
.collect(),
|
||||
))),
|
||||
Ok(None) => panic!("network has session yet no validators"),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
@@ -153,7 +157,7 @@ impl Validators {
|
||||
}
|
||||
|
||||
/// Update the view of the validators.
|
||||
pub(crate) async fn update(&mut self) -> Result<(), SeraiError> {
|
||||
pub(crate) async fn update(&mut self) -> Result<(), RpcError> {
|
||||
let session_changes = Self::session_changes(&*self.serai, &self.sessions).await?;
|
||||
self.incorporate_session_changes(session_changes);
|
||||
Ok(())
|
||||
@@ -206,7 +210,7 @@ impl ContinuallyRan for UpdateValidatorsTask {
|
||||
const DELAY_BETWEEN_ITERATIONS: u64 = 60;
|
||||
const MAX_DELAY_BETWEEN_ITERATIONS: u64 = 5 * 60;
|
||||
|
||||
type Error = SeraiError;
|
||||
type Error = RpcError;
|
||||
|
||||
fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, Self::Error>> {
|
||||
async move {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use core::future::Future;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use serai_primitives::{MAX_KEY_SHARES_PER_SET, ExternalValidatorSet};
|
||||
use serai_primitives::validator_sets::{ExternalValidatorSet, KeyShares};
|
||||
|
||||
use futures_lite::FutureExt;
|
||||
|
||||
@@ -30,7 +30,7 @@ pub const MIN_BLOCKS_PER_BATCH: usize = BLOCKS_PER_MINUTE + 1;
|
||||
/// commit is `8 + (validators * 32) + (32 + (validators * 32))` (for the time, list of validators,
|
||||
/// and aggregate signature). Accordingly, this should be a safe over-estimate.
|
||||
pub const BATCH_SIZE_LIMIT: usize = MIN_BLOCKS_PER_BATCH *
|
||||
(tributary_sdk::BLOCK_SIZE_LIMIT + 32 + ((MAX_KEY_SHARES_PER_SET as usize) * 128));
|
||||
(tributary_sdk::BLOCK_SIZE_LIMIT + 32 + ((KeyShares::MAX_PER_SET as usize) * 128));
|
||||
|
||||
/// Sends a heartbeat to other validators on regular intervals informing them of our Tributary's
|
||||
/// tip.
|
||||
|
||||
@@ -5,9 +5,10 @@ use serai_db::{create_db, db_channel};
|
||||
|
||||
use dkg::Participant;
|
||||
|
||||
use serai_client::{
|
||||
primitives::ExternalNetworkId,
|
||||
validator_sets::primitives::{Session, ExternalValidatorSet, KeyPair},
|
||||
use serai_client_serai::abi::primitives::{
|
||||
crypto::KeyPair,
|
||||
network_id::ExternalNetworkId,
|
||||
validator_sets::{Session, ExternalValidatorSet},
|
||||
};
|
||||
|
||||
use serai_cosign::SignedCosign;
|
||||
@@ -49,6 +50,7 @@ fn tributary_db_folder(set: ExternalValidatorSet) -> String {
|
||||
ExternalNetworkId::Bitcoin => "Bitcoin",
|
||||
ExternalNetworkId::Ethereum => "Ethereum",
|
||||
ExternalNetworkId::Monero => "Monero",
|
||||
_ => panic!("unrecognized `ExternalNetworkId`"),
|
||||
};
|
||||
format!("{root_path}/tributary-{network}-{}", set.session.0)
|
||||
}
|
||||
@@ -103,7 +105,7 @@ mod _internal_db {
|
||||
// Tributary transactions to publish from the DKG confirmation task
|
||||
TributaryTransactionsFromDkgConfirmation: (set: ExternalValidatorSet) -> Transaction,
|
||||
// Participants to remove
|
||||
RemoveParticipant: (set: ExternalValidatorSet) -> Participant,
|
||||
RemoveParticipant: (set: ExternalValidatorSet) -> u16,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,10 +141,11 @@ impl RemoveParticipant {
|
||||
pub(crate) fn send(txn: &mut impl DbTxn, set: ExternalValidatorSet, participant: Participant) {
|
||||
// If this set has yet to be retired, send this transaction
|
||||
if RetiredTributary::get(txn, set.network).map(|session| session.0) < Some(set.session.0) {
|
||||
_internal_db::RemoveParticipant::send(txn, set, &participant);
|
||||
_internal_db::RemoveParticipant::send(txn, set, &u16::from(participant));
|
||||
}
|
||||
}
|
||||
pub(crate) fn try_recv(txn: &mut impl DbTxn, set: ExternalValidatorSet) -> Option<Participant> {
|
||||
_internal_db::RemoveParticipant::try_recv(txn, set)
|
||||
.map(|i| Participant::new(i).expect("sent invalid participant index for removal"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,8 @@ use frost_schnorrkel::{
|
||||
|
||||
use serai_db::{DbTxn, Db as DbTrait};
|
||||
|
||||
use serai_client::{
|
||||
primitives::SeraiAddress,
|
||||
validator_sets::primitives::{ExternalValidatorSet, musig_context, set_keys_message},
|
||||
};
|
||||
#[rustfmt::skip]
|
||||
use serai_client_serai::abi::primitives::{validator_sets::ExternalValidatorSet, address::SeraiAddress};
|
||||
|
||||
use serai_task::{DoesNotError, ContinuallyRan};
|
||||
|
||||
@@ -160,7 +158,7 @@ impl<CD: DbTrait, TD: DbTrait> ConfirmDkgTask<CD, TD> {
|
||||
let (machine, preprocess) = AlgorithmMachine::new(
|
||||
schnorrkel(),
|
||||
// We use a 1-of-1 Musig here as we don't know who will actually be in this Musig yet
|
||||
musig(musig_context(set.into()), key, &[public_key]).unwrap(),
|
||||
musig(ExternalValidatorSet::musig_context(&set), key, &[public_key]).unwrap(),
|
||||
)
|
||||
.preprocess(&mut OsRng);
|
||||
// We take the preprocess so we can use it in a distinct machine with the actual Musig
|
||||
@@ -260,9 +258,12 @@ impl<CD: DbTrait, TD: DbTrait> ContinuallyRan for ConfirmDkgTask<CD, TD> {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let keys =
|
||||
musig(musig_context(self.set.set.into()), self.key.clone(), &musig_public_keys)
|
||||
.unwrap();
|
||||
let keys = musig(
|
||||
ExternalValidatorSet::musig_context(&self.set.set),
|
||||
self.key.clone(),
|
||||
&musig_public_keys,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Rebuild the machine
|
||||
let (machine, preprocess_from_cache) =
|
||||
@@ -296,9 +297,10 @@ impl<CD: DbTrait, TD: DbTrait> ContinuallyRan for ConfirmDkgTask<CD, TD> {
|
||||
};
|
||||
|
||||
// Calculate our share
|
||||
let (machine, share) = match handle_frost_error(
|
||||
machine.sign(preprocesses, &set_keys_message(&self.set.set, &key_pair)),
|
||||
) {
|
||||
let (machine, share) = match handle_frost_error(machine.sign(
|
||||
preprocesses,
|
||||
&ExternalValidatorSet::set_keys_message(&self.set.set, &key_pair),
|
||||
)) {
|
||||
Ok((machine, share)) => (machine, share),
|
||||
// This yields the *musig participant index*
|
||||
Err(participant) => {
|
||||
|
||||
@@ -14,9 +14,14 @@ use borsh::BorshDeserialize;
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use serai_client::{
|
||||
primitives::{ExternalNetworkId, PublicKey, SeraiAddress, Signature},
|
||||
validator_sets::primitives::{ExternalValidatorSet, KeyPair},
|
||||
use serai_client_serai::{
|
||||
abi::primitives::{
|
||||
BlockHash,
|
||||
crypto::{Public, Signature, ExternalKey, KeyPair},
|
||||
network_id::ExternalNetworkId,
|
||||
validator_sets::ExternalValidatorSet,
|
||||
address::SeraiAddress,
|
||||
},
|
||||
Serai,
|
||||
};
|
||||
use message_queue::{Service, client::MessageQueue};
|
||||
@@ -61,9 +66,7 @@ async fn serai() -> Arc<Serai> {
|
||||
let Ok(serai) = Serai::new(format!(
|
||||
"http://{}:9944",
|
||||
serai_env::var("SERAI_HOSTNAME").expect("Serai hostname wasn't provided")
|
||||
))
|
||||
.await
|
||||
else {
|
||||
)) else {
|
||||
log::error!("couldn't connect to the Serai node");
|
||||
tokio::time::sleep(delay).await;
|
||||
delay = (delay + SERAI_CONNECTION_DELAY).min(MAX_SERAI_CONNECTION_DELAY);
|
||||
@@ -213,10 +216,12 @@ async fn handle_network(
|
||||
&mut txn,
|
||||
ExternalValidatorSet { network, session },
|
||||
&KeyPair(
|
||||
PublicKey::from_raw(substrate_key),
|
||||
network_key
|
||||
.try_into()
|
||||
.expect("generated a network key which exceeds the maximum key length"),
|
||||
Public(substrate_key),
|
||||
ExternalKey(
|
||||
network_key
|
||||
.try_into()
|
||||
.expect("generated a network key which exceeds the maximum key length"),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -284,12 +289,13 @@ async fn handle_network(
|
||||
&mut txn,
|
||||
ExternalValidatorSet { network, session },
|
||||
slash_report,
|
||||
Signature::from(signature),
|
||||
Signature(signature),
|
||||
);
|
||||
}
|
||||
},
|
||||
messages::ProcessorMessage::Substrate(msg) => match msg {
|
||||
messages::substrate::ProcessorMessage::SubstrateBlockAck { block, plans } => {
|
||||
let block = BlockHash(block);
|
||||
let mut by_session = HashMap::new();
|
||||
for plan in plans {
|
||||
by_session
|
||||
@@ -481,7 +487,7 @@ async fn main() {
|
||||
);
|
||||
|
||||
// Handle each of the networks
|
||||
for network in serai_client::primitives::EXTERNAL_NETWORKS {
|
||||
for network in ExternalNetworkId::all() {
|
||||
tokio::spawn(handle_network(db.clone(), message_queue.clone(), serai.clone(), network));
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ use tokio::sync::mpsc;
|
||||
|
||||
use serai_db::{DbTxn, Db as DbTrait};
|
||||
|
||||
use serai_client::validator_sets::primitives::{Session, ExternalValidatorSet};
|
||||
use serai_client_serai::abi::primitives::{
|
||||
network_id::ExternalNetworkId,
|
||||
validator_sets::{Session, ExternalValidatorSet},
|
||||
};
|
||||
use message_queue::{Service, Metadata, client::MessageQueue};
|
||||
|
||||
use tributary_sdk::Tributary;
|
||||
@@ -39,7 +42,7 @@ impl<P: P2p> ContinuallyRan for SubstrateTask<P> {
|
||||
let mut made_progress = false;
|
||||
|
||||
// Handle the Canonical events
|
||||
for network in serai_client::primitives::EXTERNAL_NETWORKS {
|
||||
for network in ExternalNetworkId::all() {
|
||||
loop {
|
||||
let mut txn = self.db.txn();
|
||||
let Some(msg) = serai_coordinator_substrate::Canonical::try_recv(&mut txn, network)
|
||||
|
||||
@@ -11,7 +11,7 @@ use tokio::sync::mpsc;
|
||||
|
||||
use serai_db::{Get, DbTxn, Db as DbTrait, create_db, db_channel};
|
||||
|
||||
use serai_client::validator_sets::primitives::ExternalValidatorSet;
|
||||
use serai_client_serai::abi::primitives::validator_sets::ExternalValidatorSet;
|
||||
|
||||
use tributary_sdk::{TransactionKind, TransactionError, ProvidedError, TransactionTrait, Tributary};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ borsh = { version = "1", default-features = false, features = ["std", "derive",
|
||||
|
||||
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std"] }
|
||||
|
||||
serai-client = { path = "../../substrate/client", version = "0.1", default-features = false, features = ["serai"] }
|
||||
serai-client-serai = { path = "../../substrate/client/serai", default-features = false }
|
||||
|
||||
log = { version = "0.4", default-features = false, features = ["std"] }
|
||||
|
||||
|
||||
@@ -3,7 +3,13 @@ use std::sync::Arc;
|
||||
|
||||
use futures::stream::{StreamExt, FuturesOrdered};
|
||||
|
||||
use serai_client::{validator_sets::primitives::ExternalValidatorSet, Serai};
|
||||
use serai_client_serai::{
|
||||
abi::{
|
||||
self,
|
||||
primitives::{network_id::ExternalNetworkId, validator_sets::ExternalValidatorSet},
|
||||
},
|
||||
Serai,
|
||||
};
|
||||
|
||||
use messages::substrate::{InInstructionResult, ExecutedBatch, CoordinatorMessage};
|
||||
|
||||
@@ -15,6 +21,7 @@ use serai_cosign::Cosigning;
|
||||
create_db!(
|
||||
CoordinatorSubstrateCanonical {
|
||||
NextBlock: () -> u64,
|
||||
LastIndexedBatchId: (network: ExternalNetworkId) -> u32,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -45,10 +52,10 @@ impl<D: Db> ContinuallyRan for CanonicalEventStream<D> {
|
||||
// These are all the events which generate canonical messages
|
||||
struct CanonicalEvents {
|
||||
time: u64,
|
||||
key_gen_events: Vec<serai_client::validator_sets::ValidatorSetsEvent>,
|
||||
set_retired_events: Vec<serai_client::validator_sets::ValidatorSetsEvent>,
|
||||
batch_events: Vec<serai_client::in_instructions::InInstructionsEvent>,
|
||||
burn_events: Vec<serai_client::coins::CoinsEvent>,
|
||||
set_keys_events: Vec<abi::validator_sets::Event>,
|
||||
slash_report_events: Vec<abi::validator_sets::Event>,
|
||||
batch_events: Vec<abi::in_instructions::Event>,
|
||||
burn_events: Vec<abi::coins::Event>,
|
||||
}
|
||||
|
||||
// For a cosigned block, fetch all relevant events
|
||||
@@ -66,16 +73,16 @@ impl<D: Db> ContinuallyRan for CanonicalEventStream<D> {
|
||||
}
|
||||
Err(serai_cosign::Faulted) => return Err("cosigning process faulted".to_string()),
|
||||
};
|
||||
let temporal_serai = serai.as_of(block_hash);
|
||||
let temporal_serai = serai.as_of(block_hash).await.map_err(|e| format!("{e}"))?;
|
||||
let temporal_serai_validators = temporal_serai.validator_sets();
|
||||
let temporal_serai_instructions = temporal_serai.in_instructions();
|
||||
let temporal_serai_coins = temporal_serai.coins();
|
||||
|
||||
let (block, key_gen_events, set_retired_events, batch_events, burn_events) =
|
||||
let (block, set_keys_events, slash_report_events, batch_events, burn_events) =
|
||||
tokio::try_join!(
|
||||
serai.block(block_hash),
|
||||
temporal_serai_validators.key_gen_events(),
|
||||
temporal_serai_validators.set_retired_events(),
|
||||
temporal_serai_validators.set_keys_events(),
|
||||
temporal_serai_validators.slash_report_events(),
|
||||
temporal_serai_instructions.batch_events(),
|
||||
temporal_serai_coins.burn_with_instruction_events(),
|
||||
)
|
||||
@@ -84,22 +91,14 @@ impl<D: Db> ContinuallyRan for CanonicalEventStream<D> {
|
||||
Err(format!("Serai node didn't have cosigned block #{block_number}"))?
|
||||
};
|
||||
|
||||
let time = if block_number == 0 {
|
||||
block.time().unwrap_or(0)
|
||||
} else {
|
||||
// Serai's block time is in milliseconds
|
||||
block
|
||||
.time()
|
||||
.ok_or_else(|| "non-genesis Serai block didn't have a time".to_string())? /
|
||||
1000
|
||||
};
|
||||
|
||||
// We use time in seconds, not milliseconds, here
|
||||
let time = block.header.unix_time_in_millis() / 1000;
|
||||
Ok((
|
||||
block_number,
|
||||
CanonicalEvents {
|
||||
time,
|
||||
key_gen_events,
|
||||
set_retired_events,
|
||||
set_keys_events,
|
||||
slash_report_events,
|
||||
batch_events,
|
||||
burn_events,
|
||||
},
|
||||
@@ -131,10 +130,9 @@ impl<D: Db> ContinuallyRan for CanonicalEventStream<D> {
|
||||
|
||||
let mut txn = self.db.txn();
|
||||
|
||||
for key_gen in block.key_gen_events {
|
||||
let serai_client::validator_sets::ValidatorSetsEvent::KeyGen { set, key_pair } = &key_gen
|
||||
else {
|
||||
panic!("KeyGen event wasn't a KeyGen event: {key_gen:?}");
|
||||
for set_keys in block.set_keys_events {
|
||||
let abi::validator_sets::Event::SetKeys { set, key_pair } = &set_keys else {
|
||||
panic!("`SetKeys` event wasn't a `SetKeys` event: {set_keys:?}");
|
||||
};
|
||||
crate::Canonical::send(
|
||||
&mut txn,
|
||||
@@ -147,12 +145,10 @@ impl<D: Db> ContinuallyRan for CanonicalEventStream<D> {
|
||||
);
|
||||
}
|
||||
|
||||
for set_retired in block.set_retired_events {
|
||||
let serai_client::validator_sets::ValidatorSetsEvent::SetRetired { set } = &set_retired
|
||||
else {
|
||||
panic!("SetRetired event wasn't a SetRetired event: {set_retired:?}");
|
||||
for slash_report in block.slash_report_events {
|
||||
let abi::validator_sets::Event::SlashReport { set } = &slash_report else {
|
||||
panic!("`SlashReport` event wasn't a `SlashReport` event: {slash_report:?}");
|
||||
};
|
||||
let Ok(set) = ExternalValidatorSet::try_from(*set) else { continue };
|
||||
crate::Canonical::send(
|
||||
&mut txn,
|
||||
set.network,
|
||||
@@ -160,10 +156,12 @@ impl<D: Db> ContinuallyRan for CanonicalEventStream<D> {
|
||||
);
|
||||
}
|
||||
|
||||
for network in serai_client::primitives::EXTERNAL_NETWORKS {
|
||||
for network in ExternalNetworkId::all() {
|
||||
let mut batch = None;
|
||||
for this_batch in &block.batch_events {
|
||||
let serai_client::in_instructions::InInstructionsEvent::Batch {
|
||||
// Only irrefutable as this is the only member of the enum at this time
|
||||
#[expect(irrefutable_let_patterns)]
|
||||
let abi::in_instructions::Event::Batch {
|
||||
network: batch_network,
|
||||
publishing_session,
|
||||
id,
|
||||
@@ -194,14 +192,19 @@ impl<D: Db> ContinuallyRan for CanonicalEventStream<D> {
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
|
||||
if LastIndexedBatchId::get(&txn, network) != id.checked_sub(1) {
|
||||
panic!(
|
||||
"next batch from Serai's ID was not an increment of the last indexed batch's ID"
|
||||
);
|
||||
}
|
||||
LastIndexedBatchId::set(&mut txn, network, id);
|
||||
}
|
||||
}
|
||||
|
||||
let mut burns = vec![];
|
||||
for burn in &block.burn_events {
|
||||
let serai_client::coins::CoinsEvent::BurnWithInstruction { from: _, instruction } =
|
||||
&burn
|
||||
else {
|
||||
let abi::coins::Event::BurnWithInstruction { from: _, instruction } = &burn else {
|
||||
panic!("BurnWithInstruction event wasn't a BurnWithInstruction event: {burn:?}");
|
||||
};
|
||||
if instruction.balance.coin.network() == network {
|
||||
@@ -223,3 +226,7 @@ impl<D: Db> ContinuallyRan for CanonicalEventStream<D> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn last_indexed_batch_id(txn: &impl DbTxn, network: ExternalNetworkId) -> Option<u32> {
|
||||
LastIndexedBatchId::get(txn, network)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,14 @@ use std::sync::Arc;
|
||||
|
||||
use futures::stream::{StreamExt, FuturesOrdered};
|
||||
|
||||
use serai_client::{
|
||||
primitives::{SeraiAddress, EmbeddedEllipticCurve},
|
||||
validator_sets::primitives::{MAX_KEY_SHARES_PER_SET, ExternalValidatorSet},
|
||||
use serai_client_serai::{
|
||||
abi::primitives::{
|
||||
BlockHash,
|
||||
crypto::EmbeddedEllipticCurveKeys,
|
||||
network_id::ExternalNetworkId,
|
||||
validator_sets::{KeyShares, ExternalValidatorSet},
|
||||
address::SeraiAddress,
|
||||
},
|
||||
Serai,
|
||||
};
|
||||
|
||||
@@ -49,10 +54,10 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
|
||||
|
||||
// These are all the events which generate canonical messages
|
||||
struct EphemeralEvents {
|
||||
block_hash: [u8; 32],
|
||||
block_hash: BlockHash,
|
||||
time: u64,
|
||||
new_set_events: Vec<serai_client::validator_sets::ValidatorSetsEvent>,
|
||||
accepted_handover_events: Vec<serai_client::validator_sets::ValidatorSetsEvent>,
|
||||
set_decided_events: Vec<serai_client_serai::abi::validator_sets::Event>,
|
||||
accepted_handover_events: Vec<serai_client_serai::abi::validator_sets::Event>,
|
||||
}
|
||||
|
||||
// For a cosigned block, fetch all relevant events
|
||||
@@ -71,11 +76,11 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
|
||||
Err(serai_cosign::Faulted) => return Err("cosigning process faulted".to_string()),
|
||||
};
|
||||
|
||||
let temporal_serai = serai.as_of(block_hash);
|
||||
let temporal_serai = serai.as_of(block_hash).await.map_err(|e| format!("{e}"))?;
|
||||
let temporal_serai_validators = temporal_serai.validator_sets();
|
||||
let (block, new_set_events, accepted_handover_events) = tokio::try_join!(
|
||||
let (block, set_decided_events, accepted_handover_events) = tokio::try_join!(
|
||||
serai.block(block_hash),
|
||||
temporal_serai_validators.new_set_events(),
|
||||
temporal_serai_validators.set_decided_events(),
|
||||
temporal_serai_validators.accepted_handover_events(),
|
||||
)
|
||||
.map_err(|e| format!("{e:?}"))?;
|
||||
@@ -83,19 +88,11 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
|
||||
Err(format!("Serai node didn't have cosigned block #{block_number}"))?
|
||||
};
|
||||
|
||||
let time = if block_number == 0 {
|
||||
block.time().unwrap_or(0)
|
||||
} else {
|
||||
// Serai's block time is in milliseconds
|
||||
block
|
||||
.time()
|
||||
.ok_or_else(|| "non-genesis Serai block didn't have a time".to_string())? /
|
||||
1000
|
||||
};
|
||||
|
||||
// We use time in seconds, not milliseconds, here
|
||||
let time = block.header.unix_time_in_millis() / 1000;
|
||||
Ok((
|
||||
block_number,
|
||||
EphemeralEvents { block_hash, time, new_set_events, accepted_handover_events },
|
||||
EphemeralEvents { block_hash, time, set_decided_events, accepted_handover_events },
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -126,48 +123,40 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
|
||||
|
||||
let mut txn = self.db.txn();
|
||||
|
||||
for new_set in block.new_set_events {
|
||||
let serai_client::validator_sets::ValidatorSetsEvent::NewSet { set } = &new_set else {
|
||||
panic!("NewSet event wasn't a NewSet event: {new_set:?}");
|
||||
for set_decided in block.set_decided_events {
|
||||
let serai_client_serai::abi::validator_sets::Event::SetDecided { set, validators } =
|
||||
&set_decided
|
||||
else {
|
||||
panic!("`SetDecided` event wasn't a `SetDecided` event: {set_decided:?}");
|
||||
};
|
||||
// We only coordinate over external networks
|
||||
let Ok(set) = ExternalValidatorSet::try_from(*set) else { continue };
|
||||
|
||||
let serai = self.serai.as_of(block.block_hash);
|
||||
let serai = self.serai.as_of(block.block_hash).await.map_err(|e| format!("{e}"))?;
|
||||
let serai = serai.validator_sets();
|
||||
let Some(validators) =
|
||||
serai.participants(set.network.into()).await.map_err(|e| format!("{e:?}"))?
|
||||
else {
|
||||
Err(format!(
|
||||
"block #{block_number} declared a new set but didn't have the participants"
|
||||
))?
|
||||
};
|
||||
let validators = validators
|
||||
.into_iter()
|
||||
.map(|(validator, weight)| (SeraiAddress::from(validator), weight))
|
||||
.collect::<Vec<_>>();
|
||||
let validators =
|
||||
validators.iter().map(|(validator, weight)| (*validator, weight)).collect::<Vec<_>>();
|
||||
let in_set = validators.iter().any(|(validator, _)| *validator == self.validator);
|
||||
if in_set {
|
||||
if u16::try_from(validators.len()).is_err() {
|
||||
Err("more than u16::MAX validators sent")?;
|
||||
}
|
||||
|
||||
let Ok(validators) = validators
|
||||
let validators = validators
|
||||
.into_iter()
|
||||
.map(|(validator, weight)| u16::try_from(weight).map(|weight| (validator, weight)))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
else {
|
||||
Err("validator's weight exceeded u16::MAX".to_string())?
|
||||
};
|
||||
.map(|(validator, weight)| (validator, weight.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Do the summation in u32 so we don't risk a u16 overflow
|
||||
let total_weight = validators.iter().map(|(_, weight)| u32::from(*weight)).sum::<u32>();
|
||||
if total_weight > u32::from(MAX_KEY_SHARES_PER_SET) {
|
||||
if total_weight > u32::from(KeyShares::MAX_PER_SET) {
|
||||
Err(format!(
|
||||
"{set:?} has {total_weight} key shares when the max is {MAX_KEY_SHARES_PER_SET}"
|
||||
"{set:?} has {total_weight} key shares when the max is {}",
|
||||
KeyShares::MAX_PER_SET
|
||||
))?;
|
||||
}
|
||||
let total_weight = u16::try_from(total_weight).unwrap();
|
||||
let total_weight = u16::try_from(total_weight)
|
||||
.expect("value smaller than `u16` constant but doesn't fit in `u16`");
|
||||
|
||||
// Fetch all of the validators' embedded elliptic curve keys
|
||||
let mut embedded_elliptic_curve_keys = FuturesOrdered::new();
|
||||
@@ -175,52 +164,51 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
|
||||
let validator = *validator;
|
||||
// try_join doesn't return a future so we need to wrap it in this additional async
|
||||
// block
|
||||
embedded_elliptic_curve_keys.push_back(async move {
|
||||
tokio::try_join!(
|
||||
// One future to fetch the substrate embedded key
|
||||
serai.embedded_elliptic_curve_key(
|
||||
validator.into(),
|
||||
EmbeddedEllipticCurve::Embedwards25519
|
||||
),
|
||||
// One future to fetch the external embedded key, if there is a distinct curve
|
||||
async {
|
||||
// `embedded_elliptic_curves` is documented to have the second entry be the
|
||||
// network-specific curve (if it exists and is distinct from Embedwards25519)
|
||||
if let Some(curve) = set.network.embedded_elliptic_curves().get(1) {
|
||||
serai.embedded_elliptic_curve_key(validator.into(), *curve).await.map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
embedded_elliptic_curve_keys.push_back({
|
||||
let serai = serai.clone();
|
||||
async move {
|
||||
match serai.embedded_elliptic_curve_keys(validator, set.network).await {
|
||||
Ok(Some(keys)) => Ok(Some((
|
||||
validator,
|
||||
match keys {
|
||||
EmbeddedEllipticCurveKeys::Bitcoin(substrate, external) => {
|
||||
assert_eq!(set.network, ExternalNetworkId::Bitcoin);
|
||||
(substrate, external.as_slice().to_vec())
|
||||
}
|
||||
EmbeddedEllipticCurveKeys::Ethereum(substrate, external) => {
|
||||
assert_eq!(set.network, ExternalNetworkId::Ethereum);
|
||||
(substrate, external.as_slice().to_vec())
|
||||
}
|
||||
EmbeddedEllipticCurveKeys::Monero(substrate) => {
|
||||
assert_eq!(set.network, ExternalNetworkId::Monero);
|
||||
(substrate, substrate.as_slice().to_vec())
|
||||
}
|
||||
},
|
||||
))),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
)
|
||||
.map(|(substrate_embedded_key, external_embedded_key)| {
|
||||
(validator, substrate_embedded_key, external_embedded_key)
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut evrf_public_keys = Vec::with_capacity(usize::from(total_weight));
|
||||
for (validator, weight) in &validators {
|
||||
let (future_validator, substrate_embedded_key, external_embedded_key) =
|
||||
embedded_elliptic_curve_keys.next().await.unwrap().map_err(|e| format!("{e:?}"))?;
|
||||
let Some((future_validator, (substrate_embedded_key, external_embedded_key))) =
|
||||
embedded_elliptic_curve_keys.next().await.unwrap().map_err(|e| format!("{e:?}"))?
|
||||
else {
|
||||
Err("`SetDecided` with validator missing an embedded key".to_string())?
|
||||
};
|
||||
assert_eq!(*validator, future_validator);
|
||||
let external_embedded_key =
|
||||
external_embedded_key.unwrap_or(substrate_embedded_key.clone());
|
||||
match (substrate_embedded_key, external_embedded_key) {
|
||||
(Some(substrate_embedded_key), Some(external_embedded_key)) => {
|
||||
let substrate_embedded_key = <[u8; 32]>::try_from(substrate_embedded_key)
|
||||
.map_err(|_| "Embedwards25519 key wasn't 32 bytes".to_string())?;
|
||||
for _ in 0 .. *weight {
|
||||
evrf_public_keys.push((substrate_embedded_key, external_embedded_key.clone()));
|
||||
}
|
||||
}
|
||||
_ => Err("NewSet with validator missing an embedded key".to_string())?,
|
||||
|
||||
for _ in 0 .. *weight {
|
||||
evrf_public_keys.push((substrate_embedded_key, external_embedded_key.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_set = NewSetInformation {
|
||||
set,
|
||||
serai_block: block.block_hash,
|
||||
serai_block: block.block_hash.0,
|
||||
declaration_time: block.time,
|
||||
// TODO: This should be inlined into the Processor's key gen code
|
||||
// It's legacy from when we removed participants from the key gen
|
||||
@@ -238,7 +226,7 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
|
||||
}
|
||||
|
||||
for accepted_handover in block.accepted_handover_events {
|
||||
let serai_client::validator_sets::ValidatorSetsEvent::AcceptedHandover { set } =
|
||||
let serai_client_serai::abi::validator_sets::Event::AcceptedHandover { set } =
|
||||
&accepted_handover
|
||||
else {
|
||||
panic!("AcceptedHandover event wasn't a AcceptedHandover event: {accepted_handover:?}");
|
||||
|
||||
@@ -8,10 +8,14 @@ use borsh::{BorshSerialize, BorshDeserialize};
|
||||
|
||||
use dkg::Participant;
|
||||
|
||||
use serai_client::{
|
||||
primitives::{ExternalNetworkId, SeraiAddress, Signature},
|
||||
validator_sets::primitives::{Session, ExternalValidatorSet, KeyPair, SlashReport},
|
||||
in_instructions::primitives::SignedBatch,
|
||||
use serai_client_serai::abi::{
|
||||
primitives::{
|
||||
network_id::ExternalNetworkId,
|
||||
validator_sets::{Session, ExternalValidatorSet, SlashReport},
|
||||
crypto::{Signature, KeyPair},
|
||||
address::SeraiAddress,
|
||||
instructions::SignedBatch,
|
||||
},
|
||||
Transaction,
|
||||
};
|
||||
|
||||
@@ -19,6 +23,7 @@ use serai_db::*;
|
||||
|
||||
mod canonical;
|
||||
pub use canonical::CanonicalEventStream;
|
||||
use canonical::last_indexed_batch_id;
|
||||
mod ephemeral;
|
||||
pub use ephemeral::EphemeralEventStream;
|
||||
|
||||
@@ -37,7 +42,7 @@ pub struct NewSetInformation {
|
||||
pub set: ExternalValidatorSet,
|
||||
/// The Serai block which declared it.
|
||||
pub serai_block: [u8; 32],
|
||||
/// The time of the block which declared it, in seconds.
|
||||
/// The time of the block which declared it, in seconds since the epoch.
|
||||
pub declaration_time: u64,
|
||||
/// The threshold to use.
|
||||
pub threshold: u16,
|
||||
@@ -96,9 +101,9 @@ mod _public_db {
|
||||
create_db!(
|
||||
CoordinatorSubstrate {
|
||||
// Keys to set on the Serai network
|
||||
Keys: (network: ExternalNetworkId) -> (Session, Vec<u8>),
|
||||
Keys: (network: ExternalNetworkId) -> (Session, Transaction),
|
||||
// Slash reports to publish onto the Serai network
|
||||
SlashReports: (network: ExternalNetworkId) -> (Session, Vec<u8>),
|
||||
SlashReports: (network: ExternalNetworkId) -> (Session, Transaction),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -171,7 +176,7 @@ impl Keys {
|
||||
}
|
||||
}
|
||||
|
||||
let tx = serai_client::validator_sets::SeraiValidatorSets::set_keys(
|
||||
let tx = serai_client_serai::ValidatorSets::set_keys(
|
||||
set.network,
|
||||
key_pair,
|
||||
signature_participants,
|
||||
@@ -192,7 +197,7 @@ pub struct SignedBatches;
|
||||
impl SignedBatches {
|
||||
/// Send a `SignedBatch` to publish onto Serai.
|
||||
pub fn send(txn: &mut impl DbTxn, batch: &SignedBatch) {
|
||||
_public_db::SignedBatches::send(txn, batch.batch.network, batch);
|
||||
_public_db::SignedBatches::send(txn, batch.batch.network(), batch);
|
||||
}
|
||||
pub(crate) fn try_recv(txn: &mut impl DbTxn, network: ExternalNetworkId) -> Option<SignedBatch> {
|
||||
_public_db::SignedBatches::try_recv(txn, network)
|
||||
@@ -219,11 +224,8 @@ impl SlashReports {
|
||||
}
|
||||
}
|
||||
|
||||
let tx = serai_client::validator_sets::SeraiValidatorSets::report_slashes(
|
||||
set.network,
|
||||
slash_report,
|
||||
signature,
|
||||
);
|
||||
let tx =
|
||||
serai_client_serai::ValidatorSets::report_slashes(set.network, slash_report, signature);
|
||||
_public_db::SlashReports::set(txn, set.network, &(set.session, tx));
|
||||
}
|
||||
pub(crate) fn take(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use core::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[rustfmt::skip]
|
||||
use serai_client::{primitives::ExternalNetworkId, in_instructions::primitives::SignedBatch, SeraiError, Serai};
|
||||
use serai_client_serai::{
|
||||
abi::primitives::{network_id::ExternalNetworkId, instructions::SignedBatch},
|
||||
RpcError, Serai,
|
||||
};
|
||||
|
||||
use serai_db::{Get, DbTxn, Db, create_db};
|
||||
use serai_task::ContinuallyRan;
|
||||
@@ -31,7 +33,7 @@ impl<D: Db> PublishBatchTask<D> {
|
||||
}
|
||||
|
||||
impl<D: Db> ContinuallyRan for PublishBatchTask<D> {
|
||||
type Error = SeraiError;
|
||||
type Error = RpcError;
|
||||
|
||||
fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, Self::Error>> {
|
||||
async move {
|
||||
@@ -43,8 +45,8 @@ impl<D: Db> ContinuallyRan for PublishBatchTask<D> {
|
||||
};
|
||||
|
||||
// If this is a Batch not yet published, save it into our unordered mapping
|
||||
if LastPublishedBatch::get(&txn, self.network) < Some(batch.batch.id) {
|
||||
BatchesToPublish::set(&mut txn, self.network, batch.batch.id, &batch);
|
||||
if LastPublishedBatch::get(&txn, self.network) < Some(batch.batch.id()) {
|
||||
BatchesToPublish::set(&mut txn, self.network, batch.batch.id(), &batch);
|
||||
}
|
||||
|
||||
txn.commit();
|
||||
@@ -52,12 +54,8 @@ impl<D: Db> ContinuallyRan for PublishBatchTask<D> {
|
||||
|
||||
// Synchronize our last published batch with the Serai network's
|
||||
let next_to_publish = {
|
||||
// This uses the latest finalized block, not the latest cosigned block, which should be
|
||||
// fine as in the worst case, the only impact is no longer attempting TX publication
|
||||
let serai = self.serai.as_of_latest_finalized_block().await?;
|
||||
let last_batch = serai.in_instructions().last_batch_for_network(self.network).await?;
|
||||
|
||||
let mut txn = self.db.txn();
|
||||
let last_batch = crate::last_indexed_batch_id(&txn, self.network);
|
||||
let mut our_last_batch = LastPublishedBatch::get(&txn, self.network);
|
||||
while our_last_batch < last_batch {
|
||||
let next_batch = our_last_batch.map(|batch| batch + 1).unwrap_or(0);
|
||||
@@ -68,6 +66,7 @@ impl<D: Db> ContinuallyRan for PublishBatchTask<D> {
|
||||
if let Some(last_batch) = our_last_batch {
|
||||
LastPublishedBatch::set(&mut txn, self.network, &last_batch);
|
||||
}
|
||||
txn.commit();
|
||||
last_batch.map(|batch| batch + 1).unwrap_or(0)
|
||||
};
|
||||
|
||||
@@ -75,7 +74,7 @@ impl<D: Db> ContinuallyRan for PublishBatchTask<D> {
|
||||
if let Some(batch) = BatchesToPublish::get(&self.db, self.network, next_to_publish) {
|
||||
self
|
||||
.serai
|
||||
.publish(&serai_client::in_instructions::SeraiInInstructions::execute_batch(batch))
|
||||
.publish_transaction(&serai_client_serai::InInstructions::execute_batch(batch))
|
||||
.await?;
|
||||
true
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,10 @@ use std::sync::Arc;
|
||||
|
||||
use serai_db::{DbTxn, Db};
|
||||
|
||||
use serai_client::{primitives::ExternalNetworkId, validator_sets::primitives::Session, Serai};
|
||||
use serai_client_serai::{
|
||||
abi::primitives::{network_id::ExternalNetworkId, validator_sets::Session},
|
||||
Serai,
|
||||
};
|
||||
|
||||
use serai_task::ContinuallyRan;
|
||||
|
||||
@@ -36,7 +39,8 @@ impl<D: Db> PublishSlashReportTask<D> {
|
||||
let serai = self.serai.as_of_latest_finalized_block().await.map_err(|e| format!("{e:?}"))?;
|
||||
let serai = serai.validator_sets();
|
||||
let session_after_slash_report = Session(session.0 + 1);
|
||||
let current_session = serai.session(network.into()).await.map_err(|e| format!("{e:?}"))?;
|
||||
let current_session =
|
||||
serai.current_session(network.into()).await.map_err(|e| format!("{e:?}"))?;
|
||||
let current_session = current_session.map(|session| session.0);
|
||||
// Only attempt to publish the slash report for session #n while session #n+1 is still
|
||||
// active
|
||||
@@ -55,14 +59,13 @@ impl<D: Db> PublishSlashReportTask<D> {
|
||||
}
|
||||
|
||||
// If this session which should publish a slash report already has, move on
|
||||
let key_pending_slash_report =
|
||||
serai.key_pending_slash_report(network).await.map_err(|e| format!("{e:?}"))?;
|
||||
if key_pending_slash_report.is_none() {
|
||||
if !serai.pending_slash_report(network).await.map_err(|e| format!("{e:?}"))? {
|
||||
txn.commit();
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
match self.serai.publish(&slash_report).await {
|
||||
// Since this slash report is still pending, publish it
|
||||
match self.serai.publish_transaction(&slash_report).await {
|
||||
Ok(()) => {
|
||||
txn.commit();
|
||||
Ok(true)
|
||||
@@ -84,7 +87,7 @@ impl<D: Db> ContinuallyRan for PublishSlashReportTask<D> {
|
||||
async move {
|
||||
let mut made_progress = false;
|
||||
let mut error = None;
|
||||
for network in serai_client::primitives::EXTERNAL_NETWORKS {
|
||||
for network in ExternalNetworkId::all() {
|
||||
let network_res = self.publish(network).await;
|
||||
// We made progress if any network successfully published their slash report
|
||||
made_progress |= network_res == Ok(true);
|
||||
|
||||
@@ -3,7 +3,10 @@ use std::sync::Arc;
|
||||
|
||||
use serai_db::{DbTxn, Db};
|
||||
|
||||
use serai_client::{validator_sets::primitives::ExternalValidatorSet, Serai};
|
||||
use serai_client_serai::{
|
||||
abi::primitives::{network_id::ExternalNetworkId, validator_sets::ExternalValidatorSet},
|
||||
Serai,
|
||||
};
|
||||
|
||||
use serai_task::ContinuallyRan;
|
||||
|
||||
@@ -28,7 +31,7 @@ impl<D: Db> ContinuallyRan for SetKeysTask<D> {
|
||||
fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, Self::Error>> {
|
||||
async move {
|
||||
let mut made_progress = false;
|
||||
for network in serai_client::primitives::EXTERNAL_NETWORKS {
|
||||
for network in ExternalNetworkId::all() {
|
||||
let mut txn = self.db.txn();
|
||||
let Some((session, keys)) = Keys::take(&mut txn, network) else {
|
||||
// No keys to set
|
||||
@@ -40,7 +43,8 @@ impl<D: Db> ContinuallyRan for SetKeysTask<D> {
|
||||
let serai =
|
||||
self.serai.as_of_latest_finalized_block().await.map_err(|e| format!("{e:?}"))?;
|
||||
let serai = serai.validator_sets();
|
||||
let current_session = serai.session(network.into()).await.map_err(|e| format!("{e:?}"))?;
|
||||
let current_session =
|
||||
serai.current_session(network.into()).await.map_err(|e| format!("{e:?}"))?;
|
||||
let current_session = current_session.map(|session| session.0);
|
||||
// Only attempt to set these keys if this isn't a retired session
|
||||
if Some(session.0) < current_session {
|
||||
@@ -67,7 +71,7 @@ impl<D: Db> ContinuallyRan for SetKeysTask<D> {
|
||||
continue;
|
||||
};
|
||||
|
||||
match self.serai.publish(&keys).await {
|
||||
match self.serai.publish_transaction(&keys).await {
|
||||
Ok(()) => {
|
||||
txn.commit();
|
||||
made_progress = true;
|
||||
|
||||
@@ -36,7 +36,7 @@ serai-task = { path = "../../common/task", version = "0.1" }
|
||||
|
||||
tributary-sdk = { path = "../tributary-sdk" }
|
||||
|
||||
serai-cosign = { path = "../cosign" }
|
||||
serai-cosign-types = { path = "../cosign/types" }
|
||||
serai-coordinator-substrate = { path = "../substrate" }
|
||||
|
||||
messages = { package = "serai-processor-messages", path = "../../processor/messages" }
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
#![expect(clippy::cast_possible_truncation)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use borsh::{BorshSerialize, BorshDeserialize};
|
||||
|
||||
use serai_primitives::{address::SeraiAddress, validator_sets::primitives::ExternalValidatorSet};
|
||||
use serai_primitives::{BlockHash, validator_sets::ExternalValidatorSet, address::SeraiAddress};
|
||||
|
||||
use messages::sign::{VariantSignId, SignId};
|
||||
|
||||
use serai_db::*;
|
||||
|
||||
use serai_cosign::CosignIntent;
|
||||
use serai_cosign_types::CosignIntent;
|
||||
|
||||
use crate::transaction::SigningProtocolRound;
|
||||
|
||||
@@ -124,7 +122,7 @@ impl Topic {
|
||||
Topic::DkgConfirmation { attempt, round: _ } => Some({
|
||||
let id = {
|
||||
let mut id = [0; 32];
|
||||
let encoded_set = borsh::to_vec(set).unwrap();
|
||||
let encoded_set = borsh::to_vec(&set).unwrap();
|
||||
id[.. encoded_set.len()].copy_from_slice(&encoded_set);
|
||||
VariantSignId::Batch(id)
|
||||
};
|
||||
@@ -234,18 +232,18 @@ create_db!(
|
||||
SlashPoints: (set: ExternalValidatorSet, validator: SeraiAddress) -> u32,
|
||||
|
||||
// The cosign intent for a Substrate block
|
||||
CosignIntents: (set: ExternalValidatorSet, substrate_block_hash: [u8; 32]) -> CosignIntent,
|
||||
CosignIntents: (set: ExternalValidatorSet, substrate_block_hash: BlockHash) -> CosignIntent,
|
||||
// The latest Substrate block to cosign.
|
||||
LatestSubstrateBlockToCosign: (set: ExternalValidatorSet) -> [u8; 32],
|
||||
LatestSubstrateBlockToCosign: (set: ExternalValidatorSet) -> BlockHash,
|
||||
// The hash of the block we're actively cosigning.
|
||||
ActivelyCosigning: (set: ExternalValidatorSet) -> [u8; 32],
|
||||
ActivelyCosigning: (set: ExternalValidatorSet) -> BlockHash,
|
||||
// If this block has already been cosigned.
|
||||
Cosigned: (set: ExternalValidatorSet, substrate_block_hash: [u8; 32]) -> (),
|
||||
Cosigned: (set: ExternalValidatorSet, substrate_block_hash: BlockHash) -> (),
|
||||
|
||||
// The plans to recognize upon a `Transaction::SubstrateBlock` being included on-chain.
|
||||
SubstrateBlockPlans: (
|
||||
set: ExternalValidatorSet,
|
||||
substrate_block_hash: [u8; 32]
|
||||
substrate_block_hash: BlockHash
|
||||
) -> Vec<[u8; 32]>,
|
||||
|
||||
// The weight accumulated for a topic.
|
||||
@@ -293,26 +291,26 @@ impl TributaryDb {
|
||||
pub(crate) fn latest_substrate_block_to_cosign(
|
||||
getter: &impl Get,
|
||||
set: ExternalValidatorSet,
|
||||
) -> Option<[u8; 32]> {
|
||||
) -> Option<BlockHash> {
|
||||
LatestSubstrateBlockToCosign::get(getter, set)
|
||||
}
|
||||
pub(crate) fn set_latest_substrate_block_to_cosign(
|
||||
txn: &mut impl DbTxn,
|
||||
set: ExternalValidatorSet,
|
||||
substrate_block_hash: [u8; 32],
|
||||
substrate_block_hash: BlockHash,
|
||||
) {
|
||||
LatestSubstrateBlockToCosign::set(txn, set, &substrate_block_hash);
|
||||
}
|
||||
pub(crate) fn actively_cosigning(
|
||||
txn: &mut impl DbTxn,
|
||||
set: ExternalValidatorSet,
|
||||
) -> Option<[u8; 32]> {
|
||||
) -> Option<BlockHash> {
|
||||
ActivelyCosigning::get(txn, set)
|
||||
}
|
||||
pub(crate) fn start_cosigning(
|
||||
txn: &mut impl DbTxn,
|
||||
set: ExternalValidatorSet,
|
||||
substrate_block_hash: [u8; 32],
|
||||
substrate_block_hash: BlockHash,
|
||||
substrate_block_number: u64,
|
||||
) {
|
||||
assert!(
|
||||
@@ -337,14 +335,14 @@ impl TributaryDb {
|
||||
pub(crate) fn mark_cosigned(
|
||||
txn: &mut impl DbTxn,
|
||||
set: ExternalValidatorSet,
|
||||
substrate_block_hash: [u8; 32],
|
||||
substrate_block_hash: BlockHash,
|
||||
) {
|
||||
Cosigned::set(txn, set, substrate_block_hash, &());
|
||||
}
|
||||
pub(crate) fn cosigned(
|
||||
txn: &mut impl DbTxn,
|
||||
set: ExternalValidatorSet,
|
||||
substrate_block_hash: [u8; 32],
|
||||
substrate_block_hash: BlockHash,
|
||||
) -> bool {
|
||||
Cosigned::get(txn, set, substrate_block_hash).is_some()
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ use ciphersuite::group::GroupEncoding;
|
||||
use dkg::Participant;
|
||||
|
||||
use serai_primitives::{
|
||||
address::SeraiAddress,
|
||||
BlockHash,
|
||||
validator_sets::{ExternalValidatorSet, Slash},
|
||||
address::SeraiAddress,
|
||||
};
|
||||
|
||||
use serai_db::*;
|
||||
@@ -25,7 +26,7 @@ use tributary_sdk::{
|
||||
Transaction as TributaryTransaction, Block, TributaryReader, P2p,
|
||||
};
|
||||
|
||||
use serai_cosign::CosignIntent;
|
||||
use serai_cosign_types::CosignIntent;
|
||||
use serai_coordinator_substrate::NewSetInformation;
|
||||
|
||||
use messages::sign::{VariantSignId, SignId};
|
||||
@@ -79,7 +80,7 @@ impl CosignIntents {
|
||||
fn take(
|
||||
txn: &mut impl DbTxn,
|
||||
set: ExternalValidatorSet,
|
||||
substrate_block_hash: [u8; 32],
|
||||
substrate_block_hash: BlockHash,
|
||||
) -> Option<CosignIntent> {
|
||||
db::CosignIntents::take(txn, set, substrate_block_hash)
|
||||
}
|
||||
@@ -113,7 +114,7 @@ impl SubstrateBlockPlans {
|
||||
pub fn set(
|
||||
txn: &mut impl DbTxn,
|
||||
set: ExternalValidatorSet,
|
||||
substrate_block_hash: [u8; 32],
|
||||
substrate_block_hash: BlockHash,
|
||||
plans: &Vec<[u8; 32]>,
|
||||
) {
|
||||
db::SubstrateBlockPlans::set(txn, set, substrate_block_hash, plans);
|
||||
@@ -121,7 +122,7 @@ impl SubstrateBlockPlans {
|
||||
fn take(
|
||||
txn: &mut impl DbTxn,
|
||||
set: ExternalValidatorSet,
|
||||
substrate_block_hash: [u8; 32],
|
||||
substrate_block_hash: BlockHash,
|
||||
) -> Option<Vec<[u8; 32]>> {
|
||||
db::SubstrateBlockPlans::take(txn, set, substrate_block_hash)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use schnorr::SchnorrSignature;
|
||||
|
||||
use borsh::{BorshSerialize, BorshDeserialize};
|
||||
|
||||
use serai_primitives::{addess::SeraiAddress, validator_sets::MAX_KEY_SHARES_PER_SET};
|
||||
use serai_primitives::{BlockHash, validator_sets::KeyShares, address::SeraiAddress};
|
||||
|
||||
use messages::sign::VariantSignId;
|
||||
|
||||
@@ -137,7 +137,7 @@ pub enum Transaction {
|
||||
/// be the one selected to be cosigned.
|
||||
Cosign {
|
||||
/// The hash of the Substrate block to cosign
|
||||
substrate_block_hash: [u8; 32],
|
||||
substrate_block_hash: BlockHash,
|
||||
},
|
||||
|
||||
/// Note an intended-to-be-cosigned Substrate block as cosigned
|
||||
@@ -175,7 +175,7 @@ pub enum Transaction {
|
||||
/// cosigning the block in question, it'd be safe to provide this and move on to the next cosign.
|
||||
Cosigned {
|
||||
/// The hash of the Substrate block which was cosigned
|
||||
substrate_block_hash: [u8; 32],
|
||||
substrate_block_hash: BlockHash,
|
||||
},
|
||||
|
||||
/// Acknowledge a Substrate block
|
||||
@@ -186,7 +186,7 @@ pub enum Transaction {
|
||||
/// resulting from its handling.
|
||||
SubstrateBlock {
|
||||
/// The hash of the Substrate block
|
||||
hash: [u8; 32],
|
||||
hash: BlockHash,
|
||||
},
|
||||
|
||||
/// Acknowledge a Batch
|
||||
@@ -250,11 +250,11 @@ impl TransactionTrait for Transaction {
|
||||
signed.to_tributary_signed(0),
|
||||
),
|
||||
Transaction::DkgConfirmationPreprocess { attempt, signed, .. } => TransactionKind::Signed(
|
||||
borsh::to_vec(b"DkgConfirmation".as_slice(), attempt).unwrap(),
|
||||
borsh::to_vec(&(b"DkgConfirmation".as_slice(), attempt)).unwrap(),
|
||||
signed.to_tributary_signed(0),
|
||||
),
|
||||
Transaction::DkgConfirmationShare { attempt, signed, .. } => TransactionKind::Signed(
|
||||
borsh::to_vec(b"DkgConfirmation".as_slice(), attempt).unwrap(),
|
||||
borsh::to_vec(&(b"DkgConfirmation".as_slice(), attempt)).unwrap(),
|
||||
signed.to_tributary_signed(1),
|
||||
),
|
||||
|
||||
@@ -264,7 +264,7 @@ impl TransactionTrait for Transaction {
|
||||
Transaction::Batch { .. } => TransactionKind::Provided("Batch"),
|
||||
|
||||
Transaction::Sign { id, attempt, round, signed, .. } => TransactionKind::Signed(
|
||||
borsh::to_vec(b"Sign".as_slice(), id, attempt).unwrap(),
|
||||
borsh::to_vec(&(b"Sign".as_slice(), id, attempt)).unwrap(),
|
||||
signed.to_tributary_signed(round.nonce()),
|
||||
),
|
||||
|
||||
@@ -303,14 +303,14 @@ impl TransactionTrait for Transaction {
|
||||
Transaction::Batch { .. } => {}
|
||||
|
||||
Transaction::Sign { data, .. } => {
|
||||
if data.len() > usize::from(MAX_KEY_SHARES_PER_SET) {
|
||||
if data.len() > usize::from(KeyShares::MAX_PER_SET) {
|
||||
Err(TransactionError::InvalidContent)?
|
||||
}
|
||||
// TODO: MAX_SIGN_LEN
|
||||
}
|
||||
|
||||
Transaction::SlashReport { slash_points, .. } => {
|
||||
if slash_points.len() > usize::from(MAX_KEY_SHARES_PER_SET) {
|
||||
if slash_points.len() > usize::from(KeyShares::MAX_PER_SET) {
|
||||
Err(TransactionError::InvalidContent)?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ rand_core = { version = "0.6", default-features = false }
|
||||
sha2 = { version = "0.11.0-rc.2", default-features = false, features = ["zeroize"] }
|
||||
blake2 = { version = "0.11.0-rc.2", default-features = false, features = ["zeroize"] }
|
||||
|
||||
crypto-bigint = { version = "0.5", default-features = false }
|
||||
prime-field = { path = "../prime-field", default-features = false }
|
||||
ciphersuite = { version = "0.4.2", path = "../ciphersuite", default-features = false }
|
||||
|
||||
|
||||
@@ -286,21 +286,3 @@ prime_field::odd_prime_field_with_specific_repr!(
|
||||
false,
|
||||
crate::ThirtyTwoArray
|
||||
);
|
||||
|
||||
impl FieldElement {
|
||||
/// This method is hidden as it's not part of our API commitment and has no guarantees made for
|
||||
/// it. It MAY panic for an undefined class of inputs.
|
||||
// TODO: `monero-oxide` requires this. PR `monero-oxide` to not require this.
|
||||
#[doc(hidden)]
|
||||
pub const fn from_u256(value: &crypto_bigint::U256) -> Self {
|
||||
let mut bytes = [0; 32];
|
||||
|
||||
let mut i = 0;
|
||||
while i < 256 {
|
||||
bytes[i / 32] |= (value.bit_vartime(i) as u8) << (i % 8);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
FieldElement::from_bytes(&bytes).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
28
patches/dalek-ff-group/Cargo.toml
Normal file
28
patches/dalek-ff-group/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "dalek-ff-group"
|
||||
version = "0.5.99"
|
||||
description = "ff/group bindings around curve25519-dalek"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dalek-ff-group"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["curve25519", "ed25519", "ristretto", "dalek", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.85"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false }
|
||||
|
||||
crypto-bigint-05 = { package = "crypto-bigint", version = "0.5", default-features = false, features = ["zeroize"] }
|
||||
crypto-bigint = { version = "0.6", default-features = false, features = ["zeroize"] }
|
||||
prime-field = { path = "../../crypto/prime-field", default-features = false }
|
||||
|
||||
[features]
|
||||
alloc = ["dalek-ff-group/alloc", "prime-field/alloc"]
|
||||
std = ["alloc", "dalek-ff-group/std", "prime-field/std"]
|
||||
default = ["std"]
|
||||
21
patches/dalek-ff-group/LICENSE
Normal file
21
patches/dalek-ff-group/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2025 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
4
patches/dalek-ff-group/README.md
Normal file
4
patches/dalek-ff-group/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Dalek FF/Group
|
||||
|
||||
Patch for the `crates.io` `dalek-ff-group` to use the in-tree `dalek-ff-group`,
|
||||
resolving relevant breaking changes made since.
|
||||
36
patches/dalek-ff-group/src/lib.rs
Normal file
36
patches/dalek-ff-group/src/lib.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
#![allow(deprecated)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![no_std] // Prevents writing new code, in what should be a simple wrapper, which requires std
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![allow(clippy::redundant_closure_call)]
|
||||
|
||||
pub use dalek_ff_group::{Scalar, EdwardsPoint, RistrettoPoint, Ed25519, Ristretto};
|
||||
|
||||
type ThirtyTwoArray = [u8; 32];
|
||||
prime_field::odd_prime_field_with_specific_repr!(
|
||||
FieldElement,
|
||||
"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed",
|
||||
"02",
|
||||
false,
|
||||
crate::ThirtyTwoArray
|
||||
);
|
||||
|
||||
impl FieldElement {
|
||||
/// Create a FieldElement from a `crypto_bigint::U256`.
|
||||
///
|
||||
/// This will reduce the `U256` by the modulus, into a member of the field.
|
||||
#[deprecated]
|
||||
pub const fn from_u256(u256: &crypto_bigint_05::U256) -> Self {
|
||||
const MODULUS: crypto_bigint::U256 = crypto_bigint::U256::from_be_hex(
|
||||
"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed",
|
||||
);
|
||||
let mut u256 = crypto_bigint::U256::from_words(*u256.as_words());
|
||||
loop {
|
||||
let result = FieldElement::from_bytes(&u256.to_le_bytes());
|
||||
if let Some(result) = result {
|
||||
return result;
|
||||
}
|
||||
u256 = u256.wrapping_sub(&MODULUS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub extern crate alloc;
|
||||
|
||||
@@ -83,6 +83,12 @@ impl Header {
|
||||
Header::V1(HeaderV1 { builds_upon, .. }) => *builds_upon,
|
||||
}
|
||||
}
|
||||
/// Get the UNIX time, in milliseconds since the epoch, for when this block was proposed.
|
||||
pub fn unix_time_in_millis(&self) -> u64 {
|
||||
match self {
|
||||
Header::V1(HeaderV1 { unix_time_in_millis, .. }) => *unix_time_in_millis,
|
||||
}
|
||||
}
|
||||
/// The commitment to the transactions within this block.
|
||||
pub fn transactions_commitment(&self) -> UnbalancedMerkleTree {
|
||||
match self {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
@@ -61,6 +61,37 @@ pub enum Call {
|
||||
InInstructions(in_instructions::Call) = 7,
|
||||
}
|
||||
|
||||
impl From<coins::Call> for Call {
|
||||
fn from(call: coins::Call) -> Self {
|
||||
Self::Coins(call)
|
||||
}
|
||||
}
|
||||
impl From<validator_sets::Call> for Call {
|
||||
fn from(call: validator_sets::Call) -> Self {
|
||||
Self::ValidatorSets(call)
|
||||
}
|
||||
}
|
||||
impl From<signals::Call> for Call {
|
||||
fn from(call: signals::Call) -> Self {
|
||||
Self::Signals(call)
|
||||
}
|
||||
}
|
||||
impl From<dex::Call> for Call {
|
||||
fn from(call: dex::Call) -> Self {
|
||||
Self::Dex(call)
|
||||
}
|
||||
}
|
||||
impl From<genesis_liquidity::Call> for Call {
|
||||
fn from(call: genesis_liquidity::Call) -> Self {
|
||||
Self::GenesisLiquidity(call)
|
||||
}
|
||||
}
|
||||
impl From<in_instructions::Call> for Call {
|
||||
fn from(call: in_instructions::Call) -> Self {
|
||||
Self::InInstructions(call)
|
||||
}
|
||||
}
|
||||
|
||||
impl Call {
|
||||
pub(crate) fn is_signed(&self) -> bool {
|
||||
match self {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "serai-client"
|
||||
version = "0.1.0"
|
||||
description = "Client library for the Serai network"
|
||||
description = "A client for Serai and its connected networks"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/client"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
@@ -17,58 +17,17 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
zeroize = "^1.5"
|
||||
thiserror = { version = "2", default-features = false, optional = true }
|
||||
|
||||
bitvec = { version = "1", default-features = false, features = ["alloc", "serde"] }
|
||||
|
||||
hex = "0.4"
|
||||
scale = { package = "parity-scale-codec", version = "3", optional = true }
|
||||
borsh = { version = "1", features = ["derive"] }
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
|
||||
serai-abi = { path = "../abi", version = "0.1" }
|
||||
|
||||
multiaddr = { version = "0.18", optional = true }
|
||||
sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", optional = true }
|
||||
sp-runtime = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", optional = true }
|
||||
frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", optional = true }
|
||||
|
||||
async-lock = "3"
|
||||
|
||||
simple-request = { path = "../../common/request", version = "0.3", optional = true }
|
||||
serai-client-serai = { path = "./serai", optional = true }
|
||||
|
||||
serai-client-bitcoin = { path = "./bitcoin", optional = true }
|
||||
serai-client-ethereum = { path = "./ethereum", optional = true }
|
||||
serai-client-monero = { path = "./monero", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = "0.6"
|
||||
hex = "0.4"
|
||||
|
||||
blake2 = "0.11.0-rc.0"
|
||||
|
||||
ciphersuite = { path = "../../crypto/ciphersuite" }
|
||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group" }
|
||||
ciphersuite-kp256 = { path = "../../crypto/ciphersuite/kp256" }
|
||||
dkg-musig = { path = "../../crypto/dkg/musig" }
|
||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
|
||||
schnorrkel = { path = "../../crypto/schnorrkel", package = "frost-schnorrkel" }
|
||||
|
||||
tokio = "1"
|
||||
|
||||
dockertest = "0.5"
|
||||
serai-docker-tests = { path = "../../tests/docker" }
|
||||
|
||||
[features]
|
||||
serai = ["thiserror/std", "scale", "serde", "serde_json", "multiaddr", "sp-core", "sp-runtime", "frame-system", "simple-request"]
|
||||
serai = ["serai-client-serai"]
|
||||
|
||||
networks = []
|
||||
bitcoin = ["networks", "serai-client-bitcoin"]
|
||||
ethereum = ["networks", "serai-client-ethereum"]
|
||||
monero = ["networks", "serai-client-monero"]
|
||||
bitcoin = ["serai-client-bitcoin"]
|
||||
ethereum = ["serai-client-ethereum"]
|
||||
monero = ["serai-client-monero"]
|
||||
|
||||
# Assumes the default usage is to use Serai as a DEX, which doesn't actually
|
||||
# require connecting to a Serai node
|
||||
default = ["bitcoin", "ethereum", "monero"]
|
||||
default = ["serai", "bitcoin", "ethereum", "monero"]
|
||||
|
||||
4
substrate/client/README.md
Normal file
4
substrate/client/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# serai-client
|
||||
|
||||
This crate is an umbrella crate for each of Serai's clients (the network itself
|
||||
and its connected networks).
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ simple-request = { path = "../../../common/request", version = "0.3" }
|
||||
|
||||
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
||||
borsh = { version = "1", default-features = false, features = ["std"] }
|
||||
serai-abi = { path = "../../abi", version = "0.1" }
|
||||
|
||||
bitvec = { version = "1", default-features = false, features = ["alloc", "std"] }
|
||||
serai-abi = { path = "../../abi", version = "0.1", default-features = false, features = ["std"] }
|
||||
|
||||
async-lock = "3"
|
||||
|
||||
|
||||
52
substrate/client/serai/src/in_instructions.rs
Normal file
52
substrate/client/serai/src/in_instructions.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
pub use serai_abi::{
|
||||
primitives::instructions::SignedBatch,
|
||||
in_instructions::{Call, Event},
|
||||
UnsignedCall, Transaction,
|
||||
};
|
||||
|
||||
use crate::{RpcError, TemporalSerai};
|
||||
|
||||
/// A `TemporalSerai` scoped to the in instructions module.
|
||||
#[derive(Clone)]
|
||||
pub struct InInstructions<'serai>(pub(super) &'serai TemporalSerai<'serai>);
|
||||
|
||||
impl<'serai> InInstructions<'serai> {
|
||||
/// The events from the in instructions module.
|
||||
pub async fn events(&self) -> Result<Vec<Event>, RpcError> {
|
||||
Ok(
|
||||
self
|
||||
.0
|
||||
.events_borrowed()
|
||||
.await?
|
||||
.as_ref()
|
||||
.expect("`TemporalSerai::events` returned None")
|
||||
.iter()
|
||||
.flat_map(IntoIterator::into_iter)
|
||||
.filter_map(|event| match event {
|
||||
serai_abi::Event::InInstructions(event) => Some(event.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The `Batch` events from the in instructions module.
|
||||
pub async fn batch_events(&self) -> Result<Vec<Event>, RpcError> {
|
||||
Ok(
|
||||
self
|
||||
.events()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|event| matches!(event, Event::Batch { .. }))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a transaction to execute a batch.
|
||||
pub fn execute_batch(batch: SignedBatch) -> Transaction {
|
||||
Transaction::Unsigned {
|
||||
call: UnsignedCall::try_from(serai_abi::Call::from(Call::execute_batch { batch }))
|
||||
.expect("`execute_batch` wasn't an unsigned call?"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
@@ -14,18 +14,19 @@ use borsh::BorshDeserialize;
|
||||
pub use serai_abi as abi;
|
||||
use abi::{
|
||||
primitives::{BlockHash, network_id::ExternalNetworkId},
|
||||
Block, Event,
|
||||
Transaction, Block, Event,
|
||||
};
|
||||
|
||||
use async_lock::RwLock;
|
||||
|
||||
/// RPC client functionality for the coins module.
|
||||
pub mod coins;
|
||||
use coins::*;
|
||||
mod coins;
|
||||
pub use coins::Coins;
|
||||
|
||||
/// RPC client functionality for the validator sets module.
|
||||
pub mod validator_sets;
|
||||
use validator_sets::*;
|
||||
mod validator_sets;
|
||||
pub use validator_sets::ValidatorSets;
|
||||
|
||||
mod in_instructions;
|
||||
pub use in_instructions::InInstructions;
|
||||
|
||||
/// An error from the RPC.
|
||||
#[derive(Debug, Error)]
|
||||
@@ -60,8 +61,8 @@ pub struct Serai {
|
||||
/// from this block will be cached within this. This allows future calls for events to be done
|
||||
/// cheaply.
|
||||
#[derive(Clone)]
|
||||
pub struct TemporalSerai<'a> {
|
||||
serai: &'a Serai,
|
||||
pub struct TemporalSerai<'serai> {
|
||||
serai: &'serai Serai,
|
||||
block: BlockHash,
|
||||
events: Arc<RwLock<Option<Vec<Vec<Event>>>>>,
|
||||
}
|
||||
@@ -164,17 +165,40 @@ impl Serai {
|
||||
Self::block_internal(self.call("blockchain/block", &format!(r#"{{ "block": {block} }}"#))).await
|
||||
}
|
||||
|
||||
/// Publish a transaction onto the Serai blockchain.
|
||||
pub async fn publish_transaction(&self, transaction: &Transaction) -> Result<(), RpcError> {
|
||||
self
|
||||
.call(
|
||||
"blockchain/publish_transaction",
|
||||
&format!(r#"{{ "transaction": {} }}"#, hex::encode(borsh::to_vec(transaction).unwrap())),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Scope this RPC client to the state as of a specific block.
|
||||
///
|
||||
/// This will yield an error if the block chosen isn't finalized. This ensures, given an honest
|
||||
/// node, that this scope will be available for the lifetime of this object.
|
||||
pub async fn as_of<'a>(&'a self, block: BlockHash) -> Result<TemporalSerai<'a>, RpcError> {
|
||||
pub async fn as_of(&self, block: BlockHash) -> Result<TemporalSerai<'_>, RpcError> {
|
||||
if !self.finalized(block).await? {
|
||||
Err(RpcError::NotFinalized)?;
|
||||
}
|
||||
Ok(TemporalSerai { serai: self, block, events: Arc::new(RwLock::new(None)) })
|
||||
}
|
||||
|
||||
/// Scope this RPC client to the state as of the latest finalized block.
|
||||
pub async fn as_of_latest_finalized_block(&self) -> Result<TemporalSerai<'_>, RpcError> {
|
||||
let block = self
|
||||
.block_by_number(self.latest_finalized_block_number().await?)
|
||||
.await?
|
||||
.ok_or_else(|| RpcError::InvalidNode("couldn't fetch latest finalized block".to_string()))?;
|
||||
Ok(TemporalSerai {
|
||||
serai: self,
|
||||
block: block.header.hash(),
|
||||
events: Arc::new(RwLock::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the P2P addresses for the validators of the specified network.
|
||||
pub async fn p2p_validators(&self, network: ExternalNetworkId) -> Result<Vec<String>, RpcError> {
|
||||
self
|
||||
@@ -191,7 +215,7 @@ impl Serai {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TemporalSerai<'a> {
|
||||
impl<'serai> TemporalSerai<'serai> {
|
||||
async fn call<ResponseValue: Default + JsonDeserialize>(
|
||||
&self,
|
||||
method: &str,
|
||||
@@ -258,4 +282,9 @@ impl<'a> TemporalSerai<'a> {
|
||||
pub fn validator_sets(&self) -> ValidatorSets<'_> {
|
||||
ValidatorSets(self)
|
||||
}
|
||||
|
||||
/// Scope to the in instructions module.
|
||||
pub fn in_instructions(&self) -> InInstructions<'_> {
|
||||
InInstructions(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use core::str::FromStr;
|
||||
|
||||
use borsh::BorshDeserialize;
|
||||
|
||||
pub use serai_abi::{
|
||||
primitives::{
|
||||
crypto::KeyPair,
|
||||
crypto::{Signature, KeyPair, EmbeddedEllipticCurveKeys},
|
||||
network_id::{ExternalNetworkId, NetworkId},
|
||||
validator_sets::{Session, ExternalValidatorSet, ValidatorSet},
|
||||
validator_sets::{Session, ExternalValidatorSet, ValidatorSet, SlashReport},
|
||||
balance::Amount,
|
||||
address::SeraiAddress,
|
||||
},
|
||||
validator_sets::Event,
|
||||
validator_sets::{Call, Event},
|
||||
UnsignedCall, Transaction,
|
||||
};
|
||||
|
||||
use crate::{RpcError, TemporalSerai};
|
||||
@@ -24,9 +28,9 @@ fn rpc_network(network: impl Into<NetworkId>) -> Result<&'static str, RpcError>
|
||||
|
||||
/// A `TemporalSerai` scoped to the validator sets module.
|
||||
#[derive(Clone)]
|
||||
pub struct ValidatorSets<'a>(pub(super) &'a TemporalSerai<'a>);
|
||||
pub struct ValidatorSets<'serai>(pub(super) &'serai TemporalSerai<'serai>);
|
||||
|
||||
impl<'a> ValidatorSets<'a> {
|
||||
impl<'serai> ValidatorSets<'serai> {
|
||||
/// The events from the validator sets module.
|
||||
pub async fn events(&self) -> Result<Vec<Event>, RpcError> {
|
||||
Ok(
|
||||
@@ -108,7 +112,7 @@ impl<'a> ValidatorSets<'a> {
|
||||
)
|
||||
}
|
||||
|
||||
/// The stake for the current validators for specified network.
|
||||
/// The stake for the current validators for the specified network.
|
||||
pub async fn current_stake(&self, network: NetworkId) -> Result<Option<Amount>, RpcError> {
|
||||
Ok(
|
||||
self
|
||||
@@ -142,4 +146,104 @@ impl<'a> ValidatorSets<'a> {
|
||||
.map(Some)
|
||||
.map_err(|_| RpcError::InvalidNode("validator set's keys weren't a valid key pair".to_string()))
|
||||
}
|
||||
|
||||
/// The current validators for the specified network.
|
||||
pub async fn current_validators(
|
||||
&self,
|
||||
network: NetworkId,
|
||||
) -> Result<Option<Vec<SeraiAddress>>, RpcError> {
|
||||
self
|
||||
.0
|
||||
.call::<Option<Vec<String>>>(
|
||||
"validator-sets/current_validators",
|
||||
&format!(r#", "network": {} "#, rpc_network(network)?),
|
||||
)
|
||||
.await?
|
||||
.map(|validators| {
|
||||
validators
|
||||
.into_iter()
|
||||
.map(|addr| {
|
||||
SeraiAddress::from_str(&addr)
|
||||
.map_err(|_| RpcError::InvalidNode("validator's address was invalid".to_string()))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// If the prior validators for this network is still expected to publish a slash report.
|
||||
pub async fn pending_slash_report(&self, network: ExternalNetworkId) -> Result<bool, RpcError> {
|
||||
self
|
||||
.0
|
||||
.call(
|
||||
"validator-sets/pending_slash_report",
|
||||
&format!(r#", "network": {} "#, rpc_network(network)?),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// The key on an embedded elliptic curve for the specified validator.
|
||||
pub async fn embedded_elliptic_curve_keys(
|
||||
&self,
|
||||
validator: SeraiAddress,
|
||||
network: ExternalNetworkId,
|
||||
) -> Result<Option<EmbeddedEllipticCurveKeys>, RpcError> {
|
||||
let Some(keys) = self
|
||||
.0
|
||||
.call::<Option<String>>(
|
||||
"validator-sets/embedded_elliptic_curve_keys",
|
||||
&format!(r#", "validator": {validator}, "network": {} "#, rpc_network(network)?),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
EmbeddedEllipticCurveKeys::deserialize(
|
||||
&mut hex::decode(keys)
|
||||
.map_err(|_| {
|
||||
RpcError::InvalidNode(
|
||||
"validator's embedded elliptic curve keys weren't valid hex".to_string(),
|
||||
)
|
||||
})?
|
||||
.as_slice(),
|
||||
)
|
||||
.map(Some)
|
||||
.map_err(|_| {
|
||||
RpcError::InvalidNode("validator's embedded elliptic curve keys weren't valid".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a transaction to set a validator set's keys.
|
||||
pub fn set_keys(
|
||||
network: ExternalNetworkId,
|
||||
key_pair: KeyPair,
|
||||
signature_participants: bitvec::vec::BitVec<u8, bitvec::order::Lsb0>,
|
||||
signature: Signature,
|
||||
) -> Transaction {
|
||||
Transaction::Unsigned {
|
||||
call: UnsignedCall::try_from(serai_abi::Call::from(Call::set_keys {
|
||||
network,
|
||||
key_pair,
|
||||
signature_participants,
|
||||
signature,
|
||||
}))
|
||||
.expect("`set_keys` wasn't an unsigned call?"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a transaction to report the slashes for a validator set.
|
||||
pub fn report_slashes(
|
||||
network: ExternalNetworkId,
|
||||
slashes: SlashReport,
|
||||
signature: Signature,
|
||||
) -> Transaction {
|
||||
Transaction::Unsigned {
|
||||
call: UnsignedCall::try_from(serai_abi::Call::from(Call::report_slashes {
|
||||
network,
|
||||
slashes,
|
||||
signature,
|
||||
}))
|
||||
.expect("`report_slashes` wasn't an unsigned call?"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
#[cfg(feature = "bitcoin")]
|
||||
pub use serai_client_bitcoin as bitcoin;
|
||||
#[cfg(feature = "ethereum")]
|
||||
pub mod serai_client_ethereum as ethereum;
|
||||
#[cfg(feature = "monero")]
|
||||
pub mod serai_client_monero as monero;
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
/// The client for the Serai network itself.
|
||||
#[cfg(feature = "serai")]
|
||||
pub use serai_client_serai as serai;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
/// The client for the Bitcoin integration.
|
||||
#[cfg(feature = "bitcoin")]
|
||||
pub use serai_client_bitcoin as bitcoin;
|
||||
/// The client for the Ethereum integration.
|
||||
#[cfg(feature = "ethereum")]
|
||||
pub use serai_client_ethereum as ethereum;
|
||||
/// The client for the Monero integration.
|
||||
#[cfg(feature = "monero")]
|
||||
pub use serai_client_monero as monero;
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
use scale::Encode;
|
||||
|
||||
use serai_abi::primitives::{SeraiAddress, Amount, Coin, Balance};
|
||||
pub use serai_abi::coins::primitives;
|
||||
use primitives::OutInstructionWithBalance;
|
||||
|
||||
use crate::{TemporalSerai, SeraiError};
|
||||
|
||||
const PALLET: &str = "Coins";
|
||||
|
||||
pub type CoinsEvent = serai_abi::coins::Event;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SeraiCoins<'a>(pub(crate) &'a TemporalSerai<'a>);
|
||||
impl SeraiCoins<'_> {
|
||||
pub async fn mint_events(&self) -> Result<Vec<CoinsEvent>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.events(|event| {
|
||||
if let serai_abi::Event::Coins(event) = event {
|
||||
if matches!(event, CoinsEvent::Mint { .. }) {
|
||||
Some(event.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn burn_with_instruction_events(&self) -> Result<Vec<CoinsEvent>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.events(|event| {
|
||||
if let serai_abi::Event::Coins(event) = event {
|
||||
if matches!(event, CoinsEvent::BurnWithInstruction { .. }) {
|
||||
Some(event.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn coin_supply(&self, coin: Coin) -> Result<Amount, SeraiError> {
|
||||
Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(Amount(0)))
|
||||
}
|
||||
|
||||
pub async fn coin_balance(
|
||||
&self,
|
||||
coin: Coin,
|
||||
address: SeraiAddress,
|
||||
) -> Result<Amount, SeraiError> {
|
||||
Ok(
|
||||
self
|
||||
.0
|
||||
.storage(
|
||||
PALLET,
|
||||
"Balances",
|
||||
(sp_core::hashing::blake2_128(&address.encode()), &address.0, coin),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(Amount(0)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn transfer(to: SeraiAddress, balance: Balance) -> serai_abi::Call {
|
||||
serai_abi::Call::Coins(serai_abi::coins::Call::transfer { to, balance })
|
||||
}
|
||||
|
||||
pub fn burn(balance: Balance) -> serai_abi::Call {
|
||||
serai_abi::Call::Coins(serai_abi::coins::Call::burn { balance })
|
||||
}
|
||||
|
||||
pub fn burn_with_instruction(instruction: OutInstructionWithBalance) -> serai_abi::Call {
|
||||
serai_abi::Call::Coins(serai_abi::coins::Call::burn_with_instruction { instruction })
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
use sp_core::bounded::BoundedVec;
|
||||
use serai_abi::primitives::{Amount, Coin, ExternalCoin, SeraiAddress};
|
||||
|
||||
use crate::{SeraiError, TemporalSerai};
|
||||
|
||||
pub type DexEvent = serai_abi::dex::Event;
|
||||
|
||||
const PALLET: &str = "Dex";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SeraiDex<'a>(pub(crate) &'a TemporalSerai<'a>);
|
||||
impl SeraiDex<'_> {
|
||||
pub async fn events(&self) -> Result<Vec<DexEvent>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.events(
|
||||
|event| if let serai_abi::Event::Dex(event) = event { Some(event.clone()) } else { None },
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn add_liquidity(
|
||||
coin: ExternalCoin,
|
||||
coin_amount: Amount,
|
||||
sri_amount: Amount,
|
||||
min_coin_amount: Amount,
|
||||
min_sri_amount: Amount,
|
||||
address: SeraiAddress,
|
||||
) -> serai_abi::Call {
|
||||
serai_abi::Call::Dex(serai_abi::dex::Call::add_liquidity {
|
||||
coin,
|
||||
coin_desired: coin_amount.0,
|
||||
sri_desired: sri_amount.0,
|
||||
coin_min: min_coin_amount.0,
|
||||
sri_min: min_sri_amount.0,
|
||||
mint_to: address,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn swap(
|
||||
from_coin: Coin,
|
||||
to_coin: Coin,
|
||||
amount_in: Amount,
|
||||
amount_out_min: Amount,
|
||||
address: SeraiAddress,
|
||||
) -> serai_abi::Call {
|
||||
let path = if to_coin.is_native() {
|
||||
BoundedVec::try_from(vec![from_coin, Coin::Serai]).unwrap()
|
||||
} else if from_coin.is_native() {
|
||||
BoundedVec::try_from(vec![Coin::Serai, to_coin]).unwrap()
|
||||
} else {
|
||||
BoundedVec::try_from(vec![from_coin, Coin::Serai, to_coin]).unwrap()
|
||||
};
|
||||
|
||||
serai_abi::Call::Dex(serai_abi::dex::Call::swap_exact_tokens_for_tokens {
|
||||
path,
|
||||
amount_in: amount_in.0,
|
||||
amount_out_min: amount_out_min.0,
|
||||
send_to: address,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the reserves of `coin:SRI` pool.
|
||||
pub async fn get_reserves(
|
||||
&self,
|
||||
coin: ExternalCoin,
|
||||
) -> Result<Option<(Amount, Amount)>, SeraiError> {
|
||||
self.0.runtime_api("DexApi_get_reserves", (Coin::from(coin), Coin::Serai)).await
|
||||
}
|
||||
|
||||
pub async fn oracle_value(&self, coin: ExternalCoin) -> Result<Option<Amount>, SeraiError> {
|
||||
self.0.storage(PALLET, "SecurityOracleValue", coin).await
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
pub use serai_abi::genesis_liquidity::primitives;
|
||||
use primitives::{Values, LiquidityAmount};
|
||||
|
||||
use serai_abi::primitives::*;
|
||||
|
||||
use sp_core::sr25519::Signature;
|
||||
|
||||
use scale::Encode;
|
||||
|
||||
use crate::{Serai, SeraiError, TemporalSerai, Transaction};
|
||||
|
||||
pub type GenesisLiquidityEvent = serai_abi::genesis_liquidity::Event;
|
||||
|
||||
const PALLET: &str = "GenesisLiquidity";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SeraiGenesisLiquidity<'a>(pub(crate) &'a TemporalSerai<'a>);
|
||||
impl SeraiGenesisLiquidity<'_> {
|
||||
pub async fn events(&self) -> Result<Vec<GenesisLiquidityEvent>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.events(|event| {
|
||||
if let serai_abi::Event::GenesisLiquidity(event) = event {
|
||||
Some(event.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn oraclize_values(values: Values, signature: Signature) -> Transaction {
|
||||
Serai::unsigned(serai_abi::Call::GenesisLiquidity(
|
||||
serai_abi::genesis_liquidity::Call::oraclize_values { values, signature },
|
||||
))
|
||||
}
|
||||
|
||||
pub fn remove_coin_liquidity(balance: ExternalBalance) -> serai_abi::Call {
|
||||
serai_abi::Call::GenesisLiquidity(serai_abi::genesis_liquidity::Call::remove_coin_liquidity {
|
||||
balance,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn liquidity(
|
||||
&self,
|
||||
address: &SeraiAddress,
|
||||
coin: ExternalCoin,
|
||||
) -> Result<LiquidityAmount, SeraiError> {
|
||||
Ok(
|
||||
self
|
||||
.0
|
||||
.storage(
|
||||
PALLET,
|
||||
"Liquidity",
|
||||
(coin, sp_core::hashing::blake2_128(&address.encode()), &address.0),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(LiquidityAmount::zero()),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn supply(&self, coin: ExternalCoin) -> Result<LiquidityAmount, SeraiError> {
|
||||
Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(LiquidityAmount::zero()))
|
||||
}
|
||||
|
||||
pub async fn genesis_complete_block(&self) -> Result<Option<u64>, SeraiError> {
|
||||
self.0.storage(PALLET, "GenesisCompleteBlock", ()).await
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
pub use serai_abi::in_instructions::primitives;
|
||||
use primitives::SignedBatch;
|
||||
|
||||
use crate::{primitives::ExternalNetworkId, Transaction, SeraiError, Serai, TemporalSerai};
|
||||
|
||||
pub type InInstructionsEvent = serai_abi::in_instructions::Event;
|
||||
|
||||
const PALLET: &str = "InInstructions";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SeraiInInstructions<'a>(pub(crate) &'a TemporalSerai<'a>);
|
||||
impl SeraiInInstructions<'_> {
|
||||
pub async fn last_batch_for_network(
|
||||
&self,
|
||||
network: ExternalNetworkId,
|
||||
) -> Result<Option<u32>, SeraiError> {
|
||||
self.0.storage(PALLET, "LastBatch", network).await
|
||||
}
|
||||
|
||||
pub async fn batch_events(&self) -> Result<Vec<InInstructionsEvent>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.events(|event| {
|
||||
if let serai_abi::Event::InInstructions(event) = event {
|
||||
if matches!(event, InInstructionsEvent::Batch { .. }) {
|
||||
Some(event.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn execute_batch(batch: SignedBatch) -> Transaction {
|
||||
Serai::unsigned(serai_abi::Call::InInstructions(
|
||||
serai_abi::in_instructions::Call::execute_batch { batch },
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
use scale::Encode;
|
||||
|
||||
use serai_abi::primitives::{Amount, ExternalBalance, ExternalCoin, SeraiAddress};
|
||||
|
||||
use crate::{TemporalSerai, SeraiError};
|
||||
|
||||
const PALLET: &str = "LiquidityTokens";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SeraiLiquidityTokens<'a>(pub(crate) &'a TemporalSerai<'a>);
|
||||
impl SeraiLiquidityTokens<'_> {
|
||||
pub async fn token_supply(&self, coin: ExternalCoin) -> Result<Amount, SeraiError> {
|
||||
Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(Amount(0)))
|
||||
}
|
||||
|
||||
pub async fn token_balance(
|
||||
&self,
|
||||
coin: ExternalCoin,
|
||||
address: SeraiAddress,
|
||||
) -> Result<Amount, SeraiError> {
|
||||
Ok(
|
||||
self
|
||||
.0
|
||||
.storage(
|
||||
PALLET,
|
||||
"Balances",
|
||||
(sp_core::hashing::blake2_128(&address.encode()), &address.0, coin),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(Amount(0)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn transfer(to: SeraiAddress, balance: ExternalBalance) -> serai_abi::Call {
|
||||
serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::transfer {
|
||||
to,
|
||||
balance: balance.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn burn(balance: ExternalBalance) -> serai_abi::Call {
|
||||
serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::burn {
|
||||
balance: balance.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use async_lock::RwLock;
|
||||
use simple_request::{hyper, Request, Client};
|
||||
|
||||
use scale::{Decode, Encode};
|
||||
use serde::{Serialize, Deserialize, de::DeserializeOwned};
|
||||
|
||||
pub use sp_core::{
|
||||
Pair as PairTrait,
|
||||
sr25519::{Public, Pair},
|
||||
};
|
||||
|
||||
pub use serai_abi as abi;
|
||||
pub use abi::{primitives, Transaction};
|
||||
use abi::*;
|
||||
|
||||
pub use primitives::{SeraiAddress, Signature, Amount};
|
||||
use primitives::{Header, ExternalNetworkId, QuotePriceParams};
|
||||
use crate::in_instructions::primitives::Shorthand;
|
||||
|
||||
pub mod coins;
|
||||
pub use coins::SeraiCoins;
|
||||
pub mod dex;
|
||||
pub use dex::SeraiDex;
|
||||
pub mod in_instructions;
|
||||
pub use in_instructions::SeraiInInstructions;
|
||||
pub mod validator_sets;
|
||||
pub use validator_sets::SeraiValidatorSets;
|
||||
pub mod genesis_liquidity;
|
||||
pub use genesis_liquidity::SeraiGenesisLiquidity;
|
||||
pub mod liquidity_tokens;
|
||||
pub use liquidity_tokens::SeraiLiquidityTokens;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode)]
|
||||
pub struct Block {
|
||||
pub header: Header,
|
||||
pub transactions: Vec<Transaction>,
|
||||
}
|
||||
impl Block {
|
||||
pub fn hash(&self) -> [u8; 32] {
|
||||
self.header.hash().into()
|
||||
}
|
||||
pub fn number(&self) -> u64 {
|
||||
self.header.number
|
||||
}
|
||||
|
||||
/// Returns the time of this block, set by its producer, in milliseconds since the epoch.
|
||||
pub fn time(&self) -> Option<u64> {
|
||||
for transaction in &self.transactions {
|
||||
if let Call::Timestamp(timestamp::Call::set { now }) = transaction.call() {
|
||||
return Some(*now);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SeraiError {
|
||||
#[error("failed to communicate with serai")]
|
||||
ConnectionError,
|
||||
#[error("node is faulty: {0}")]
|
||||
InvalidNode(String),
|
||||
#[error("error in response: {0}")]
|
||||
ErrorInResponse(String),
|
||||
#[error("serai-client library was intended for a different runtime version: {0}")]
|
||||
InvalidRuntime(String),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Serai {
|
||||
url: String,
|
||||
client: Client,
|
||||
genesis: [u8; 32],
|
||||
}
|
||||
|
||||
type EventsInBlock = Vec<frame_system::EventRecord<Event, [u8; 32]>>;
|
||||
pub struct TemporalSerai<'a> {
|
||||
serai: &'a Serai,
|
||||
block: [u8; 32],
|
||||
events: RwLock<Option<EventsInBlock>>,
|
||||
}
|
||||
impl Clone for TemporalSerai<'_> {
|
||||
fn clone(&self) -> Self {
|
||||
Self { serai: self.serai, block: self.block, events: RwLock::new(None) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Serai {
|
||||
pub async fn call<Req: Serialize, Res: DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: Req,
|
||||
) -> Result<Res, SeraiError> {
|
||||
let request = Request::from(
|
||||
hyper::Request::post(&self.url)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(
|
||||
serde_json::to_vec(
|
||||
&serde_json::json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params }),
|
||||
)
|
||||
.unwrap()
|
||||
.into(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Error {
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RpcResponse<T> {
|
||||
Ok { result: T },
|
||||
Err { error: Error },
|
||||
}
|
||||
|
||||
let mut res = self
|
||||
.client
|
||||
.request(request)
|
||||
.await
|
||||
.map_err(|_| SeraiError::ConnectionError)?
|
||||
.body()
|
||||
.await
|
||||
.map_err(|_| SeraiError::ConnectionError)?;
|
||||
|
||||
let res: RpcResponse<Res> = serde_json::from_reader(&mut res).map_err(|e| {
|
||||
SeraiError::InvalidRuntime(format!(
|
||||
"response was a different type than expected: {:?}",
|
||||
e.classify()
|
||||
))
|
||||
})?;
|
||||
match res {
|
||||
RpcResponse::Ok { result } => Ok(result),
|
||||
RpcResponse::Err { error } => Err(SeraiError::ErrorInResponse(error.message)),
|
||||
}
|
||||
}
|
||||
|
||||
fn hex_decode(str: String) -> Result<Vec<u8>, SeraiError> {
|
||||
(if let Some(stripped) = str.strip_prefix("0x") {
|
||||
hex::decode(stripped)
|
||||
} else {
|
||||
hex::decode(str)
|
||||
})
|
||||
.map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string()))
|
||||
}
|
||||
|
||||
pub async fn block_hash(&self, number: u64) -> Result<Option<[u8; 32]>, SeraiError> {
|
||||
let hash: Option<String> = self.call("chain_getBlockHash", [number]).await?;
|
||||
let Some(hash) = hash else { return Ok(None) };
|
||||
Self::hex_decode(hash)?
|
||||
.try_into()
|
||||
.map_err(|_| SeraiError::InvalidNode("didn't respond to getBlockHash with hash".to_string()))
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
pub async fn new(url: String) -> Result<Self, SeraiError> {
|
||||
let client = Client::with_connection_pool().map_err(|_| SeraiError::ConnectionError)?;
|
||||
let mut res = Serai { url, client, genesis: [0xfe; 32] };
|
||||
res.genesis = res.block_hash(0).await?.ok_or_else(|| {
|
||||
SeraiError::InvalidNode("node didn't have the first block's hash".to_string())
|
||||
})?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn unsigned(call: Call) -> Transaction {
|
||||
Transaction::new(call, None)
|
||||
}
|
||||
|
||||
pub fn sign(&self, signer: &Pair, call: Call, nonce: u32, tip: u64) -> Transaction {
|
||||
const SPEC_VERSION: u32 = 1;
|
||||
const TX_VERSION: u32 = 1;
|
||||
|
||||
let extra = Extra { era: sp_runtime::generic::Era::Immortal, nonce, tip };
|
||||
let signature_payload = (
|
||||
&call,
|
||||
&extra,
|
||||
SignedPayloadExtra {
|
||||
spec_version: SPEC_VERSION,
|
||||
tx_version: TX_VERSION,
|
||||
genesis: self.genesis,
|
||||
mortality_checkpoint: self.genesis,
|
||||
},
|
||||
)
|
||||
.encode();
|
||||
let signature = signer.sign(&signature_payload);
|
||||
|
||||
Transaction::new(call, Some((signer.public().into(), signature, extra)))
|
||||
}
|
||||
|
||||
pub async fn publish(&self, tx: &Transaction) -> Result<(), SeraiError> {
|
||||
// Drop the returned hash, which is the hash of the raw extrinsic, as extrinsics are allowed
|
||||
// to share hashes and this hash is accordingly useless/unsafe
|
||||
// If we are to return something, it should be block included in and position within block
|
||||
let _: String = self.call("author_submitExtrinsic", [hex::encode(tx.encode())]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn latest_finalized_block_hash(&self) -> Result<[u8; 32], SeraiError> {
|
||||
let hash: String = self.call("chain_getFinalizedHead", ()).await?;
|
||||
Self::hex_decode(hash)?.try_into().map_err(|_| {
|
||||
SeraiError::InvalidNode("didn't respond to getFinalizedHead with hash".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn header(&self, hash: [u8; 32]) -> Result<Option<Header>, SeraiError> {
|
||||
self.call("chain_getHeader", [hex::encode(hash)]).await
|
||||
}
|
||||
|
||||
pub async fn block(&self, hash: [u8; 32]) -> Result<Option<Block>, SeraiError> {
|
||||
let block: Option<String> = self.call("chain_getBlockBin", [hex::encode(hash)]).await?;
|
||||
let Some(block) = block else { return Ok(None) };
|
||||
let Ok(bytes) = Self::hex_decode(block) else {
|
||||
Err(SeraiError::InvalidNode("didn't return a hex-encoded block".to_string()))?
|
||||
};
|
||||
let Ok(block) = Block::decode(&mut bytes.as_slice()) else {
|
||||
Err(SeraiError::InvalidNode("didn't return a block".to_string()))?
|
||||
};
|
||||
Ok(Some(block))
|
||||
}
|
||||
|
||||
pub async fn latest_finalized_block(&self) -> Result<Block, SeraiError> {
|
||||
let latest = self.latest_finalized_block_hash().await?;
|
||||
let Some(block) = self.block(latest).await? else {
|
||||
Err(SeraiError::InvalidNode("node didn't have a latest block".to_string()))?
|
||||
};
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
// There is no provided method for this
|
||||
// TODO: Add one to Serai
|
||||
pub async fn is_finalized(&self, header: &Header) -> Result<bool, SeraiError> {
|
||||
// Get the latest finalized block
|
||||
let finalized = self.latest_finalized_block_hash().await?;
|
||||
// If the latest finalized block is this block, return true
|
||||
if finalized == header.hash().as_ref() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let Some(finalized) = self.header(finalized).await? else {
|
||||
Err(SeraiError::InvalidNode("couldn't get finalized header".to_string()))?
|
||||
};
|
||||
|
||||
// If the finalized block has a lower number, this block can't be finalized
|
||||
if finalized.number < header.number {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// This block, if finalized, comes before the finalized block
|
||||
// If we request the hash of this block's number, Substrate will return the hash on the main
|
||||
// chain
|
||||
// If that hash is this hash, this block is finalized
|
||||
let Some(hash) = self.block_hash(header.number).await? else {
|
||||
// This is an error since there is a finalized block at this index
|
||||
Err(SeraiError::InvalidNode(
|
||||
"couldn't get block hash for a block number below the finalized block".to_string(),
|
||||
))?
|
||||
};
|
||||
|
||||
Ok(header.hash().as_ref() == hash)
|
||||
}
|
||||
|
||||
pub async fn finalized_block_by_number(&self, number: u64) -> Result<Option<Block>, SeraiError> {
|
||||
let hash = self.block_hash(number).await?;
|
||||
let Some(hash) = hash else { return Ok(None) };
|
||||
let Some(block) = self.block(hash).await? else { return Ok(None) };
|
||||
if !self.is_finalized(&block.header).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(block))
|
||||
}
|
||||
|
||||
/*
|
||||
/// A stream which yields whenever new block(s) have been finalized.
|
||||
pub async fn newly_finalized_block(
|
||||
&self,
|
||||
) -> Result<impl Stream<Item = Result<(), SeraiError>>, SeraiError> {
|
||||
Ok(self.0.rpc().subscribe_finalized_block_headers().await
|
||||
.map_err(|_| SeraiError::ConnectionError)?.map(
|
||||
|next| {
|
||||
next.map_err(|_| SeraiError::ConnectionError)?;
|
||||
Ok(())
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn nonce(&self, address: &SeraiAddress) -> Result<u32, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.rpc()
|
||||
.system_account_next_index(&sp_core::sr25519::Public(address.0).to_string())
|
||||
.await
|
||||
.map_err(|_| SeraiError::ConnectionError)
|
||||
}
|
||||
*/
|
||||
|
||||
/// Create a TemporalSerai bound to whatever is currently the latest finalized block.
|
||||
///
|
||||
/// The binding occurs at time of call. This does not track the latest finalized block and update
|
||||
/// itself.
|
||||
pub async fn as_of_latest_finalized_block(&self) -> Result<TemporalSerai<'_>, SeraiError> {
|
||||
let latest = self.latest_finalized_block_hash().await?;
|
||||
Ok(TemporalSerai { serai: self, block: latest, events: RwLock::new(None) })
|
||||
}
|
||||
|
||||
/// Returns a TemporalSerai able to retrieve state as of the specified block.
|
||||
pub fn as_of(&self, block: [u8; 32]) -> TemporalSerai<'_> {
|
||||
TemporalSerai { serai: self, block, events: RwLock::new(None) }
|
||||
}
|
||||
|
||||
/// Return the P2P Multiaddrs for the validators of the specified network.
|
||||
pub async fn p2p_validators(
|
||||
&self,
|
||||
network: ExternalNetworkId,
|
||||
) -> Result<Vec<multiaddr::Multiaddr>, SeraiError> {
|
||||
self.call("p2p_validators", [network]).await
|
||||
}
|
||||
|
||||
// TODO: move this to SeraiValidatorSets?
|
||||
pub async fn external_network_address(
|
||||
&self,
|
||||
network: ExternalNetworkId,
|
||||
) -> Result<String, SeraiError> {
|
||||
self.call("external_network_address", [network]).await
|
||||
}
|
||||
|
||||
// TODO: move this to SeraiInInstructions?
|
||||
pub async fn encoded_shorthand(&self, shorthand: Shorthand) -> Result<Vec<u8>, SeraiError> {
|
||||
self.call("encoded_shorthand", shorthand).await
|
||||
}
|
||||
|
||||
// TODO: move this to SeraiDex?
|
||||
pub async fn quote_price(&self, params: QuotePriceParams) -> Result<u64, SeraiError> {
|
||||
self.call("quote_price", params).await
|
||||
}
|
||||
}
|
||||
|
||||
impl TemporalSerai<'_> {
|
||||
async fn events<E>(
|
||||
&self,
|
||||
filter_map: impl Fn(&Event) -> Option<E>,
|
||||
) -> Result<Vec<E>, SeraiError> {
|
||||
let mut events = self.events.read().await;
|
||||
if events.is_none() {
|
||||
drop(events);
|
||||
let mut events_write = self.events.write().await;
|
||||
if events_write.is_none() {
|
||||
*events_write = Some(self.storage("System", "Events", ()).await?.unwrap_or(vec![]));
|
||||
}
|
||||
drop(events_write);
|
||||
events = self.events.read().await;
|
||||
}
|
||||
|
||||
let mut res = vec![];
|
||||
for event in events.as_ref().unwrap() {
|
||||
if let Some(event) = filter_map(&event.event) {
|
||||
res.push(event);
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn storage<K: Encode, R: Decode>(
|
||||
&self,
|
||||
pallet: &'static str,
|
||||
name: &'static str,
|
||||
key: K,
|
||||
) -> Result<Option<R>, SeraiError> {
|
||||
// TODO: Make this const?
|
||||
let mut full_key = sp_core::hashing::twox_128(pallet.as_bytes()).to_vec();
|
||||
full_key.extend(sp_core::hashing::twox_128(name.as_bytes()));
|
||||
full_key.extend(key.encode());
|
||||
|
||||
let res: Option<String> =
|
||||
self.serai.call("state_getStorage", [hex::encode(full_key), hex::encode(self.block)]).await?;
|
||||
let Some(res) = res else { return Ok(None) };
|
||||
let res = Serai::hex_decode(res)?;
|
||||
Ok(Some(R::decode(&mut res.as_slice()).map_err(|_| {
|
||||
SeraiError::InvalidRuntime(format!(
|
||||
"different type present at storage location, raw value: {}",
|
||||
hex::encode(res)
|
||||
))
|
||||
})?))
|
||||
}
|
||||
|
||||
async fn runtime_api<P: Encode, R: Decode>(
|
||||
&self,
|
||||
method: &'static str,
|
||||
params: P,
|
||||
) -> Result<R, SeraiError> {
|
||||
let result: String = self
|
||||
.serai
|
||||
.call(
|
||||
"state_call",
|
||||
[method.to_string(), hex::encode(params.encode()), hex::encode(self.block)],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes = Serai::hex_decode(result.clone())?;
|
||||
R::decode(&mut bytes.as_slice()).map_err(|_| {
|
||||
SeraiError::InvalidRuntime(format!(
|
||||
"different type than what is expected to be returned, raw value: {}",
|
||||
hex::encode(result)
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn coins(&self) -> SeraiCoins<'_> {
|
||||
SeraiCoins(self)
|
||||
}
|
||||
|
||||
pub fn dex(&self) -> SeraiDex<'_> {
|
||||
SeraiDex(self)
|
||||
}
|
||||
|
||||
pub fn in_instructions(&self) -> SeraiInInstructions<'_> {
|
||||
SeraiInInstructions(self)
|
||||
}
|
||||
|
||||
pub fn validator_sets(&self) -> SeraiValidatorSets<'_> {
|
||||
SeraiValidatorSets(self)
|
||||
}
|
||||
|
||||
pub fn genesis_liquidity(&self) -> SeraiGenesisLiquidity<'_> {
|
||||
SeraiGenesisLiquidity(self)
|
||||
}
|
||||
|
||||
pub fn liquidity_tokens(&self) -> SeraiLiquidityTokens<'_> {
|
||||
SeraiLiquidityTokens(self)
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
use scale::Encode;
|
||||
|
||||
use sp_core::sr25519::{Public, Signature};
|
||||
use sp_runtime::BoundedVec;
|
||||
|
||||
use serai_abi::{primitives::Amount, validator_sets::primitives::ExternalValidatorSet};
|
||||
pub use serai_abi::validator_sets::primitives;
|
||||
use primitives::{MAX_KEY_LEN, Session, KeyPair, SlashReport};
|
||||
|
||||
use crate::{
|
||||
primitives::{NetworkId, ExternalNetworkId, EmbeddedEllipticCurve},
|
||||
Transaction, Serai, TemporalSerai, SeraiError,
|
||||
};
|
||||
|
||||
const PALLET: &str = "ValidatorSets";
|
||||
|
||||
pub type ValidatorSetsEvent = serai_abi::validator_sets::Event;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SeraiValidatorSets<'a>(pub(crate) &'a TemporalSerai<'a>);
|
||||
impl SeraiValidatorSets<'_> {
|
||||
pub async fn new_set_events(&self) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.events(|event| {
|
||||
if let serai_abi::Event::ValidatorSets(event) = event {
|
||||
if matches!(event, ValidatorSetsEvent::NewSet { .. }) {
|
||||
Some(event.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn participant_removed_events(&self) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.events(|event| {
|
||||
if let serai_abi::Event::ValidatorSets(event) = event {
|
||||
if matches!(event, ValidatorSetsEvent::ParticipantRemoved { .. }) {
|
||||
Some(event.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn key_gen_events(&self) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.events(|event| {
|
||||
if let serai_abi::Event::ValidatorSets(event) = event {
|
||||
if matches!(event, ValidatorSetsEvent::KeyGen { .. }) {
|
||||
Some(event.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn accepted_handover_events(&self) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.events(|event| {
|
||||
if let serai_abi::Event::ValidatorSets(event) = event {
|
||||
if matches!(event, ValidatorSetsEvent::AcceptedHandover { .. }) {
|
||||
Some(event.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_retired_events(&self) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.events(|event| {
|
||||
if let serai_abi::Event::ValidatorSets(event) = event {
|
||||
if matches!(event, ValidatorSetsEvent::SetRetired { .. }) {
|
||||
Some(event.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn session(&self, network: NetworkId) -> Result<Option<Session>, SeraiError> {
|
||||
self.0.storage(PALLET, "CurrentSession", network).await
|
||||
}
|
||||
|
||||
pub async fn embedded_elliptic_curve_key(
|
||||
&self,
|
||||
validator: Public,
|
||||
embedded_elliptic_curve: EmbeddedEllipticCurve,
|
||||
) -> Result<Option<Vec<u8>>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.storage(
|
||||
PALLET,
|
||||
"EmbeddedEllipticCurveKeys",
|
||||
(sp_core::hashing::blake2_128(&validator.encode()), validator, embedded_elliptic_curve),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn participants(
|
||||
&self,
|
||||
network: NetworkId,
|
||||
) -> Result<Option<Vec<(Public, u64)>>, SeraiError> {
|
||||
self.0.storage(PALLET, "Participants", network).await
|
||||
}
|
||||
|
||||
pub async fn allocation_per_key_share(
|
||||
&self,
|
||||
network: NetworkId,
|
||||
) -> Result<Option<Amount>, SeraiError> {
|
||||
self.0.storage(PALLET, "AllocationPerKeyShare", network).await
|
||||
}
|
||||
|
||||
pub async fn total_allocated_stake(
|
||||
&self,
|
||||
network: NetworkId,
|
||||
) -> Result<Option<Amount>, SeraiError> {
|
||||
self.0.storage(PALLET, "TotalAllocatedStake", network).await
|
||||
}
|
||||
|
||||
pub async fn allocation(
|
||||
&self,
|
||||
network: NetworkId,
|
||||
key: Public,
|
||||
) -> Result<Option<Amount>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.storage(
|
||||
PALLET,
|
||||
"Allocations",
|
||||
(sp_core::hashing::blake2_128(&(network, key).encode()), (network, key)),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn pending_deallocations(
|
||||
&self,
|
||||
network: NetworkId,
|
||||
account: Public,
|
||||
session: Session,
|
||||
) -> Result<Option<Amount>, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.storage(
|
||||
PALLET,
|
||||
"PendingDeallocations",
|
||||
(sp_core::hashing::blake2_128(&(network, account).encode()), (network, account, session)),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn active_network_validators(
|
||||
&self,
|
||||
network: NetworkId,
|
||||
) -> Result<Vec<Public>, SeraiError> {
|
||||
self.0.runtime_api("ValidatorSetsApi_validators", network).await
|
||||
}
|
||||
|
||||
// TODO: Store these separately since we almost never need both at once?
|
||||
pub async fn keys(&self, set: ExternalValidatorSet) -> Result<Option<KeyPair>, SeraiError> {
|
||||
self.0.storage(PALLET, "Keys", (sp_core::hashing::twox_64(&set.encode()), set)).await
|
||||
}
|
||||
|
||||
pub async fn key_pending_slash_report(
|
||||
&self,
|
||||
network: ExternalNetworkId,
|
||||
) -> Result<Option<Public>, SeraiError> {
|
||||
self.0.storage(PALLET, "PendingSlashReport", network).await
|
||||
}
|
||||
|
||||
pub async fn session_begin_block(
|
||||
&self,
|
||||
network: NetworkId,
|
||||
session: Session,
|
||||
) -> Result<Option<u64>, SeraiError> {
|
||||
self.0.storage(PALLET, "SessionBeginBlock", (network, session)).await
|
||||
}
|
||||
|
||||
pub fn set_keys(
|
||||
network: ExternalNetworkId,
|
||||
key_pair: KeyPair,
|
||||
signature_participants: bitvec::vec::BitVec<u8, bitvec::order::Lsb0>,
|
||||
signature: Signature,
|
||||
) -> Transaction {
|
||||
Serai::unsigned(serai_abi::Call::ValidatorSets(serai_abi::validator_sets::Call::set_keys {
|
||||
network,
|
||||
key_pair,
|
||||
signature_participants,
|
||||
signature,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn set_embedded_elliptic_curve_key(
|
||||
embedded_elliptic_curve: EmbeddedEllipticCurve,
|
||||
key: BoundedVec<u8, sp_core::ConstU32<{ MAX_KEY_LEN }>>,
|
||||
) -> serai_abi::Call {
|
||||
serai_abi::Call::ValidatorSets(
|
||||
serai_abi::validator_sets::Call::set_embedded_elliptic_curve_key {
|
||||
embedded_elliptic_curve,
|
||||
key,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn allocate(network: NetworkId, amount: Amount) -> serai_abi::Call {
|
||||
serai_abi::Call::ValidatorSets(serai_abi::validator_sets::Call::allocate { network, amount })
|
||||
}
|
||||
|
||||
pub fn deallocate(network: NetworkId, amount: Amount) -> serai_abi::Call {
|
||||
serai_abi::Call::ValidatorSets(serai_abi::validator_sets::Call::deallocate { network, amount })
|
||||
}
|
||||
|
||||
pub fn report_slashes(
|
||||
network: ExternalNetworkId,
|
||||
slashes: SlashReport,
|
||||
signature: Signature,
|
||||
) -> Transaction {
|
||||
Serai::unsigned(serai_abi::Call::ValidatorSets(
|
||||
serai_abi::validator_sets::Call::report_slashes { network, slashes, signature },
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
#[cfg(feature = "networks")]
|
||||
mod networks;
|
||||
@@ -1 +0,0 @@
|
||||
// TODO: Test the address back and forth
|
||||
@@ -1,5 +0,0 @@
|
||||
#[cfg(feature = "bitcoin")]
|
||||
mod bitcoin;
|
||||
|
||||
#[cfg(feature = "monero")]
|
||||
mod monero;
|
||||
@@ -1 +0,0 @@
|
||||
// TODO: Test the address back and forth
|
||||
@@ -1,76 +0,0 @@
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use blake2::{
|
||||
digest::{consts::U32, Digest},
|
||||
Blake2b,
|
||||
};
|
||||
|
||||
use scale::Encode;
|
||||
|
||||
use serai_client::{
|
||||
primitives::{BlockHash, ExternalCoin, Amount, ExternalBalance, SeraiAddress},
|
||||
coins::CoinsEvent,
|
||||
validator_sets::primitives::Session,
|
||||
in_instructions::{
|
||||
primitives::{InInstruction, InInstructionWithBalance, Batch},
|
||||
InInstructionsEvent,
|
||||
},
|
||||
Serai,
|
||||
};
|
||||
|
||||
mod common;
|
||||
use common::in_instructions::provide_batch;
|
||||
|
||||
serai_test!(
|
||||
publish_batch: (|serai: Serai| async move {
|
||||
let id = 0;
|
||||
|
||||
let mut address = SeraiAddress::new([0; 32]);
|
||||
OsRng.fill_bytes(&mut address.0);
|
||||
|
||||
let coin = ExternalCoin::Bitcoin;
|
||||
let network = coin.network();
|
||||
let amount = Amount(OsRng.next_u64().saturating_add(1));
|
||||
let balance = ExternalBalance { coin, amount };
|
||||
|
||||
let mut external_network_block_hash = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut external_network_block_hash.0);
|
||||
|
||||
let batch = Batch {
|
||||
network,
|
||||
id,
|
||||
external_network_block_hash,
|
||||
instructions: vec![InInstructionWithBalance {
|
||||
instruction: InInstruction::Transfer(address),
|
||||
balance,
|
||||
}],
|
||||
};
|
||||
|
||||
let block = provide_batch(&serai, batch.clone()).await;
|
||||
|
||||
let serai = serai.as_of(block);
|
||||
{
|
||||
let serai = serai.in_instructions();
|
||||
let batches = serai.batch_events().await.unwrap();
|
||||
assert_eq!(
|
||||
batches,
|
||||
vec![InInstructionsEvent::Batch {
|
||||
network,
|
||||
publishing_session: Session(0),
|
||||
id,
|
||||
external_network_block_hash,
|
||||
in_instructions_hash: Blake2b::<U32>::digest(batch.instructions.encode()).into(),
|
||||
in_instruction_results: bitvec::bitvec![u8, bitvec::order::Lsb0; 1; 1],
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
let serai = serai.coins();
|
||||
assert_eq!(
|
||||
serai.mint_events().await.unwrap(),
|
||||
vec![CoinsEvent::Mint { to: address, balance: balance.into() }]
|
||||
);
|
||||
assert_eq!(serai.coin_supply(coin.into()).await.unwrap(), amount);
|
||||
assert_eq!(serai.coin_balance(coin.into(), address).await.unwrap(), amount);
|
||||
})
|
||||
);
|
||||
@@ -1,105 +0,0 @@
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use blake2::{
|
||||
digest::{consts::U32, Digest},
|
||||
Blake2b,
|
||||
};
|
||||
|
||||
use scale::Encode;
|
||||
|
||||
use sp_core::Pair;
|
||||
|
||||
use serai_client::{
|
||||
primitives::{
|
||||
BlockHash, ExternalCoin, Amount, ExternalBalance, SeraiAddress, ExternalAddress,
|
||||
insecure_pair_from_name,
|
||||
},
|
||||
coins::{
|
||||
primitives::{OutInstruction, OutInstructionWithBalance},
|
||||
CoinsEvent,
|
||||
},
|
||||
validator_sets::primitives::Session,
|
||||
in_instructions::{
|
||||
InInstructionsEvent,
|
||||
primitives::{InInstruction, InInstructionWithBalance, Batch},
|
||||
},
|
||||
Serai, SeraiCoins,
|
||||
};
|
||||
|
||||
mod common;
|
||||
use common::{tx::publish_tx, in_instructions::provide_batch};
|
||||
|
||||
serai_test!(
|
||||
burn: (|serai: Serai| async move {
|
||||
let id = 0;
|
||||
let mut block_hash = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut block_hash.0);
|
||||
|
||||
let pair = insecure_pair_from_name("Dave");
|
||||
let public = pair.public();
|
||||
let address = SeraiAddress::from(public);
|
||||
|
||||
let coin = ExternalCoin::Bitcoin;
|
||||
let network = coin.network();
|
||||
let amount = Amount(OsRng.next_u64().saturating_add(1));
|
||||
let balance = ExternalBalance { coin, amount };
|
||||
|
||||
let batch = Batch {
|
||||
network,
|
||||
id,
|
||||
external_network_block_hash: block_hash,
|
||||
instructions: vec![InInstructionWithBalance {
|
||||
instruction: InInstruction::Transfer(address),
|
||||
balance,
|
||||
}],
|
||||
};
|
||||
|
||||
let block = provide_batch(&serai, batch.clone()).await;
|
||||
|
||||
let instruction = {
|
||||
let serai = serai.as_of(block);
|
||||
let batches = serai.in_instructions().batch_events().await.unwrap();
|
||||
assert_eq!(
|
||||
batches,
|
||||
vec![InInstructionsEvent::Batch {
|
||||
network,
|
||||
publishing_session: Session(0),
|
||||
id,
|
||||
external_network_block_hash: block_hash,
|
||||
in_instructions_hash: Blake2b::<U32>::digest(batch.instructions.encode()).into(),
|
||||
in_instruction_results: bitvec::bitvec![u8, bitvec::order::Lsb0; 1; 1],
|
||||
}]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serai.coins().mint_events().await.unwrap(),
|
||||
vec![CoinsEvent::Mint { to: address, balance: balance.into() }]
|
||||
);
|
||||
assert_eq!(serai.coins().coin_supply(coin.into()).await.unwrap(), amount);
|
||||
assert_eq!(serai.coins().coin_balance(coin.into(), address).await.unwrap(), amount);
|
||||
|
||||
// Now burn it
|
||||
let mut rand_bytes = vec![0; 32];
|
||||
OsRng.fill_bytes(&mut rand_bytes);
|
||||
let external_address = ExternalAddress::new(rand_bytes).unwrap();
|
||||
|
||||
OutInstructionWithBalance {
|
||||
balance,
|
||||
instruction: OutInstruction { address: external_address },
|
||||
}
|
||||
};
|
||||
|
||||
let block = publish_tx(
|
||||
&serai,
|
||||
&serai.sign(&pair, SeraiCoins::burn_with_instruction(instruction.clone()), 0, 0),
|
||||
)
|
||||
.await;
|
||||
|
||||
let serai = serai.as_of(block);
|
||||
let serai = serai.coins();
|
||||
let events = serai.burn_with_instruction_events().await.unwrap();
|
||||
assert_eq!(events, vec![CoinsEvent::BurnWithInstruction { from: address, instruction }]);
|
||||
assert_eq!(serai.coin_supply(coin.into()).await.unwrap(), Amount(0));
|
||||
assert_eq!(serai.coin_balance(coin.into(), address).await.unwrap(), Amount(0));
|
||||
})
|
||||
);
|
||||
@@ -1,49 +0,0 @@
|
||||
use serai_abi::primitives::{Amount, Coin, ExternalCoin};
|
||||
|
||||
use serai_client::{Serai, SeraiDex};
|
||||
use sp_core::{sr25519::Pair, Pair as PairTrait};
|
||||
|
||||
use crate::common::tx::publish_tx;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn add_liquidity(
|
||||
serai: &Serai,
|
||||
coin: ExternalCoin,
|
||||
coin_amount: Amount,
|
||||
sri_amount: Amount,
|
||||
nonce: u32,
|
||||
pair: Pair,
|
||||
) -> [u8; 32] {
|
||||
let address = pair.public();
|
||||
|
||||
let tx = serai.sign(
|
||||
&pair,
|
||||
SeraiDex::add_liquidity(coin, coin_amount, sri_amount, Amount(1), Amount(1), address.into()),
|
||||
nonce,
|
||||
0,
|
||||
);
|
||||
|
||||
publish_tx(serai, &tx).await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn swap(
|
||||
serai: &Serai,
|
||||
from_coin: Coin,
|
||||
to_coin: Coin,
|
||||
amount_in: Amount,
|
||||
amount_out_min: Amount,
|
||||
nonce: u32,
|
||||
pair: Pair,
|
||||
) -> [u8; 32] {
|
||||
let address = pair.public();
|
||||
|
||||
let tx = serai.sign(
|
||||
&pair,
|
||||
SeraiDex::swap(from_coin, to_coin, amount_in, amount_out_min, address.into()),
|
||||
nonce,
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
publish_tx(serai, &tx).await
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rand_core::{RngCore, OsRng};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use frost::curve::Ristretto;
|
||||
use ciphersuite::{WrappedGroup, GroupIo};
|
||||
use dkg_musig::musig;
|
||||
use schnorrkel::Schnorrkel;
|
||||
|
||||
use sp_core::{sr25519::Signature, Pair as PairTrait};
|
||||
|
||||
use serai_abi::{
|
||||
primitives::{
|
||||
EXTERNAL_COINS, BlockHash, ExternalNetworkId, NetworkId, ExternalCoin, Amount, ExternalBalance,
|
||||
SeraiAddress, insecure_pair_from_name,
|
||||
},
|
||||
validator_sets::primitives::{Session, ValidatorSet, musig_context},
|
||||
genesis_liquidity::primitives::{Values, oraclize_values_message},
|
||||
in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch},
|
||||
};
|
||||
|
||||
use serai_client::{Serai, SeraiGenesisLiquidity};
|
||||
|
||||
use crate::common::{in_instructions::provide_batch, tx::publish_tx};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn set_up_genesis(
|
||||
serai: &Serai,
|
||||
values: &HashMap<ExternalCoin, u64>,
|
||||
) -> (HashMap<ExternalCoin, Vec<(SeraiAddress, Amount)>>, HashMap<ExternalNetworkId, u32>) {
|
||||
// make accounts with amounts
|
||||
let mut accounts = HashMap::new();
|
||||
for coin in EXTERNAL_COINS {
|
||||
// make 5 accounts per coin
|
||||
let mut values = vec![];
|
||||
for _ in 0 .. 5 {
|
||||
let mut address = SeraiAddress::new([0; 32]);
|
||||
OsRng.fill_bytes(&mut address.0);
|
||||
values.push((address, Amount(OsRng.next_u64() % 10u64.pow(coin.decimals()))));
|
||||
}
|
||||
accounts.insert(coin, values);
|
||||
}
|
||||
|
||||
// send a batch per coin
|
||||
let mut batch_ids: HashMap<ExternalNetworkId, u32> = HashMap::new();
|
||||
for coin in EXTERNAL_COINS {
|
||||
// set up instructions
|
||||
let instructions = accounts[&coin]
|
||||
.iter()
|
||||
.map(|(addr, amount)| InInstructionWithBalance {
|
||||
instruction: InInstruction::GenesisLiquidity(*addr),
|
||||
balance: ExternalBalance { coin, amount: *amount },
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// set up block hash
|
||||
let mut block = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut block.0);
|
||||
|
||||
// set up batch id
|
||||
batch_ids
|
||||
.entry(coin.network())
|
||||
.and_modify(|v| {
|
||||
*v += 1;
|
||||
})
|
||||
.or_insert(0);
|
||||
|
||||
let batch = Batch {
|
||||
network: coin.network(),
|
||||
external_network_block_hash: block,
|
||||
id: batch_ids[&coin.network()],
|
||||
instructions,
|
||||
};
|
||||
provide_batch(serai, batch).await;
|
||||
}
|
||||
|
||||
// set values relative to each other. We can do that without checking for genesis period blocks
|
||||
// since we are running in test(fast-epoch) mode.
|
||||
// TODO: Random values here
|
||||
let values = Values {
|
||||
monero: values[&ExternalCoin::Monero],
|
||||
ether: values[&ExternalCoin::Ether],
|
||||
dai: values[&ExternalCoin::Dai],
|
||||
};
|
||||
set_values(serai, &values).await;
|
||||
|
||||
(accounts, batch_ids)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn set_values(serai: &Serai, values: &Values) {
|
||||
// prepare a Musig tx to oraclize the relative values
|
||||
let pair = insecure_pair_from_name("Alice");
|
||||
let public = pair.public();
|
||||
// we publish the tx in set 1
|
||||
let set = ValidatorSet { session: Session(1), network: NetworkId::Serai };
|
||||
|
||||
let public_key = <Ristretto as GroupIo>::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap();
|
||||
let secret_key =
|
||||
<Ristretto as GroupIo>::read_F::<&[u8]>(&mut pair.as_ref().secret.to_bytes()[.. 32].as_ref())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(Ristretto::generator() * secret_key, public_key);
|
||||
let threshold_keys =
|
||||
musig::<Ristretto>(musig_context(set), Zeroizing::new(secret_key), &[public_key]).unwrap();
|
||||
|
||||
let sig = frost::tests::sign_without_caching(
|
||||
&mut OsRng,
|
||||
frost::tests::algorithm_machines(
|
||||
&mut OsRng,
|
||||
&Schnorrkel::new(b"substrate"),
|
||||
&HashMap::from([(threshold_keys.params().i(), threshold_keys)]),
|
||||
),
|
||||
&oraclize_values_message(&set, values),
|
||||
);
|
||||
|
||||
// oraclize values
|
||||
let _ =
|
||||
publish_tx(serai, &SeraiGenesisLiquidity::oraclize_values(*values, Signature(sig.to_bytes())))
|
||||
.await;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
use rand_core::{RngCore, OsRng};
|
||||
use blake2::{
|
||||
digest::{consts::U32, Digest},
|
||||
Blake2b,
|
||||
};
|
||||
|
||||
use scale::Encode;
|
||||
|
||||
use sp_core::Pair;
|
||||
|
||||
use serai_client::{
|
||||
primitives::{BlockHash, ExternalBalance, SeraiAddress, insecure_pair_from_name},
|
||||
validator_sets::primitives::{ExternalValidatorSet, KeyPair},
|
||||
in_instructions::{
|
||||
primitives::{Batch, SignedBatch, batch_message, InInstruction, InInstructionWithBalance},
|
||||
InInstructionsEvent,
|
||||
},
|
||||
SeraiInInstructions, Serai,
|
||||
};
|
||||
|
||||
use crate::common::{tx::publish_tx, validator_sets::set_keys};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn provide_batch(serai: &Serai, batch: Batch) -> [u8; 32] {
|
||||
let serai_latest = serai.as_of_latest_finalized_block().await.unwrap();
|
||||
let session = serai_latest.validator_sets().session(batch.network.into()).await.unwrap().unwrap();
|
||||
let set = ExternalValidatorSet { session, network: batch.network };
|
||||
|
||||
let pair = insecure_pair_from_name(&format!("ValidatorSet {set:?}"));
|
||||
let keys = if let Some(keys) = serai_latest.validator_sets().keys(set).await.unwrap() {
|
||||
keys
|
||||
} else {
|
||||
let keys = KeyPair(pair.public(), vec![].try_into().unwrap());
|
||||
set_keys(serai, set, keys.clone(), &[insecure_pair_from_name("Alice")]).await;
|
||||
keys
|
||||
};
|
||||
assert_eq!(keys.0, pair.public());
|
||||
|
||||
let block = publish_tx(
|
||||
serai,
|
||||
&SeraiInInstructions::execute_batch(SignedBatch {
|
||||
batch: batch.clone(),
|
||||
signature: pair.sign(&batch_message(&batch)),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
{
|
||||
let mut batches = serai.as_of(block).in_instructions().batch_events().await.unwrap();
|
||||
assert_eq!(batches.len(), 1);
|
||||
let InInstructionsEvent::Batch {
|
||||
network,
|
||||
publishing_session,
|
||||
id,
|
||||
external_network_block_hash,
|
||||
in_instructions_hash,
|
||||
in_instruction_results: _,
|
||||
} = batches.swap_remove(0)
|
||||
else {
|
||||
panic!("Batch event wasn't Batch event")
|
||||
};
|
||||
assert_eq!(network, batch.network);
|
||||
assert_eq!(publishing_session, session);
|
||||
assert_eq!(id, batch.id);
|
||||
assert_eq!(external_network_block_hash, batch.external_network_block_hash);
|
||||
assert_eq!(
|
||||
in_instructions_hash,
|
||||
<[u8; 32]>::from(Blake2b::<U32>::digest(batch.instructions.encode()))
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Check the tokens events
|
||||
|
||||
block
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn mint_coin(
|
||||
serai: &Serai,
|
||||
balance: ExternalBalance,
|
||||
batch_id: u32,
|
||||
address: SeraiAddress,
|
||||
) -> [u8; 32] {
|
||||
let mut block_hash = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut block_hash.0);
|
||||
|
||||
let batch = Batch {
|
||||
network: balance.coin.network(),
|
||||
id: batch_id,
|
||||
external_network_block_hash: block_hash,
|
||||
instructions: vec![InInstructionWithBalance {
|
||||
instruction: InInstruction::Transfer(address),
|
||||
balance,
|
||||
}],
|
||||
};
|
||||
|
||||
provide_batch(serai, batch).await
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
pub mod tx;
|
||||
pub mod validator_sets;
|
||||
pub mod in_instructions;
|
||||
pub mod dex;
|
||||
pub mod genesis_liquidity;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! serai_test {
|
||||
($($name: ident: $test: expr)*) => {
|
||||
$(
|
||||
#[tokio::test]
|
||||
async fn $name() {
|
||||
use std::collections::HashMap;
|
||||
use dockertest::{
|
||||
PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image,
|
||||
TestBodySpecification, DockerTest,
|
||||
};
|
||||
|
||||
serai_docker_tests::build("serai".to_string());
|
||||
|
||||
let handle = concat!("serai_client-serai_node-", stringify!($name));
|
||||
|
||||
let composition = TestBodySpecification::with_image(
|
||||
Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never),
|
||||
)
|
||||
.replace_cmd(vec![
|
||||
"serai-node".to_string(),
|
||||
"--dev".to_string(),
|
||||
"--unsafe-rpc-external".to_string(),
|
||||
"--rpc-cors".to_string(),
|
||||
"all".to_string(),
|
||||
])
|
||||
.replace_env(
|
||||
HashMap::from([
|
||||
("RUST_LOG".to_string(), "runtime=debug".to_string()),
|
||||
("KEY".to_string(), " ".to_string()),
|
||||
])
|
||||
)
|
||||
.set_publish_all_ports(true)
|
||||
.set_handle(handle)
|
||||
.set_start_policy(StartPolicy::Strict)
|
||||
.set_log_options(Some(LogOptions {
|
||||
action: LogAction::Forward,
|
||||
policy: LogPolicy::Always,
|
||||
source: LogSource::Both,
|
||||
}));
|
||||
|
||||
let mut test = DockerTest::new().with_network(dockertest::Network::Isolated);
|
||||
test.provide_container(composition);
|
||||
test.run_async(|ops| async move {
|
||||
// Sleep until the Substrate RPC starts
|
||||
let mut ticks = 0;
|
||||
let serai_rpc = loop {
|
||||
// Bound execution to 60 seconds
|
||||
if ticks > 60 {
|
||||
panic!("Serai node didn't start within 60 seconds");
|
||||
}
|
||||
tokio::time::sleep(core::time::Duration::from_secs(1)).await;
|
||||
ticks += 1;
|
||||
|
||||
let Some(serai_rpc) = ops.handle(handle).host_port(9944) else { continue };
|
||||
let serai_rpc = format!("http://{}:{}", serai_rpc.0, serai_rpc.1);
|
||||
|
||||
let Ok(client) = Serai::new(serai_rpc.clone()).await else { continue };
|
||||
if client.latest_finalized_block_hash().await.is_err() {
|
||||
continue;
|
||||
}
|
||||
break serai_rpc;
|
||||
};
|
||||
#[allow(clippy::redundant_closure_call)]
|
||||
$test(Serai::new(serai_rpc).await.unwrap()).await;
|
||||
}).await;
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! serai_test_fast_epoch {
|
||||
($($name: ident: $test: expr)*) => {
|
||||
$(
|
||||
#[tokio::test]
|
||||
async fn $name() {
|
||||
use std::collections::HashMap;
|
||||
use dockertest::{
|
||||
PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image,
|
||||
TestBodySpecification, DockerTest,
|
||||
};
|
||||
|
||||
serai_docker_tests::build("serai-fast-epoch".to_string());
|
||||
|
||||
let handle = concat!("serai_client-serai_node-", stringify!($name));
|
||||
|
||||
let composition = TestBodySpecification::with_image(
|
||||
Image::with_repository("serai-dev-serai-fast-epoch").pull_policy(PullPolicy::Never),
|
||||
)
|
||||
.replace_cmd(vec![
|
||||
"serai-node".to_string(),
|
||||
"--dev".to_string(),
|
||||
"--unsafe-rpc-external".to_string(),
|
||||
"--rpc-cors".to_string(),
|
||||
"all".to_string(),
|
||||
])
|
||||
.replace_env(
|
||||
HashMap::from([
|
||||
("RUST_LOG".to_string(), "runtime=debug".to_string()),
|
||||
("KEY".to_string(), " ".to_string()),
|
||||
])
|
||||
)
|
||||
.set_publish_all_ports(true)
|
||||
.set_handle(handle)
|
||||
.set_start_policy(StartPolicy::Strict)
|
||||
.set_log_options(Some(LogOptions {
|
||||
action: LogAction::Forward,
|
||||
policy: LogPolicy::Always,
|
||||
source: LogSource::Both,
|
||||
}));
|
||||
|
||||
let mut test = DockerTest::new().with_network(dockertest::Network::Isolated);
|
||||
test.provide_container(composition);
|
||||
test.run_async(|ops| async move {
|
||||
// Sleep until the Substrate RPC starts
|
||||
let serai_rpc = ops.handle(handle).host_port(9944).unwrap();
|
||||
let serai_rpc = format!("http://{}:{}", serai_rpc.0, serai_rpc.1);
|
||||
// Bound execution to 60 seconds
|
||||
for _ in 0 .. 60 {
|
||||
tokio::time::sleep(core::time::Duration::from_secs(1)).await;
|
||||
let Ok(client) = Serai::new(serai_rpc.clone()).await else { continue };
|
||||
if client.latest_finalized_block_hash().await.is_err() {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
#[allow(clippy::redundant_closure_call)]
|
||||
$test(Serai::new(serai_rpc).await.unwrap()).await;
|
||||
}).await;
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
use core::time::Duration;
|
||||
|
||||
use tokio::time::sleep;
|
||||
|
||||
use serai_client::{Transaction, Serai};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn publish_tx(serai: &Serai, tx: &Transaction) -> [u8; 32] {
|
||||
let mut latest = serai
|
||||
.block(serai.latest_finalized_block_hash().await.unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.number();
|
||||
|
||||
serai.publish(tx).await.unwrap();
|
||||
|
||||
// Get the block it was included in
|
||||
// TODO: Add an RPC method for this/check the guarantee on the subscription
|
||||
let mut ticks = 0;
|
||||
loop {
|
||||
latest += 1;
|
||||
|
||||
let block = {
|
||||
let mut block;
|
||||
while {
|
||||
block = serai.finalized_block_by_number(latest).await.unwrap();
|
||||
block.is_none()
|
||||
} {
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
ticks += 1;
|
||||
|
||||
if ticks > 60 {
|
||||
panic!("60 seconds without inclusion in a finalized block");
|
||||
}
|
||||
}
|
||||
block.unwrap()
|
||||
};
|
||||
|
||||
for transaction in &block.transactions {
|
||||
if transaction == tx {
|
||||
return block.hash();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
use rand_core::OsRng;
|
||||
|
||||
use frost::curve::Ristretto;
|
||||
use ciphersuite::{WrappedGroup, GroupIo};
|
||||
use dkg_musig::musig;
|
||||
use schnorrkel::Schnorrkel;
|
||||
|
||||
use sp_core::{
|
||||
ConstU32,
|
||||
bounded::BoundedVec,
|
||||
sr25519::{Pair, Signature},
|
||||
Pair as PairTrait,
|
||||
};
|
||||
|
||||
use serai_abi::primitives::NetworkId;
|
||||
|
||||
use serai_client::{
|
||||
primitives::{EmbeddedEllipticCurve, Amount},
|
||||
validator_sets::{
|
||||
primitives::{MAX_KEY_LEN, ExternalValidatorSet, KeyPair, musig_context, set_keys_message},
|
||||
ValidatorSetsEvent,
|
||||
},
|
||||
SeraiValidatorSets, Serai,
|
||||
};
|
||||
|
||||
use crate::common::tx::publish_tx;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn set_keys(
|
||||
serai: &Serai,
|
||||
set: ExternalValidatorSet,
|
||||
key_pair: KeyPair,
|
||||
pairs: &[Pair],
|
||||
) -> [u8; 32] {
|
||||
let mut pub_keys = vec![];
|
||||
for pair in pairs {
|
||||
let public_key =
|
||||
<Ristretto as GroupIo>::read_G::<&[u8]>(&mut pair.public().0.as_ref()).unwrap();
|
||||
pub_keys.push(public_key);
|
||||
}
|
||||
|
||||
let mut threshold_keys = vec![];
|
||||
for i in 0 .. pairs.len() {
|
||||
let secret_key = <Ristretto as GroupIo>::read_F::<&[u8]>(
|
||||
&mut pairs[i].as_ref().secret.to_bytes()[.. 32].as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(Ristretto::generator() * secret_key, pub_keys[i]);
|
||||
|
||||
threshold_keys.push(
|
||||
musig::<Ristretto>(musig_context(set.into()), Zeroizing::new(secret_key), &pub_keys).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut musig_keys = HashMap::new();
|
||||
for threshold_keys in threshold_keys {
|
||||
musig_keys.insert(threshold_keys.params().i(), threshold_keys);
|
||||
}
|
||||
|
||||
let sig = frost::tests::sign_without_caching(
|
||||
&mut OsRng,
|
||||
frost::tests::algorithm_machines(&mut OsRng, &Schnorrkel::new(b"substrate"), &musig_keys),
|
||||
&set_keys_message(&set, &key_pair),
|
||||
);
|
||||
|
||||
// Set the key pair
|
||||
let block = publish_tx(
|
||||
serai,
|
||||
&SeraiValidatorSets::set_keys(
|
||||
set.network,
|
||||
key_pair.clone(),
|
||||
bitvec::bitvec!(u8, bitvec::prelude::Lsb0; 1; musig_keys.len()),
|
||||
Signature(sig.to_bytes()),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
serai.as_of(block).validator_sets().key_gen_events().await.unwrap(),
|
||||
vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }]
|
||||
);
|
||||
assert_eq!(serai.as_of(block).validator_sets().keys(set).await.unwrap(), Some(key_pair));
|
||||
|
||||
block
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn set_embedded_elliptic_curve_key(
|
||||
serai: &Serai,
|
||||
pair: &Pair,
|
||||
embedded_elliptic_curve: EmbeddedEllipticCurve,
|
||||
key: BoundedVec<u8, ConstU32<{ MAX_KEY_LEN }>>,
|
||||
nonce: u32,
|
||||
) -> [u8; 32] {
|
||||
// get the call
|
||||
let tx = serai.sign(
|
||||
pair,
|
||||
SeraiValidatorSets::set_embedded_elliptic_curve_key(embedded_elliptic_curve, key),
|
||||
nonce,
|
||||
0,
|
||||
);
|
||||
publish_tx(serai, &tx).await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn allocate_stake(
|
||||
serai: &Serai,
|
||||
network: NetworkId,
|
||||
amount: Amount,
|
||||
pair: &Pair,
|
||||
nonce: u32,
|
||||
) -> [u8; 32] {
|
||||
// get the call
|
||||
let tx = serai.sign(pair, SeraiValidatorSets::allocate(network, amount), nonce, 0);
|
||||
publish_tx(serai, &tx).await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn deallocate_stake(
|
||||
serai: &Serai,
|
||||
network: NetworkId,
|
||||
amount: Amount,
|
||||
pair: &Pair,
|
||||
nonce: u32,
|
||||
) -> [u8; 32] {
|
||||
// get the call
|
||||
let tx = serai.sign(pair, SeraiValidatorSets::deallocate(network, amount), nonce, 0);
|
||||
publish_tx(serai, &tx).await
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use sp_core::{Pair as PairTrait, bounded::BoundedVec};
|
||||
|
||||
use serai_abi::in_instructions::primitives::DexCall;
|
||||
|
||||
use serai_client::{
|
||||
primitives::{
|
||||
BlockHash, ExternalCoin, Coin, Amount, ExternalBalance, Balance, SeraiAddress, ExternalAddress,
|
||||
insecure_pair_from_name,
|
||||
},
|
||||
in_instructions::primitives::{
|
||||
InInstruction, InInstructionWithBalance, Batch, IN_INSTRUCTION_EXECUTOR, OutAddress,
|
||||
},
|
||||
dex::DexEvent,
|
||||
Serai,
|
||||
};
|
||||
|
||||
mod common;
|
||||
use common::{
|
||||
in_instructions::{provide_batch, mint_coin},
|
||||
dex::{add_liquidity as common_add_liquidity, swap as common_swap},
|
||||
};
|
||||
|
||||
// TODO: Calculate all constants in the following tests
|
||||
// TODO: Check LP token, coin balances
|
||||
// TODO: Modularize common code
|
||||
// TODO: Check Transfer events
|
||||
serai_test!(
|
||||
add_liquidity: (|serai: Serai| async move {
|
||||
let coin = ExternalCoin::Monero;
|
||||
let pair = insecure_pair_from_name("Ferdie");
|
||||
|
||||
// mint sriXMR in the account so that we can add liq.
|
||||
// Ferdie account is already pre-funded with SRI.
|
||||
mint_coin(
|
||||
&serai,
|
||||
ExternalBalance { coin, amount: Amount(100_000_000_000_000) },
|
||||
0,
|
||||
pair.clone().public().into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// add liquidity
|
||||
let coin_amount = Amount(50_000_000_000_000);
|
||||
let sri_amount = Amount(50_000_000_000_000);
|
||||
let block = common_add_liquidity(&serai,
|
||||
coin,
|
||||
coin_amount,
|
||||
sri_amount,
|
||||
0,
|
||||
pair.clone()
|
||||
).await;
|
||||
// get only the add liq events
|
||||
let mut events = serai.as_of(block).dex().events().await.unwrap();
|
||||
events.retain(|e| matches!(e, DexEvent::LiquidityAdded { .. }));
|
||||
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![DexEvent::LiquidityAdded {
|
||||
who: pair.public().into(),
|
||||
mint_to: pair.public().into(),
|
||||
pool_id: coin,
|
||||
coin_amount: coin_amount.0,
|
||||
sri_amount: sri_amount.0,
|
||||
lp_token_minted: 49_999999990000
|
||||
}]
|
||||
);
|
||||
})
|
||||
|
||||
// Tests coin -> SRI and SRI -> coin swaps.
|
||||
swap_coin_to_sri: (|serai: Serai| async move {
|
||||
let coin = ExternalCoin::Ether;
|
||||
let pair = insecure_pair_from_name("Ferdie");
|
||||
|
||||
// mint sriXMR in the account so that we can add liq.
|
||||
// Ferdie account is already pre-funded with SRI.
|
||||
mint_coin(
|
||||
&serai,
|
||||
ExternalBalance { coin, amount: Amount(100_000_000_000_000) },
|
||||
0,
|
||||
pair.clone().public().into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// add liquidity
|
||||
common_add_liquidity(&serai,
|
||||
coin,
|
||||
Amount(50_000_000_000_000),
|
||||
Amount(50_000_000_000_000),
|
||||
0,
|
||||
pair.clone()
|
||||
).await;
|
||||
|
||||
// now that we have our liquid pool, swap some coin to SRI.
|
||||
let mut amount_in = Amount(25_000_000_000_000);
|
||||
let mut block = common_swap(
|
||||
&serai,
|
||||
coin.into(),
|
||||
Coin::Serai,
|
||||
amount_in,
|
||||
Amount(1),
|
||||
1,
|
||||
pair.clone())
|
||||
.await;
|
||||
|
||||
// get only the swap events
|
||||
let mut events = serai.as_of(block).dex().events().await.unwrap();
|
||||
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
|
||||
|
||||
let mut path = BoundedVec::try_from(vec![coin.into(), Coin::Serai]).unwrap();
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![DexEvent::SwapExecuted {
|
||||
who: pair.clone().public().into(),
|
||||
send_to: pair.public().into(),
|
||||
path,
|
||||
amount_in: amount_in.0,
|
||||
amount_out: 16633299966633
|
||||
}]
|
||||
);
|
||||
|
||||
// now swap some SRI to coin
|
||||
amount_in = Amount(10_000_000_000_000);
|
||||
block = common_swap(
|
||||
&serai,
|
||||
Coin::Serai,
|
||||
coin.into(),
|
||||
amount_in,
|
||||
Amount(1),
|
||||
2,
|
||||
pair.clone()
|
||||
).await;
|
||||
|
||||
// get only the swap events
|
||||
let mut events = serai.as_of(block).dex().events().await.unwrap();
|
||||
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
|
||||
|
||||
path = BoundedVec::try_from(vec![Coin::Serai, coin.into()]).unwrap();
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![DexEvent::SwapExecuted {
|
||||
who: pair.clone().public().into(),
|
||||
send_to: pair.public().into(),
|
||||
path,
|
||||
amount_in: amount_in.0,
|
||||
amount_out: 17254428681101
|
||||
}]
|
||||
);
|
||||
})
|
||||
|
||||
swap_coin_to_coin: (|serai: Serai| async move {
|
||||
let coin1 = ExternalCoin::Monero;
|
||||
let coin2 = ExternalCoin::Dai;
|
||||
let pair = insecure_pair_from_name("Ferdie");
|
||||
|
||||
// mint coins
|
||||
mint_coin(
|
||||
&serai,
|
||||
ExternalBalance { coin: coin1, amount: Amount(100_000_000_000_000) },
|
||||
0,
|
||||
pair.clone().public().into(),
|
||||
)
|
||||
.await;
|
||||
mint_coin(
|
||||
&serai,
|
||||
ExternalBalance { coin: coin2, amount: Amount(100_000_000_000_000) },
|
||||
0,
|
||||
pair.clone().public().into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// add liquidity to pools
|
||||
common_add_liquidity(&serai,
|
||||
coin1,
|
||||
Amount(50_000_000_000_000),
|
||||
Amount(50_000_000_000_000),
|
||||
0,
|
||||
pair.clone()
|
||||
).await;
|
||||
common_add_liquidity(&serai,
|
||||
coin2,
|
||||
Amount(50_000_000_000_000),
|
||||
Amount(50_000_000_000_000),
|
||||
1,
|
||||
pair.clone()
|
||||
).await;
|
||||
|
||||
// swap coin1 -> coin2
|
||||
let amount_in = Amount(25_000_000_000_000);
|
||||
let block = common_swap(
|
||||
&serai,
|
||||
coin1.into(),
|
||||
coin2.into(),
|
||||
amount_in,
|
||||
Amount(1),
|
||||
2,
|
||||
pair.clone()
|
||||
).await;
|
||||
|
||||
// get only the swap events
|
||||
let mut events = serai.as_of(block).dex().events().await.unwrap();
|
||||
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
|
||||
|
||||
let path = BoundedVec::try_from(vec![coin1.into(), Coin::Serai, coin2.into()]).unwrap();
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![DexEvent::SwapExecuted {
|
||||
who: pair.clone().public().into(),
|
||||
send_to: pair.public().into(),
|
||||
path,
|
||||
amount_in: amount_in.0,
|
||||
amount_out: 12453103964435,
|
||||
}]
|
||||
);
|
||||
})
|
||||
|
||||
add_liquidity_in_instructions: (|serai: Serai| async move {
|
||||
let coin = ExternalCoin::Bitcoin;
|
||||
let pair = insecure_pair_from_name("Ferdie");
|
||||
let mut batch_id = 0;
|
||||
|
||||
// mint sriBTC in the account so that we can add liq.
|
||||
// Ferdie account is already pre-funded with SRI.
|
||||
mint_coin(
|
||||
&serai,
|
||||
ExternalBalance { coin, amount: Amount(100_000_000_000_000) },
|
||||
batch_id,
|
||||
pair.clone().public().into(),
|
||||
)
|
||||
.await;
|
||||
batch_id += 1;
|
||||
|
||||
// add liquidity
|
||||
common_add_liquidity(&serai,
|
||||
coin,
|
||||
Amount(5_000_000_000_000),
|
||||
Amount(500_000_000_000),
|
||||
0,
|
||||
pair.clone()
|
||||
).await;
|
||||
|
||||
// now that we have our liquid SRI/BTC pool, we can add more liquidity to it via an
|
||||
// InInstruction
|
||||
let mut block_hash = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut block_hash.0);
|
||||
let batch = Batch {
|
||||
network: coin.network(),
|
||||
id: batch_id,
|
||||
external_network_block_hash: block_hash,
|
||||
instructions: vec![InInstructionWithBalance {
|
||||
instruction: InInstruction::Dex(DexCall::SwapAndAddLiquidity(pair.public().into())),
|
||||
balance: ExternalBalance { coin, amount: Amount(20_000_000_000_000) },
|
||||
}],
|
||||
};
|
||||
|
||||
let block = provide_batch(&serai, batch).await;
|
||||
let mut events = serai.as_of(block).dex().events().await.unwrap();
|
||||
events.retain(|e| matches!(e, DexEvent::LiquidityAdded { .. }));
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![DexEvent::LiquidityAdded {
|
||||
who: IN_INSTRUCTION_EXECUTOR,
|
||||
mint_to: pair.public().into(),
|
||||
pool_id: coin,
|
||||
coin_amount: 10_000_000_000_000, // half of sent amount
|
||||
sri_amount: 111_333_778_668,
|
||||
lp_token_minted: 1_054_092_553_383
|
||||
}]
|
||||
);
|
||||
})
|
||||
|
||||
swap_in_instructions: (|serai: Serai| async move {
|
||||
let coin1 = ExternalCoin::Monero;
|
||||
let coin2 = ExternalCoin::Ether;
|
||||
let pair = insecure_pair_from_name("Ferdie");
|
||||
let mut coin1_batch_id = 0;
|
||||
let mut coin2_batch_id = 0;
|
||||
|
||||
// mint coins
|
||||
mint_coin(
|
||||
&serai,
|
||||
ExternalBalance { coin: coin1, amount: Amount(10_000_000_000_000_000) },
|
||||
coin1_batch_id,
|
||||
pair.clone().public().into(),
|
||||
)
|
||||
.await;
|
||||
coin1_batch_id += 1;
|
||||
mint_coin(
|
||||
&serai,
|
||||
ExternalBalance { coin: coin2, amount: Amount(100_000_000_000_000) },
|
||||
coin2_batch_id,
|
||||
pair.clone().public().into(),
|
||||
)
|
||||
.await;
|
||||
coin2_batch_id += 1;
|
||||
|
||||
// add liquidity to pools
|
||||
common_add_liquidity(&serai,
|
||||
coin1,
|
||||
Amount(5_000_000_000_000_000), // monero has 12 decimals
|
||||
Amount(50_000_000_000),
|
||||
0,
|
||||
pair.clone()
|
||||
).await;
|
||||
common_add_liquidity(&serai,
|
||||
coin2,
|
||||
Amount(5_000_000_000_000), // ether still has 8 in our codebase
|
||||
Amount(500_000_000_000),
|
||||
1,
|
||||
pair.clone()
|
||||
).await;
|
||||
|
||||
// rand address bytes
|
||||
let mut rand_bytes = vec![0; 32];
|
||||
OsRng.fill_bytes(&mut rand_bytes);
|
||||
|
||||
// XMR -> ETH
|
||||
{
|
||||
// make an out address
|
||||
let out_address = OutAddress::External(ExternalAddress::new(rand_bytes.clone()).unwrap());
|
||||
|
||||
// amount is the min out amount
|
||||
let out_balance = Balance { coin: coin2.into(), amount: Amount(1) };
|
||||
|
||||
// now that we have our pools, we can try to swap
|
||||
let mut block_hash = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut block_hash.0);
|
||||
let batch = Batch {
|
||||
network: coin1.network(),
|
||||
id: coin1_batch_id,
|
||||
external_network_block_hash: block_hash,
|
||||
instructions: vec![InInstructionWithBalance {
|
||||
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address)),
|
||||
balance: ExternalBalance { coin: coin1, amount: Amount(200_000_000_000_000) },
|
||||
}],
|
||||
};
|
||||
|
||||
let block = provide_batch(&serai, batch).await;
|
||||
coin1_batch_id += 1;
|
||||
let mut events = serai.as_of(block).dex().events().await.unwrap();
|
||||
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
|
||||
|
||||
let path = BoundedVec::try_from(vec![coin1.into(), Coin::Serai, coin2.into()]).unwrap();
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![DexEvent::SwapExecuted {
|
||||
who: IN_INSTRUCTION_EXECUTOR,
|
||||
send_to: IN_INSTRUCTION_EXECUTOR,
|
||||
path,
|
||||
amount_in: 200_000_000_000_000,
|
||||
amount_out: 19_044_944_233
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
// ETH -> sriXMR
|
||||
{
|
||||
// make an out address
|
||||
let out_address =
|
||||
OutAddress::Serai(SeraiAddress::new(rand_bytes.clone().try_into().unwrap()));
|
||||
|
||||
// amount is the min out amount
|
||||
let out_balance = Balance { coin: coin1.into(), amount: Amount(1) };
|
||||
|
||||
// now that we have our pools, we can try to swap
|
||||
let mut block_hash = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut block_hash.0);
|
||||
let batch = Batch {
|
||||
network: coin2.network(),
|
||||
id: coin2_batch_id,
|
||||
external_network_block_hash: block_hash,
|
||||
instructions: vec![InInstructionWithBalance {
|
||||
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())),
|
||||
balance: ExternalBalance { coin: coin2, amount: Amount(200_000_000_000) },
|
||||
}],
|
||||
};
|
||||
|
||||
let block = provide_batch(&serai, batch).await;
|
||||
let mut events = serai.as_of(block).dex().events().await.unwrap();
|
||||
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
|
||||
|
||||
let path = BoundedVec::try_from(vec![coin2.into(), Coin::Serai, coin1.into()]).unwrap();
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![DexEvent::SwapExecuted {
|
||||
who: IN_INSTRUCTION_EXECUTOR,
|
||||
send_to: out_address.as_native().unwrap(),
|
||||
path,
|
||||
amount_in: 200_000_000_000,
|
||||
amount_out: 1487294253782353
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
// XMR -> SRI
|
||||
{
|
||||
// make an out address
|
||||
let out_address = OutAddress::Serai(SeraiAddress::new(rand_bytes.try_into().unwrap()));
|
||||
|
||||
// amount is the min out amount
|
||||
let out_balance = Balance { coin: Coin::Serai, amount: Amount(1) };
|
||||
|
||||
// now that we have our pools, we can try to swap
|
||||
let mut block_hash = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut block_hash.0);
|
||||
let batch = Batch {
|
||||
network: coin1.network(),
|
||||
id: coin1_batch_id,
|
||||
external_network_block_hash: block_hash,
|
||||
instructions: vec![InInstructionWithBalance {
|
||||
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())),
|
||||
balance: ExternalBalance { coin: coin1, amount: Amount(100_000_000_000_000) },
|
||||
}],
|
||||
};
|
||||
|
||||
let block = provide_batch(&serai, batch).await;
|
||||
let mut events = serai.as_of(block).dex().events().await.unwrap();
|
||||
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
|
||||
|
||||
let path = BoundedVec::try_from(vec![coin1.into(), Coin::Serai]).unwrap();
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![DexEvent::SwapExecuted {
|
||||
who: IN_INSTRUCTION_EXECUTOR,
|
||||
send_to: out_address.as_native().unwrap(),
|
||||
path,
|
||||
amount_in: 100_000_000_000_000,
|
||||
amount_out: 1_762_662_819
|
||||
}]
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -1,53 +0,0 @@
|
||||
use serai_client::{primitives::ExternalNetworkId, Serai};
|
||||
|
||||
#[tokio::test]
|
||||
async fn dht() {
|
||||
use dockertest::{
|
||||
PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image,
|
||||
TestBodySpecification, DockerTest,
|
||||
};
|
||||
|
||||
serai_docker_tests::build("serai".to_string());
|
||||
|
||||
let handle = |name: &str| format!("serai_client-serai_node-{name}");
|
||||
let composition = |name: &str| {
|
||||
TestBodySpecification::with_image(
|
||||
Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never),
|
||||
)
|
||||
.replace_env(
|
||||
[("SERAI_NAME".to_string(), name.to_string()), ("KEY".to_string(), " ".to_string())].into(),
|
||||
)
|
||||
.set_publish_all_ports(true)
|
||||
.set_handle(handle(name))
|
||||
.set_start_policy(StartPolicy::Strict)
|
||||
.set_log_options(Some(LogOptions {
|
||||
action: LogAction::Forward,
|
||||
policy: LogPolicy::Always,
|
||||
source: LogSource::Both,
|
||||
}))
|
||||
};
|
||||
|
||||
let mut test = DockerTest::new().with_network(dockertest::Network::Isolated);
|
||||
test.provide_container(composition("alice"));
|
||||
test.provide_container(composition("bob"));
|
||||
test.provide_container(composition("charlie"));
|
||||
test.provide_container(composition("dave"));
|
||||
test
|
||||
.run_async(|ops| async move {
|
||||
// Sleep until the Substrate RPC starts
|
||||
let alice = handle("alice");
|
||||
let serai_rpc = ops.handle(&alice).host_port(9944).unwrap();
|
||||
let serai_rpc = format!("http://{}:{}", serai_rpc.0, serai_rpc.1);
|
||||
// Sleep for a minute
|
||||
tokio::time::sleep(core::time::Duration::from_secs(60)).await;
|
||||
// Check the DHT has been populated
|
||||
assert!(!Serai::new(serai_rpc.clone())
|
||||
.await
|
||||
.unwrap()
|
||||
.p2p_validators(ExternalNetworkId::Bitcoin)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
})
|
||||
.await;
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
use std::{time::Duration, collections::HashMap};
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use serai_client::TemporalSerai;
|
||||
|
||||
use serai_abi::{
|
||||
primitives::{
|
||||
EXTERNAL_NETWORKS, NETWORKS, TARGET_BLOCK_TIME, FAST_EPOCH_DURATION, FAST_EPOCH_INITIAL_PERIOD,
|
||||
BlockHash, ExternalNetworkId, NetworkId, ExternalCoin, Amount, ExternalBalance,
|
||||
},
|
||||
validator_sets::primitives::Session,
|
||||
emissions::primitives::{INITIAL_REWARD_PER_BLOCK, SECURE_BY},
|
||||
in_instructions::primitives::Batch,
|
||||
};
|
||||
|
||||
use serai_client::Serai;
|
||||
|
||||
mod common;
|
||||
use common::{genesis_liquidity::set_up_genesis, in_instructions::provide_batch};
|
||||
|
||||
serai_test_fast_epoch!(
|
||||
emissions: (|serai: Serai| async move {
|
||||
test_emissions(serai).await;
|
||||
})
|
||||
);
|
||||
|
||||
async fn send_batches(serai: &Serai, ids: &mut HashMap<ExternalNetworkId, u32>) {
|
||||
for network in EXTERNAL_NETWORKS {
|
||||
// set up batch id
|
||||
ids
|
||||
.entry(network)
|
||||
.and_modify(|v| {
|
||||
*v += 1;
|
||||
})
|
||||
.or_insert(0);
|
||||
|
||||
// set up block hash
|
||||
let mut block = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut block.0);
|
||||
|
||||
provide_batch(
|
||||
serai,
|
||||
Batch {
|
||||
network,
|
||||
id: ids[&network],
|
||||
external_network_block_hash: block,
|
||||
instructions: vec![],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_emissions(serai: Serai) {
|
||||
// set up the genesis
|
||||
let values = HashMap::from([
|
||||
(ExternalCoin::Monero, 184100),
|
||||
(ExternalCoin::Ether, 4785000),
|
||||
(ExternalCoin::Dai, 1500),
|
||||
]);
|
||||
let (_, mut batch_ids) = set_up_genesis(&serai, &values).await;
|
||||
|
||||
// wait until genesis is complete
|
||||
let mut genesis_complete_block = None;
|
||||
while genesis_complete_block.is_none() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
genesis_complete_block = serai
|
||||
.as_of_latest_finalized_block()
|
||||
.await
|
||||
.unwrap()
|
||||
.genesis_liquidity()
|
||||
.genesis_complete_block()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
for _ in 0 .. 3 {
|
||||
// get current stakes
|
||||
let mut current_stake = HashMap::new();
|
||||
for n in NETWORKS {
|
||||
// TODO: investigate why serai network TAS isn't visible at session 0.
|
||||
let stake = serai
|
||||
.as_of_latest_finalized_block()
|
||||
.await
|
||||
.unwrap()
|
||||
.validator_sets()
|
||||
.total_allocated_stake(n)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_or(Amount(0))
|
||||
.0;
|
||||
current_stake.insert(n, stake);
|
||||
}
|
||||
|
||||
// wait for a session change
|
||||
let current_session = wait_for_session_change(&serai).await;
|
||||
|
||||
// get last block
|
||||
let last_block = serai.latest_finalized_block().await.unwrap();
|
||||
let serai_latest = serai.as_of(last_block.hash());
|
||||
let change_block_number = last_block.number();
|
||||
|
||||
// get distances to ec security & block count of the previous session
|
||||
let (distances, total_distance) = get_distances(&serai_latest, ¤t_stake).await;
|
||||
let block_count = get_session_blocks(&serai_latest, current_session - 1).await;
|
||||
|
||||
// calculate how much reward in this session
|
||||
let reward_this_epoch =
|
||||
if change_block_number < (genesis_complete_block.unwrap() + FAST_EPOCH_INITIAL_PERIOD) {
|
||||
block_count * INITIAL_REWARD_PER_BLOCK
|
||||
} else {
|
||||
let blocks_until = SECURE_BY - change_block_number;
|
||||
let block_reward = total_distance / blocks_until;
|
||||
block_count * block_reward
|
||||
};
|
||||
|
||||
let reward_per_network = distances
|
||||
.into_iter()
|
||||
.map(|(n, distance)| {
|
||||
let reward = u64::try_from(
|
||||
u128::from(reward_this_epoch).saturating_mul(u128::from(distance)) /
|
||||
u128::from(total_distance),
|
||||
)
|
||||
.unwrap();
|
||||
(n, reward)
|
||||
})
|
||||
.collect::<HashMap<NetworkId, u64>>();
|
||||
|
||||
// retire the prev-set so that TotalAllocatedStake updated.
|
||||
send_batches(&serai, &mut batch_ids).await;
|
||||
|
||||
for (n, reward) in reward_per_network {
|
||||
let stake = serai
|
||||
.as_of_latest_finalized_block()
|
||||
.await
|
||||
.unwrap()
|
||||
.validator_sets()
|
||||
.total_allocated_stake(n)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_or(Amount(0))
|
||||
.0;
|
||||
|
||||
// all reward should automatically staked for the network since we are in initial period.
|
||||
assert_eq!(stake, *current_stake.get(&n).unwrap() + reward);
|
||||
}
|
||||
|
||||
// TODO: check stake per address?
|
||||
// TODO: check post ec security era
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the required stake in terms SRI for a given `Balance`.
|
||||
async fn required_stake(serai: &TemporalSerai<'_>, balance: ExternalBalance) -> u64 {
|
||||
// This is inclusive to an increase in accuracy
|
||||
let sri_per_coin = serai.dex().oracle_value(balance.coin).await.unwrap().unwrap_or(Amount(0));
|
||||
|
||||
// See dex-pallet for the reasoning on these
|
||||
let coin_decimals = balance.coin.decimals().max(5);
|
||||
let accuracy_increase = u128::from(10u64.pow(coin_decimals));
|
||||
|
||||
let total_coin_value =
|
||||
u64::try_from(u128::from(balance.amount.0) * u128::from(sri_per_coin.0) / accuracy_increase)
|
||||
.unwrap_or(u64::MAX);
|
||||
|
||||
// required stake formula (COIN_VALUE * 1.5) + margin(20%)
|
||||
let required_stake = total_coin_value.saturating_mul(3).saturating_div(2);
|
||||
required_stake.saturating_add(total_coin_value.saturating_div(5))
|
||||
}
|
||||
|
||||
async fn wait_for_session_change(serai: &Serai) -> u32 {
|
||||
let current_session = serai
|
||||
.as_of_latest_finalized_block()
|
||||
.await
|
||||
.unwrap()
|
||||
.validator_sets()
|
||||
.session(NetworkId::Serai)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.0;
|
||||
let next_session = current_session + 1;
|
||||
|
||||
// lets wait double the epoch time.
|
||||
tokio::time::timeout(
|
||||
tokio::time::Duration::from_secs(FAST_EPOCH_DURATION * TARGET_BLOCK_TIME * 2),
|
||||
async {
|
||||
while serai
|
||||
.as_of_latest_finalized_block()
|
||||
.await
|
||||
.unwrap()
|
||||
.validator_sets()
|
||||
.session(NetworkId::Serai)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.0 <
|
||||
next_session
|
||||
{
|
||||
tokio::time::sleep(Duration::from_secs(6)).await;
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
next_session
|
||||
}
|
||||
|
||||
async fn get_distances(
|
||||
serai: &TemporalSerai<'_>,
|
||||
current_stake: &HashMap<NetworkId, u64>,
|
||||
) -> (HashMap<NetworkId, u64>, u64) {
|
||||
// we should be in the initial period, so calculate how much each network supposedly get..
|
||||
// we can check the supply to see how much coin hence liability we have.
|
||||
let mut distances: HashMap<NetworkId, u64> = HashMap::new();
|
||||
let mut total_distance = 0;
|
||||
for n in EXTERNAL_NETWORKS {
|
||||
let mut required = 0;
|
||||
for c in n.coins() {
|
||||
let amount = serai.coins().coin_supply(c.into()).await.unwrap();
|
||||
required += required_stake(serai, ExternalBalance { coin: c, amount }).await;
|
||||
}
|
||||
|
||||
let mut current = *current_stake.get(&n.into()).unwrap();
|
||||
if current > required {
|
||||
current = required;
|
||||
}
|
||||
|
||||
let distance = required - current;
|
||||
total_distance += distance;
|
||||
|
||||
distances.insert(n.into(), distance);
|
||||
}
|
||||
|
||||
// add serai network portion(20%)
|
||||
let new_total_distance = total_distance.saturating_mul(10) / 8;
|
||||
distances.insert(NetworkId::Serai, new_total_distance - total_distance);
|
||||
total_distance = new_total_distance;
|
||||
|
||||
(distances, total_distance)
|
||||
}
|
||||
|
||||
async fn get_session_blocks(serai: &TemporalSerai<'_>, session: u32) -> u64 {
|
||||
let begin_block = serai
|
||||
.validator_sets()
|
||||
.session_begin_block(NetworkId::Serai, Session(session))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let next_begin_block = serai
|
||||
.validator_sets()
|
||||
.session_begin_block(NetworkId::Serai, Session(session + 1))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
next_begin_block.saturating_sub(begin_block)
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
use std::{time::Duration, collections::HashMap};
|
||||
|
||||
use serai_client::Serai;
|
||||
|
||||
use serai_abi::primitives::{Amount, Coin, ExternalCoin, COINS, EXTERNAL_COINS, GENESIS_SRI};
|
||||
|
||||
use serai_client::genesis_liquidity::primitives::{
|
||||
GENESIS_LIQUIDITY_ACCOUNT, INITIAL_GENESIS_LP_SHARES,
|
||||
};
|
||||
|
||||
mod common;
|
||||
use common::genesis_liquidity::set_up_genesis;
|
||||
|
||||
serai_test_fast_epoch!(
|
||||
genesis_liquidity: (|serai: Serai| async move {
|
||||
test_genesis_liquidity(serai).await;
|
||||
})
|
||||
);
|
||||
|
||||
pub async fn test_genesis_liquidity(serai: Serai) {
|
||||
// set up the genesis
|
||||
let values = HashMap::from([
|
||||
(ExternalCoin::Monero, 184100),
|
||||
(ExternalCoin::Ether, 4785000),
|
||||
(ExternalCoin::Dai, 1500),
|
||||
]);
|
||||
let (accounts, _) = set_up_genesis(&serai, &values).await;
|
||||
|
||||
// wait until genesis is complete
|
||||
while serai
|
||||
.as_of_latest_finalized_block()
|
||||
.await
|
||||
.unwrap()
|
||||
.genesis_liquidity()
|
||||
.genesis_complete_block()
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
{
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
// check total SRI supply is +100M
|
||||
// there are 6 endowed accounts in dev-net. Take this into consideration when checking
|
||||
// for the total sri minted at this time.
|
||||
let serai = serai.as_of_latest_finalized_block().await.unwrap();
|
||||
let sri = serai.coins().coin_supply(Coin::Serai).await.unwrap();
|
||||
let endowed_amount: u64 = 1 << 60;
|
||||
let total_sri = (6 * endowed_amount) + GENESIS_SRI;
|
||||
assert_eq!(sri, Amount(total_sri));
|
||||
|
||||
// check genesis account has no coins, all transferred to pools.
|
||||
for coin in COINS {
|
||||
let amount = serai.coins().coin_balance(coin, GENESIS_LIQUIDITY_ACCOUNT).await.unwrap();
|
||||
assert_eq!(amount.0, 0);
|
||||
}
|
||||
|
||||
// check pools has proper liquidity
|
||||
let mut pool_amounts = HashMap::new();
|
||||
let mut total_value = 0u128;
|
||||
for coin in EXTERNAL_COINS {
|
||||
let total_coin = accounts[&coin].iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0));
|
||||
let value = if coin != ExternalCoin::Bitcoin {
|
||||
(total_coin * u128::from(values[&coin])) / 10u128.pow(coin.decimals())
|
||||
} else {
|
||||
total_coin
|
||||
};
|
||||
|
||||
total_value += value;
|
||||
pool_amounts.insert(coin, (total_coin, value));
|
||||
}
|
||||
|
||||
// check distributed SRI per pool
|
||||
let mut total_sri_distributed = 0u128;
|
||||
for coin in EXTERNAL_COINS {
|
||||
let sri = if coin == *EXTERNAL_COINS.last().unwrap() {
|
||||
u128::from(GENESIS_SRI).checked_sub(total_sri_distributed).unwrap()
|
||||
} else {
|
||||
(pool_amounts[&coin].1 * u128::from(GENESIS_SRI)) / total_value
|
||||
};
|
||||
total_sri_distributed += sri;
|
||||
|
||||
let reserves = serai.dex().get_reserves(coin).await.unwrap().unwrap();
|
||||
assert_eq!(u128::from(reserves.0 .0), pool_amounts[&coin].0); // coin side
|
||||
assert_eq!(u128::from(reserves.1 .0), sri); // SRI side
|
||||
}
|
||||
|
||||
// check each liquidity provider got liquidity tokens proportional to their value
|
||||
for coin in EXTERNAL_COINS {
|
||||
let liq_supply = serai.genesis_liquidity().supply(coin).await.unwrap();
|
||||
for (acc, amount) in &accounts[&coin] {
|
||||
let acc_liq_shares = serai.genesis_liquidity().liquidity(acc, coin).await.unwrap().shares;
|
||||
|
||||
// since we can't test the ratios directly(due to integer division giving 0)
|
||||
// we test whether they give the same result when multiplied by another constant.
|
||||
// Following test ensures the account in fact has the right amount of shares.
|
||||
let mut shares_ratio = (INITIAL_GENESIS_LP_SHARES * acc_liq_shares) / liq_supply.shares;
|
||||
let amounts_ratio =
|
||||
(INITIAL_GENESIS_LP_SHARES * amount.0) / u64::try_from(pool_amounts[&coin].0).unwrap();
|
||||
|
||||
// we can tolerate 1 unit diff between them due to integer division.
|
||||
if shares_ratio.abs_diff(amounts_ratio) == 1 {
|
||||
shares_ratio = amounts_ratio;
|
||||
}
|
||||
|
||||
assert_eq!(shares_ratio, amounts_ratio);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test remove the liq before/after genesis ended.
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use scale::Decode;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use ciphersuite::{
|
||||
group::{ff::Field, GroupEncoding},
|
||||
WrappedGroup,
|
||||
};
|
||||
use dalek_ff_group::Ed25519;
|
||||
use ciphersuite_kp256::Secp256k1;
|
||||
|
||||
use sp_core::{
|
||||
Pair as PairTrait,
|
||||
sr25519::{Public, Pair},
|
||||
};
|
||||
|
||||
use serai_abi::{
|
||||
in_instructions::primitives::Shorthand,
|
||||
primitives::{
|
||||
insecure_pair_from_name, ExternalBalance, ExternalCoin, ExternalNetworkId, QuotePriceParams,
|
||||
Amount,
|
||||
},
|
||||
validator_sets::primitives::{ExternalValidatorSet, KeyPair, Session},
|
||||
};
|
||||
use serai_client::{Serai, SeraiAddress};
|
||||
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
mod common;
|
||||
use common::{validator_sets::set_keys, in_instructions::mint_coin, dex::add_liquidity};
|
||||
|
||||
serai_test!(
|
||||
external_address: (|serai: Serai| async move {
|
||||
test_external_address(serai).await;
|
||||
})
|
||||
|
||||
encoded_shorthand: (|serai: Serai| async move {
|
||||
test_encoded_shorthand(serai).await;
|
||||
})
|
||||
|
||||
dex_quote_price: (|serai: Serai| async move {
|
||||
test_dex_quote_price(serai).await;
|
||||
})
|
||||
);
|
||||
|
||||
async fn set_network_keys<C: WrappedGroup>(
|
||||
serai: &Serai,
|
||||
set: ExternalValidatorSet,
|
||||
pairs: &[Pair],
|
||||
) {
|
||||
// Ristretto key
|
||||
let mut ristretto_key = [0; 32];
|
||||
OsRng.fill_bytes(&mut ristretto_key);
|
||||
|
||||
// network key
|
||||
let network_priv_key = Zeroizing::new(C::F::random(&mut OsRng));
|
||||
let network_key = (C::generator() * *network_priv_key).to_bytes().as_ref().to_vec();
|
||||
|
||||
let key_pair = KeyPair(Public(ristretto_key), network_key.try_into().unwrap());
|
||||
let _ = set_keys(serai, set, key_pair, pairs).await;
|
||||
}
|
||||
|
||||
async fn test_external_address(serai: Serai) {
|
||||
let pair = insecure_pair_from_name("Alice");
|
||||
|
||||
// set btc keys
|
||||
let network = ExternalNetworkId::Bitcoin;
|
||||
set_network_keys::<Secp256k1>(
|
||||
&serai,
|
||||
ExternalValidatorSet { session: Session(0), network },
|
||||
core::slice::from_ref(&pair),
|
||||
)
|
||||
.await;
|
||||
|
||||
// get the address from the node
|
||||
let btc_address: String = serai.external_network_address(network).await.unwrap();
|
||||
|
||||
// make sure it is a valid address
|
||||
let _ = bitcoin::Address::from_str(&btc_address)
|
||||
.unwrap()
|
||||
.require_network(bitcoin::Network::Bitcoin)
|
||||
.unwrap();
|
||||
|
||||
// set monero keys
|
||||
let network = ExternalNetworkId::Monero;
|
||||
set_network_keys::<Ed25519>(
|
||||
&serai,
|
||||
ExternalValidatorSet { session: Session(0), network },
|
||||
&[pair],
|
||||
)
|
||||
.await;
|
||||
|
||||
// get the address from the node
|
||||
let xmr_address: String = serai.external_network_address(network).await.unwrap();
|
||||
|
||||
// make sure it is a valid address
|
||||
let _ = monero_address::MoneroAddress::from_str(monero_address::Network::Mainnet, &xmr_address)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn test_encoded_shorthand(serai: Serai) {
|
||||
let shorthand = Shorthand::transfer(None, SeraiAddress::new([0u8; 32]));
|
||||
let encoded = serai.encoded_shorthand(shorthand.clone()).await.unwrap();
|
||||
|
||||
assert_eq!(Shorthand::decode::<&[u8]>(&mut encoded.as_slice()).unwrap(), shorthand);
|
||||
}
|
||||
|
||||
async fn test_dex_quote_price(serai: Serai) {
|
||||
// make a liquid pool to get the quote on
|
||||
let coin1 = ExternalCoin::Bitcoin;
|
||||
let coin2 = ExternalCoin::Monero;
|
||||
let amount1 = Amount(10u64.pow(coin1.decimals()));
|
||||
let amount2 = Amount(10u64.pow(coin2.decimals()));
|
||||
let pair = insecure_pair_from_name("Ferdie");
|
||||
|
||||
// mint sriBTC in the account so that we can add liq.
|
||||
// Ferdie account is already pre-funded with SRI.
|
||||
mint_coin(
|
||||
&serai,
|
||||
ExternalBalance { coin: coin1, amount: amount1 },
|
||||
0,
|
||||
pair.clone().public().into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// add liquidity
|
||||
let coin_amount = Amount(amount1.0 / 2);
|
||||
let sri_amount = Amount(amount1.0 / 2);
|
||||
let _ = add_liquidity(&serai, coin1, coin_amount, sri_amount, 0, pair.clone()).await;
|
||||
|
||||
// same for xmr
|
||||
mint_coin(
|
||||
&serai,
|
||||
ExternalBalance { coin: coin2, amount: amount2 },
|
||||
0,
|
||||
pair.clone().public().into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// add liquidity
|
||||
let coin_amount = Amount(amount2.0 / 2);
|
||||
let sri_amount = Amount(amount2.0 / 2);
|
||||
let _ = add_liquidity(&serai, coin2, coin_amount, sri_amount, 1, pair.clone()).await;
|
||||
|
||||
// price for BTC -> SRI -> XMR path
|
||||
let params = QuotePriceParams {
|
||||
coin1: coin1.into(),
|
||||
coin2: coin2.into(),
|
||||
amount: coin_amount.0 / 2,
|
||||
include_fee: true,
|
||||
exact_in: true,
|
||||
};
|
||||
|
||||
let res = serai.quote_price(params).await.unwrap();
|
||||
assert!(res > 0);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use tokio::time::sleep;
|
||||
|
||||
use serai_client::Serai;
|
||||
|
||||
mod common;
|
||||
|
||||
serai_test!(
|
||||
time: (|serai: Serai| async move {
|
||||
let mut number = serai.latest_finalized_block().await.unwrap().number();
|
||||
let mut done = 0;
|
||||
while done < 3 {
|
||||
// Wait for the next block
|
||||
let block = serai.latest_finalized_block().await.unwrap();
|
||||
if block.number() == number {
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
number = block.number();
|
||||
|
||||
// Make sure the time we extract from the block is within 5 seconds of now
|
||||
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
|
||||
assert!(now.saturating_sub(block.time().unwrap()) < 5);
|
||||
done += 1;
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -1,428 +0,0 @@
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use sp_core::{
|
||||
sr25519::{Public, Pair},
|
||||
Pair as PairTrait,
|
||||
};
|
||||
|
||||
use serai_client::{
|
||||
primitives::{
|
||||
FAST_EPOCH_DURATION, TARGET_BLOCK_TIME, NETWORKS, BlockHash, ExternalNetworkId, NetworkId,
|
||||
EmbeddedEllipticCurve, Amount, insecure_pair_from_name,
|
||||
},
|
||||
validator_sets::{
|
||||
primitives::{Session, ExternalValidatorSet, ValidatorSet, KeyPair},
|
||||
ValidatorSetsEvent,
|
||||
},
|
||||
in_instructions::{
|
||||
primitives::{Batch, SignedBatch, batch_message},
|
||||
SeraiInInstructions,
|
||||
},
|
||||
Serai,
|
||||
};
|
||||
|
||||
mod common;
|
||||
use common::{
|
||||
tx::publish_tx,
|
||||
validator_sets::{set_embedded_elliptic_curve_key, allocate_stake, deallocate_stake, set_keys},
|
||||
};
|
||||
|
||||
fn get_random_key_pair() -> KeyPair {
|
||||
let mut ristretto_key = [0; 32];
|
||||
OsRng.fill_bytes(&mut ristretto_key);
|
||||
let mut external_key = vec![0; 33];
|
||||
OsRng.fill_bytes(&mut external_key);
|
||||
KeyPair(Public(ristretto_key), external_key.try_into().unwrap())
|
||||
}
|
||||
|
||||
async fn get_ordered_keys(serai: &Serai, network: NetworkId, accounts: &[Pair]) -> Vec<Pair> {
|
||||
// retrieve the current session validators so that we know the order of the keys
|
||||
// that is necessary for the correct musig signature.
|
||||
let validators = serai
|
||||
.as_of_latest_finalized_block()
|
||||
.await
|
||||
.unwrap()
|
||||
.validator_sets()
|
||||
.active_network_validators(network)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// collect the pairs of the validators
|
||||
let mut pairs = vec![];
|
||||
for v in validators {
|
||||
let p = accounts.iter().find(|pair| pair.public() == v).unwrap().clone();
|
||||
pairs.push(p);
|
||||
}
|
||||
|
||||
pairs
|
||||
}
|
||||
|
||||
serai_test!(
|
||||
set_keys_test: (|serai: Serai| async move {
|
||||
let network = ExternalNetworkId::Bitcoin;
|
||||
let set = ExternalValidatorSet { session: Session(0), network };
|
||||
|
||||
let pair = insecure_pair_from_name("Alice");
|
||||
let public = pair.public();
|
||||
|
||||
// Neither of these keys are validated
|
||||
// The external key is infeasible to validate on-chain, the Ristretto key is feasible
|
||||
// TODO: Should the Ristretto key be validated?
|
||||
let key_pair = get_random_key_pair();
|
||||
|
||||
// Make sure the genesis is as expected
|
||||
assert_eq!(
|
||||
serai
|
||||
.as_of(serai.finalized_block_by_number(0).await.unwrap().unwrap().hash())
|
||||
.validator_sets()
|
||||
.new_set_events()
|
||||
.await
|
||||
.unwrap(),
|
||||
NETWORKS
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|network| ValidatorSetsEvent::NewSet {
|
||||
set: ValidatorSet { session: Session(0), network }
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
{
|
||||
let vs_serai = serai.as_of_latest_finalized_block().await.unwrap();
|
||||
let vs_serai = vs_serai.validator_sets();
|
||||
let participants = vs_serai.participants(set.network.into()).await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|(k, _)| k)
|
||||
.collect::<Vec<_>>();
|
||||
let participants_ref: &[_] = participants.as_ref();
|
||||
assert_eq!(participants_ref, [public].as_ref());
|
||||
}
|
||||
|
||||
let block = set_keys(&serai, set, key_pair.clone(), &[pair]).await;
|
||||
|
||||
// While the set_keys function should handle this, it's beneficial to
|
||||
// independently test it
|
||||
let serai = serai.as_of(block);
|
||||
let serai = serai.validator_sets();
|
||||
assert_eq!(
|
||||
serai.key_gen_events().await.unwrap(),
|
||||
vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }]
|
||||
);
|
||||
assert_eq!(serai.keys(set).await.unwrap(), Some(key_pair));
|
||||
})
|
||||
);
|
||||
|
||||
#[tokio::test]
|
||||
async fn validator_set_rotation() {
|
||||
use dockertest::{
|
||||
PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image,
|
||||
TestBodySpecification, DockerTest,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
serai_docker_tests::build("serai-fast-epoch".to_string());
|
||||
|
||||
let handle = |name| format!("serai_client-serai_node-{name}");
|
||||
let composition = |name| {
|
||||
TestBodySpecification::with_image(
|
||||
Image::with_repository("serai-dev-serai-fast-epoch").pull_policy(PullPolicy::Never),
|
||||
)
|
||||
.replace_cmd(vec![
|
||||
"serai-node".to_string(),
|
||||
"--unsafe-rpc-external".to_string(),
|
||||
"--rpc-cors".to_string(),
|
||||
"all".to_string(),
|
||||
"--chain".to_string(),
|
||||
"local".to_string(),
|
||||
format!("--{name}"),
|
||||
])
|
||||
.replace_env(HashMap::from([
|
||||
("RUST_LOG".to_string(), "runtime=debug".to_string()),
|
||||
("KEY".to_string(), " ".to_string()),
|
||||
]))
|
||||
.set_publish_all_ports(true)
|
||||
.set_handle(handle(name))
|
||||
.set_start_policy(StartPolicy::Strict)
|
||||
.set_log_options(Some(LogOptions {
|
||||
action: LogAction::Forward,
|
||||
policy: LogPolicy::Always,
|
||||
source: LogSource::Both,
|
||||
}))
|
||||
};
|
||||
|
||||
let mut test = DockerTest::new().with_network(dockertest::Network::Isolated);
|
||||
test.provide_container(composition("alice"));
|
||||
test.provide_container(composition("bob"));
|
||||
test.provide_container(composition("charlie"));
|
||||
test.provide_container(composition("dave"));
|
||||
test.provide_container(composition("eve"));
|
||||
test
|
||||
.run_async(|ops| async move {
|
||||
// Sleep until the Substrate RPC starts
|
||||
let alice = handle("alice");
|
||||
let alice_rpc = ops.handle(&alice).host_port(9944).unwrap();
|
||||
let alice_rpc = format!("http://{}:{}", alice_rpc.0, alice_rpc.1);
|
||||
|
||||
// Sleep for some time
|
||||
tokio::time::sleep(core::time::Duration::from_secs(20)).await;
|
||||
let serai = Serai::new(alice_rpc.clone()).await.unwrap();
|
||||
|
||||
// Make sure the genesis is as expected
|
||||
assert_eq!(
|
||||
serai
|
||||
.as_of(serai.finalized_block_by_number(0).await.unwrap().unwrap().hash())
|
||||
.validator_sets()
|
||||
.new_set_events()
|
||||
.await
|
||||
.unwrap(),
|
||||
NETWORKS
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|network| ValidatorSetsEvent::NewSet {
|
||||
set: ValidatorSet { session: Session(0), network }
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
// genesis accounts
|
||||
let accounts = vec![
|
||||
insecure_pair_from_name("Alice"),
|
||||
insecure_pair_from_name("Bob"),
|
||||
insecure_pair_from_name("Charlie"),
|
||||
insecure_pair_from_name("Dave"),
|
||||
insecure_pair_from_name("Eve"),
|
||||
];
|
||||
|
||||
// amounts for single key share per network
|
||||
let key_shares = HashMap::from([
|
||||
(NetworkId::Serai, Amount(50_000 * 10_u64.pow(8))),
|
||||
(NetworkId::External(ExternalNetworkId::Bitcoin), Amount(1_000_000 * 10_u64.pow(8))),
|
||||
(NetworkId::External(ExternalNetworkId::Monero), Amount(100_000 * 10_u64.pow(8))),
|
||||
(NetworkId::External(ExternalNetworkId::Ethereum), Amount(1_000_000 * 10_u64.pow(8))),
|
||||
]);
|
||||
|
||||
// genesis participants per network
|
||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||
let default_participants =
|
||||
accounts[.. 4].to_vec().iter().map(|pair| pair.public()).collect::<Vec<_>>();
|
||||
let mut participants = HashMap::from([
|
||||
(NetworkId::Serai, default_participants.clone()),
|
||||
(NetworkId::External(ExternalNetworkId::Bitcoin), default_participants.clone()),
|
||||
(NetworkId::External(ExternalNetworkId::Monero), default_participants.clone()),
|
||||
(NetworkId::External(ExternalNetworkId::Ethereum), default_participants),
|
||||
]);
|
||||
|
||||
// test the set rotation
|
||||
for (i, network) in NETWORKS.into_iter().enumerate() {
|
||||
let participants = participants.get_mut(&network).unwrap();
|
||||
|
||||
// we start the chain with 4 default participants that has a single key share each
|
||||
participants.sort();
|
||||
verify_session_and_active_validators(&serai, network, 0, participants).await;
|
||||
|
||||
// add 1 participant
|
||||
let last_participant = accounts[4].clone();
|
||||
|
||||
// If this is the first iteration, set embedded elliptic curve keys
|
||||
if i == 0 {
|
||||
for (i, embedded_elliptic_curve) in
|
||||
[EmbeddedEllipticCurve::Embedwards25519, EmbeddedEllipticCurve::Secq256k1]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
set_embedded_elliptic_curve_key(
|
||||
&serai,
|
||||
&last_participant,
|
||||
embedded_elliptic_curve,
|
||||
vec![
|
||||
0;
|
||||
match embedded_elliptic_curve {
|
||||
EmbeddedEllipticCurve::Embedwards25519 => 32,
|
||||
EmbeddedEllipticCurve::Secq256k1 => 33,
|
||||
}
|
||||
]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
i.try_into().unwrap(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let hash = allocate_stake(
|
||||
&serai,
|
||||
network,
|
||||
key_shares[&network],
|
||||
&last_participant,
|
||||
(2 + i).try_into().unwrap(),
|
||||
)
|
||||
.await;
|
||||
participants.push(last_participant.public());
|
||||
// the session at which set changes becomes active
|
||||
let activation_session = get_session_at_which_changes_activate(&serai, network, hash).await;
|
||||
|
||||
// set the keys if it is an external set
|
||||
if network != NetworkId::Serai {
|
||||
let set =
|
||||
ExternalValidatorSet { session: Session(0), network: network.try_into().unwrap() };
|
||||
let key_pair = get_random_key_pair();
|
||||
let pairs = get_ordered_keys(&serai, network, &accounts).await;
|
||||
set_keys(&serai, set, key_pair, &pairs).await;
|
||||
}
|
||||
|
||||
// verify
|
||||
participants.sort();
|
||||
verify_session_and_active_validators(&serai, network, activation_session, participants)
|
||||
.await;
|
||||
|
||||
// remove 1 participant
|
||||
let participant_to_remove = accounts[1].clone();
|
||||
let hash = deallocate_stake(
|
||||
&serai,
|
||||
network,
|
||||
key_shares[&network],
|
||||
&participant_to_remove,
|
||||
i.try_into().unwrap(),
|
||||
)
|
||||
.await;
|
||||
participants.swap_remove(
|
||||
participants.iter().position(|k| *k == participant_to_remove.public()).unwrap(),
|
||||
);
|
||||
let activation_session = get_session_at_which_changes_activate(&serai, network, hash).await;
|
||||
|
||||
if network != NetworkId::Serai {
|
||||
// set the keys if it is an external set
|
||||
let set =
|
||||
ExternalValidatorSet { session: Session(1), network: network.try_into().unwrap() };
|
||||
|
||||
// we need the whole substrate key pair to sign the batch
|
||||
let (substrate_pair, key_pair) = {
|
||||
let pair = insecure_pair_from_name("session-1-key-pair");
|
||||
let public = pair.public();
|
||||
|
||||
let mut external_key = vec![0; 33];
|
||||
OsRng.fill_bytes(&mut external_key);
|
||||
|
||||
(pair, KeyPair(public, external_key.try_into().unwrap()))
|
||||
};
|
||||
let pairs = get_ordered_keys(&serai, network, &accounts).await;
|
||||
set_keys(&serai, set, key_pair, &pairs).await;
|
||||
|
||||
// provide a batch to complete the handover and retire the previous set
|
||||
let mut block_hash = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut block_hash.0);
|
||||
let batch = Batch {
|
||||
network: network.try_into().unwrap(),
|
||||
id: 0,
|
||||
external_network_block_hash: block_hash,
|
||||
instructions: vec![],
|
||||
};
|
||||
publish_tx(
|
||||
&serai,
|
||||
&SeraiInInstructions::execute_batch(SignedBatch {
|
||||
batch: batch.clone(),
|
||||
signature: substrate_pair.sign(&batch_message(&batch)),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// verify
|
||||
participants.sort();
|
||||
verify_session_and_active_validators(&serai, network, activation_session, participants)
|
||||
.await;
|
||||
|
||||
// check pending deallocations
|
||||
let pending = serai
|
||||
.as_of_latest_finalized_block()
|
||||
.await
|
||||
.unwrap()
|
||||
.validator_sets()
|
||||
.pending_deallocations(
|
||||
network,
|
||||
participant_to_remove.public(),
|
||||
Session(activation_session + 1),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(pending, Some(key_shares[&network]));
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn session_for_block(serai: &Serai, block: [u8; 32], network: NetworkId) -> u32 {
|
||||
serai.as_of(block).validator_sets().session(network).await.unwrap().unwrap().0
|
||||
}
|
||||
|
||||
async fn verify_session_and_active_validators(
|
||||
serai: &Serai,
|
||||
network: NetworkId,
|
||||
session: u32,
|
||||
participants: &[Public],
|
||||
) {
|
||||
// wait until the active session.
|
||||
let block = tokio::time::timeout(
|
||||
core::time::Duration::from_secs(FAST_EPOCH_DURATION * TARGET_BLOCK_TIME * 2),
|
||||
async move {
|
||||
loop {
|
||||
let mut block = serai.latest_finalized_block_hash().await.unwrap();
|
||||
if session_for_block(serai, block, network).await < session {
|
||||
// Sleep a block
|
||||
tokio::time::sleep(core::time::Duration::from_secs(TARGET_BLOCK_TIME)).await;
|
||||
continue;
|
||||
}
|
||||
while session_for_block(serai, block, network).await > session {
|
||||
block = serai.block(block).await.unwrap().unwrap().header.parent_hash.0;
|
||||
}
|
||||
assert_eq!(session_for_block(serai, block, network).await, session);
|
||||
break block;
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let serai_for_block = serai.as_of(block);
|
||||
|
||||
// verify session
|
||||
let s = serai_for_block.validator_sets().session(network).await.unwrap().unwrap();
|
||||
assert_eq!(s.0, session);
|
||||
|
||||
// verify participants
|
||||
let mut validators =
|
||||
serai_for_block.validator_sets().active_network_validators(network).await.unwrap();
|
||||
validators.sort();
|
||||
assert_eq!(validators, participants);
|
||||
|
||||
// make sure finalization continues as usual after the changes
|
||||
let current_finalized_block = serai.latest_finalized_block().await.unwrap().header.number;
|
||||
tokio::time::timeout(core::time::Duration::from_secs(TARGET_BLOCK_TIME * 10), async move {
|
||||
let mut finalized_block = serai.latest_finalized_block().await.unwrap().header.number;
|
||||
while finalized_block <= current_finalized_block + 2 {
|
||||
tokio::time::sleep(core::time::Duration::from_secs(TARGET_BLOCK_TIME)).await;
|
||||
finalized_block = serai.latest_finalized_block().await.unwrap().header.number;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// TODO: verify key shares as well?
|
||||
}
|
||||
|
||||
async fn get_session_at_which_changes_activate(
|
||||
serai: &Serai,
|
||||
network: NetworkId,
|
||||
hash: [u8; 32],
|
||||
) -> u32 {
|
||||
let session = session_for_block(serai, hash, network).await;
|
||||
|
||||
// changes should be active in the next session
|
||||
if network == NetworkId::Serai {
|
||||
// it takes 1 extra session for serai net to make the changes active.
|
||||
session + 2
|
||||
} else {
|
||||
session + 1
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use sp_io::hashing::blake2_256;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{sync::Arc, ops::Deref, collections::HashSet};
|
||||
use core::{ops::Deref, future::Future};
|
||||
use std::{sync::Arc, collections::HashSet};
|
||||
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
@@ -8,8 +9,9 @@ use sp_consensus::BlockStatus;
|
||||
use sp_block_builder::BlockBuilder;
|
||||
use sp_api::ProvideRuntimeApi;
|
||||
use sc_client_api::BlockBackend;
|
||||
use sc_transaction_pool_api::TransactionPool;
|
||||
|
||||
use serai_abi::{primitives::prelude::*, SubstrateBlock as Block};
|
||||
use serai_abi::{primitives::prelude::*, Transaction, SubstrateBlock as Block};
|
||||
|
||||
use serai_runtime::SeraiApi;
|
||||
|
||||
@@ -27,6 +29,7 @@ pub(crate) fn module<
|
||||
+ ProvideRuntimeApi<Block, Api: SeraiApi<Block>>,
|
||||
>(
|
||||
client: Arc<C>,
|
||||
pool: Arc<impl 'static + TransactionPool<Block = Block>>,
|
||||
) -> Result<RpcModule<impl 'static + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut module = RpcModule::new(client);
|
||||
|
||||
@@ -80,5 +83,36 @@ pub(crate) fn module<
|
||||
)
|
||||
})?;
|
||||
|
||||
module.register_async_method("blockchain/publish_transaction", move |params, client, _ext| {
|
||||
let pool = pool.clone();
|
||||
async move {
|
||||
#[derive(sp_core::serde::Deserialize)]
|
||||
#[serde(crate = "sp_core::serde")]
|
||||
struct TransactionRequest {
|
||||
transaction: String,
|
||||
};
|
||||
let Ok(transaction) = params.parse::<TransactionRequest>() else {
|
||||
return Err(Error::InvalidRequest(r#"missing `string` "transaction" field"#));
|
||||
};
|
||||
let Ok(transaction) = hex::decode(transaction.transaction) else {
|
||||
Err(Error::InvalidRequest(r#"transaction was not hex-encoded"#))?
|
||||
};
|
||||
let Ok(transaction) =
|
||||
<Transaction as borsh::BorshDeserialize>::deserialize_reader(&mut transaction.as_slice())
|
||||
else {
|
||||
Err(Error::InvalidRequest(r#"transaction could not be deserialized"#))?
|
||||
};
|
||||
pool
|
||||
.submit_one(
|
||||
client.info().best_hash,
|
||||
sc_transaction_pool_api::TransactionSource::External,
|
||||
transaction,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Error::InvalidTransaction(format!("{e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(module)
|
||||
}
|
||||
|
||||
@@ -36,14 +36,14 @@ pub fn create_full<
|
||||
+ HeaderBackend<Block>
|
||||
+ HeaderMetadata<Block, Error = BlockchainError>
|
||||
+ BlockBackend<Block>,
|
||||
P: 'static + TransactionPool,
|
||||
P: 'static + TransactionPool<Block = Block>,
|
||||
>(
|
||||
deps: FullDeps<C, P>,
|
||||
) -> Result<RpcModule<()>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let FullDeps { id, client, pool, authority_discovery } = deps;
|
||||
|
||||
let mut root = RpcModule::new(());
|
||||
root.merge(blockchain::module(client.clone())?)?;
|
||||
root.merge(blockchain::module(client.clone(), pool)?)?;
|
||||
root.merge(validator_sets::module(client.clone()))?;
|
||||
if let Some(authority_discovery) = authority_discovery {
|
||||
root.merge(p2p_validators::module(id, client, authority_discovery)?)?;
|
||||
|
||||
@@ -48,7 +48,10 @@ pub(crate) fn module<
|
||||
Err(e) => Err(e)?,
|
||||
};
|
||||
// Always return the protocol's bootnodes
|
||||
let mut all_p2p_addresses = crate::chain_spec::bootnode_multiaddrs(id);
|
||||
let mut all_p2p_addresses = crate::chain_spec::bootnode_multiaddrs(id)
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
// Additionally returns validators found over the DHT
|
||||
for validator in validators {
|
||||
let mut returned_addresses = authority_discovery
|
||||
@@ -66,9 +69,11 @@ pub(crate) fn module<
|
||||
// It isn't beneficial to use multiple addresses for a single peer here
|
||||
if !returned_addresses.is_empty() {
|
||||
all_p2p_addresses.push(
|
||||
returned_addresses
|
||||
.remove(usize::try_from(OsRng.next_u64() >> 32).unwrap() % returned_addresses.len())
|
||||
.into(),
|
||||
libp2p::Multiaddr::from(
|
||||
returned_addresses
|
||||
.remove(usize::try_from(OsRng.next_u64() >> 32).unwrap() % returned_addresses.len()),
|
||||
)
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ pub(super) enum Error {
|
||||
Internal(&'static str),
|
||||
InvalidRequest(&'static str),
|
||||
InvalidStateReference,
|
||||
InvalidTransaction(String),
|
||||
}
|
||||
|
||||
impl From<Error> for jsonrpsee::types::error::ErrorObjectOwned {
|
||||
@@ -19,10 +20,15 @@ impl From<Error> for jsonrpsee::types::error::ErrorObjectOwned {
|
||||
jsonrpsee::types::error::ErrorObjectOwned::owned(-2, str, Option::<()>::None)
|
||||
}
|
||||
Error::InvalidStateReference => jsonrpsee::types::error::ErrorObjectOwned::owned(
|
||||
-4,
|
||||
-3,
|
||||
"the block used as the reference was not locally held",
|
||||
Option::<()>::None,
|
||||
),
|
||||
Error::InvalidTransaction(str) => jsonrpsee::types::error::ErrorObjectOwned::owned(
|
||||
-4,
|
||||
format!("transaction was not accepted to the mempool: {str}"),
|
||||
Option::<()>::None,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{sync::Arc, ops::Deref, convert::AsRef, collections::HashSet};
|
||||
use core::{ops::Deref, convert::AsRef, str::FromStr};
|
||||
use std::{sync::Arc, collections::HashSet};
|
||||
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
@@ -111,5 +112,74 @@ pub(crate) fn module<
|
||||
Ok(key_pair.map(|key_pair| hex::encode(borsh::to_vec(&key_pair).unwrap())))
|
||||
});
|
||||
|
||||
module.register_method(
|
||||
"validator-sets/current_validators",
|
||||
|params, client, _ext| -> Result<_, Error> {
|
||||
let Some(block_hash) = block_hash(&**client, ¶ms)? else {
|
||||
Err(Error::InvalidStateReference)?
|
||||
};
|
||||
let network = network(¶ms)?;
|
||||
let Ok(validators) = client.runtime_api().current_validators(block_hash, network) else {
|
||||
Err(Error::Internal("couldn't fetch the current validators for the requested network"))?
|
||||
};
|
||||
Ok(
|
||||
validators.map(|validators| validators.iter().map(ToString::to_string).collect::<Vec<_>>()),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
module.register_method(
|
||||
"validator-sets/pending_slash_report",
|
||||
|params, client, _ext| -> Result<_, Error> {
|
||||
let Some(block_hash) = block_hash(&**client, ¶ms)? else {
|
||||
Err(Error::InvalidStateReference)?
|
||||
};
|
||||
let Ok(network) = ExternalNetworkId::try_from(network(¶ms)?) else {
|
||||
Err(Error::InvalidRequest(
|
||||
"asking if a non-external validator set has a pending slash report",
|
||||
))?
|
||||
};
|
||||
client
|
||||
.runtime_api()
|
||||
.pending_slash_report(block_hash, network)
|
||||
.map_err(|_| Error::Internal("couldn't fetch if this network has a pending slash report"))
|
||||
},
|
||||
);
|
||||
|
||||
module.register_method(
|
||||
"validator-sets/embedded_elliptic_curve_keys",
|
||||
|params, client, _ext| -> Result<_, Error> {
|
||||
let Some(block_hash) = block_hash(&**client, ¶ms)? else {
|
||||
Err(Error::InvalidStateReference)?
|
||||
};
|
||||
|
||||
#[derive(sp_core::serde::Deserialize)]
|
||||
#[serde(crate = "sp_core::serde")]
|
||||
struct Validator {
|
||||
validator: String,
|
||||
}
|
||||
let Ok(validator) = params.parse::<Validator>() else {
|
||||
Err(Error::InvalidRequest(r#"missing `string` "validator" field"#))?
|
||||
};
|
||||
let Ok(validator) = SeraiAddress::from_str(&validator.validator) else {
|
||||
Err(Error::InvalidRequest(r#"validator had an invalid address"#))?
|
||||
};
|
||||
|
||||
let Ok(network) = ExternalNetworkId::try_from(network(¶ms)?) else {
|
||||
Err(Error::InvalidRequest(
|
||||
"asking for the embedded elliptic curve keys for a non-external network",
|
||||
))?
|
||||
};
|
||||
let Ok(embedded_elliptic_curve_keys) =
|
||||
client.runtime_api().embedded_elliptic_curve_keys(block_hash, validator, network)
|
||||
else {
|
||||
Err(Error::Internal("couldn't fetch the keys for the requested validator set"))?
|
||||
};
|
||||
Ok(embedded_elliptic_curve_keys.map(|embedded_elliptic_curve_keys| {
|
||||
hex::encode(borsh::to_vec(&embedded_elliptic_curve_keys).unwrap())
|
||||
}))
|
||||
},
|
||||
);
|
||||
|
||||
module
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
@@ -217,6 +217,7 @@ pub struct SlashReport(
|
||||
);
|
||||
|
||||
/// An error when converting from a `Vec`.
|
||||
#[derive(Debug)]
|
||||
pub enum FromVecError {
|
||||
/// The source `Vec` was too long to be converted.
|
||||
TooLong,
|
||||
|
||||
@@ -6,10 +6,11 @@ extern crate alloc;
|
||||
use alloc::vec::Vec;
|
||||
use serai_abi::{
|
||||
primitives::{
|
||||
crypto::{Public, SignedEmbeddedEllipticCurveKeys, KeyPair},
|
||||
network_id::NetworkId,
|
||||
crypto::{Public, EmbeddedEllipticCurveKeys, SignedEmbeddedEllipticCurveKeys, KeyPair},
|
||||
network_id::{ExternalNetworkId, NetworkId},
|
||||
validator_sets::{Session, ExternalValidatorSet, ValidatorSet},
|
||||
balance::{Amount, Balance},
|
||||
address::SeraiAddress,
|
||||
},
|
||||
Event,
|
||||
};
|
||||
@@ -39,6 +40,12 @@ sp_api::decl_runtime_apis! {
|
||||
fn current_session(network: NetworkId) -> Option<Session>;
|
||||
fn current_stake(network: NetworkId) -> Option<Amount>;
|
||||
fn keys(set: ExternalValidatorSet) -> Option<KeyPair>;
|
||||
fn current_validators(network: NetworkId) -> Option<Vec<SeraiAddress>>;
|
||||
fn pending_slash_report(network: ExternalNetworkId) -> bool;
|
||||
fn embedded_elliptic_curve_keys(
|
||||
validator: SeraiAddress,
|
||||
network: ExternalNetworkId,
|
||||
) -> Option<EmbeddedEllipticCurveKeys>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +190,7 @@ mod apis {
|
||||
unimplemented!("runtime is only implemented when WASM")
|
||||
}
|
||||
fn validators(
|
||||
network: serai_abi::primitives::network_id::NetworkId
|
||||
network: NetworkId
|
||||
) -> Vec<serai_abi::primitives::crypto::Public> {
|
||||
unimplemented!("runtime is only implemented when WASM")
|
||||
}
|
||||
@@ -196,6 +203,18 @@ mod apis {
|
||||
fn keys(set: ExternalValidatorSet) -> Option<KeyPair> {
|
||||
unimplemented!("runtime is only implemented when WASM")
|
||||
}
|
||||
fn current_validators(network: NetworkId) -> Option<Vec<SeraiAddress>> {
|
||||
unimplemented!("runtime is only implemented when WASM")
|
||||
}
|
||||
fn pending_slash_report(network: ExternalNetworkId) -> bool {
|
||||
unimplemented!("runtime is only implemented when WASM")
|
||||
}
|
||||
fn embedded_elliptic_curve_keys(
|
||||
validator: SeraiAddress,
|
||||
network: ExternalNetworkId,
|
||||
) -> Option<EmbeddedEllipticCurveKeys> {
|
||||
unimplemented!("runtime is only implemented when WASM")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use sp_version::RuntimeVersion;
|
||||
|
||||
use serai_abi::{
|
||||
primitives::{
|
||||
crypto::EmbeddedEllipticCurveKeys,
|
||||
network_id::{ExternalNetworkId, NetworkId},
|
||||
balance::{Amount, ExternalBalance},
|
||||
validator_sets::{Session, ExternalValidatorSet, ValidatorSet},
|
||||
@@ -582,6 +583,23 @@ sp_api::impl_runtime_apis! {
|
||||
})
|
||||
})
|
||||
}
|
||||
fn current_validators(network: NetworkId) -> Option<Vec<SeraiAddress>> {
|
||||
let session = ValidatorSets::current_session(network)?;
|
||||
Some(
|
||||
ValidatorSets::selected_validators(ValidatorSet { network, session })
|
||||
.map(|(key, _key_shares)| SeraiAddress::from(key))
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
fn pending_slash_report(network: ExternalNetworkId) -> bool {
|
||||
ValidatorSets::pending_slash_report(network)
|
||||
}
|
||||
fn embedded_elliptic_curve_keys(
|
||||
validator: SeraiAddress,
|
||||
network: ExternalNetworkId,
|
||||
) -> Option<EmbeddedEllipticCurveKeys> {
|
||||
ValidatorSets::embedded_elliptic_curve_keys(validator.into(), network)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use sp_core::sr25519::Public;
|
||||
|
||||
use serai_abi::primitives::{crypto::SignedEmbeddedEllipticCurveKeys, network_id::*};
|
||||
use serai_abi::primitives::{
|
||||
crypto::{
|
||||
EmbeddedEllipticCurveKeys as EmbeddedEllipticCurveKeysStruct, SignedEmbeddedEllipticCurveKeys,
|
||||
},
|
||||
network_id::*,
|
||||
};
|
||||
|
||||
use frame_support::storage::StorageDoubleMap;
|
||||
|
||||
@@ -11,8 +16,8 @@ pub(crate) trait EmbeddedEllipticCurveKeysStorage {
|
||||
type EmbeddedEllipticCurveKeys: StorageDoubleMap<
|
||||
ExternalNetworkId,
|
||||
Public,
|
||||
serai_abi::primitives::crypto::EmbeddedEllipticCurveKeys,
|
||||
Query = Option<serai_abi::primitives::crypto::EmbeddedEllipticCurveKeys>,
|
||||
EmbeddedEllipticCurveKeysStruct,
|
||||
Query = Option<EmbeddedEllipticCurveKeysStruct>,
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -23,6 +28,13 @@ pub(crate) trait EmbeddedEllipticCurveKeys {
|
||||
validator: Public,
|
||||
keys: SignedEmbeddedEllipticCurveKeys,
|
||||
) -> Result<(), ()>;
|
||||
|
||||
/// Get a validator's embedded elliptic curve keys, for an external network.
|
||||
fn embedded_elliptic_curve_keys(
|
||||
validator: Public,
|
||||
network: ExternalNetworkId,
|
||||
) -> Option<EmbeddedEllipticCurveKeysStruct>;
|
||||
|
||||
/// Check if a validator still needs to set embedded elliptic curve keys.
|
||||
fn still_needs_to_set_embedded_elliptic_curve_keys(network: NetworkId, validator: Public)
|
||||
-> bool;
|
||||
@@ -39,6 +51,14 @@ impl<S: EmbeddedEllipticCurveKeysStorage> EmbeddedEllipticCurveKeys for S {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a validator's embedded elliptic curve keys, for an external network.
|
||||
fn embedded_elliptic_curve_keys(
|
||||
validator: Public,
|
||||
network: ExternalNetworkId,
|
||||
) -> Option<EmbeddedEllipticCurveKeysStruct> {
|
||||
S::EmbeddedEllipticCurveKeys::get(network, validator)
|
||||
}
|
||||
|
||||
/// Check if a validator still needs to set embedded elliptic curve keys.
|
||||
fn still_needs_to_set_embedded_elliptic_curve_keys(
|
||||
network: NetworkId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
@@ -32,7 +32,10 @@ mod pallet {
|
||||
|
||||
use serai_abi::{
|
||||
primitives::{
|
||||
crypto::{SignedEmbeddedEllipticCurveKeys, ExternalKey, KeyPair, Signature},
|
||||
crypto::{
|
||||
EmbeddedEllipticCurveKeys as EmbeddedEllipticCurveKeysStruct,
|
||||
SignedEmbeddedEllipticCurveKeys, ExternalKey, KeyPair, Signature,
|
||||
},
|
||||
network_id::*,
|
||||
coin::*,
|
||||
balance::*,
|
||||
@@ -315,6 +318,19 @@ mod pallet {
|
||||
Abstractions::<T>::external_key(set)
|
||||
}
|
||||
|
||||
pub fn pending_slash_report(network: ExternalNetworkId) -> bool {
|
||||
Abstractions::<T>::waiting_for_slash_report(network).is_some()
|
||||
}
|
||||
|
||||
pub fn embedded_elliptic_curve_keys(
|
||||
validator: Public,
|
||||
network: ExternalNetworkId,
|
||||
) -> Option<EmbeddedEllipticCurveKeysStruct> {
|
||||
<Abstractions<T> as crate::EmbeddedEllipticCurveKeys>::embedded_elliptic_curve_keys(
|
||||
validator, network,
|
||||
)
|
||||
}
|
||||
|
||||
/* TODO
|
||||
pub fn distribute_block_rewards(
|
||||
network: NetworkId,
|
||||
|
||||
Reference in New Issue
Block a user