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

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

67
Cargo.lock generated
View File

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

View File

@@ -110,7 +110,7 @@ panic = "unwind"
lazy_static = { git = "https://github.com/rust-lang-nursery/lazy-static.rs", rev = "5735630d46572f1e5377c8f2ba0f79d18f53b10c" } 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 # 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 # wasmtime pulls in an old version for this
zstd = { path = "patches/zstd" } 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-core = { version = "0.7", default-features = false }
alloy-sol-types = { version = "0.7", default-features = false, features = ["json"] } 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-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false, features = ["k256"] }
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false } alloy-network = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false }
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", 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-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] [dev-dependencies]
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false, features = ["tests"] } frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false, features = ["tests"] }
tokio = { version = "1", features = ["macros"] } 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] [features]
tests = [] tests = ["alloy-node-bindings"]

View File

@@ -21,8 +21,8 @@ tower = "0.4"
serde_json = { version = "1", default-features = false } serde_json = { version = "1", default-features = false }
simple-request = { path = "../../../common/request", 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-json-rpc = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false }
alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false } alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false }
[features] [features]
default = ["tls"] 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() 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!( assert!(
tx.chain_id.is_none(), tx.chain_id.is_none(),
"chain ID was Some when deterministically signing a TX (causing a non-deterministic signer)" "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_sol_types::{SolInterface, SolEvent};
use alloy_rpc_types::{BlockNumberOrTag, Filter}; use alloy_rpc_types::Filter;
use alloy_simple_request_transport::SimpleRequest; use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider}; use alloy_provider::{Provider, RootProvider};
@@ -25,22 +25,8 @@ pub struct TopLevelErc20Transfer {
pub struct Erc20(Arc<RootProvider<SimpleRequest>>, Address); pub struct Erc20(Arc<RootProvider<SimpleRequest>>, Address);
impl Erc20 { impl Erc20 {
/// Construct a new view of the specified ERC20 contract. /// Construct a new view of the specified ERC20 contract.
/// pub fn new(provider: Arc<RootProvider<SimpleRequest>>, address: [u8; 20]) -> Self {
/// This checks a contract is deployed at that address yet does not check the contract is Self(provider, Address::from(&address))
/// 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 async fn top_level_transfers( pub async fn top_level_transfers(
@@ -65,7 +51,8 @@ impl Erc20 {
} }
let tx_id = log.transaction_hash.ok_or(Error::ConnectionError)?; 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 this is a top-level call...
if tx.to == Some(self.1) { if tx.to == Some(self.1) {

View File

@@ -1,12 +1,17 @@
use thiserror::Error; use thiserror::Error;
pub use alloy_core; pub mod alloy {
pub use alloy_consensus; 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_consensus as consensus;
pub use alloy_simple_request_transport; pub use alloy_network as network;
pub use alloy_rpc_client; pub use alloy_rpc_types as rpc_types;
pub use alloy_provider; 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; pub mod crypto;
@@ -18,8 +23,8 @@ pub mod router;
pub mod machine; pub mod machine;
#[cfg(test)] #[cfg(any(test, feature = "tests"))]
mod tests; pub mod tests;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
pub enum Error { pub enum Error {

View File

@@ -159,11 +159,12 @@ impl Router {
#[cfg(test)] #[cfg(test)]
pub async fn serai_key(&self, at: [u8; 32]) -> Result<PublicKey, Error> { pub async fn serai_key(&self, at: [u8; 32]) -> Result<PublicKey, Error> {
let call = TransactionRequest::default() let call = TransactionRequest::default()
.to(Some(self.1)) .to(self.1)
.input(TransactionInput::new(abi::seraiKeyCall::new(()).abi_encode().into())); .input(TransactionInput::new(abi::seraiKeyCall::new(()).abi_encode().into()));
let bytes = self let bytes = self
.0 .0
.call(&call, Some(BlockId::Hash(B256::from(at).into()))) .call(&call)
.block(BlockId::Hash(B256::from(at).into()))
.await .await
.map_err(|_| Error::ConnectionError)?; .map_err(|_| Error::ConnectionError)?;
let res = let res =
@@ -197,11 +198,12 @@ impl Router {
#[cfg(test)] #[cfg(test)]
pub async fn nonce(&self, at: [u8; 32]) -> Result<U256, Error> { pub async fn nonce(&self, at: [u8; 32]) -> Result<U256, Error> {
let call = TransactionRequest::default() let call = TransactionRequest::default()
.to(Some(self.1)) .to(self.1)
.input(TransactionInput::new(abi::nonceCall::new(()).abi_encode().into())); .input(TransactionInput::new(abi::nonceCall::new(()).abi_encode().into()));
let bytes = self let bytes = self
.0 .0
.call(&call, Some(BlockId::Hash(B256::from(at).into()))) .call(&call)
.block(BlockId::Hash(B256::from(at).into()))
.await .await
.map_err(|_| Error::ConnectionError)?; .map_err(|_| Error::ConnectionError)?;
let res = 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::new().from_block(0).to_block(block).address(self.1);
let filter = filter.event_signature(SeraiKeyUpdated::SIGNATURE_HASH); let filter = filter.event_signature(SeraiKeyUpdated::SIGNATURE_HASH);
let all_keys = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?; 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_log = all_keys.last().ok_or(Error::ConnectionError)?;
let last_key_x_coordinate = last_key_x_coordinate_log 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[0] = u8::from(sec1::Tag::CompressedEvenY);
compressed_point[1 ..].copy_from_slice(last_key_x_coordinate.as_slice()); 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( pub async fn in_instructions(
@@ -254,7 +261,9 @@ impl Router {
block: u64, block: u64,
allowed_tokens: &HashSet<[u8; 20]>, allowed_tokens: &HashSet<[u8; 20]>,
) -> Result<Vec<InInstruction>, Error> { ) -> 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::new().from_block(block).to_block(block).address(self.1);
let filter = filter.event_signature(InInstructionEvent::SIGNATURE_HASH); 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_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 = let log =
log.log_decode::<InInstructionEvent>().map_err(|_| Error::ConnectionError)?.inner.data; 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_consensus::{SignableTransaction, TxLegacy};
use alloy_rpc_types::TransactionReceipt; use alloy_rpc_types::{BlockNumberOrTag, TransactionReceipt};
use alloy_simple_request_transport::SimpleRequest; use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider}; use alloy_provider::{Provider, RootProvider};
use crate::crypto::{address, deterministically_sign, PublicKey}; use crate::crypto::{address, deterministically_sign, PublicKey};
#[cfg(test)]
mod crypto; mod crypto;
#[cfg(test)]
mod abi; mod abi;
#[cfg(test)]
mod schnorr; mod schnorr;
#[cfg(test)]
mod router; mod router;
pub fn key_gen() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, PublicKey) { 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(); // let chain_id = provider.get_chain_id().await.unwrap();
// tx.chain_id = Some(chain_id); // tx.chain_id = Some(chain_id);
tx.chain_id = None; 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 // 100 gwei
tx.gas_price = 100_000_000_000u128; tx.gas_price = 100_000_000_000u128;
let sig = wallet.sign_prehash_recoverable(tx.signature_hash().as_ref()).unwrap(); 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_eq!(address, tx.clone().into_signed(sig.into()).recover_signer().unwrap());
assert!( 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) ((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 px: [u8; 32] = public_key.px.to_repr().into();
let c_bytes: [u8; 32] = signature.c.to_repr().into(); let c_bytes: [u8; 32] = signature.c.to_repr().into();
let s_bytes: [u8; 32] = signature.s.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::verifyCall::new((px.into(), message.to_vec().into(), c_bytes.into(), s_bytes.into()))
.abi_encode() .abi_encode()
.into(), .into(),
)); ));
let bytes = provider.call(&call, None).await.map_err(|_| Error::ConnectionError)?; let bytes = provider.call(&call).await.map_err(|_| Error::ConnectionError)?;
let res = let res =
abi::verifyCall::abi_decode_returns(&bytes, true).map_err(|_| Error::ConnectionError)?; abi::verifyCall::abi_decode_returns(&bytes, true).map_err(|_| Error::ConnectionError)?;

View File

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

@@ -103,5 +103,5 @@ allow-git = [
"https://github.com/serai-dex/substrate", "https://github.com/serai-dex/substrate",
"https://github.com/alloy-rs/alloy", "https://github.com/alloy-rs/alloy",
"https://github.com/monero-rs/base58-monero", "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 #!/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"] secp256k1 = ["k256", "frost/secp256k1"]
bitcoin = ["dep:secp256k1", "secp256k1", "bitcoin-serai", "serai-client/bitcoin"] bitcoin = ["dep:secp256k1", "secp256k1", "bitcoin-serai", "serai-client/bitcoin"]
ethereum = ["secp256k1", "ethereum-serai"] ethereum = ["secp256k1", "ethereum-serai/tests"]
ed25519 = ["dalek-ff-group", "frost/ed25519"] ed25519 = ["dalek-ff-group", "frost/ed25519"]
monero = ["ed25519", "monero-serai", "serai-client/monero"] 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)); 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![]; let mut plans = vec![];
for chunk in payments.as_slice().chunks(N::MAX_OUTPUTS) { for chunk in payments.as_slice().chunks(N::MAX_OUTPUTS) {
// Once we rotate, all further payments should be scheduled via the new multisig // 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()) .and_then(|key_bytes| <N::Curve as Ciphersuite>::read_G(&mut key_bytes.as_slice()).ok())
.unwrap_or(self.key); .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)); LastNonce::set(txn, &(nonce + 1));
Plan { Plan {
key: current_key, key: current_key,

View File

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

View File

@@ -432,9 +432,12 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Debug {
let plan_id = plan.id(); let plan_id = plan.id();
let Plan { key, inputs, mut payments, change, scheduler_addendum } = plan; 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>() - 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 { let Some(tx_fee) = self.needed_fee(block_number, &inputs, &payments, &change).await? else {
// This Plan is not fulfillable // 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 std::collections::HashMap;
use rand_core::OsRng; use rand_core::OsRng;
@@ -82,8 +82,9 @@ async fn spend<N: UtxoNetwork, D: Db>(
} }
} }
pub async fn test_addresses<N: UtxoNetwork>(network: N) pub async fn test_addresses<N: UtxoNetwork>(
where new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
) where
<N::Scheduler as Scheduler<N>>::Addendum: From<()>, <N::Scheduler as Scheduler<N>>::Addendum: From<()>,
{ {
let mut keys = frost::tests::key_gen::<_, N::Curve>(&mut OsRng); 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 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 // Mine blocks so there's a confirmed block
for _ in 0 .. N::CONFIRMATIONS { for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await; network.mine_block().await;
} }
let mut db = MemDb::new();
let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone()); let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone());
assert!(current_keys.is_empty()); assert!(current_keys.is_empty());
let mut txn = db.txn(); let mut txn = db.txn();

View File

@@ -3,6 +3,8 @@ use dockertest::{
TestBodySpecification, DockerOperations, DockerTest, TestBodySpecification, DockerOperations, DockerTest,
}; };
use serai_db::MemDb;
#[cfg(feature = "bitcoin")] #[cfg(feature = "bitcoin")]
mod bitcoin { mod bitcoin {
use std::sync::Arc; use std::sync::Arc;
@@ -33,8 +35,6 @@ mod bitcoin {
sync::Mutex, sync::Mutex,
}; };
use serai_db::MemDb;
use super::*; use super::*;
use crate::{ use crate::{
networks::{Network, Bitcoin, Output, OutputType, Block}, networks::{Network, Bitcoin, Output, OutputType, Block},
@@ -57,7 +57,7 @@ mod bitcoin {
fn test_receive_data_from_input() { fn test_receive_data_from_input() {
let docker = spawn_bitcoin(); let docker = spawn_bitcoin();
docker.run(|ops| async move { 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 // generate a multisig address to receive the coins
let mut keys = frost::tests::key_gen::<_, <Bitcoin as Network>::Curve>(&mut OsRng) let mut keys = frost::tests::key_gen::<_, <Bitcoin as Network>::Curve>(&mut OsRng)
@@ -208,23 +208,26 @@ mod bitcoin {
test 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 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.fresh_chain().await;
bitcoin move |_db| Box::pin(Bitcoin::new(url.clone()))
} }
test_network!( test_utxo_network!(
Bitcoin, Bitcoin,
spawn_bitcoin, spawn_bitcoin,
bitcoin, bitcoin,
bitcoin_key_gen, bitcoin_key_gen,
bitcoin_scanner, bitcoin_scanner,
bitcoin_no_deadlock_in_multisig_completed,
bitcoin_signer, bitcoin_signer,
bitcoin_wallet, bitcoin_wallet,
bitcoin_addresses, bitcoin_addresses,
bitcoin_no_deadlock_in_multisig_completed,
); );
} }
@@ -252,24 +255,185 @@ mod monero {
test 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 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 { while monero.get_latest_block_number().await.unwrap() < 150 {
monero.mine_block().await; monero.mine_block().await;
} }
monero move |_db| Box::pin(Monero::new(url.clone()))
} }
test_network!( test_utxo_network!(
Monero, Monero,
spawn_monero, spawn_monero,
monero, monero,
monero_key_gen, monero_key_gen,
monero_scanner, monero_scanner,
monero_no_deadlock_in_multisig_completed,
monero_signer, monero_signer,
monero_wallet, monero_wallet,
monero_addresses, 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; use std::sync::OnceLock;
mod key_gen; mod key_gen;
pub(crate) use key_gen::test_key_gen;
mod scanner; mod scanner;
pub(crate) use scanner::{test_scanner, test_no_deadlock_in_multisig_completed};
mod signer; mod signer;
pub(crate) use signer::{sign, test_signer}; pub(crate) use signer::sign;
mod cosigner; mod cosigner;
mod batch_signer; mod batch_signer;
mod wallet; mod wallet;
pub(crate) use wallet::test_wallet;
mod addresses; mod addresses;
pub(crate) use addresses::test_addresses;
// Effective Once // Effective Once
static INIT_LOGGER_CELL: OnceLock<()> = OnceLock::new(); static INIT_LOGGER_CELL: OnceLock<()> = OnceLock::new();
@@ -27,22 +23,25 @@ fn init_logger() {
#[macro_export] #[macro_export]
macro_rules! test_network { macro_rules! test_network {
( (
$N: ident, $N: ty,
$docker: ident, $docker: ident,
$network: ident, $network: ident,
$key_gen: ident, $key_gen: ident,
$scanner: ident, $scanner: ident,
$no_deadlock_in_multisig_completed: ident,
$signer: ident, $signer: ident,
$wallet: ident, $wallet: ident,
$addresses: ident,
$no_deadlock_in_multisig_completed: ident,
) => { ) => {
use core::{pin::Pin, future::Future};
use $crate::tests::{ use $crate::tests::{
init_logger, test_key_gen, test_scanner, test_no_deadlock_in_multisig_completed, test_signer, init_logger,
test_wallet, test_addresses, 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] #[tokio::test]
async fn $key_gen() { async fn $key_gen() {
init_logger(); init_logger();
@@ -54,34 +53,8 @@ macro_rules! test_network {
init_logger(); init_logger();
let docker = $docker(); let docker = $docker();
docker.run(|ops| async move { docker.run(|ops| async move {
test_scanner($network(&ops).await).await; let new_network = $network(&ops).await;
}); test_scanner(new_network).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;
}); });
} }
@@ -90,7 +63,66 @@ macro_rules! test_network {
init_logger(); init_logger();
let docker = $docker(); let docker = $docker();
docker.run(|ops| async move { 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 std::sync::Arc;
use ciphersuite::Ciphersuite;
use rand_core::OsRng; use rand_core::OsRng;
use ciphersuite::{group::GroupEncoding, Ciphersuite};
use frost::{Participant, tests::key_gen}; use frost::{Participant, tests::key_gen};
use tokio::{sync::Mutex, time::timeout}; use tokio::{sync::Mutex, time::timeout};
use serai_db::{DbTxn, Db, MemDb}; use serai_db::{DbTxn, Db, MemDb};
use serai_client::validator_sets::primitives::Session;
use crate::{ use crate::{
networks::{OutputType, Output, Block, UtxoNetwork}, networks::{OutputType, Output, Block, Network},
key_gen::NetworkKeyDb,
multisigs::scanner::{ScannerEvent, Scanner, ScannerHandle}, 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, network: &N,
db: &D, db: &D,
group_key: <N::Curve as Ciphersuite>::G, group_key: <N::Curve as Ciphersuite>::G,
@@ -40,18 +42,27 @@ pub async fn new_scanner<N: UtxoNetwork, D: Db>(
scanner 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 = let mut keys =
frost::tests::key_gen::<_, N::Curve>(&mut OsRng).remove(&Participant::new(1).unwrap()).unwrap(); frost::tests::key_gen::<_, N::Curve>(&mut OsRng).remove(&Participant::new(1).unwrap()).unwrap();
N::tweak_keys(&mut keys); N::tweak_keys(&mut keys);
let group_key = keys.group_key(); 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 // Mine blocks so there's a confirmed block
for _ in 0 .. N::CONFIRMATIONS { for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await; network.mine_block().await;
} }
let db = MemDb::new();
let first = Arc::new(Mutex::new(true)); let first = Arc::new(Mutex::new(true));
let scanner = new_scanner(&network, &db, group_key, &first).await; 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()); .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 // Mine blocks so there's a confirmed block
for _ in 0 .. N::CONFIRMATIONS { for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await; network.mine_block().await;
} }
let mut db = MemDb::new();
let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone()); let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone());
assert!(current_keys.is_empty()); assert!(current_keys.is_empty());
let mut txn = db.txn();
// Register keys to cause Block events at CONFIRMATIONS (dropped since first keys), // Register keys to cause Block events at CONFIRMATIONS (dropped since first keys),
// CONFIRMATIONS + 1, and CONFIRMATIONS + 2 // CONFIRMATIONS + 1, and CONFIRMATIONS + 2
for i in 0 .. 3 { 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 scanner
.register_key( .register_key(
&mut txn, &mut txn,
network.get_latest_block_number().await.unwrap() + N::CONFIRMATIONS + i, network.get_latest_block_number().await.unwrap() + N::CONFIRMATIONS + i,
{ key,
let mut keys = key_gen(&mut OsRng);
for keys in keys.values_mut() {
N::tweak_keys(keys);
}
keys[&Participant::new(1).unwrap()].group_key()
},
) )
.await; .await;
txn.commit();
} }
txn.commit();
for _ in 0 .. (3 * N::CONFIRMATIONS) { for _ in 0 .. (3 * N::CONFIRMATIONS) {
network.mine_block().await; network.mine_block().await;

View File

@@ -1,7 +1,9 @@
use core::{pin::Pin, future::Future};
use std::collections::HashMap; use std::collections::HashMap;
use rand_core::{RngCore, OsRng}; use rand_core::{RngCore, OsRng};
use ciphersuite::group::GroupEncoding;
use frost::{ use frost::{
Participant, ThresholdKeys, Participant, ThresholdKeys,
dkg::tests::{key_gen, clone_without}, dkg::tests::{key_gen, clone_without},
@@ -16,14 +18,15 @@ use serai_client::{
use messages::sign::*; use messages::sign::*;
use crate::{ use crate::{
Payment, Plan, Payment,
networks::{Output, Transaction, Eventuality, UtxoNetwork}, networks::{Output, Transaction, Eventuality, Network},
key_gen::NetworkKeyDb,
multisigs::scheduler::Scheduler, multisigs::scheduler::Scheduler,
signer::Signer, signer::Signer,
}; };
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub async fn sign<N: UtxoNetwork>( pub async fn sign<N: Network>(
network: N, network: N,
session: Session, session: Session,
mut keys_txs: HashMap< mut keys_txs: HashMap<
@@ -153,53 +156,55 @@ pub async fn sign<N: UtxoNetwork>(
typed_claim typed_claim
} }
pub async fn test_signer<N: UtxoNetwork>(network: N) pub async fn test_signer<N: Network>(
where new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
<N::Scheduler as Scheduler<N>>::Addendum: From<()>, ) {
{
let mut keys = key_gen(&mut OsRng); let mut keys = key_gen(&mut OsRng);
for keys in keys.values_mut() { for keys in keys.values_mut() {
N::tweak_keys(keys); N::tweak_keys(keys);
} }
let key = keys[&Participant::new(1).unwrap()].group_key(); 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 let outputs = network
.get_outputs(&network.test_send(N::external_address(&network, key).await).await, key) .get_outputs(&network.test_send(N::external_address(&network, key).await).await, key)
.await; .await;
let sync_block = network.get_latest_block_number().await.unwrap() - N::CONFIRMATIONS; 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 keys_txs = HashMap::new();
let mut eventualities = vec![]; let mut eventualities = vec![];
for (i, keys) in keys.drain() { for (i, keys) in keys.drain() {
let (signable, eventuality) = network let (signable, eventuality) =
.prepare_send( network.prepare_send(sync_block, plan.clone(), 0).await.unwrap().tx.unwrap();
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();
eventualities.push(eventuality.clone()); eventualities.push(eventuality.clone());
keys_txs.insert(i, (keys, (signable, eventuality))); keys_txs.insert(i, (keys, (signable, eventuality)));
@@ -217,11 +222,21 @@ where
key, key,
) )
.await; .await;
assert_eq!(outputs.len(), 2); // Don't run if Ethereum as the received output will revert by the contract
// Adjust the amount for the fees // (and therefore not actually exist)
let amount = amount - tx.fee(&network).await; if N::NETWORK != NetworkId::Ethereum {
// Check either output since Monero will randomize its output order assert_eq!(outputs.len(), 1 + usize::from(u8::from(plan.change.is_some())));
assert!((outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount)); // 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 // Check the eventualities pass
for eventuality in eventualities { 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 rand_core::OsRng;
use ciphersuite::group::GroupEncoding;
use frost::{Participant, dkg::tests::key_gen}; use frost::{Participant, dkg::tests::key_gen};
use tokio::time::timeout; use tokio::time::timeout;
@@ -15,21 +17,19 @@ use serai_client::{
use crate::{ use crate::{
Payment, Plan, Payment, Plan,
networks::{Output, Transaction, Eventuality, Block, UtxoNetwork}, networks::{Output, Transaction, Eventuality, Block, Network},
key_gen::NetworkKeyDb,
multisigs::{ multisigs::{
scanner::{ScannerEvent, Scanner}, scanner::{ScannerEvent, Scanner},
scheduler::Scheduler, scheduler::{self, Scheduler},
}, },
tests::sign, tests::sign,
}; };
// Tests the Scanner, Scheduler, and Signer together // Tests the Scanner, Scheduler, and Signer together
pub async fn test_wallet<N: UtxoNetwork>(network: N) { pub async fn test_wallet<N: Network>(
// Mine blocks so there's a confirmed block new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
for _ in 0 .. N::CONFIRMATIONS { ) {
network.mine_block().await;
}
let mut keys = key_gen(&mut OsRng); let mut keys = key_gen(&mut OsRng);
for keys in keys.values_mut() { for keys in keys.values_mut() {
N::tweak_keys(keys); 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 key = keys[&Participant::new(1).unwrap()].group_key();
let mut db = MemDb::new(); 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()); let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone());
assert!(current_keys.is_empty()); assert!(current_keys.is_empty());
let (block_id, outputs) = { let (block_id, outputs) = {
@@ -93,7 +105,13 @@ pub async fn test_wallet<N: UtxoNetwork>(network: N) {
txn.commit(); txn.commit();
assert_eq!(plans.len(), 1); assert_eq!(plans.len(), 1);
assert_eq!(plans[0].key, key); 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!( assert_eq!(
plans[0].payments, plans[0].payments,
vec![Payment { 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![]; 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 tx = network.get_transaction_by_eventuality(block_number, &eventualities[0]).await;
let block = network.get_block(block_number).await.unwrap(); let block = network.get_block(block_number).await.unwrap();
let outputs = network.get_outputs(&block, key).await; let outputs = network.get_outputs(&block, key).await;
assert_eq!(outputs.len(), 2);
let amount = amount - tx.fee(&network).await; // Don't run if Ethereum as the received output will revert by the contract
assert!((outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount)); // (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 { for eventuality in eventualities {
let completion = network.confirm_completion(&eventuality, &claim).await.unwrap().unwrap(); 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; network.mine_block().await;
} }
match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() { if N::NETWORK != NetworkId::Ethereum {
ScannerEvent::Block { is_retirement_block, block: block_id, outputs: these_outputs } => { match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() {
scanner.multisig_completed.send(false).unwrap(); ScannerEvent::Block { is_retirement_block, block: block_id, outputs: these_outputs } => {
assert!(!is_retirement_block); scanner.multisig_completed.send(false).unwrap();
assert_eq!(block_id, block.id()); assert!(!is_retirement_block);
assert_eq!(these_outputs, outputs); 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 // Check the Scanner DB can reload the outputs
let mut txn = db.txn(); let mut txn = db.txn();
assert_eq!(scanner.ack_block(&mut txn, block.id()).await.1, outputs); assert_eq!(scanner.ack_block(&mut txn, block.id()).await.1, outputs);
scanner.release_lock().await; scanner.release_lock().await;
txn.commit(); txn.commit();
}
} }

View File

@@ -124,7 +124,7 @@ pub fn build(name: String) {
// Check any additionally specified paths // Check any additionally specified paths
let meta = |path: PathBuf| (path.clone(), fs::metadata(path)); let meta = |path: PathBuf| (path.clone(), fs::metadata(path));
let mut metadatas = match name.as_str() { let mut metadatas = match name.as_str() {
"bitcoin" | "monero" => vec![], "bitcoin" | "ethereum" | "monero" => vec![],
"message-queue" => vec![ "message-queue" => vec![
meta(repo_path.join("common")), meta(repo_path.join("common")),
meta(repo_path.join("crypto")), 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"] } dkg = { path = "../../crypto/dkg", default-features = false, features = ["tests"] }
bitcoin-serai = { path = "../../coins/bitcoin" } bitcoin-serai = { path = "../../coins/bitcoin" }
ethereum-serai = { path = "../../coins/ethereum" }
monero-serai = { path = "../../coins/monero" } monero-serai = { path = "../../coins/monero" }
messages = { package = "serai-processor-messages", path = "../../processor/messages" } messages = { package = "serai-processor-messages", path = "../../processor/messages" }
scale = { package = "parity-scale-codec", version = "3" } scale = { package = "parity-scale-codec", version = "3" }
serai-client = { path = "../../substrate/client" } serai-client = { path = "../../substrate/client" }
serai-db = { path = "../../common/db", default-features = false }
serai-message-queue = { path = "../../message-queue" } serai-message-queue = { path = "../../message-queue" }
borsh = { version = "1", features = ["de_strict_order"] } borsh = { version = "1", features = ["de_strict_order"] }

View File

@@ -181,7 +181,28 @@ impl Coordinator {
break; 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 => { NetworkId::Monero => {
use monero_serai::rpc::HttpRpc; use monero_serai::rpc::HttpRpc;
@@ -271,7 +292,45 @@ impl Coordinator {
block.consensus_encode(&mut block_buf).unwrap(); block.consensus_encode(&mut block_buf).unwrap();
(hash, block_buf) (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 => { NetworkId::Monero => {
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar}; use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar};
use monero_serai::{ 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]) { pub async fn sync(&self, ops: &DockerOperations, others: &[Coordinator]) {
let rpc_url = network_rpc(self.network, ops, &self.network_handle); let rpc_url = network_rpc(self.network, ops, &self.network_handle);
match self.network { 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 rpc = Rpc::new(rpc_url).await.expect("couldn't connect to the Bitcoin RPC");
let to = rpc.get_latest_block_number().await.unwrap(); let to = rpc.get_latest_block_number().await.unwrap();
for coordinator in others { for coordinator in others {
let from = Rpc::new(network_rpc(self.network, ops, &coordinator.network_handle)) let from = rpc.get_latest_block_number().await.unwrap() + 1;
.await
.expect("couldn't connect to the Bitcoin RPC")
.get_latest_block_number()
.await
.unwrap() +
1;
for b in from ..= to { for b in from ..= to {
let mut buf = vec![]; let mut buf = vec![];
rpc rpc
@@ -360,11 +381,40 @@ impl Coordinator {
.unwrap() .unwrap()
.consensus_encode(&mut buf) .consensus_encode(&mut buf)
.unwrap(); .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 => { NetworkId::Monero => {
use monero_serai::rpc::HttpRpc; use monero_serai::rpc::HttpRpc;
@@ -378,12 +428,21 @@ impl Coordinator {
.await .await
.unwrap(); .unwrap();
for b in from .. to { for b in from .. to {
coordinator let block =
.broadcast_block( rpc.get_block(rpc.get_block_hash(b).await.unwrap()).await.unwrap().serialize();
ops,
&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; .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::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(); 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 => { NetworkId::Monero => {
use monero_serai::{transaction::Transaction, rpc::HttpRpc}; use monero_serai::{transaction::Transaction, rpc::HttpRpc};
@@ -445,7 +516,26 @@ impl Coordinator {
None 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 => { NetworkId::Monero => {
use monero_serai::rpc::HttpRpc; 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 RPC_PASS: &str = "seraidex";
pub const BTC_PORT: u32 = 8332; pub const BTC_PORT: u32 = 8332;
pub const ETH_PORT: u32 = 8545;
pub const XMR_PORT: u32 = 18081; pub const XMR_PORT: u32 = 18081;
pub fn bitcoin_instance() -> (TestBodySpecification, u32) { pub fn bitcoin_instance() -> (TestBodySpecification, u32) {
@@ -31,6 +32,17 @@ pub fn bitcoin_instance() -> (TestBodySpecification, u32) {
(composition, BTC_PORT) (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) { pub fn monero_instance() -> (TestBodySpecification, u32) {
serai_docker_tests::build("monero".to_string()); 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) { pub fn network_instance(network: NetworkId) -> (TestBodySpecification, u32) {
match network { match network {
NetworkId::Bitcoin => bitcoin_instance(), NetworkId::Bitcoin => bitcoin_instance(),
NetworkId::Ethereum => todo!(), NetworkId::Ethereum => ethereum_instance(),
NetworkId::Monero => monero_instance(), NetworkId::Monero => monero_instance(),
NetworkId::Serai => { NetworkId::Serai => {
panic!("Serai is not a valid network to spawn an instance of for a processor") 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) .handle(handle)
.host_port(match network { .host_port(match network {
NetworkId::Bitcoin => BTC_PORT, NetworkId::Bitcoin => BTC_PORT,
NetworkId::Ethereum => todo!(), NetworkId::Ethereum => ETH_PORT,
NetworkId::Monero => XMR_PORT, NetworkId::Monero => XMR_PORT,
NetworkId::Serai => panic!("getting port for external network yet it was Serai"), 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::*; use processor::networks::*;
match network { match network {
NetworkId::Bitcoin => Bitcoin::CONFIRMATIONS, NetworkId::Bitcoin => Bitcoin::CONFIRMATIONS,
NetworkId::Ethereum => todo!(), NetworkId::Ethereum => Ethereum::<serai_db::MemDb>::CONFIRMATIONS,
NetworkId::Monero => Monero::CONFIRMATIONS, NetworkId::Monero => Monero::CONFIRMATIONS,
NetworkId::Serai => panic!("getting confirmations required for Serai"), NetworkId::Serai => panic!("getting confirmations required for Serai"),
} }
@@ -83,6 +95,10 @@ pub enum Wallet {
public_key: bitcoin_serai::bitcoin::PublicKey, public_key: bitcoin_serai::bitcoin::PublicKey,
input_tx: bitcoin_serai::bitcoin::Transaction, input_tx: bitcoin_serai::bitcoin::Transaction,
}, },
Ethereum {
key: <ciphersuite::Secp256k1 as Ciphersuite>::F,
nonce: u64,
},
Monero { Monero {
handle: String, handle: String,
spend_key: Zeroizing<curve25519_dalek::scalar::Scalar>, spend_key: Zeroizing<curve25519_dalek::scalar::Scalar>,
@@ -138,7 +154,37 @@ impl Wallet {
Wallet::Bitcoin { private_key, public_key, input_tx: funds } 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 => { NetworkId::Monero => {
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar}; use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar};
@@ -282,6 +328,24 @@ impl Wallet {
(buf, Balance { coin: Coin::Bitcoin, amount: Amount(AMOUNT) }) (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 } => { Wallet::Monero { handle, ref spend_key, ref view_pair, ref mut inputs } => {
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT; use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
use monero_serai::{ use monero_serai::{
@@ -374,6 +438,13 @@ impl Wallet {
) )
.unwrap() .unwrap()
} }
Wallet::Ethereum { key, .. } => {
use ciphersuite::{Ciphersuite, Secp256k1};
ExternalAddress::new(
ethereum_serai::crypto::address(&(Secp256k1::generator() * key)).into(),
)
.unwrap()
}
Wallet::Monero { view_pair, .. } => { Wallet::Monero { view_pair, .. } => {
use monero_serai::wallet::address::{Network, AddressSpec}; use monero_serai::wallet::address::{Network, AddressSpec};
ExternalAddress::new( ExternalAddress::new(