Merge branch 'emissions' of https://github.com/akildemir/serai into block-emissions

This commit is contained in:
akildemir
2024-05-16 11:35:56 +03:00
27 changed files with 856 additions and 374 deletions

67
Cargo.lock generated
View File

@@ -102,7 +102,7 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "alloy-consensus"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-eips",
"alloy-primitives",
@@ -110,7 +110,6 @@ dependencies = [
"alloy-serde",
"c-kzg",
"serde",
"sha2",
]
[[package]]
@@ -125,7 +124,7 @@ dependencies = [
[[package]]
name = "alloy-eips"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-primitives",
"alloy-rlp",
@@ -133,23 +132,25 @@ dependencies = [
"c-kzg",
"once_cell",
"serde",
"sha2",
]
[[package]]
name = "alloy-genesis"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-primitives",
"alloy-serde",
"serde",
"serde_json",
]
[[package]]
name = "alloy-json-abi"
version = "0.7.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a35ddfd27576474322a5869e4c123e5f3e7b2177297c18e4e82ea501cb125b"
checksum = "786689872ec4e7d354810ab0dffd48bb40b838c047522eb031cbd47d15634849"
dependencies = [
"alloy-primitives",
"alloy-sol-type-parser",
@@ -159,7 +160,7 @@ dependencies = [
[[package]]
name = "alloy-json-rpc"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-primitives",
"serde",
@@ -171,7 +172,7 @@ dependencies = [
[[package]]
name = "alloy-network"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -179,6 +180,7 @@ dependencies = [
"alloy-primitives",
"alloy-rpc-types",
"alloy-signer",
"alloy-sol-types",
"async-trait",
"futures-utils-wasm",
"thiserror",
@@ -187,7 +189,7 @@ dependencies = [
[[package]]
name = "alloy-node-bindings"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-genesis",
"alloy-primitives",
@@ -201,9 +203,9 @@ dependencies = [
[[package]]
name = "alloy-primitives"
version = "0.7.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99bbad0a6b588ef4aec1b5ddbbfdacd9ef04e00b979617765b03174318ee1f3a"
checksum = "525448f6afc1b70dd0f9d0a8145631bf2f5e434678ab23ab18409ca264cae6b3"
dependencies = [
"alloy-rlp",
"bytes",
@@ -224,7 +226,7 @@ dependencies = [
[[package]]
name = "alloy-provider"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-eips",
"alloy-json-rpc",
@@ -271,7 +273,7 @@ dependencies = [
[[package]]
name = "alloy-rpc-client"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-json-rpc",
"alloy-transport",
@@ -289,7 +291,7 @@ dependencies = [
[[package]]
name = "alloy-rpc-types"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -307,7 +309,7 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-trace"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-primitives",
"alloy-rpc-types",
@@ -319,7 +321,7 @@ dependencies = [
[[package]]
name = "alloy-serde"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-primitives",
"serde",
@@ -329,7 +331,7 @@ dependencies = [
[[package]]
name = "alloy-signer"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-primitives",
"async-trait",
@@ -352,9 +354,9 @@ dependencies = [
[[package]]
name = "alloy-sol-macro"
version = "0.7.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "452d929748ac948a10481fff4123affead32c553cf362841c5103dd508bdfc16"
checksum = "89c80a2cb97e7aa48611cbb63950336f9824a174cdf670527cc6465078a26ea1"
dependencies = [
"alloy-json-abi",
"alloy-sol-macro-input",
@@ -371,9 +373,9 @@ dependencies = [
[[package]]
name = "alloy-sol-macro-input"
version = "0.7.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df64e094f6d2099339f9e82b5b38440b159757b6920878f28316243f8166c8d1"
checksum = "c58894b58ac50979eeac6249661991ac40b9d541830d9a725f7714cc9ef08c23"
dependencies = [
"alloy-json-abi",
"const-hex",
@@ -388,18 +390,18 @@ dependencies = [
[[package]]
name = "alloy-sol-type-parser"
version = "0.7.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "715f4d09a330cc181fc7c361b5c5c2766408fa59a0bac60349dcb7baabd404cc"
checksum = "7da8e71ea68e780cc203919e03f69f59e7afe92d2696fb1dcb6662f61e4031b6"
dependencies = [
"winnow 0.6.6",
]
[[package]]
name = "alloy-sol-types"
version = "0.7.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43bc2d6dfc2a19fd56644494479510f98b1ee929e04cf0d4aa45e98baa3e545b"
checksum = "399287f68d1081ed8b1f4903c49687658b95b142207d7cb4ae2f4813915343ef"
dependencies = [
"alloy-json-abi",
"alloy-primitives",
@@ -410,7 +412,7 @@ dependencies = [
[[package]]
name = "alloy-transport"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-json-rpc",
"base64 0.22.0",
@@ -428,7 +430,7 @@ dependencies = [
[[package]]
name = "alloy-transport-http"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy?rev=037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6#037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6"
source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42"
dependencies = [
"alloy-transport",
"url",
@@ -2123,7 +2125,7 @@ dependencies = [
[[package]]
name = "dockertest"
version = "0.4.0"
source = "git+https://github.com/kayabaNerve/dockertest-rs?branch=arc#c0ea77997048f9edc9987984bbe20e43fac74e06"
source = "git+https://github.com/orcalabs/dockertest-rs?rev=4dd6ae24738aa6dc5c89444cc822ea4745517493#4dd6ae24738aa6dc5c89444cc822ea4745517493"
dependencies = [
"anyhow",
"async-trait",
@@ -2331,6 +2333,7 @@ version = "0.1.0"
dependencies = [
"alloy-consensus",
"alloy-core",
"alloy-network",
"alloy-node-bindings",
"alloy-provider",
"alloy-rpc-client",
@@ -3790,7 +3793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.4",
]
[[package]]
@@ -8123,11 +8126,13 @@ dependencies = [
"curve25519-dalek",
"dkg",
"dockertest",
"ethereum-serai",
"hex",
"monero-serai",
"parity-scale-codec",
"rand_core",
"serai-client",
"serai-db",
"serai-docker-tests",
"serai-message-queue",
"serai-message-queue-tests",
@@ -9401,9 +9406,9 @@ dependencies = [
[[package]]
name = "syn-solidity"
version = "0.7.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4497156948bd342b52038035a6fa514a89626e37af9d2c52a5e8d8ebcc7ee479"
checksum = "5aa0cefd02f532035d83cfec82647c6eb53140b0485220760e669f4bad489e36"
dependencies = [
"paste",
"proc-macro2",

View File

@@ -110,7 +110,7 @@ panic = "unwind"
lazy_static = { git = "https://github.com/rust-lang-nursery/lazy-static.rs", rev = "5735630d46572f1e5377c8f2ba0f79d18f53b10c" }
# Needed due to dockertest's usage of `Rc`s when we need `Arc`s
dockertest = { git = "https://github.com/kayabaNerve/dockertest-rs", branch = "arc" }
dockertest = { git = "https://github.com/orcalabs/dockertest-rs", rev = "4dd6ae24738aa6dc5c89444cc822ea4745517493" }
# wasmtime pulls in an old version for this
zstd = { path = "patches/zstd" }

View File

@@ -29,18 +29,21 @@ frost = { package = "modular-frost", path = "../../crypto/frost", default-featur
alloy-core = { version = "0.7", default-features = false }
alloy-sol-types = { version = "0.7", default-features = false, features = ["json"] }
alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false, features = ["k256"] }
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false }
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false }
alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false, features = ["k256"] }
alloy-network = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false }
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false }
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false }
alloy-simple-request-transport = { path = "./alloy-simple-request-transport", default-features = false }
alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false }
alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false }
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false, optional = true }
[dev-dependencies]
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false, features = ["tests"] }
tokio = { version = "1", features = ["macros"] }
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false }
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false }
[features]
tests = []
tests = ["alloy-node-bindings"]

View File

@@ -21,8 +21,8 @@ tower = "0.4"
serde_json = { version = "1", default-features = false }
simple-request = { path = "../../../common/request", default-features = false }
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false }
alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false }
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false }
alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false }
[features]
default = ["tls"]

View File

@@ -31,7 +31,10 @@ pub fn address(point: &ProjectivePoint) -> [u8; 20] {
keccak256(&encoded_point.as_ref()[1 .. 65])[12 ..].try_into().unwrap()
}
pub(crate) fn deterministically_sign(tx: &TxLegacy) -> Signed<TxLegacy> {
/// Deterministically sign a transaction.
///
/// This function panics if passed a transaction with a non-None chain ID.
pub fn deterministically_sign(tx: &TxLegacy) -> Signed<TxLegacy> {
assert!(
tx.chain_id.is_none(),
"chain ID was Some when deterministically signing a TX (causing a non-deterministic signer)"

View File

@@ -4,7 +4,7 @@ use alloy_core::primitives::{Address, B256, U256};
use alloy_sol_types::{SolInterface, SolEvent};
use alloy_rpc_types::{BlockNumberOrTag, Filter};
use alloy_rpc_types::Filter;
use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider};
@@ -25,22 +25,8 @@ pub struct TopLevelErc20Transfer {
pub struct Erc20(Arc<RootProvider<SimpleRequest>>, Address);
impl Erc20 {
/// Construct a new view of the specified ERC20 contract.
///
/// This checks a contract is deployed at that address yet does not check the contract is
/// actually an ERC20.
pub async fn new(
provider: Arc<RootProvider<SimpleRequest>>,
address: [u8; 20],
) -> Result<Option<Self>, Error> {
let code = provider
.get_code_at(address.into(), BlockNumberOrTag::Finalized.into())
.await
.map_err(|_| Error::ConnectionError)?;
// Contract has yet to be deployed
if code.is_empty() {
return Ok(None);
}
Ok(Some(Self(provider.clone(), Address::from(&address))))
pub fn new(provider: Arc<RootProvider<SimpleRequest>>, address: [u8; 20]) -> Self {
Self(provider, Address::from(&address))
}
pub async fn top_level_transfers(
@@ -65,7 +51,8 @@ impl Erc20 {
}
let tx_id = log.transaction_hash.ok_or(Error::ConnectionError)?;
let tx = self.0.get_transaction_by_hash(tx_id).await.map_err(|_| Error::ConnectionError)?;
let tx =
self.0.get_transaction_by_hash(tx_id).await.ok().flatten().ok_or(Error::ConnectionError)?;
// If this is a top-level call...
if tx.to == Some(self.1) {

View File

@@ -1,12 +1,17 @@
use thiserror::Error;
pub use alloy_core;
pub use alloy_consensus;
pub mod alloy {
pub use alloy_core::primitives;
pub use alloy_core as core;
pub use alloy_sol_types as sol_types;
pub use alloy_rpc_types;
pub use alloy_simple_request_transport;
pub use alloy_rpc_client;
pub use alloy_provider;
pub use alloy_consensus as consensus;
pub use alloy_network as network;
pub use alloy_rpc_types as rpc_types;
pub use alloy_simple_request_transport as simple_request_transport;
pub use alloy_rpc_client as rpc_client;
pub use alloy_provider as provider;
}
pub mod crypto;
@@ -18,8 +23,8 @@ pub mod router;
pub mod machine;
#[cfg(test)]
mod tests;
#[cfg(any(test, feature = "tests"))]
pub mod tests;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
pub enum Error {

View File

@@ -159,11 +159,12 @@ impl Router {
#[cfg(test)]
pub async fn serai_key(&self, at: [u8; 32]) -> Result<PublicKey, Error> {
let call = TransactionRequest::default()
.to(Some(self.1))
.to(self.1)
.input(TransactionInput::new(abi::seraiKeyCall::new(()).abi_encode().into()));
let bytes = self
.0
.call(&call, Some(BlockId::Hash(B256::from(at).into())))
.call(&call)
.block(BlockId::Hash(B256::from(at).into()))
.await
.map_err(|_| Error::ConnectionError)?;
let res =
@@ -197,11 +198,12 @@ impl Router {
#[cfg(test)]
pub async fn nonce(&self, at: [u8; 32]) -> Result<U256, Error> {
let call = TransactionRequest::default()
.to(Some(self.1))
.to(self.1)
.input(TransactionInput::new(abi::nonceCall::new(()).abi_encode().into()));
let bytes = self
.0
.call(&call, Some(BlockId::Hash(B256::from(at).into())))
.call(&call)
.block(BlockId::Hash(B256::from(at).into()))
.await
.map_err(|_| Error::ConnectionError)?;
let res =
@@ -229,10 +231,13 @@ impl Router {
}
}
pub async fn key_at_end_of_block(&self, block: u64) -> Result<ProjectivePoint, Error> {
pub async fn key_at_end_of_block(&self, block: u64) -> Result<Option<ProjectivePoint>, Error> {
let filter = Filter::new().from_block(0).to_block(block).address(self.1);
let filter = filter.event_signature(SeraiKeyUpdated::SIGNATURE_HASH);
let all_keys = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
if all_keys.is_empty() {
return Ok(None);
};
let last_key_x_coordinate_log = all_keys.last().ok_or(Error::ConnectionError)?;
let last_key_x_coordinate = last_key_x_coordinate_log
@@ -246,7 +251,9 @@ impl Router {
compressed_point[0] = u8::from(sec1::Tag::CompressedEvenY);
compressed_point[1 ..].copy_from_slice(last_key_x_coordinate.as_slice());
Option::from(ProjectivePoint::from_bytes(&compressed_point)).ok_or(Error::ConnectionError)
let key =
Option::from(ProjectivePoint::from_bytes(&compressed_point)).ok_or(Error::ConnectionError)?;
Ok(Some(key))
}
pub async fn in_instructions(
@@ -254,7 +261,9 @@ impl Router {
block: u64,
allowed_tokens: &HashSet<[u8; 20]>,
) -> Result<Vec<InInstruction>, Error> {
let key_at_end_of_block = self.key_at_end_of_block(block).await?;
let Some(key_at_end_of_block) = self.key_at_end_of_block(block).await? else {
return Ok(vec![]);
};
let filter = Filter::new().from_block(block).to_block(block).address(self.1);
let filter = filter.event_signature(InInstructionEvent::SIGNATURE_HASH);
@@ -274,7 +283,13 @@ impl Router {
);
let tx_hash = log.transaction_hash.ok_or(Error::ConnectionError)?;
let tx = self.0.get_transaction_by_hash(tx_hash).await.map_err(|_| Error::ConnectionError)?;
let tx = self
.0
.get_transaction_by_hash(tx_hash)
.await
.ok()
.flatten()
.ok_or(Error::ConnectionError)?;
let log =
log.log_decode::<InInstructionEvent>().map_err(|_| Error::ConnectionError)?.inner.data;

View File

@@ -11,16 +11,20 @@ use alloy_core::{
};
use alloy_consensus::{SignableTransaction, TxLegacy};
use alloy_rpc_types::TransactionReceipt;
use alloy_rpc_types::{BlockNumberOrTag, TransactionReceipt};
use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider};
use crate::crypto::{address, deterministically_sign, PublicKey};
#[cfg(test)]
mod crypto;
#[cfg(test)]
mod abi;
#[cfg(test)]
mod schnorr;
#[cfg(test)]
mod router;
pub fn key_gen() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, PublicKey) {
@@ -53,14 +57,15 @@ pub async fn send(
// let chain_id = provider.get_chain_id().await.unwrap();
// tx.chain_id = Some(chain_id);
tx.chain_id = None;
tx.nonce = provider.get_transaction_count(address, None).await.unwrap();
tx.nonce =
provider.get_transaction_count(address, BlockNumberOrTag::Latest.into()).await.unwrap();
// 100 gwei
tx.gas_price = 100_000_000_000u128;
let sig = wallet.sign_prehash_recoverable(tx.signature_hash().as_ref()).unwrap();
assert_eq!(address, tx.clone().into_signed(sig.into()).recover_signer().unwrap());
assert!(
provider.get_balance(address, None).await.unwrap() >
provider.get_balance(address, BlockNumberOrTag::Latest.into()).await.unwrap() >
((U256::from(tx.gas_price) * U256::from(tx.gas_limit)) + tx.value)
);

View File

@@ -56,12 +56,12 @@ pub async fn call_verify(
let px: [u8; 32] = public_key.px.to_repr().into();
let c_bytes: [u8; 32] = signature.c.to_repr().into();
let s_bytes: [u8; 32] = signature.s.to_repr().into();
let call = TransactionRequest::default().to(Some(contract)).input(TransactionInput::new(
let call = TransactionRequest::default().to(contract).input(TransactionInput::new(
abi::verifyCall::new((px.into(), message.to_vec().into(), c_bytes.into(), s_bytes.into()))
.abi_encode()
.into(),
));
let bytes = provider.call(&call, None).await.map_err(|_| Error::ConnectionError)?;
let bytes = provider.call(&call).await.map_err(|_| Error::ConnectionError)?;
let res =
abi::verifyCall::abi_decode_returns(&bytes, true).map_err(|_| Error::ConnectionError)?;

View File

@@ -55,6 +55,8 @@ impl Client {
fn connector() -> Connector {
let mut res = HttpConnector::new();
res.set_keepalive(Some(core::time::Duration::from_secs(60)));
res.set_nodelay(true);
res.set_reuse_address(true);
#[cfg(feature = "tls")]
let res = HttpsConnectorBuilder::new()
.with_native_roots()
@@ -68,7 +70,9 @@ impl Client {
pub fn with_connection_pool() -> Client {
Client {
connection: Connection::ConnectionPool(
HyperClient::builder(TokioExecutor::new()).build(Self::connector()),
HyperClient::builder(TokioExecutor::new())
.pool_idle_timeout(core::time::Duration::from_secs(60))
.build(Self::connector()),
),
}
}

View File

@@ -104,5 +104,5 @@ allow-git = [
"https://github.com/serai-dex/substrate",
"https://github.com/alloy-rs/alloy",
"https://github.com/monero-rs/base58-monero",
"https://github.com/kayabaNerve/dockertest-rs",
"https://github.com/orcalabs/dockertest-rs",
]

View File

@@ -1,3 +1,3 @@
#!/bin/sh
~/.foundry/bin/anvil --no-mining --slots-in-an-epoch 32
~/.foundry/bin/anvil --host 0.0.0.0 --no-cors --no-mining --slots-in-an-epoch 32 --silent

View File

@@ -84,7 +84,7 @@ serai-docker-tests = { path = "../tests/docker" }
secp256k1 = ["k256", "frost/secp256k1"]
bitcoin = ["dep:secp256k1", "secp256k1", "bitcoin-serai", "serai-client/bitcoin"]
ethereum = ["secp256k1", "ethereum-serai"]
ethereum = ["secp256k1", "ethereum-serai/tests"]
ed25519 = ["dalek-ff-group", "frost/ed25519"]
monero = ["ed25519", "monero-serai", "serai-client/monero"]

View File

@@ -116,7 +116,7 @@ impl<N: Network<Scheduler = Self>> SchedulerTrait<N> for Scheduler<N> {
assert!(self.coins.contains(&utxo.balance().coin));
}
let mut nonce = LastNonce::get(txn).map_or(0, |nonce| nonce + 1);
let mut nonce = LastNonce::get(txn).map_or(1, |nonce| nonce + 1);
let mut plans = vec![];
for chunk in payments.as_slice().chunks(N::MAX_OUTPUTS) {
// Once we rotate, all further payments should be scheduled via the new multisig
@@ -179,7 +179,7 @@ impl<N: Network<Scheduler = Self>> SchedulerTrait<N> for Scheduler<N> {
.and_then(|key_bytes| <N::Curve as Ciphersuite>::read_G(&mut key_bytes.as_slice()).ok())
.unwrap_or(self.key);
let nonce = LastNonce::get(txn).map_or(0, |nonce| nonce + 1);
let nonce = LastNonce::get(txn).map_or(1, |nonce| nonce + 1);
LastNonce::set(txn, &(nonce + 1));
Plan {
key: current_key,

View File

@@ -11,11 +11,13 @@ use ciphersuite::{group::GroupEncoding, Ciphersuite, Secp256k1};
use frost::ThresholdKeys;
use ethereum_serai::{
alloy_core::primitives::U256,
alloy_rpc_types::{BlockNumberOrTag, Transaction},
alloy_simple_request_transport::SimpleRequest,
alloy_rpc_client::ClientBuilder,
alloy_provider::{Provider, RootProvider},
alloy::{
primitives::U256,
rpc_types::{BlockNumberOrTag, Transaction},
simple_request_transport::SimpleRequest,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
},
crypto::{PublicKey, Signature},
erc20::Erc20,
deployer::Deployer,
@@ -23,7 +25,7 @@ use ethereum_serai::{
machine::*,
};
#[cfg(test)]
use ethereum_serai::alloy_core::primitives::B256;
use ethereum_serai::alloy::primitives::B256;
use tokio::{
time::sleep,
@@ -112,7 +114,7 @@ impl TryInto<Vec<u8>> for Address {
impl fmt::Display for Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
ethereum_serai::alloy_core::primitives::Address::from(self.0).fmt(f)
ethereum_serai::alloy::primitives::Address::from(self.0).fmt(f)
}
}
@@ -124,7 +126,7 @@ impl SignableTransaction for RouterCommand {
}
#[async_trait]
impl<D: fmt::Debug + Db> TransactionTrait<Ethereum<D>> for Transaction {
impl<D: Db> TransactionTrait<Ethereum<D>> for Transaction {
type Id = [u8; 32];
fn id(&self) -> Self::Id {
self.hash.0
@@ -157,7 +159,7 @@ impl Epoch {
}
#[async_trait]
impl<D: fmt::Debug + Db> Block<Ethereum<D>> for Epoch {
impl<D: Db> Block<Ethereum<D>> for Epoch {
type Id = [u8; 32];
fn id(&self) -> [u8; 32] {
self.end_hash
@@ -170,7 +172,7 @@ impl<D: fmt::Debug + Db> Block<Ethereum<D>> for Epoch {
}
}
impl<D: fmt::Debug + Db> Output<Ethereum<D>> for EthereumInInstruction {
impl<D: Db> Output<Ethereum<D>> for EthereumInInstruction {
type Id = [u8; 32];
fn kind(&self) -> OutputType {
@@ -181,7 +183,7 @@ impl<D: fmt::Debug + Db> Output<Ethereum<D>> for EthereumInInstruction {
let mut id = [0; 40];
id[.. 32].copy_from_slice(&self.id.0);
id[32 ..].copy_from_slice(&self.id.1.to_le_bytes());
*ethereum_serai::alloy_core::primitives::keccak256(id)
*ethereum_serai::alloy::primitives::keccak256(id)
}
fn tx_id(&self) -> [u8; 32] {
self.id.0
@@ -282,8 +284,8 @@ impl EventualityTrait for Eventuality {
}
}
#[derive(Clone, Debug)]
pub struct Ethereum<D: fmt::Debug + Db> {
#[derive(Clone)]
pub struct Ethereum<D: Db> {
// This DB is solely used to access the first key generated, as needed to determine the Router's
// address. Accordingly, all methods present are consistent to a Serai chain with a finalized
// first key (regardless of local state), and this is safe.
@@ -292,20 +294,26 @@ pub struct Ethereum<D: fmt::Debug + Db> {
deployer: Deployer,
router: Arc<RwLock<Option<Router>>>,
}
impl<D: fmt::Debug + Db> PartialEq for Ethereum<D> {
impl<D: Db> PartialEq for Ethereum<D> {
fn eq(&self, _other: &Ethereum<D>) -> bool {
true
}
}
impl<D: fmt::Debug + Db> Ethereum<D> {
impl<D: Db> fmt::Debug for Ethereum<D> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt
.debug_struct("Ethereum")
.field("deployer", &self.deployer)
.field("router", &self.router)
.finish_non_exhaustive()
}
}
impl<D: Db> Ethereum<D> {
pub async fn new(db: D, url: String) -> Self {
let provider = Arc::new(RootProvider::new(
ClientBuilder::default().transport(SimpleRequest::new(url), true),
));
#[cfg(test)] // TODO: Move to test code
provider.raw_request::<_, ()>("evm_setAutomine".into(), false).await.unwrap();
let mut deployer = Deployer::new(provider.clone()).await;
while !matches!(deployer, Ok(Some(_))) {
log::error!("Deployer wasn't deployed yet or networking error");
@@ -362,7 +370,7 @@ impl<D: fmt::Debug + Db> Ethereum<D> {
}
#[async_trait]
impl<D: fmt::Debug + Db> Network for Ethereum<D> {
impl<D: Db> Network for Ethereum<D> {
type Curve = Secp256k1;
type Transaction = Transaction;
@@ -479,7 +487,8 @@ impl<D: fmt::Debug + Db> Network for Ethereum<D> {
// Grab the key at the end of the epoch
let key_at_end_of_block = loop {
match router.key_at_end_of_block(block.start + 31).await {
Ok(key) => break key,
Ok(Some(key)) => break key,
Ok(None) => return vec![],
Err(e) => {
log::error!("couldn't connect to router for the key at the end of the block: {e:?}");
sleep(Duration::from_secs(5)).await;
@@ -491,17 +500,7 @@ impl<D: fmt::Debug + Db> Network for Ethereum<D> {
let mut all_events = vec![];
let mut top_level_txids = HashSet::new();
for erc20_addr in [DAI] {
let erc20 = loop {
let Ok(Some(erc20)) = Erc20::new(self.provider.clone(), erc20_addr).await else {
log::error!(
"couldn't connect to Ethereum node for an ERC20: {}",
hex::encode(erc20_addr)
);
sleep(Duration::from_secs(5)).await;
continue;
};
break erc20;
};
let erc20 = Erc20::new(self.provider.clone(), erc20_addr);
for block in block.start .. (block.start + 32) {
let transfers = loop {
@@ -722,22 +721,6 @@ impl<D: fmt::Debug + Db> Network for Ethereum<D> {
// Publish this using a dummy account we fund with magic RPC commands
#[cfg(test)]
{
use rand_core::OsRng;
use ciphersuite::group::ff::Field;
let key = <Secp256k1 as Ciphersuite>::F::random(&mut OsRng);
let address = ethereum_serai::crypto::address(&(Secp256k1::generator() * key));
// Set a 1.1 ETH balance
self
.provider
.raw_request::<_, ()>(
"anvil_setBalance".into(),
[Address(address).to_string(), "1100000000000000000".into()],
)
.await
.unwrap();
let router = self.router().await;
let router = router.as_ref().unwrap();
@@ -750,17 +733,30 @@ impl<D: fmt::Debug + Db> Network for Ethereum<D> {
completion.signature(),
),
};
tx.gas_price = 100_000_000_000u128;
tx.gas_limit = 1_000_000u64.into();
tx.gas_price = 1_000_000_000u64.into();
let tx = ethereum_serai::crypto::deterministically_sign(&tx);
use ethereum_serai::alloy_consensus::SignableTransaction;
let sig =
k256::ecdsa::SigningKey::from(k256::elliptic_curve::NonZeroScalar::new(key).unwrap())
.sign_prehash_recoverable(tx.signature_hash().as_ref())
if self.provider.get_transaction_by_hash(*tx.hash()).await.unwrap().is_none() {
self
.provider
.raw_request::<_, ()>(
"anvil_setBalance".into(),
[
tx.recover_signer().unwrap().to_string(),
(U256::from(tx.tx().gas_limit) * U256::from(tx.tx().gas_price)).to_string(),
],
)
.await
.unwrap();
let mut bytes = vec![];
tx.encode_with_signature_fields(&sig.into(), &mut bytes);
let _ = self.provider.send_raw_transaction(&bytes).await.ok().unwrap();
let (tx, sig, _) = tx.into_parts();
let mut bytes = vec![];
tx.encode_with_signature_fields(&sig, &mut bytes);
let pending_tx = self.provider.send_raw_transaction(&bytes).await.unwrap();
self.mine_block().await;
assert!(pending_tx.get_receipt().await.unwrap().status());
}
Ok(())
}
@@ -804,46 +800,62 @@ impl<D: fmt::Debug + Db> Network for Ethereum<D> {
block: usize,
eventuality: &Self::Eventuality,
) -> Self::Transaction {
match eventuality.1 {
RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => {
let router = self.router().await;
let router = router.as_ref().unwrap();
// We mine 96 blocks to ensure the 32 blocks relevant are finalized
// Back-check the prior two epochs in response to this
// TODO: Review why this is sub(3) and not sub(2)
for block in block.saturating_sub(3) ..= block {
match eventuality.1 {
RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => {
let router = self.router().await;
let router = router.as_ref().unwrap();
let block = u64::try_from(block).unwrap();
let filter = router
.key_updated_filter()
.from_block(block * 32)
.to_block(((block + 1) * 32) - 1)
.topic1(nonce);
let logs = self.provider.get_logs(&filter).await.unwrap();
if let Some(log) = logs.first() {
let block = u64::try_from(block).unwrap();
let filter = router
.key_updated_filter()
.from_block(block * 32)
.to_block(((block + 1) * 32) - 1)
.topic1(nonce);
let logs = self.provider.get_logs(&filter).await.unwrap();
if let Some(log) = logs.first() {
return self
.provider
.get_transaction_by_hash(log.clone().transaction_hash.unwrap())
.await
.unwrap()
.unwrap();
};
let filter = router
.executed_filter()
.from_block(block * 32)
.to_block(((block + 1) * 32) - 1)
.topic1(nonce);
let logs = self.provider.get_logs(&filter).await.unwrap();
if logs.is_empty() {
continue;
}
return self
.provider
.get_transaction_by_hash(log.clone().transaction_hash.unwrap())
.get_transaction_by_hash(logs[0].transaction_hash.unwrap())
.await
.unwrap()
.unwrap();
};
let filter = router
.executed_filter()
.from_block(block * 32)
.to_block(((block + 1) * 32) - 1)
.topic1(nonce);
let logs = self.provider.get_logs(&filter).await.unwrap();
self.provider.get_transaction_by_hash(logs[0].transaction_hash.unwrap()).await.unwrap()
}
}
}
panic!("couldn't find completion in any three of checked blocks");
}
#[cfg(test)]
async fn mine_block(&self) {
self.provider.raw_request::<_, ()>("anvil_mine".into(), [32]).await.unwrap();
self.provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap();
}
#[cfg(test)]
async fn test_send(&self, send_to: Self::Address) -> Self::Block {
use rand_core::OsRng;
use ciphersuite::group::ff::Field;
use ethereum_serai::alloy::sol_types::SolCall;
let key = <Secp256k1 as Ciphersuite>::F::random(&mut OsRng);
let address = ethereum_serai::crypto::address(&(Secp256k1::generator() * key));
@@ -858,18 +870,25 @@ impl<D: fmt::Debug + Db> Network for Ethereum<D> {
.await
.unwrap();
let tx = ethereum_serai::alloy_consensus::TxLegacy {
let value = U256::from_str_radix("1000000000000000000", 10).unwrap();
let tx = ethereum_serai::alloy::consensus::TxLegacy {
chain_id: None,
nonce: 0,
gas_price: 100_000_000_000u128,
gas_limit: 21_0000u128,
to: ethereum_serai::alloy_core::primitives::TxKind::Call(send_to.0.into()),
gas_price: 1_000_000_000u128,
gas_limit: 200_000u128,
to: ethereum_serai::alloy::primitives::TxKind::Call(send_to.0.into()),
// 1 ETH
value: U256::from_str_radix("1000000000000000000", 10).unwrap(),
input: vec![].into(),
value,
input: ethereum_serai::router::abi::inInstructionCall::new((
[0; 20].into(),
value,
vec![].into(),
))
.abi_encode()
.into(),
};
use ethereum_serai::alloy_consensus::SignableTransaction;
use ethereum_serai::alloy::consensus::SignableTransaction;
let sig = k256::ecdsa::SigningKey::from(k256::elliptic_curve::NonZeroScalar::new(key).unwrap())
.sign_prehash_recoverable(tx.signature_hash().as_ref())
.unwrap();

View File

@@ -432,9 +432,12 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Debug {
let plan_id = plan.id();
let Plan { key, inputs, mut payments, change, scheduler_addendum } = plan;
let theoretical_change_amount =
let theoretical_change_amount = if change.is_some() {
inputs.iter().map(|input| input.balance().amount.0).sum::<u64>() -
payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>();
payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>()
} else {
0
};
let Some(tx_fee) = self.needed_fee(block_number, &inputs, &payments, &change).await? else {
// This Plan is not fulfillable

View File

@@ -1,4 +1,4 @@
use core::time::Duration;
use core::{time::Duration, pin::Pin, future::Future};
use std::collections::HashMap;
use rand_core::OsRng;
@@ -82,8 +82,9 @@ async fn spend<N: UtxoNetwork, D: Db>(
}
}
pub async fn test_addresses<N: UtxoNetwork>(network: N)
where
pub async fn test_addresses<N: UtxoNetwork>(
new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
) where
<N::Scheduler as Scheduler<N>>::Addendum: From<()>,
{
let mut keys = frost::tests::key_gen::<_, N::Curve>(&mut OsRng);
@@ -92,12 +93,14 @@ where
}
let key = keys[&Participant::new(1).unwrap()].group_key();
let mut db = MemDb::new();
let network = new_network(db.clone()).await;
// Mine blocks so there's a confirmed block
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
let mut db = MemDb::new();
let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone());
assert!(current_keys.is_empty());
let mut txn = db.txn();

View File

@@ -3,6 +3,8 @@ use dockertest::{
TestBodySpecification, DockerOperations, DockerTest,
};
use serai_db::MemDb;
#[cfg(feature = "bitcoin")]
mod bitcoin {
use std::sync::Arc;
@@ -33,8 +35,6 @@ mod bitcoin {
sync::Mutex,
};
use serai_db::MemDb;
use super::*;
use crate::{
networks::{Network, Bitcoin, Output, OutputType, Block},
@@ -57,7 +57,7 @@ mod bitcoin {
fn test_receive_data_from_input() {
let docker = spawn_bitcoin();
docker.run(|ops| async move {
let btc = bitcoin(&ops).await;
let btc = bitcoin(&ops).await(MemDb::new()).await;
// generate a multisig address to receive the coins
let mut keys = frost::tests::key_gen::<_, <Bitcoin as Network>::Curve>(&mut OsRng)
@@ -208,23 +208,26 @@ mod bitcoin {
test
}
async fn bitcoin(ops: &DockerOperations) -> Bitcoin {
async fn bitcoin(
ops: &DockerOperations,
) -> impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = Bitcoin>>> {
let handle = ops.handle("serai-dev-bitcoin").host_port(8332).unwrap();
let bitcoin = Bitcoin::new(format!("http://serai:seraidex@{}:{}", handle.0, handle.1)).await;
let url = format!("http://serai:seraidex@{}:{}", handle.0, handle.1);
let bitcoin = Bitcoin::new(url.clone()).await;
bitcoin.fresh_chain().await;
bitcoin
move |_db| Box::pin(Bitcoin::new(url.clone()))
}
test_network!(
test_utxo_network!(
Bitcoin,
spawn_bitcoin,
bitcoin,
bitcoin_key_gen,
bitcoin_scanner,
bitcoin_no_deadlock_in_multisig_completed,
bitcoin_signer,
bitcoin_wallet,
bitcoin_addresses,
bitcoin_no_deadlock_in_multisig_completed,
);
}
@@ -252,24 +255,185 @@ mod monero {
test
}
async fn monero(ops: &DockerOperations) -> Monero {
async fn monero(
ops: &DockerOperations,
) -> impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = Monero>>> {
let handle = ops.handle("serai-dev-monero").host_port(18081).unwrap();
let monero = Monero::new(format!("http://serai:seraidex@{}:{}", handle.0, handle.1)).await;
let url = format!("http://serai:seraidex@{}:{}", handle.0, handle.1);
let monero = Monero::new(url.clone()).await;
while monero.get_latest_block_number().await.unwrap() < 150 {
monero.mine_block().await;
}
monero
move |_db| Box::pin(Monero::new(url.clone()))
}
test_network!(
test_utxo_network!(
Monero,
spawn_monero,
monero,
monero_key_gen,
monero_scanner,
monero_no_deadlock_in_multisig_completed,
monero_signer,
monero_wallet,
monero_addresses,
monero_no_deadlock_in_multisig_completed,
);
}
#[cfg(feature = "ethereum")]
mod ethereum {
use super::*;
use ciphersuite::{Ciphersuite, Secp256k1};
use serai_client::validator_sets::primitives::Session;
use crate::networks::Ethereum;
fn spawn_ethereum() -> DockerTest {
serai_docker_tests::build("ethereum".to_string());
let composition = TestBodySpecification::with_image(
Image::with_repository("serai-dev-ethereum").pull_policy(PullPolicy::Never),
)
.set_start_policy(StartPolicy::Strict)
.set_log_options(Some(LogOptions {
action: LogAction::Forward,
policy: LogPolicy::OnError,
source: LogSource::Both,
}))
.set_publish_all_ports(true);
let mut test = DockerTest::new();
test.provide_container(composition);
test
}
async fn ethereum(
ops: &DockerOperations,
) -> impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = Ethereum<MemDb>>>> {
use std::sync::Arc;
use ethereum_serai::{
alloy::{
primitives::U256,
simple_request_transport::SimpleRequest,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
},
deployer::Deployer,
};
let handle = ops.handle("serai-dev-ethereum").host_port(8545).unwrap();
let url = format!("http://{}:{}", handle.0, handle.1);
tokio::time::sleep(core::time::Duration::from_secs(15)).await;
{
let provider = Arc::new(RootProvider::new(
ClientBuilder::default().transport(SimpleRequest::new(url.clone()), true),
));
provider.raw_request::<_, ()>("evm_setAutomine".into(), [false]).await.unwrap();
provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap();
// Perform deployment
{
// Make sure the Deployer constructor returns None, as it doesn't exist yet
assert!(Deployer::new(provider.clone()).await.unwrap().is_none());
// Deploy the Deployer
let tx = Deployer::deployment_tx();
provider
.raw_request::<_, ()>(
"anvil_setBalance".into(),
[
tx.recover_signer().unwrap().to_string(),
(U256::from(tx.tx().gas_limit) * U256::from(tx.tx().gas_price)).to_string(),
],
)
.await
.unwrap();
let (tx, sig, _) = tx.into_parts();
let mut bytes = vec![];
tx.encode_with_signature_fields(&sig, &mut bytes);
let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap();
provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap();
//tokio::time::sleep(core::time::Duration::from_secs(15)).await;
let receipt = pending_tx.get_receipt().await.unwrap();
assert!(receipt.status());
let _ = Deployer::new(provider.clone())
.await
.expect("network error")
.expect("deployer wasn't deployed");
}
}
move |db| {
let url = url.clone();
Box::pin(async move {
{
let db = db.clone();
let url = url.clone();
// Spawn a task to deploy the proper Router when the time comes
tokio::spawn(async move {
let key = loop {
let Some(key) = crate::key_gen::NetworkKeyDb::get(&db, Session(0)) else {
tokio::time::sleep(core::time::Duration::from_secs(1)).await;
continue;
};
break ethereum_serai::crypto::PublicKey::new(
Secp256k1::read_G(&mut key.as_slice()).unwrap(),
)
.unwrap();
};
let provider = Arc::new(RootProvider::new(
ClientBuilder::default().transport(SimpleRequest::new(url.clone()), true),
));
let deployer = Deployer::new(provider.clone()).await.unwrap().unwrap();
let mut tx = deployer.deploy_router(&key);
tx.gas_limit = 1_000_000u64.into();
tx.gas_price = 1_000_000_000u64.into();
let tx = ethereum_serai::crypto::deterministically_sign(&tx);
provider
.raw_request::<_, ()>(
"anvil_setBalance".into(),
[
tx.recover_signer().unwrap().to_string(),
(U256::from(tx.tx().gas_limit) * U256::from(tx.tx().gas_price)).to_string(),
],
)
.await
.unwrap();
let (tx, sig, _) = tx.into_parts();
let mut bytes = vec![];
tx.encode_with_signature_fields(&sig, &mut bytes);
let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap();
provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap();
let receipt = pending_tx.get_receipt().await.unwrap();
assert!(receipt.status());
let _router = deployer.find_router(provider.clone(), &key).await.unwrap().unwrap();
});
}
Ethereum::new(db, url.clone()).await
})
}
}
test_network!(
Ethereum<MemDb>,
spawn_ethereum,
ethereum,
ethereum_key_gen,
ethereum_scanner,
ethereum_no_deadlock_in_multisig_completed,
ethereum_signer,
ethereum_wallet,
);
}

View File

@@ -1,22 +1,18 @@
use std::sync::OnceLock;
mod key_gen;
pub(crate) use key_gen::test_key_gen;
mod scanner;
pub(crate) use scanner::{test_scanner, test_no_deadlock_in_multisig_completed};
mod signer;
pub(crate) use signer::{sign, test_signer};
pub(crate) use signer::sign;
mod cosigner;
mod batch_signer;
mod wallet;
pub(crate) use wallet::test_wallet;
mod addresses;
pub(crate) use addresses::test_addresses;
// Effective Once
static INIT_LOGGER_CELL: OnceLock<()> = OnceLock::new();
@@ -27,22 +23,25 @@ fn init_logger() {
#[macro_export]
macro_rules! test_network {
(
$N: ident,
$N: ty,
$docker: ident,
$network: ident,
$key_gen: ident,
$scanner: ident,
$no_deadlock_in_multisig_completed: ident,
$signer: ident,
$wallet: ident,
$addresses: ident,
$no_deadlock_in_multisig_completed: ident,
) => {
use core::{pin::Pin, future::Future};
use $crate::tests::{
init_logger, test_key_gen, test_scanner, test_no_deadlock_in_multisig_completed, test_signer,
test_wallet, test_addresses,
init_logger,
key_gen::test_key_gen,
scanner::{test_scanner, test_no_deadlock_in_multisig_completed},
signer::test_signer,
wallet::test_wallet,
};
// This doesn't interact with a node and accordingly doesn't need to be run
// This doesn't interact with a node and accordingly doesn't need to be spawn one
#[tokio::test]
async fn $key_gen() {
init_logger();
@@ -54,34 +53,8 @@ macro_rules! test_network {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
test_scanner($network(&ops).await).await;
});
}
#[test]
fn $signer() {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
test_signer($network(&ops).await).await;
});
}
#[test]
fn $wallet() {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
test_wallet($network(&ops).await).await;
});
}
#[test]
fn $addresses() {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
test_addresses($network(&ops).await).await;
let new_network = $network(&ops).await;
test_scanner(new_network).await;
});
}
@@ -90,7 +63,66 @@ macro_rules! test_network {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
test_no_deadlock_in_multisig_completed($network(&ops).await).await;
let new_network = $network(&ops).await;
test_no_deadlock_in_multisig_completed(new_network).await;
});
}
#[test]
fn $signer() {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
let new_network = $network(&ops).await;
test_signer(new_network).await;
});
}
#[test]
fn $wallet() {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
let new_network = $network(&ops).await;
test_wallet(new_network).await;
});
}
};
}
#[macro_export]
macro_rules! test_utxo_network {
(
$N: ty,
$docker: ident,
$network: ident,
$key_gen: ident,
$scanner: ident,
$no_deadlock_in_multisig_completed: ident,
$signer: ident,
$wallet: ident,
$addresses: ident,
) => {
use $crate::tests::addresses::test_addresses;
test_network!(
$N,
$docker,
$network,
$key_gen,
$scanner,
$no_deadlock_in_multisig_completed,
$signer,
$wallet,
);
#[test]
fn $addresses() {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
let new_network = $network(&ops).await;
test_addresses(new_network).await;
});
}
};

View File

@@ -1,21 +1,23 @@
use core::time::Duration;
use core::{pin::Pin, time::Duration, future::Future};
use std::sync::Arc;
use ciphersuite::Ciphersuite;
use rand_core::OsRng;
use ciphersuite::{group::GroupEncoding, Ciphersuite};
use frost::{Participant, tests::key_gen};
use tokio::{sync::Mutex, time::timeout};
use serai_db::{DbTxn, Db, MemDb};
use serai_client::validator_sets::primitives::Session;
use crate::{
networks::{OutputType, Output, Block, UtxoNetwork},
networks::{OutputType, Output, Block, Network},
key_gen::NetworkKeyDb,
multisigs::scanner::{ScannerEvent, Scanner, ScannerHandle},
};
pub async fn new_scanner<N: UtxoNetwork, D: Db>(
pub async fn new_scanner<N: Network, D: Db>(
network: &N,
db: &D,
group_key: <N::Curve as Ciphersuite>::G,
@@ -40,18 +42,27 @@ pub async fn new_scanner<N: UtxoNetwork, D: Db>(
scanner
}
pub async fn test_scanner<N: UtxoNetwork>(network: N) {
pub async fn test_scanner<N: Network>(
new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
) {
let mut keys =
frost::tests::key_gen::<_, N::Curve>(&mut OsRng).remove(&Participant::new(1).unwrap()).unwrap();
N::tweak_keys(&mut keys);
let group_key = keys.group_key();
let mut db = MemDb::new();
{
let mut txn = db.txn();
NetworkKeyDb::set(&mut txn, Session(0), &group_key.to_bytes().as_ref().to_vec());
txn.commit();
}
let network = new_network(db.clone()).await;
// Mine blocks so there's a confirmed block
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
let db = MemDb::new();
let first = Arc::new(Mutex::new(true));
let scanner = new_scanner(&network, &db, group_key, &first).await;
@@ -101,35 +112,47 @@ pub async fn test_scanner<N: UtxoNetwork>(network: N) {
.is_err());
}
pub async fn test_no_deadlock_in_multisig_completed<N: UtxoNetwork>(network: N) {
pub async fn test_no_deadlock_in_multisig_completed<N: Network>(
new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
) {
let mut db = MemDb::new();
let network = new_network(db.clone()).await;
// Mine blocks so there's a confirmed block
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
let mut db = MemDb::new();
let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone());
assert!(current_keys.is_empty());
let mut txn = db.txn();
// Register keys to cause Block events at CONFIRMATIONS (dropped since first keys),
// CONFIRMATIONS + 1, and CONFIRMATIONS + 2
for i in 0 .. 3 {
let key = {
let mut keys = key_gen(&mut OsRng);
for keys in keys.values_mut() {
N::tweak_keys(keys);
}
let key = keys[&Participant::new(1).unwrap()].group_key();
if i == 0 {
let mut txn = db.txn();
NetworkKeyDb::set(&mut txn, Session(0), &key.to_bytes().as_ref().to_vec());
txn.commit();
}
key
};
let mut txn = db.txn();
scanner
.register_key(
&mut txn,
network.get_latest_block_number().await.unwrap() + N::CONFIRMATIONS + i,
{
let mut keys = key_gen(&mut OsRng);
for keys in keys.values_mut() {
N::tweak_keys(keys);
}
keys[&Participant::new(1).unwrap()].group_key()
},
key,
)
.await;
txn.commit();
}
txn.commit();
for _ in 0 .. (3 * N::CONFIRMATIONS) {
network.mine_block().await;

View File

@@ -1,7 +1,9 @@
use core::{pin::Pin, future::Future};
use std::collections::HashMap;
use rand_core::{RngCore, OsRng};
use ciphersuite::group::GroupEncoding;
use frost::{
Participant, ThresholdKeys,
dkg::tests::{key_gen, clone_without},
@@ -16,14 +18,15 @@ use serai_client::{
use messages::sign::*;
use crate::{
Payment, Plan,
networks::{Output, Transaction, Eventuality, UtxoNetwork},
Payment,
networks::{Output, Transaction, Eventuality, Network},
key_gen::NetworkKeyDb,
multisigs::scheduler::Scheduler,
signer::Signer,
};
#[allow(clippy::type_complexity)]
pub async fn sign<N: UtxoNetwork>(
pub async fn sign<N: Network>(
network: N,
session: Session,
mut keys_txs: HashMap<
@@ -153,53 +156,55 @@ pub async fn sign<N: UtxoNetwork>(
typed_claim
}
pub async fn test_signer<N: UtxoNetwork>(network: N)
where
<N::Scheduler as Scheduler<N>>::Addendum: From<()>,
{
pub async fn test_signer<N: Network>(
new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
) {
let mut keys = key_gen(&mut OsRng);
for keys in keys.values_mut() {
N::tweak_keys(keys);
}
let key = keys[&Participant::new(1).unwrap()].group_key();
let mut db = MemDb::new();
{
let mut txn = db.txn();
NetworkKeyDb::set(&mut txn, Session(0), &key.to_bytes().as_ref().to_vec());
txn.commit();
}
let network = new_network(db.clone()).await;
let outputs = network
.get_outputs(&network.test_send(N::external_address(&network, key).await).await, key)
.await;
let sync_block = network.get_latest_block_number().await.unwrap() - N::CONFIRMATIONS;
let amount = 2 * N::DUST;
let amount = (2 * N::DUST) + 1000;
let plan = {
let mut txn = db.txn();
let mut scheduler = N::Scheduler::new::<MemDb>(&mut txn, key, N::NETWORK);
let payments = vec![Payment {
address: N::external_address(&network, key).await,
data: None,
balance: Balance {
coin: match N::NETWORK {
NetworkId::Serai => panic!("test_signer called with Serai"),
NetworkId::Bitcoin => Coin::Bitcoin,
NetworkId::Ethereum => Coin::Ether,
NetworkId::Monero => Coin::Monero,
},
amount: Amount(amount),
},
}];
let mut plans = scheduler.schedule::<MemDb>(&mut txn, outputs.clone(), payments, key, false);
assert_eq!(plans.len(), 1);
plans.swap_remove(0)
};
let mut keys_txs = HashMap::new();
let mut eventualities = vec![];
for (i, keys) in keys.drain() {
let (signable, eventuality) = network
.prepare_send(
sync_block,
Plan {
key,
inputs: outputs.clone(),
payments: vec![Payment {
address: N::external_address(&network, key).await,
data: None,
balance: Balance {
coin: match N::NETWORK {
NetworkId::Serai => panic!("test_signer called with Serai"),
NetworkId::Bitcoin => Coin::Bitcoin,
NetworkId::Ethereum => Coin::Ether,
NetworkId::Monero => Coin::Monero,
},
amount: Amount(amount),
},
}],
change: Some(N::change_address(key).unwrap()),
scheduler_addendum: ().into(),
},
0,
)
.await
.unwrap()
.tx
.unwrap();
let (signable, eventuality) =
network.prepare_send(sync_block, plan.clone(), 0).await.unwrap().tx.unwrap();
eventualities.push(eventuality.clone());
keys_txs.insert(i, (keys, (signable, eventuality)));
@@ -217,11 +222,21 @@ where
key,
)
.await;
assert_eq!(outputs.len(), 2);
// Adjust the amount for the fees
let amount = amount - tx.fee(&network).await;
// Check either output since Monero will randomize its output order
assert!((outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount));
// Don't run if Ethereum as the received output will revert by the contract
// (and therefore not actually exist)
if N::NETWORK != NetworkId::Ethereum {
assert_eq!(outputs.len(), 1 + usize::from(u8::from(plan.change.is_some())));
// Adjust the amount for the fees
let amount = amount - tx.fee(&network).await;
if plan.change.is_some() {
// Check either output since Monero will randomize its output order
assert!(
(outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount)
);
} else {
assert!(outputs[0].balance().amount.0 == amount);
}
}
// Check the eventualities pass
for eventuality in eventualities {

View File

@@ -1,7 +1,9 @@
use std::{time::Duration, collections::HashMap};
use core::{time::Duration, pin::Pin, future::Future};
use std::collections::HashMap;
use rand_core::OsRng;
use ciphersuite::group::GroupEncoding;
use frost::{Participant, dkg::tests::key_gen};
use tokio::time::timeout;
@@ -15,21 +17,19 @@ use serai_client::{
use crate::{
Payment, Plan,
networks::{Output, Transaction, Eventuality, Block, UtxoNetwork},
networks::{Output, Transaction, Eventuality, Block, Network},
key_gen::NetworkKeyDb,
multisigs::{
scanner::{ScannerEvent, Scanner},
scheduler::Scheduler,
scheduler::{self, Scheduler},
},
tests::sign,
};
// Tests the Scanner, Scheduler, and Signer together
pub async fn test_wallet<N: UtxoNetwork>(network: N) {
// Mine blocks so there's a confirmed block
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
pub async fn test_wallet<N: Network>(
new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
) {
let mut keys = key_gen(&mut OsRng);
for keys in keys.values_mut() {
N::tweak_keys(keys);
@@ -37,6 +37,18 @@ pub async fn test_wallet<N: UtxoNetwork>(network: N) {
let key = keys[&Participant::new(1).unwrap()].group_key();
let mut db = MemDb::new();
{
let mut txn = db.txn();
NetworkKeyDb::set(&mut txn, Session(0), &key.to_bytes().as_ref().to_vec());
txn.commit();
}
let network = new_network(db.clone()).await;
// Mine blocks so there's a confirmed block
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone());
assert!(current_keys.is_empty());
let (block_id, outputs) = {
@@ -93,7 +105,13 @@ pub async fn test_wallet<N: UtxoNetwork>(network: N) {
txn.commit();
assert_eq!(plans.len(), 1);
assert_eq!(plans[0].key, key);
assert_eq!(plans[0].inputs, outputs);
if std::any::TypeId::of::<N::Scheduler>() ==
std::any::TypeId::of::<scheduler::smart_contract::Scheduler<N>>()
{
assert_eq!(plans[0].inputs, vec![]);
} else {
assert_eq!(plans[0].inputs, outputs);
}
assert_eq!(
plans[0].payments,
vec![Payment {
@@ -110,7 +128,7 @@ pub async fn test_wallet<N: UtxoNetwork>(network: N) {
}
}]
);
assert_eq!(plans[0].change, Some(N::change_address(key).unwrap()));
assert_eq!(plans[0].change, N::change_address(key));
{
let mut buf = vec![];
@@ -139,9 +157,22 @@ pub async fn test_wallet<N: UtxoNetwork>(network: N) {
let tx = network.get_transaction_by_eventuality(block_number, &eventualities[0]).await;
let block = network.get_block(block_number).await.unwrap();
let outputs = network.get_outputs(&block, key).await;
assert_eq!(outputs.len(), 2);
let amount = amount - tx.fee(&network).await;
assert!((outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount));
// Don't run if Ethereum as the received output will revert by the contract
// (and therefore not actually exist)
if N::NETWORK != NetworkId::Ethereum {
assert_eq!(outputs.len(), 1 + usize::from(u8::from(plans[0].change.is_some())));
// Adjust the amount for the fees
let amount = amount - tx.fee(&network).await;
if plans[0].change.is_some() {
// Check either output since Monero will randomize its output order
assert!(
(outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount)
);
} else {
assert!(outputs[0].balance().amount.0 == amount);
}
}
for eventuality in eventualities {
let completion = network.confirm_completion(&eventuality, &claim).await.unwrap().unwrap();
@@ -152,21 +183,23 @@ pub async fn test_wallet<N: UtxoNetwork>(network: N) {
network.mine_block().await;
}
match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() {
ScannerEvent::Block { is_retirement_block, block: block_id, outputs: these_outputs } => {
scanner.multisig_completed.send(false).unwrap();
assert!(!is_retirement_block);
assert_eq!(block_id, block.id());
assert_eq!(these_outputs, outputs);
if N::NETWORK != NetworkId::Ethereum {
match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() {
ScannerEvent::Block { is_retirement_block, block: block_id, outputs: these_outputs } => {
scanner.multisig_completed.send(false).unwrap();
assert!(!is_retirement_block);
assert_eq!(block_id, block.id());
assert_eq!(these_outputs, outputs);
}
ScannerEvent::Completed(_, _, _, _, _) => {
panic!("unexpectedly got eventuality completion");
}
}
ScannerEvent::Completed(_, _, _, _, _) => {
panic!("unexpectedly got eventuality completion");
}
}
// Check the Scanner DB can reload the outputs
let mut txn = db.txn();
assert_eq!(scanner.ack_block(&mut txn, block.id()).await.1, outputs);
scanner.release_lock().await;
txn.commit();
// Check the Scanner DB can reload the outputs
let mut txn = db.txn();
assert_eq!(scanner.ack_block(&mut txn, block.id()).await.1, outputs);
scanner.release_lock().await;
txn.commit();
}
}

View File

@@ -124,7 +124,7 @@ pub fn build(name: String) {
// Check any additionally specified paths
let meta = |path: PathBuf| (path.clone(), fs::metadata(path));
let mut metadatas = match name.as_str() {
"bitcoin" | "monero" => vec![],
"bitcoin" | "ethereum" | "monero" => vec![],
"message-queue" => vec![
meta(repo_path.join("common")),
meta(repo_path.join("crypto")),

View File

@@ -27,12 +27,14 @@ ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, fea
dkg = { path = "../../crypto/dkg", default-features = false, features = ["tests"] }
bitcoin-serai = { path = "../../coins/bitcoin" }
ethereum-serai = { path = "../../coins/ethereum" }
monero-serai = { path = "../../coins/monero" }
messages = { package = "serai-processor-messages", path = "../../processor/messages" }
scale = { package = "parity-scale-codec", version = "3" }
serai-client = { path = "../../substrate/client" }
serai-db = { path = "../../common/db", default-features = false }
serai-message-queue = { path = "../../message-queue" }
borsh = { version = "1", features = ["de_strict_order"] }

View File

@@ -181,7 +181,28 @@ impl Coordinator {
break;
}
}
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => {
use ethereum_serai::alloy::{
simple_request_transport::SimpleRequest,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
network::Ethereum,
};
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
loop {
if handle
.block_on(provider.raw_request::<_, ()>("evm_setAutomine".into(), [false]))
.is_ok()
{
break;
}
handle.block_on(tokio::time::sleep(core::time::Duration::from_secs(1)));
}
}
NetworkId::Monero => {
use monero_serai::rpc::HttpRpc;
@@ -271,7 +292,45 @@ impl Coordinator {
block.consensus_encode(&mut block_buf).unwrap();
(hash, block_buf)
}
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => {
use ethereum_serai::alloy::{
simple_request_transport::SimpleRequest,
rpc_types::BlockNumberOrTag,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
network::Ethereum,
};
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
let start = provider
.get_block(BlockNumberOrTag::Latest.into(), false)
.await
.unwrap()
.unwrap()
.header
.number
.unwrap();
// We mine 96 blocks to mine one epoch, then cause its finalization
provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap();
let end_of_epoch = start + 31;
let hash = provider
.get_block(BlockNumberOrTag::Number(end_of_epoch).into(), false)
.await
.unwrap()
.unwrap()
.header
.hash
.unwrap();
let state = provider
.raw_request::<_, String>("anvil_dumpState".into(), ())
.await
.unwrap()
.into_bytes();
(hash.into(), state)
}
NetworkId::Monero => {
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar};
use monero_serai::{
@@ -303,39 +362,6 @@ impl Coordinator {
}
}
pub async fn broadcast_block(&self, ops: &DockerOperations, block: &[u8]) {
let rpc_url = network_rpc(self.network, ops, &self.network_handle);
match self.network {
NetworkId::Bitcoin => {
use bitcoin_serai::rpc::Rpc;
let rpc =
Rpc::new(rpc_url).await.expect("couldn't connect to the coordinator's Bitcoin RPC");
let res: Option<String> =
rpc.rpc_call("submitblock", serde_json::json!([hex::encode(block)])).await.unwrap();
if let Some(err) = res {
panic!("submitblock failed: {err}");
}
}
NetworkId::Ethereum => todo!(),
NetworkId::Monero => {
use monero_serai::rpc::HttpRpc;
let rpc =
HttpRpc::new(rpc_url).await.expect("couldn't connect to the coordinator's Monero RPC");
let res: serde_json::Value = rpc
.json_rpc_call("submit_block", Some(serde_json::json!([hex::encode(block)])))
.await
.unwrap();
let err = res.get("error");
if err.is_some() && (err.unwrap() != &serde_json::Value::Null) {
panic!("failed to submit Monero block: {res}");
}
}
NetworkId::Serai => panic!("processor tests broadcasting block to Serai"),
}
}
pub async fn sync(&self, ops: &DockerOperations, others: &[Coordinator]) {
let rpc_url = network_rpc(self.network, ops, &self.network_handle);
match self.network {
@@ -345,13 +371,8 @@ impl Coordinator {
let rpc = Rpc::new(rpc_url).await.expect("couldn't connect to the Bitcoin RPC");
let to = rpc.get_latest_block_number().await.unwrap();
for coordinator in others {
let from = Rpc::new(network_rpc(self.network, ops, &coordinator.network_handle))
.await
.expect("couldn't connect to the Bitcoin RPC")
.get_latest_block_number()
.await
.unwrap() +
1;
let from = rpc.get_latest_block_number().await.unwrap() + 1;
for b in from ..= to {
let mut buf = vec![];
rpc
@@ -360,11 +381,40 @@ impl Coordinator {
.unwrap()
.consensus_encode(&mut buf)
.unwrap();
coordinator.broadcast_block(ops, &buf).await;
let rpc_url = network_rpc(coordinator.network, ops, &coordinator.network_handle);
let rpc =
Rpc::new(rpc_url).await.expect("couldn't connect to the coordinator's Bitcoin RPC");
let res: Option<String> =
rpc.rpc_call("submitblock", serde_json::json!([hex::encode(buf)])).await.unwrap();
if let Some(err) = res {
panic!("submitblock failed: {err}");
}
}
}
}
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => {
use ethereum_serai::alloy::{
simple_request_transport::SimpleRequest,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
network::Ethereum,
};
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
let state = provider.raw_request::<_, String>("anvil_dumpState".into(), ()).await.unwrap();
for coordinator in others {
let rpc_url = network_rpc(coordinator.network, ops, &coordinator.network_handle);
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
provider.raw_request::<_, ()>("anvil_loadState".into(), &state).await.unwrap();
}
}
NetworkId::Monero => {
use monero_serai::rpc::HttpRpc;
@@ -378,12 +428,21 @@ impl Coordinator {
.await
.unwrap();
for b in from .. to {
coordinator
.broadcast_block(
ops,
&rpc.get_block(rpc.get_block_hash(b).await.unwrap()).await.unwrap().serialize(),
)
.await;
let block =
rpc.get_block(rpc.get_block_hash(b).await.unwrap()).await.unwrap().serialize();
let rpc_url = network_rpc(coordinator.network, ops, &coordinator.network_handle);
let rpc = HttpRpc::new(rpc_url)
.await
.expect("couldn't connect to the coordinator's Monero RPC");
let res: serde_json::Value = rpc
.json_rpc_call("submit_block", Some(serde_json::json!([hex::encode(block)])))
.await
.unwrap();
let err = res.get("error");
if err.is_some() && (err.unwrap() != &serde_json::Value::Null) {
panic!("failed to submit Monero block: {res}");
}
}
}
}
@@ -404,7 +463,19 @@ impl Coordinator {
Rpc::new(rpc_url).await.expect("couldn't connect to the coordinator's Bitcoin RPC");
rpc.send_raw_transaction(&Transaction::consensus_decode(&mut &*tx).unwrap()).await.unwrap();
}
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => {
use ethereum_serai::alloy::{
simple_request_transport::SimpleRequest,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
network::Ethereum,
};
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
let _ = provider.send_raw_transaction(tx).await.unwrap();
}
NetworkId::Monero => {
use monero_serai::{transaction::Transaction, rpc::HttpRpc};
@@ -445,7 +516,26 @@ impl Coordinator {
None
}
}
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => {
use ethereum_serai::alloy::{
consensus::{TxLegacy, Signed},
simple_request_transport::SimpleRequest,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
network::Ethereum,
};
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
let mut hash = [0; 32];
hash.copy_from_slice(tx);
let tx = provider.get_transaction_by_hash(hash.into()).await.unwrap()?;
let (tx, sig, _) = Signed::<TxLegacy>::try_from(tx).unwrap().into_parts();
let mut bytes = vec![];
tx.encode_with_signature_fields(&sig, &mut bytes);
Some(bytes)
}
NetworkId::Monero => {
use monero_serai::rpc::HttpRpc;

View File

@@ -19,6 +19,7 @@ pub const RPC_USER: &str = "serai";
pub const RPC_PASS: &str = "seraidex";
pub const BTC_PORT: u32 = 8332;
pub const ETH_PORT: u32 = 8545;
pub const XMR_PORT: u32 = 18081;
pub fn bitcoin_instance() -> (TestBodySpecification, u32) {
@@ -31,6 +32,17 @@ pub fn bitcoin_instance() -> (TestBodySpecification, u32) {
(composition, BTC_PORT)
}
pub fn ethereum_instance() -> (TestBodySpecification, u32) {
serai_docker_tests::build("ethereum".to_string());
let composition = TestBodySpecification::with_image(
Image::with_repository("serai-dev-ethereum").pull_policy(PullPolicy::Never),
)
.set_start_policy(StartPolicy::Strict)
.set_publish_all_ports(true);
(composition, ETH_PORT)
}
pub fn monero_instance() -> (TestBodySpecification, u32) {
serai_docker_tests::build("monero".to_string());
@@ -45,7 +57,7 @@ pub fn monero_instance() -> (TestBodySpecification, u32) {
pub fn network_instance(network: NetworkId) -> (TestBodySpecification, u32) {
match network {
NetworkId::Bitcoin => bitcoin_instance(),
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => ethereum_instance(),
NetworkId::Monero => monero_instance(),
NetworkId::Serai => {
panic!("Serai is not a valid network to spawn an instance of for a processor")
@@ -58,7 +70,7 @@ pub fn network_rpc(network: NetworkId, ops: &DockerOperations, handle: &str) ->
.handle(handle)
.host_port(match network {
NetworkId::Bitcoin => BTC_PORT,
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => ETH_PORT,
NetworkId::Monero => XMR_PORT,
NetworkId::Serai => panic!("getting port for external network yet it was Serai"),
})
@@ -70,7 +82,7 @@ pub fn confirmations(network: NetworkId) -> usize {
use processor::networks::*;
match network {
NetworkId::Bitcoin => Bitcoin::CONFIRMATIONS,
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => Ethereum::<serai_db::MemDb>::CONFIRMATIONS,
NetworkId::Monero => Monero::CONFIRMATIONS,
NetworkId::Serai => panic!("getting confirmations required for Serai"),
}
@@ -83,6 +95,10 @@ pub enum Wallet {
public_key: bitcoin_serai::bitcoin::PublicKey,
input_tx: bitcoin_serai::bitcoin::Transaction,
},
Ethereum {
key: <ciphersuite::Secp256k1 as Ciphersuite>::F,
nonce: u64,
},
Monero {
handle: String,
spend_key: Zeroizing<curve25519_dalek::scalar::Scalar>,
@@ -138,7 +154,37 @@ impl Wallet {
Wallet::Bitcoin { private_key, public_key, input_tx: funds }
}
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => {
use ciphersuite::{group::ff::Field, Ciphersuite, Secp256k1};
use ethereum_serai::alloy::{
primitives::{U256, Address},
simple_request_transport::SimpleRequest,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
network::Ethereum,
};
let key = <Secp256k1 as Ciphersuite>::F::random(&mut OsRng);
let address =
ethereum_serai::crypto::address(&(<Secp256k1 as Ciphersuite>::generator() * key));
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
provider
.raw_request::<_, ()>(
"anvil_setBalance".into(),
[Address(address.into()).to_string(), {
let nine_decimals = U256::from(1_000_000_000u64);
(U256::from(100u64) * nine_decimals * nine_decimals).to_string()
}],
)
.await
.unwrap();
Wallet::Ethereum { key, nonce: 0 }
}
NetworkId::Monero => {
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar};
@@ -282,6 +328,24 @@ impl Wallet {
(buf, Balance { coin: Coin::Bitcoin, amount: Amount(AMOUNT) })
}
Wallet::Ethereum { key, ref mut nonce } => {
/*
use ethereum_serai::alloy::primitives::U256;
let eight_decimals = U256::from(100_000_000u64);
let nine_decimals = eight_decimals * U256::from(10u64);
let eighteen_decimals = nine_decimals * nine_decimals;
let tx = todo!("send to router");
*nonce += 1;
(tx, Balance { coin: Coin::Ether, amount: Amount(u64::try_from(eight_decimals).unwrap()) })
*/
let _ = key;
let _ = nonce;
todo!()
}
Wallet::Monero { handle, ref spend_key, ref view_pair, ref mut inputs } => {
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
use monero_serai::{
@@ -374,6 +438,13 @@ impl Wallet {
)
.unwrap()
}
Wallet::Ethereum { key, .. } => {
use ciphersuite::{Ciphersuite, Secp256k1};
ExternalAddress::new(
ethereum_serai::crypto::address(&(Secp256k1::generator() * key)).into(),
)
.unwrap()
}
Wallet::Monero { view_pair, .. } => {
use monero_serai::wallet::address::{Network, AddressSpec};
ExternalAddress::new(