mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-22 19:49:22 +00:00
Merge branch 'develop' of https://github.com/akildemir/serai into move-emissions-tests
This commit is contained in:
commit
e791c659af
197 changed files with 3552 additions and 46006 deletions
2
.github/actions/monero-wallet-rpc/action.yml
vendored
2
.github/actions/monero-wallet-rpc/action.yml
vendored
|
@ -5,7 +5,7 @@ inputs:
|
||||||
version:
|
version:
|
||||||
description: "Version to download and run"
|
description: "Version to download and run"
|
||||||
required: false
|
required: false
|
||||||
default: v0.18.3.1
|
default: v0.18.3.4
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
|
|
2
.github/actions/monero/action.yml
vendored
2
.github/actions/monero/action.yml
vendored
|
@ -5,7 +5,7 @@ inputs:
|
||||||
version:
|
version:
|
||||||
description: "Version to download and run"
|
description: "Version to download and run"
|
||||||
required: false
|
required: false
|
||||||
default: v0.18.3.1
|
default: v0.18.3.4
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
|
|
2
.github/actions/test-dependencies/action.yml
vendored
2
.github/actions/test-dependencies/action.yml
vendored
|
@ -5,7 +5,7 @@ inputs:
|
||||||
monero-version:
|
monero-version:
|
||||||
description: "Monero version to download and run as a regtest node"
|
description: "Monero version to download and run as a regtest node"
|
||||||
required: false
|
required: false
|
||||||
default: v0.18.3.1
|
default: v0.18.3.4
|
||||||
|
|
||||||
bitcoin-version:
|
bitcoin-version:
|
||||||
description: "Bitcoin version to download and run as a regtest node"
|
description: "Bitcoin version to download and run as a regtest node"
|
||||||
|
|
1
.github/workflows/common-tests.yml
vendored
1
.github/workflows/common-tests.yml
vendored
|
@ -27,6 +27,7 @@ jobs:
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
||||||
-p std-shims \
|
-p std-shims \
|
||||||
-p zalloc \
|
-p zalloc \
|
||||||
|
-p patchable-async-sleep \
|
||||||
-p serai-db \
|
-p serai-db \
|
||||||
-p serai-env \
|
-p serai-env \
|
||||||
-p simple-request
|
-p simple-request
|
||||||
|
|
9
.github/workflows/monero-tests.yaml
vendored
9
.github/workflows/monero-tests.yaml
vendored
|
@ -39,9 +39,6 @@ jobs:
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --lib
|
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --lib
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-address --lib
|
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-address --lib
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --lib
|
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --lib
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-seed --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package polyseed --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet-util --lib
|
|
||||||
|
|
||||||
# Doesn't run unit tests with features as the tests workflow will
|
# Doesn't run unit tests with features as the tests workflow will
|
||||||
|
|
||||||
|
@ -50,7 +47,7 @@ jobs:
|
||||||
# Test against all supported protocol versions
|
# Test against all supported protocol versions
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
version: [v0.17.3.2, v0.18.2.0]
|
version: [v0.17.3.2, v0.18.3.4]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||||
|
@ -65,13 +62,11 @@ jobs:
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --test '*'
|
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --test '*'
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --test '*'
|
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --test '*'
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --test '*'
|
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --test '*'
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet-util --test '*'
|
|
||||||
|
|
||||||
- name: Run Integration Tests
|
- name: Run Integration Tests
|
||||||
# Don't run if the the tests workflow also will
|
# Don't run if the the tests workflow also will
|
||||||
if: ${{ matrix.version != 'v0.18.2.0' }}
|
if: ${{ matrix.version != 'v0.18.3.4' }}
|
||||||
run: |
|
run: |
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --all-features --test '*'
|
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --all-features --test '*'
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --test '*'
|
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --test '*'
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --all-features --test '*'
|
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --all-features --test '*'
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet-util --all-features --test '*'
|
|
||||||
|
|
3
.github/workflows/networks-tests.yml
vendored
3
.github/workflows/networks-tests.yml
vendored
|
@ -45,7 +45,4 @@ jobs:
|
||||||
-p monero-simple-request-rpc \
|
-p monero-simple-request-rpc \
|
||||||
-p monero-address \
|
-p monero-address \
|
||||||
-p monero-wallet \
|
-p monero-wallet \
|
||||||
-p monero-seed \
|
|
||||||
-p polyseed \
|
|
||||||
-p monero-wallet-util \
|
|
||||||
-p monero-serai-verify-chain
|
-p monero-serai-verify-chain
|
||||||
|
|
778
Cargo.lock
generated
778
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
13
Cargo.toml
13
Cargo.toml
|
@ -6,7 +6,6 @@ members = [
|
||||||
"patches/parking_lot",
|
"patches/parking_lot",
|
||||||
"patches/zstd",
|
"patches/zstd",
|
||||||
"patches/rocksdb",
|
"patches/rocksdb",
|
||||||
"patches/proc-macro-crate",
|
|
||||||
|
|
||||||
# std patches
|
# std patches
|
||||||
"patches/matches",
|
"patches/matches",
|
||||||
|
@ -18,6 +17,7 @@ members = [
|
||||||
|
|
||||||
"common/std-shims",
|
"common/std-shims",
|
||||||
"common/zalloc",
|
"common/zalloc",
|
||||||
|
"common/patchable-async-sleep",
|
||||||
"common/db",
|
"common/db",
|
||||||
"common/env",
|
"common/env",
|
||||||
"common/request",
|
"common/request",
|
||||||
|
@ -55,9 +55,6 @@ members = [
|
||||||
"networks/monero/rpc/simple-request",
|
"networks/monero/rpc/simple-request",
|
||||||
"networks/monero/wallet/address",
|
"networks/monero/wallet/address",
|
||||||
"networks/monero/wallet",
|
"networks/monero/wallet",
|
||||||
"networks/monero/wallet/seed",
|
|
||||||
"networks/monero/wallet/polyseed",
|
|
||||||
"networks/monero/wallet/util",
|
|
||||||
"networks/monero/verify-chain",
|
"networks/monero/verify-chain",
|
||||||
|
|
||||||
"message-queue",
|
"message-queue",
|
||||||
|
@ -138,17 +135,12 @@ panic = "unwind"
|
||||||
# https://github.com/rust-lang-nursery/lazy-static.rs/issues/201
|
# https://github.com/rust-lang-nursery/lazy-static.rs/issues/201
|
||||||
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
|
|
||||||
dockertest = { git = "https://github.com/orcalabs/dockertest-rs", rev = "4dd6ae24738aa6dc5c89444cc822ea4745517493" }
|
|
||||||
|
|
||||||
parking_lot_core = { path = "patches/parking_lot_core" }
|
parking_lot_core = { path = "patches/parking_lot_core" }
|
||||||
parking_lot = { path = "patches/parking_lot" }
|
parking_lot = { path = "patches/parking_lot" }
|
||||||
# wasmtime pulls in an old version for this
|
# wasmtime pulls in an old version for this
|
||||||
zstd = { path = "patches/zstd" }
|
zstd = { path = "patches/zstd" }
|
||||||
# Needed for WAL compression
|
# Needed for WAL compression
|
||||||
rocksdb = { path = "patches/rocksdb" }
|
rocksdb = { path = "patches/rocksdb" }
|
||||||
# proc-macro-crate 2 binds to an old version of toml for msrv so we patch to 3
|
|
||||||
proc-macro-crate = { path = "patches/proc-macro-crate" }
|
|
||||||
|
|
||||||
# is-terminal now has an std-based solution with an equivalent API
|
# is-terminal now has an std-based solution with an equivalent API
|
||||||
is-terminal = { path = "patches/is-terminal" }
|
is-terminal = { path = "patches/is-terminal" }
|
||||||
|
@ -163,6 +155,9 @@ matches = { path = "patches/matches" }
|
||||||
option-ext = { path = "patches/option-ext" }
|
option-ext = { path = "patches/option-ext" }
|
||||||
directories-next = { path = "patches/directories-next" }
|
directories-next = { path = "patches/directories-next" }
|
||||||
|
|
||||||
|
# https://github.com/alloy-rs/core/issues/717
|
||||||
|
alloy-sol-type-parser = { git = "https://github.com/alloy-rs/core", rev = "446b9d2fbce12b88456152170709a3eaac929af0" }
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
unwrap_or_default = "allow"
|
unwrap_or_default = "allow"
|
||||||
borrow_as_ptr = "deny"
|
borrow_as_ptr = "deny"
|
||||||
|
|
19
common/patchable-async-sleep/Cargo.toml
Normal file
19
common/patchable-async-sleep/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "patchable-async-sleep"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "An async sleep function, patchable to the preferred runtime"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/serai-dex/serai/tree/develop/common/patchable-async-sleep"
|
||||||
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
|
keywords = ["async", "sleep", "tokio", "smol", "async-std"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", default-features = false, features = [ "time"] }
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
Copyright (c) 2024 Luke Parker
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
7
common/patchable-async-sleep/README.md
Normal file
7
common/patchable-async-sleep/README.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Patchable Async Sleep
|
||||||
|
|
||||||
|
An async sleep function, patchable to the preferred runtime.
|
||||||
|
|
||||||
|
This crate is `tokio`-backed. Applications which don't want to use `tokio`
|
||||||
|
should patch this crate to one which works witht heir preferred runtime. The
|
||||||
|
point of it is to have a minimal API surface to trivially facilitate such work.
|
10
common/patchable-async-sleep/src/lib.rs
Normal file
10
common/patchable-async-sleep/src/lib.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
use core::time::Duration;
|
||||||
|
|
||||||
|
/// Sleep for the specified duration.
|
||||||
|
pub fn sleep(duration: Duration) -> impl core::future::Future<Output = ()> {
|
||||||
|
tokio::time::sleep(duration)
|
||||||
|
}
|
|
@ -12,9 +12,9 @@ use tokio::{
|
||||||
use borsh::BorshSerialize;
|
use borsh::BorshSerialize;
|
||||||
use sp_application_crypto::RuntimePublic;
|
use sp_application_crypto::RuntimePublic;
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::{NETWORKS, NetworkId, Signature},
|
primitives::{ExternalNetworkId, Signature, EXTERNAL_NETWORKS},
|
||||||
validator_sets::primitives::{Session, ValidatorSet},
|
validator_sets::primitives::{ExternalValidatorSet, Session},
|
||||||
SeraiError, TemporalSerai, Serai,
|
Serai, SeraiError, TemporalSerai,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serai_db::{Get, DbTxn, Db, create_db};
|
use serai_db::{Get, DbTxn, Db, create_db};
|
||||||
|
@ -28,17 +28,17 @@ use crate::{
|
||||||
|
|
||||||
create_db! {
|
create_db! {
|
||||||
CosignDb {
|
CosignDb {
|
||||||
ReceivedCosign: (set: ValidatorSet, block: [u8; 32]) -> CosignedBlock,
|
ReceivedCosign: (set: ExternalValidatorSet, block: [u8; 32]) -> CosignedBlock,
|
||||||
LatestCosign: (network: NetworkId) -> CosignedBlock,
|
LatestCosign: (network: ExternalNetworkId) -> CosignedBlock,
|
||||||
DistinctChain: (set: ValidatorSet) -> (),
|
DistinctChain: (set: ExternalValidatorSet) -> (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CosignEvaluator<D: Db> {
|
pub struct CosignEvaluator<D: Db> {
|
||||||
db: Mutex<D>,
|
db: Mutex<D>,
|
||||||
serai: Arc<Serai>,
|
serai: Arc<Serai>,
|
||||||
stakes: RwLock<Option<HashMap<NetworkId, u64>>>,
|
stakes: RwLock<Option<HashMap<ExternalNetworkId, u64>>>,
|
||||||
latest_cosigns: RwLock<HashMap<NetworkId, CosignedBlock>>,
|
latest_cosigns: RwLock<HashMap<ExternalNetworkId, CosignedBlock>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: Db> CosignEvaluator<D> {
|
impl<D: Db> CosignEvaluator<D> {
|
||||||
|
@ -79,7 +79,7 @@ impl<D: Db> CosignEvaluator<D> {
|
||||||
let serai = self.serai.as_of_latest_finalized_block().await?;
|
let serai = self.serai.as_of_latest_finalized_block().await?;
|
||||||
|
|
||||||
let mut stakes = HashMap::new();
|
let mut stakes = HashMap::new();
|
||||||
for network in NETWORKS {
|
for network in EXTERNAL_NETWORKS {
|
||||||
// Use if this network has published a Batch for a short-circuit of if they've ever set a key
|
// Use if this network has published a Batch for a short-circuit of if they've ever set a key
|
||||||
let set_key = serai.in_instructions().last_batch_for_network(network).await?.is_some();
|
let set_key = serai.in_instructions().last_batch_for_network(network).await?.is_some();
|
||||||
if set_key {
|
if set_key {
|
||||||
|
@ -87,7 +87,7 @@ impl<D: Db> CosignEvaluator<D> {
|
||||||
network,
|
network,
|
||||||
serai
|
serai
|
||||||
.validator_sets()
|
.validator_sets()
|
||||||
.total_allocated_stake(network)
|
.total_allocated_stake(network.into())
|
||||||
.await?
|
.await?
|
||||||
.expect("network which published a batch didn't have a stake set")
|
.expect("network which published a batch didn't have a stake set")
|
||||||
.0,
|
.0,
|
||||||
|
@ -126,9 +126,9 @@ impl<D: Db> CosignEvaluator<D> {
|
||||||
|
|
||||||
async fn set_with_keys_fn(
|
async fn set_with_keys_fn(
|
||||||
serai: &TemporalSerai<'_>,
|
serai: &TemporalSerai<'_>,
|
||||||
network: NetworkId,
|
network: ExternalNetworkId,
|
||||||
) -> Result<Option<ValidatorSet>, SeraiError> {
|
) -> Result<Option<ExternalValidatorSet>, SeraiError> {
|
||||||
let Some(latest_session) = serai.validator_sets().session(network).await? else {
|
let Some(latest_session) = serai.validator_sets().session(network.into()).await? else {
|
||||||
log::warn!("received cosign from {:?}, which doesn't yet have a session", network);
|
log::warn!("received cosign from {:?}, which doesn't yet have a session", network);
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
@ -136,13 +136,13 @@ impl<D: Db> CosignEvaluator<D> {
|
||||||
Ok(Some(
|
Ok(Some(
|
||||||
if serai
|
if serai
|
||||||
.validator_sets()
|
.validator_sets()
|
||||||
.keys(ValidatorSet { network, session: prior_session })
|
.keys(ExternalValidatorSet { network, session: prior_session })
|
||||||
.await?
|
.await?
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
ValidatorSet { network, session: prior_session }
|
ExternalValidatorSet { network, session: prior_session }
|
||||||
} else {
|
} else {
|
||||||
ValidatorSet { network, session: latest_session }
|
ExternalValidatorSet { network, session: latest_session }
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -204,16 +204,12 @@ impl<D: Db> CosignEvaluator<D> {
|
||||||
|
|
||||||
let mut total_stake = 0;
|
let mut total_stake = 0;
|
||||||
let mut total_on_distinct_chain = 0;
|
let mut total_on_distinct_chain = 0;
|
||||||
for network in NETWORKS {
|
for network in EXTERNAL_NETWORKS {
|
||||||
if network == NetworkId::Serai {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current set for this network
|
// Get the current set for this network
|
||||||
let set_with_keys = {
|
let set_with_keys = {
|
||||||
let mut res;
|
let mut res;
|
||||||
while {
|
while {
|
||||||
res = set_with_keys_fn(&serai, cosign.network).await;
|
res = set_with_keys_fn(&serai, network).await;
|
||||||
res.is_err()
|
res.is_err()
|
||||||
} {
|
} {
|
||||||
log::error!(
|
log::error!(
|
||||||
|
@ -231,7 +227,8 @@ impl<D: Db> CosignEvaluator<D> {
|
||||||
let stake = {
|
let stake = {
|
||||||
let mut res;
|
let mut res;
|
||||||
while {
|
while {
|
||||||
res = serai.validator_sets().total_allocated_stake(set_with_keys.network).await;
|
res =
|
||||||
|
serai.validator_sets().total_allocated_stake(set_with_keys.network.into()).await;
|
||||||
res.is_err()
|
res.is_err()
|
||||||
} {
|
} {
|
||||||
log::error!(
|
log::error!(
|
||||||
|
@ -271,7 +268,7 @@ impl<D: Db> CosignEvaluator<D> {
|
||||||
#[allow(clippy::new_ret_no_self)]
|
#[allow(clippy::new_ret_no_self)]
|
||||||
pub fn new<P: P2p>(db: D, p2p: P, serai: Arc<Serai>) -> mpsc::UnboundedSender<CosignedBlock> {
|
pub fn new<P: P2p>(db: D, p2p: P, serai: Arc<Serai>) -> mpsc::UnboundedSender<CosignedBlock> {
|
||||||
let mut latest_cosigns = HashMap::new();
|
let mut latest_cosigns = HashMap::new();
|
||||||
for network in NETWORKS {
|
for network in EXTERNAL_NETWORKS {
|
||||||
if let Some(cosign) = LatestCosign::get(&db, network) {
|
if let Some(cosign) = LatestCosign::get(&db, network) {
|
||||||
latest_cosigns.insert(network, cosign);
|
latest_cosigns.insert(network, cosign);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@ use blake2::{
|
||||||
use scale::Encode;
|
use scale::Encode;
|
||||||
use borsh::{BorshSerialize, BorshDeserialize};
|
use borsh::{BorshSerialize, BorshDeserialize};
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::NetworkId,
|
|
||||||
validator_sets::primitives::{Session, ValidatorSet},
|
|
||||||
in_instructions::primitives::{Batch, SignedBatch},
|
in_instructions::primitives::{Batch, SignedBatch},
|
||||||
|
primitives::ExternalNetworkId,
|
||||||
|
validator_sets::primitives::{ExternalValidatorSet, Session},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use serai_db::*;
|
pub use serai_db::*;
|
||||||
|
@ -18,21 +18,21 @@ use crate::tributary::{TributarySpec, Transaction, scanner::RecognizedIdType};
|
||||||
|
|
||||||
create_db!(
|
create_db!(
|
||||||
MainDb {
|
MainDb {
|
||||||
HandledMessageDb: (network: NetworkId) -> u64,
|
HandledMessageDb: (network: ExternalNetworkId) -> u64,
|
||||||
ActiveTributaryDb: () -> Vec<u8>,
|
ActiveTributaryDb: () -> Vec<u8>,
|
||||||
RetiredTributaryDb: (set: ValidatorSet) -> (),
|
RetiredTributaryDb: (set: ExternalValidatorSet) -> (),
|
||||||
FirstPreprocessDb: (
|
FirstPreprocessDb: (
|
||||||
network: NetworkId,
|
network: ExternalNetworkId,
|
||||||
id_type: RecognizedIdType,
|
id_type: RecognizedIdType,
|
||||||
id: &[u8]
|
id: &[u8]
|
||||||
) -> Vec<Vec<u8>>,
|
) -> Vec<Vec<u8>>,
|
||||||
LastReceivedBatchDb: (network: NetworkId) -> u32,
|
LastReceivedBatchDb: (network: ExternalNetworkId) -> u32,
|
||||||
ExpectedBatchDb: (network: NetworkId, id: u32) -> [u8; 32],
|
ExpectedBatchDb: (network: ExternalNetworkId, id: u32) -> [u8; 32],
|
||||||
BatchDb: (network: NetworkId, id: u32) -> SignedBatch,
|
BatchDb: (network: ExternalNetworkId, id: u32) -> SignedBatch,
|
||||||
LastVerifiedBatchDb: (network: NetworkId) -> u32,
|
LastVerifiedBatchDb: (network: ExternalNetworkId) -> u32,
|
||||||
HandoverBatchDb: (set: ValidatorSet) -> u32,
|
HandoverBatchDb: (set: ExternalValidatorSet) -> u32,
|
||||||
LookupHandoverBatchDb: (network: NetworkId, batch: u32) -> Session,
|
LookupHandoverBatchDb: (network: ExternalNetworkId, batch: u32) -> Session,
|
||||||
QueuedBatchesDb: (set: ValidatorSet) -> Vec<u8>
|
QueuedBatchesDb: (set: ExternalValidatorSet) -> Vec<u8>
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ impl ActiveTributaryDb {
|
||||||
ActiveTributaryDb::set(txn, &existing_bytes);
|
ActiveTributaryDb::set(txn, &existing_bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn retire_tributary(txn: &mut impl DbTxn, set: ValidatorSet) {
|
pub fn retire_tributary(txn: &mut impl DbTxn, set: ExternalValidatorSet) {
|
||||||
let mut active = Self::active_tributaries(txn).1;
|
let mut active = Self::active_tributaries(txn).1;
|
||||||
for i in 0 .. active.len() {
|
for i in 0 .. active.len() {
|
||||||
if active[i].set() == set {
|
if active[i].set() == set {
|
||||||
|
@ -82,7 +82,7 @@ impl ActiveTributaryDb {
|
||||||
impl FirstPreprocessDb {
|
impl FirstPreprocessDb {
|
||||||
pub fn save_first_preprocess(
|
pub fn save_first_preprocess(
|
||||||
txn: &mut impl DbTxn,
|
txn: &mut impl DbTxn,
|
||||||
network: NetworkId,
|
network: ExternalNetworkId,
|
||||||
id_type: RecognizedIdType,
|
id_type: RecognizedIdType,
|
||||||
id: &[u8],
|
id: &[u8],
|
||||||
preprocess: &Vec<Vec<u8>>,
|
preprocess: &Vec<Vec<u8>>,
|
||||||
|
@ -108,19 +108,19 @@ impl ExpectedBatchDb {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HandoverBatchDb {
|
impl HandoverBatchDb {
|
||||||
pub fn set_handover_batch(txn: &mut impl DbTxn, set: ValidatorSet, batch: u32) {
|
pub fn set_handover_batch(txn: &mut impl DbTxn, set: ExternalValidatorSet, batch: u32) {
|
||||||
Self::set(txn, set, &batch);
|
Self::set(txn, set, &batch);
|
||||||
LookupHandoverBatchDb::set(txn, set.network, batch, &set.session);
|
LookupHandoverBatchDb::set(txn, set.network, batch, &set.session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl QueuedBatchesDb {
|
impl QueuedBatchesDb {
|
||||||
pub fn queue(txn: &mut impl DbTxn, set: ValidatorSet, batch: &Transaction) {
|
pub fn queue(txn: &mut impl DbTxn, set: ExternalValidatorSet, batch: &Transaction) {
|
||||||
let mut batches = Self::get(txn, set).unwrap_or_default();
|
let mut batches = Self::get(txn, set).unwrap_or_default();
|
||||||
batch.write(&mut batches).unwrap();
|
batch.write(&mut batches).unwrap();
|
||||||
Self::set(txn, set, &batches);
|
Self::set(txn, set, &batches);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn take(txn: &mut impl DbTxn, set: ValidatorSet) -> Vec<Transaction> {
|
pub fn take(txn: &mut impl DbTxn, set: ExternalValidatorSet) -> Vec<Transaction> {
|
||||||
let batches_vec = Self::get(txn, set).unwrap_or_default();
|
let batches_vec = Self::get(txn, set).unwrap_or_default();
|
||||||
txn.del(Self::key(set));
|
txn.del(Self::key(set));
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,8 @@ use serai_db::{DbTxn, Db};
|
||||||
use scale::Encode;
|
use scale::Encode;
|
||||||
use borsh::BorshSerialize;
|
use borsh::BorshSerialize;
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::NetworkId,
|
primitives::ExternalNetworkId,
|
||||||
validator_sets::primitives::{Session, ValidatorSet, KeyPair},
|
validator_sets::primitives::{ExternalValidatorSet, KeyPair, Session},
|
||||||
Public, Serai, SeraiInInstructions,
|
Public, Serai, SeraiInInstructions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ pub struct ActiveTributary<D: Db, P: P2p> {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum TributaryEvent<D: Db, P: P2p> {
|
pub enum TributaryEvent<D: Db, P: P2p> {
|
||||||
NewTributary(ActiveTributary<D, P>),
|
NewTributary(ActiveTributary<D, P>),
|
||||||
TributaryRetired(ValidatorSet),
|
TributaryRetired(ExternalValidatorSet),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new tributary and sends it to all listeners.
|
// Creates a new tributary and sends it to all listeners.
|
||||||
|
@ -145,7 +145,7 @@ async fn handle_processor_message<D: Db, P: P2p>(
|
||||||
p2p: &P,
|
p2p: &P,
|
||||||
cosign_channel: &mpsc::UnboundedSender<CosignedBlock>,
|
cosign_channel: &mpsc::UnboundedSender<CosignedBlock>,
|
||||||
tributaries: &HashMap<Session, ActiveTributary<D, P>>,
|
tributaries: &HashMap<Session, ActiveTributary<D, P>>,
|
||||||
network: NetworkId,
|
network: ExternalNetworkId,
|
||||||
msg: &processors::Message,
|
msg: &processors::Message,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
#[allow(clippy::nonminimal_bool)]
|
#[allow(clippy::nonminimal_bool)]
|
||||||
|
@ -193,7 +193,8 @@ async fn handle_processor_message<D: Db, P: P2p>(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|plan| plan.session)
|
.map(|plan| plan.session)
|
||||||
.filter(|session| {
|
.filter(|session| {
|
||||||
RetiredTributaryDb::get(&txn, ValidatorSet { network, session: *session }).is_none()
|
RetiredTributaryDb::get(&txn, ExternalValidatorSet { network, session: *session })
|
||||||
|
.is_none()
|
||||||
})
|
})
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
@ -265,7 +266,7 @@ async fn handle_processor_message<D: Db, P: P2p>(
|
||||||
}
|
}
|
||||||
// This causes an action on Substrate yet not on any Tributary
|
// This causes an action on Substrate yet not on any Tributary
|
||||||
coordinator::ProcessorMessage::SignedSlashReport { session, signature } => {
|
coordinator::ProcessorMessage::SignedSlashReport { session, signature } => {
|
||||||
let set = ValidatorSet { network, session: *session };
|
let set = ExternalValidatorSet { network, session: *session };
|
||||||
let signature: &[u8] = signature.as_ref();
|
let signature: &[u8] = signature.as_ref();
|
||||||
let signature = serai_client::Signature(signature.try_into().unwrap());
|
let signature = serai_client::Signature(signature.try_into().unwrap());
|
||||||
|
|
||||||
|
@ -393,7 +394,7 @@ async fn handle_processor_message<D: Db, P: P2p>(
|
||||||
if let Some(relevant_tributary_value) = relevant_tributary {
|
if let Some(relevant_tributary_value) = relevant_tributary {
|
||||||
if RetiredTributaryDb::get(
|
if RetiredTributaryDb::get(
|
||||||
&txn,
|
&txn,
|
||||||
ValidatorSet { network: msg.network, session: relevant_tributary_value },
|
ExternalValidatorSet { network: msg.network, session: relevant_tributary_value },
|
||||||
)
|
)
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
|
@ -782,7 +783,7 @@ async fn handle_processor_messages<D: Db, Pro: Processors, P: P2p>(
|
||||||
processors: Pro,
|
processors: Pro,
|
||||||
p2p: P,
|
p2p: P,
|
||||||
cosign_channel: mpsc::UnboundedSender<CosignedBlock>,
|
cosign_channel: mpsc::UnboundedSender<CosignedBlock>,
|
||||||
network: NetworkId,
|
network: ExternalNetworkId,
|
||||||
mut tributary_event: mpsc::UnboundedReceiver<TributaryEvent<D, P>>,
|
mut tributary_event: mpsc::UnboundedReceiver<TributaryEvent<D, P>>,
|
||||||
) {
|
) {
|
||||||
let mut tributaries = HashMap::new();
|
let mut tributaries = HashMap::new();
|
||||||
|
@ -831,7 +832,7 @@ async fn handle_processor_messages<D: Db, Pro: Processors, P: P2p>(
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn handle_cosigns_and_batch_publication<D: Db, P: P2p>(
|
async fn handle_cosigns_and_batch_publication<D: Db, P: P2p>(
|
||||||
mut db: D,
|
mut db: D,
|
||||||
network: NetworkId,
|
network: ExternalNetworkId,
|
||||||
mut tributary_event: mpsc::UnboundedReceiver<TributaryEvent<D, P>>,
|
mut tributary_event: mpsc::UnboundedReceiver<TributaryEvent<D, P>>,
|
||||||
) {
|
) {
|
||||||
let mut tributaries = HashMap::new();
|
let mut tributaries = HashMap::new();
|
||||||
|
@ -905,7 +906,7 @@ async fn handle_cosigns_and_batch_publication<D: Db, P: P2p>(
|
||||||
for batch in start_id ..= last_id {
|
for batch in start_id ..= last_id {
|
||||||
let is_pre_handover = LookupHandoverBatchDb::get(&txn, network, batch + 1);
|
let is_pre_handover = LookupHandoverBatchDb::get(&txn, network, batch + 1);
|
||||||
if let Some(session) = is_pre_handover {
|
if let Some(session) = is_pre_handover {
|
||||||
let set = ValidatorSet { network, session };
|
let set = ExternalValidatorSet { network, session };
|
||||||
let mut queued = QueuedBatchesDb::take(&mut txn, set);
|
let mut queued = QueuedBatchesDb::take(&mut txn, set);
|
||||||
// is_handover_batch is only set for handover `Batch`s we're participating in, making
|
// is_handover_batch is only set for handover `Batch`s we're participating in, making
|
||||||
// this safe
|
// this safe
|
||||||
|
@ -923,7 +924,8 @@ async fn handle_cosigns_and_batch_publication<D: Db, P: P2p>(
|
||||||
|
|
||||||
let is_handover = LookupHandoverBatchDb::get(&txn, network, batch);
|
let is_handover = LookupHandoverBatchDb::get(&txn, network, batch);
|
||||||
if let Some(session) = is_handover {
|
if let Some(session) = is_handover {
|
||||||
for queued in QueuedBatchesDb::take(&mut txn, ValidatorSet { network, session }) {
|
for queued in QueuedBatchesDb::take(&mut txn, ExternalValidatorSet { network, session })
|
||||||
|
{
|
||||||
to_publish.push((session, queued));
|
to_publish.push((session, queued));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -970,10 +972,7 @@ pub async fn handle_processors<D: Db, Pro: Processors, P: P2p>(
|
||||||
mut tributary_event: broadcast::Receiver<TributaryEvent<D, P>>,
|
mut tributary_event: broadcast::Receiver<TributaryEvent<D, P>>,
|
||||||
) {
|
) {
|
||||||
let mut channels = HashMap::new();
|
let mut channels = HashMap::new();
|
||||||
for network in serai_client::primitives::NETWORKS {
|
for network in serai_client::primitives::EXTERNAL_NETWORKS {
|
||||||
if network == NetworkId::Serai {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let (processor_send, processor_recv) = mpsc::unbounded_channel();
|
let (processor_send, processor_recv) = mpsc::unbounded_channel();
|
||||||
tokio::spawn(handle_processor_messages(
|
tokio::spawn(handle_processor_messages(
|
||||||
db.clone(),
|
db.clone(),
|
||||||
|
@ -1195,7 +1194,7 @@ pub async fn run<D: Db, Pro: Processors, P: P2p>(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
move |set: ValidatorSet, genesis, id_type, id: Vec<u8>| {
|
move |set: ExternalValidatorSet, genesis, id_type, id: Vec<u8>| {
|
||||||
log::debug!("recognized ID {:?} {}", id_type, hex::encode(&id));
|
log::debug!("recognized ID {:?} {}", id_type, hex::encode(&id));
|
||||||
let mut raw_db = raw_db.clone();
|
let mut raw_db = raw_db.clone();
|
||||||
let key = key.clone();
|
let key = key.clone();
|
||||||
|
|
|
@ -11,7 +11,9 @@ use rand_core::{RngCore, OsRng};
|
||||||
|
|
||||||
use scale::{Decode, Encode};
|
use scale::{Decode, Encode};
|
||||||
use borsh::{BorshSerialize, BorshDeserialize};
|
use borsh::{BorshSerialize, BorshDeserialize};
|
||||||
use serai_client::{primitives::NetworkId, validator_sets::primitives::ValidatorSet, Serai};
|
use serai_client::{
|
||||||
|
primitives::ExternalNetworkId, validator_sets::primitives::ExternalValidatorSet, Serai,
|
||||||
|
};
|
||||||
|
|
||||||
use serai_db::Db;
|
use serai_db::Db;
|
||||||
|
|
||||||
|
@ -69,7 +71,7 @@ const BLOCKS_PER_BATCH: usize = BLOCKS_PER_MINUTE + 1;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, BorshSerialize, BorshDeserialize)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, BorshSerialize, BorshDeserialize)]
|
||||||
pub struct CosignedBlock {
|
pub struct CosignedBlock {
|
||||||
pub network: NetworkId,
|
pub network: ExternalNetworkId,
|
||||||
pub block_number: u64,
|
pub block_number: u64,
|
||||||
pub block: [u8; 32],
|
pub block: [u8; 32],
|
||||||
pub signature: [u8; 64],
|
pub signature: [u8; 64],
|
||||||
|
@ -208,8 +210,8 @@ pub struct HeartbeatBatch {
|
||||||
pub trait P2p: Send + Sync + Clone + fmt::Debug + TributaryP2p {
|
pub trait P2p: Send + Sync + Clone + fmt::Debug + TributaryP2p {
|
||||||
type Id: Send + Sync + Clone + Copy + fmt::Debug;
|
type Id: Send + Sync + Clone + Copy + fmt::Debug;
|
||||||
|
|
||||||
async fn subscribe(&self, set: ValidatorSet, genesis: [u8; 32]);
|
async fn subscribe(&self, set: ExternalValidatorSet, genesis: [u8; 32]);
|
||||||
async fn unsubscribe(&self, set: ValidatorSet, genesis: [u8; 32]);
|
async fn unsubscribe(&self, set: ExternalValidatorSet, genesis: [u8; 32]);
|
||||||
|
|
||||||
async fn send_raw(&self, to: Self::Id, msg: Vec<u8>);
|
async fn send_raw(&self, to: Self::Id, msg: Vec<u8>);
|
||||||
async fn broadcast_raw(&self, kind: P2pMessageKind, msg: Vec<u8>);
|
async fn broadcast_raw(&self, kind: P2pMessageKind, msg: Vec<u8>);
|
||||||
|
@ -309,7 +311,7 @@ struct Behavior {
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct LibP2p {
|
pub struct LibP2p {
|
||||||
subscribe: Arc<Mutex<mpsc::UnboundedSender<(bool, ValidatorSet, [u8; 32])>>>,
|
subscribe: Arc<Mutex<mpsc::UnboundedSender<(bool, ExternalValidatorSet, [u8; 32])>>>,
|
||||||
send: Arc<Mutex<mpsc::UnboundedSender<(PeerId, Vec<u8>)>>>,
|
send: Arc<Mutex<mpsc::UnboundedSender<(PeerId, Vec<u8>)>>>,
|
||||||
broadcast: Arc<Mutex<mpsc::UnboundedSender<(P2pMessageKind, Vec<u8>)>>>,
|
broadcast: Arc<Mutex<mpsc::UnboundedSender<(P2pMessageKind, Vec<u8>)>>>,
|
||||||
receive: Arc<Mutex<mpsc::UnboundedReceiver<Message<Self>>>>,
|
receive: Arc<Mutex<mpsc::UnboundedReceiver<Message<Self>>>>,
|
||||||
|
@ -397,7 +399,7 @@ impl LibP2p {
|
||||||
let (receive_send, receive_recv) = mpsc::unbounded_channel();
|
let (receive_send, receive_recv) = mpsc::unbounded_channel();
|
||||||
let (subscribe_send, mut subscribe_recv) = mpsc::unbounded_channel();
|
let (subscribe_send, mut subscribe_recv) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
fn topic_for_set(set: ValidatorSet) -> IdentTopic {
|
fn topic_for_set(set: ExternalValidatorSet) -> IdentTopic {
|
||||||
IdentTopic::new(format!("{LIBP2P_TOPIC}-{}", hex::encode(set.encode())))
|
IdentTopic::new(format!("{LIBP2P_TOPIC}-{}", hex::encode(set.encode())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,7 +409,8 @@ impl LibP2p {
|
||||||
// The addrs we're currently dialing, and the networks associated with them
|
// The addrs we're currently dialing, and the networks associated with them
|
||||||
let dialing_peers = Arc::new(RwLock::new(HashMap::new()));
|
let dialing_peers = Arc::new(RwLock::new(HashMap::new()));
|
||||||
// The peers we're currently connected to, and the networks associated with them
|
// The peers we're currently connected to, and the networks associated with them
|
||||||
let connected_peers = Arc::new(RwLock::new(HashMap::<Multiaddr, HashSet<NetworkId>>::new()));
|
let connected_peers =
|
||||||
|
Arc::new(RwLock::new(HashMap::<Multiaddr, HashSet<ExternalNetworkId>>::new()));
|
||||||
|
|
||||||
// Find and connect to peers
|
// Find and connect to peers
|
||||||
let (connect_to_network_send, mut connect_to_network_recv) =
|
let (connect_to_network_send, mut connect_to_network_recv) =
|
||||||
|
@ -420,7 +423,7 @@ impl LibP2p {
|
||||||
let connect_to_network_send = connect_to_network_send.clone();
|
let connect_to_network_send = connect_to_network_send.clone();
|
||||||
async move {
|
async move {
|
||||||
loop {
|
loop {
|
||||||
let connect = |network: NetworkId, addr: Multiaddr| {
|
let connect = |network: ExternalNetworkId, addr: Multiaddr| {
|
||||||
let dialing_peers = dialing_peers.clone();
|
let dialing_peers = dialing_peers.clone();
|
||||||
let connected_peers = connected_peers.clone();
|
let connected_peers = connected_peers.clone();
|
||||||
let to_dial_send = to_dial_send.clone();
|
let to_dial_send = to_dial_send.clone();
|
||||||
|
@ -507,7 +510,7 @@ impl LibP2p {
|
||||||
connect_to_network_networks.insert(network);
|
connect_to_network_networks.insert(network);
|
||||||
}
|
}
|
||||||
for network in connect_to_network_networks {
|
for network in connect_to_network_networks {
|
||||||
if let Ok(mut nodes) = serai.p2p_validators(network).await {
|
if let Ok(mut nodes) = serai.p2p_validators(network.into()).await {
|
||||||
// If there's an insufficient amount of nodes known, connect to all yet add it
|
// If there's an insufficient amount of nodes known, connect to all yet add it
|
||||||
// back and break
|
// back and break
|
||||||
if nodes.len() < TARGET_PEERS {
|
if nodes.len() < TARGET_PEERS {
|
||||||
|
@ -557,7 +560,7 @@ impl LibP2p {
|
||||||
|
|
||||||
// Subscribe to any new topics
|
// Subscribe to any new topics
|
||||||
set = subscribe_recv.recv() => {
|
set = subscribe_recv.recv() => {
|
||||||
let (subscribe, set, genesis): (_, ValidatorSet, [u8; 32]) =
|
let (subscribe, set, genesis): (_, ExternalValidatorSet, [u8; 32]) =
|
||||||
set.expect("subscribe_recv closed. are we shutting down?");
|
set.expect("subscribe_recv closed. are we shutting down?");
|
||||||
let topic = topic_for_set(set);
|
let topic = topic_for_set(set);
|
||||||
if subscribe {
|
if subscribe {
|
||||||
|
@ -776,7 +779,7 @@ impl LibP2p {
|
||||||
impl P2p for LibP2p {
|
impl P2p for LibP2p {
|
||||||
type Id = PeerId;
|
type Id = PeerId;
|
||||||
|
|
||||||
async fn subscribe(&self, set: ValidatorSet, genesis: [u8; 32]) {
|
async fn subscribe(&self, set: ExternalValidatorSet, genesis: [u8; 32]) {
|
||||||
self
|
self
|
||||||
.subscribe
|
.subscribe
|
||||||
.lock()
|
.lock()
|
||||||
|
@ -785,7 +788,7 @@ impl P2p for LibP2p {
|
||||||
.expect("subscribe_send closed. are we shutting down?");
|
.expect("subscribe_send closed. are we shutting down?");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unsubscribe(&self, set: ValidatorSet, genesis: [u8; 32]) {
|
async fn unsubscribe(&self, set: ExternalValidatorSet, genesis: [u8; 32]) {
|
||||||
self
|
self
|
||||||
.subscribe
|
.subscribe
|
||||||
.lock()
|
.lock()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use serai_client::primitives::NetworkId;
|
use serai_client::primitives::ExternalNetworkId;
|
||||||
use processor_messages::{ProcessorMessage, CoordinatorMessage};
|
use processor_messages::{ProcessorMessage, CoordinatorMessage};
|
||||||
|
|
||||||
use message_queue::{Service, Metadata, client::MessageQueue};
|
use message_queue::{Service, Metadata, client::MessageQueue};
|
||||||
|
@ -8,27 +8,27 @@ use message_queue::{Service, Metadata, client::MessageQueue};
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub network: NetworkId,
|
pub network: ExternalNetworkId,
|
||||||
pub msg: ProcessorMessage,
|
pub msg: ProcessorMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait Processors: 'static + Send + Sync + Clone {
|
pub trait Processors: 'static + Send + Sync + Clone {
|
||||||
async fn send(&self, network: NetworkId, msg: impl Send + Into<CoordinatorMessage>);
|
async fn send(&self, network: ExternalNetworkId, msg: impl Send + Into<CoordinatorMessage>);
|
||||||
async fn recv(&self, network: NetworkId) -> Message;
|
async fn recv(&self, network: ExternalNetworkId) -> Message;
|
||||||
async fn ack(&self, msg: Message);
|
async fn ack(&self, msg: Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl Processors for Arc<MessageQueue> {
|
impl Processors for Arc<MessageQueue> {
|
||||||
async fn send(&self, network: NetworkId, msg: impl Send + Into<CoordinatorMessage>) {
|
async fn send(&self, network: ExternalNetworkId, msg: impl Send + Into<CoordinatorMessage>) {
|
||||||
let msg: CoordinatorMessage = msg.into();
|
let msg: CoordinatorMessage = msg.into();
|
||||||
let metadata =
|
let metadata =
|
||||||
Metadata { from: self.service, to: Service::Processor(network), intent: msg.intent() };
|
Metadata { from: self.service, to: Service::Processor(network), intent: msg.intent() };
|
||||||
let msg = borsh::to_vec(&msg).unwrap();
|
let msg = borsh::to_vec(&msg).unwrap();
|
||||||
self.queue(metadata, msg).await;
|
self.queue(metadata, msg).await;
|
||||||
}
|
}
|
||||||
async fn recv(&self, network: NetworkId) -> Message {
|
async fn recv(&self, network: ExternalNetworkId) -> Message {
|
||||||
let msg = self.next(Service::Processor(network)).await;
|
let msg = self.next(Service::Processor(network)).await;
|
||||||
assert_eq!(msg.from, Service::Processor(network));
|
assert_eq!(msg.from, Service::Processor(network));
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,9 @@ use ciphersuite::{Ciphersuite, Ristretto};
|
||||||
use borsh::{BorshSerialize, BorshDeserialize};
|
use borsh::{BorshSerialize, BorshDeserialize};
|
||||||
|
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
SeraiError, Serai,
|
primitives::ExternalNetworkId,
|
||||||
primitives::NetworkId,
|
validator_sets::primitives::{ExternalValidatorSet, Session},
|
||||||
validator_sets::primitives::{Session, ValidatorSet},
|
Serai, SeraiError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serai_db::*;
|
use serai_db::*;
|
||||||
|
@ -70,13 +70,18 @@ impl LatestCosignedBlock {
|
||||||
|
|
||||||
db_channel! {
|
db_channel! {
|
||||||
SubstrateDbChannels {
|
SubstrateDbChannels {
|
||||||
CosignTransactions: (network: NetworkId) -> (Session, u64, [u8; 32]),
|
CosignTransactions: (network: ExternalNetworkId) -> (Session, u64, [u8; 32]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CosignTransactions {
|
impl CosignTransactions {
|
||||||
// Append a cosign transaction.
|
// Append a cosign transaction.
|
||||||
pub fn append_cosign(txn: &mut impl DbTxn, set: ValidatorSet, number: u64, hash: [u8; 32]) {
|
pub fn append_cosign(
|
||||||
|
txn: &mut impl DbTxn,
|
||||||
|
set: ExternalValidatorSet,
|
||||||
|
number: u64,
|
||||||
|
hash: [u8; 32],
|
||||||
|
) {
|
||||||
CosignTransactions::send(txn, set.network, &(set.session, number, hash))
|
CosignTransactions::send(txn, set.network, &(set.session, number, hash))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,22 +261,22 @@ async fn advance_cosign_protocol_inner(
|
||||||
// Using the keys of the prior block ensures this deadlock isn't reached
|
// Using the keys of the prior block ensures this deadlock isn't reached
|
||||||
let serai = serai.as_of(actual_block.header.parent_hash.into());
|
let serai = serai.as_of(actual_block.header.parent_hash.into());
|
||||||
|
|
||||||
for network in serai_client::primitives::NETWORKS {
|
for network in serai_client::primitives::EXTERNAL_NETWORKS {
|
||||||
// Get the latest session to have set keys
|
// Get the latest session to have set keys
|
||||||
let set_with_keys = {
|
let set_with_keys = {
|
||||||
let Some(latest_session) = serai.validator_sets().session(network).await? else {
|
let Some(latest_session) = serai.validator_sets().session(network.into()).await? else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let prior_session = Session(latest_session.0.saturating_sub(1));
|
let prior_session = Session(latest_session.0.saturating_sub(1));
|
||||||
if serai
|
if serai
|
||||||
.validator_sets()
|
.validator_sets()
|
||||||
.keys(ValidatorSet { network, session: prior_session })
|
.keys(ExternalValidatorSet { network, session: prior_session })
|
||||||
.await?
|
.await?
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
ValidatorSet { network, session: prior_session }
|
ExternalValidatorSet { network, session: prior_session }
|
||||||
} else {
|
} else {
|
||||||
let set = ValidatorSet { network, session: latest_session };
|
let set = ExternalValidatorSet { network, session: latest_session };
|
||||||
if serai.validator_sets().keys(set).await?.is_none() {
|
if serai.validator_sets().keys(set).await?.is_none() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -280,7 +285,7 @@ async fn advance_cosign_protocol_inner(
|
||||||
};
|
};
|
||||||
|
|
||||||
log::debug!("{:?} will be cosigning {block}", set_with_keys.network);
|
log::debug!("{:?} will be cosigning {block}", set_with_keys.network);
|
||||||
cosigning.push((set_with_keys, in_set(key, &serai, set_with_keys).await?.unwrap()));
|
cosigning.push((set_with_keys, in_set(key, &serai, set_with_keys.into()).await?.unwrap()));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use serai_client::primitives::NetworkId;
|
use serai_client::primitives::ExternalNetworkId;
|
||||||
|
|
||||||
pub use serai_db::*;
|
pub use serai_db::*;
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ mod inner_db {
|
||||||
SubstrateDb {
|
SubstrateDb {
|
||||||
NextBlock: () -> u64,
|
NextBlock: () -> u64,
|
||||||
HandledEvent: (block: [u8; 32]) -> u32,
|
HandledEvent: (block: [u8; 32]) -> u32,
|
||||||
BatchInstructionsHashDb: (network: NetworkId, id: u32) -> [u8; 32]
|
BatchInstructionsHashDb: (network: ExternalNetworkId, id: u32) -> [u8; 32]
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,14 @@ use zeroize::Zeroizing;
|
||||||
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
||||||
|
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
SeraiError, Block, Serai, TemporalSerai,
|
|
||||||
primitives::{BlockHash, NetworkId},
|
|
||||||
validator_sets::{primitives::ValidatorSet, ValidatorSetsEvent},
|
|
||||||
in_instructions::InInstructionsEvent,
|
|
||||||
coins::CoinsEvent,
|
coins::CoinsEvent,
|
||||||
|
in_instructions::InInstructionsEvent,
|
||||||
|
primitives::{BlockHash, ExternalNetworkId},
|
||||||
|
validator_sets::{
|
||||||
|
primitives::{ExternalValidatorSet, ValidatorSet},
|
||||||
|
ValidatorSetsEvent,
|
||||||
|
},
|
||||||
|
Block, Serai, SeraiError, TemporalSerai,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serai_db::DbTxn;
|
use serai_db::DbTxn;
|
||||||
|
@ -52,9 +55,9 @@ async fn handle_new_set<D: Db>(
|
||||||
new_tributary_spec: &mpsc::UnboundedSender<TributarySpec>,
|
new_tributary_spec: &mpsc::UnboundedSender<TributarySpec>,
|
||||||
serai: &Serai,
|
serai: &Serai,
|
||||||
block: &Block,
|
block: &Block,
|
||||||
set: ValidatorSet,
|
set: ExternalValidatorSet,
|
||||||
) -> Result<(), SeraiError> {
|
) -> Result<(), SeraiError> {
|
||||||
if in_set(key, &serai.as_of(block.hash()), set)
|
if in_set(key, &serai.as_of(block.hash()), set.into())
|
||||||
.await?
|
.await?
|
||||||
.expect("NewSet for set which doesn't exist")
|
.expect("NewSet for set which doesn't exist")
|
||||||
{
|
{
|
||||||
|
@ -64,7 +67,7 @@ async fn handle_new_set<D: Db>(
|
||||||
let serai = serai.as_of(block.hash());
|
let serai = serai.as_of(block.hash());
|
||||||
let serai = serai.validator_sets();
|
let serai = serai.validator_sets();
|
||||||
let set_participants =
|
let set_participants =
|
||||||
serai.participants(set.network).await?.expect("NewSet for set which doesn't exist");
|
serai.participants(set.network.into()).await?.expect("NewSet for set which doesn't exist");
|
||||||
|
|
||||||
set_participants.into_iter().map(|(k, w)| (k, u16::try_from(w).unwrap())).collect::<Vec<_>>()
|
set_participants.into_iter().map(|(k, w)| (k, u16::try_from(w).unwrap())).collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
|
@ -131,7 +134,7 @@ async fn handle_batch_and_burns<Pro: Processors>(
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut batch_block = HashMap::new();
|
let mut batch_block = HashMap::new();
|
||||||
let mut batches = HashMap::<NetworkId, Vec<u32>>::new();
|
let mut batches = HashMap::<ExternalNetworkId, Vec<u32>>::new();
|
||||||
let mut burns = HashMap::new();
|
let mut burns = HashMap::new();
|
||||||
|
|
||||||
let serai = serai.as_of(block.hash());
|
let serai = serai.as_of(block.hash());
|
||||||
|
@ -205,8 +208,8 @@ async fn handle_block<D: Db, Pro: Processors>(
|
||||||
db: &mut D,
|
db: &mut D,
|
||||||
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
|
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
|
||||||
new_tributary_spec: &mpsc::UnboundedSender<TributarySpec>,
|
new_tributary_spec: &mpsc::UnboundedSender<TributarySpec>,
|
||||||
perform_slash_report: &mpsc::UnboundedSender<ValidatorSet>,
|
perform_slash_report: &mpsc::UnboundedSender<ExternalValidatorSet>,
|
||||||
tributary_retired: &mpsc::UnboundedSender<ValidatorSet>,
|
tributary_retired: &mpsc::UnboundedSender<ExternalValidatorSet>,
|
||||||
processors: &Pro,
|
processors: &Pro,
|
||||||
serai: &Serai,
|
serai: &Serai,
|
||||||
block: Block,
|
block: Block,
|
||||||
|
@ -226,12 +229,8 @@ async fn handle_block<D: Db, Pro: Processors>(
|
||||||
panic!("NewSet event wasn't NewSet: {new_set:?}");
|
panic!("NewSet event wasn't NewSet: {new_set:?}");
|
||||||
};
|
};
|
||||||
|
|
||||||
// If this is Serai, do nothing
|
|
||||||
// We only coordinate/process external networks
|
// We only coordinate/process external networks
|
||||||
if set.network == NetworkId::Serai {
|
let Ok(set) = ExternalValidatorSet::try_from(set) else { continue };
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if HandledEvent::is_unhandled(db, hash, event_id) {
|
if HandledEvent::is_unhandled(db, hash, event_id) {
|
||||||
log::info!("found fresh new set event {:?}", new_set);
|
log::info!("found fresh new set event {:?}", new_set);
|
||||||
let mut txn = db.txn();
|
let mut txn = db.txn();
|
||||||
|
@ -286,10 +285,7 @@ async fn handle_block<D: Db, Pro: Processors>(
|
||||||
panic!("AcceptedHandover event wasn't AcceptedHandover: {accepted_handover:?}");
|
panic!("AcceptedHandover event wasn't AcceptedHandover: {accepted_handover:?}");
|
||||||
};
|
};
|
||||||
|
|
||||||
if set.network == NetworkId::Serai {
|
let Ok(set) = ExternalValidatorSet::try_from(set) else { continue };
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if HandledEvent::is_unhandled(db, hash, event_id) {
|
if HandledEvent::is_unhandled(db, hash, event_id) {
|
||||||
log::info!("found fresh accepted handover event {:?}", accepted_handover);
|
log::info!("found fresh accepted handover event {:?}", accepted_handover);
|
||||||
// TODO: This isn't atomic with the event handling
|
// TODO: This isn't atomic with the event handling
|
||||||
|
@ -307,10 +303,7 @@ async fn handle_block<D: Db, Pro: Processors>(
|
||||||
panic!("SetRetired event wasn't SetRetired: {retired_set:?}");
|
panic!("SetRetired event wasn't SetRetired: {retired_set:?}");
|
||||||
};
|
};
|
||||||
|
|
||||||
if set.network == NetworkId::Serai {
|
let Ok(set) = ExternalValidatorSet::try_from(set) else { continue };
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if HandledEvent::is_unhandled(db, hash, event_id) {
|
if HandledEvent::is_unhandled(db, hash, event_id) {
|
||||||
log::info!("found fresh set retired event {:?}", retired_set);
|
log::info!("found fresh set retired event {:?}", retired_set);
|
||||||
let mut txn = db.txn();
|
let mut txn = db.txn();
|
||||||
|
@ -340,8 +333,8 @@ async fn handle_new_blocks<D: Db, Pro: Processors>(
|
||||||
db: &mut D,
|
db: &mut D,
|
||||||
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
|
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
|
||||||
new_tributary_spec: &mpsc::UnboundedSender<TributarySpec>,
|
new_tributary_spec: &mpsc::UnboundedSender<TributarySpec>,
|
||||||
perform_slash_report: &mpsc::UnboundedSender<ValidatorSet>,
|
perform_slash_report: &mpsc::UnboundedSender<ExternalValidatorSet>,
|
||||||
tributary_retired: &mpsc::UnboundedSender<ValidatorSet>,
|
tributary_retired: &mpsc::UnboundedSender<ExternalValidatorSet>,
|
||||||
processors: &Pro,
|
processors: &Pro,
|
||||||
serai: &Serai,
|
serai: &Serai,
|
||||||
next_block: &mut u64,
|
next_block: &mut u64,
|
||||||
|
@ -395,8 +388,8 @@ pub async fn scan_task<D: Db, Pro: Processors>(
|
||||||
processors: Pro,
|
processors: Pro,
|
||||||
serai: Arc<Serai>,
|
serai: Arc<Serai>,
|
||||||
new_tributary_spec: mpsc::UnboundedSender<TributarySpec>,
|
new_tributary_spec: mpsc::UnboundedSender<TributarySpec>,
|
||||||
perform_slash_report: mpsc::UnboundedSender<ValidatorSet>,
|
perform_slash_report: mpsc::UnboundedSender<ExternalValidatorSet>,
|
||||||
tributary_retired: mpsc::UnboundedSender<ValidatorSet>,
|
tributary_retired: mpsc::UnboundedSender<ExternalValidatorSet>,
|
||||||
) {
|
) {
|
||||||
log::info!("scanning substrate");
|
log::info!("scanning substrate");
|
||||||
let mut next_substrate_block = NextBlock::get(&db).unwrap_or_default();
|
let mut next_substrate_block = NextBlock::get(&db).unwrap_or_default();
|
||||||
|
@ -494,9 +487,12 @@ pub async fn scan_task<D: Db, Pro: Processors>(
|
||||||
/// retry.
|
/// retry.
|
||||||
pub(crate) async fn expected_next_batch(
|
pub(crate) async fn expected_next_batch(
|
||||||
serai: &Serai,
|
serai: &Serai,
|
||||||
network: NetworkId,
|
network: ExternalNetworkId,
|
||||||
) -> Result<u32, SeraiError> {
|
) -> Result<u32, SeraiError> {
|
||||||
async fn expected_next_batch_inner(serai: &Serai, network: NetworkId) -> Result<u32, SeraiError> {
|
async fn expected_next_batch_inner(
|
||||||
|
serai: &Serai,
|
||||||
|
network: ExternalNetworkId,
|
||||||
|
) -> Result<u32, SeraiError> {
|
||||||
let serai = serai.as_of_latest_finalized_block().await?;
|
let serai = serai.as_of_latest_finalized_block().await?;
|
||||||
let last = serai.in_instructions().last_batch_for_network(network).await?;
|
let last = serai.in_instructions().last_batch_for_network(network).await?;
|
||||||
Ok(if let Some(last) = last { last + 1 } else { 0 })
|
Ok(if let Some(last) = last { last + 1 } else { 0 })
|
||||||
|
@ -519,7 +515,7 @@ pub(crate) async fn expected_next_batch(
|
||||||
/// This is deemed fine.
|
/// This is deemed fine.
|
||||||
pub(crate) async fn verify_published_batches<D: Db>(
|
pub(crate) async fn verify_published_batches<D: Db>(
|
||||||
txn: &mut D::Transaction<'_>,
|
txn: &mut D::Transaction<'_>,
|
||||||
network: NetworkId,
|
network: ExternalNetworkId,
|
||||||
optimistic_up_to: u32,
|
optimistic_up_to: u32,
|
||||||
) -> Option<u32> {
|
) -> Option<u32> {
|
||||||
// TODO: Localize from MainDb to SubstrateDb
|
// TODO: Localize from MainDb to SubstrateDb
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::{
|
||||||
collections::{VecDeque, HashSet, HashMap},
|
collections::{VecDeque, HashSet, HashMap},
|
||||||
};
|
};
|
||||||
|
|
||||||
use serai_client::{primitives::NetworkId, validator_sets::primitives::ValidatorSet};
|
use serai_client::{primitives::ExternalNetworkId, validator_sets::primitives::ExternalValidatorSet};
|
||||||
|
|
||||||
use processor_messages::CoordinatorMessage;
|
use processor_messages::CoordinatorMessage;
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ use crate::{
|
||||||
pub mod tributary;
|
pub mod tributary;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MemProcessors(pub Arc<RwLock<HashMap<NetworkId, VecDeque<CoordinatorMessage>>>>);
|
pub struct MemProcessors(pub Arc<RwLock<HashMap<ExternalNetworkId, VecDeque<CoordinatorMessage>>>>);
|
||||||
impl MemProcessors {
|
impl MemProcessors {
|
||||||
#[allow(clippy::new_without_default)]
|
#[allow(clippy::new_without_default)]
|
||||||
pub fn new() -> MemProcessors {
|
pub fn new() -> MemProcessors {
|
||||||
|
@ -30,12 +30,12 @@ impl MemProcessors {
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl Processors for MemProcessors {
|
impl Processors for MemProcessors {
|
||||||
async fn send(&self, network: NetworkId, msg: impl Send + Into<CoordinatorMessage>) {
|
async fn send(&self, network: ExternalNetworkId, msg: impl Send + Into<CoordinatorMessage>) {
|
||||||
let mut processors = self.0.write().await;
|
let mut processors = self.0.write().await;
|
||||||
let processor = processors.entry(network).or_insert_with(VecDeque::new);
|
let processor = processors.entry(network).or_insert_with(VecDeque::new);
|
||||||
processor.push_back(msg.into());
|
processor.push_back(msg.into());
|
||||||
}
|
}
|
||||||
async fn recv(&self, _: NetworkId) -> Message {
|
async fn recv(&self, _: ExternalNetworkId) -> Message {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
async fn ack(&self, _: Message) {
|
async fn ack(&self, _: Message) {
|
||||||
|
@ -65,8 +65,8 @@ impl LocalP2p {
|
||||||
impl P2p for LocalP2p {
|
impl P2p for LocalP2p {
|
||||||
type Id = usize;
|
type Id = usize;
|
||||||
|
|
||||||
async fn subscribe(&self, _set: ValidatorSet, _genesis: [u8; 32]) {}
|
async fn subscribe(&self, _set: ExternalValidatorSet, _genesis: [u8; 32]) {}
|
||||||
async fn unsubscribe(&self, _set: ValidatorSet, _genesis: [u8; 32]) {}
|
async fn unsubscribe(&self, _set: ExternalValidatorSet, _genesis: [u8; 32]) {}
|
||||||
|
|
||||||
async fn send_raw(&self, to: Self::Id, msg: Vec<u8>) {
|
async fn send_raw(&self, to: Self::Id, msg: Vec<u8>) {
|
||||||
let mut msg_ref = msg.as_slice();
|
let mut msg_ref = msg.as_slice();
|
||||||
|
|
|
@ -15,8 +15,8 @@ use ciphersuite::{
|
||||||
use sp_application_crypto::sr25519;
|
use sp_application_crypto::sr25519;
|
||||||
use borsh::BorshDeserialize;
|
use borsh::BorshDeserialize;
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::NetworkId,
|
primitives::ExternalNetworkId,
|
||||||
validator_sets::primitives::{Session, ValidatorSet},
|
validator_sets::primitives::{ExternalValidatorSet, Session},
|
||||||
};
|
};
|
||||||
|
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
@ -50,7 +50,7 @@ pub fn new_spec<R: RngCore + CryptoRng>(
|
||||||
|
|
||||||
let start_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
|
let start_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
|
||||||
|
|
||||||
let set = ValidatorSet { session: Session(0), network: NetworkId::Bitcoin };
|
let set = ExternalValidatorSet { session: Session(0), network: ExternalNetworkId::Bitcoin };
|
||||||
|
|
||||||
let set_participants = keys
|
let set_participants = keys
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
@ -10,7 +10,7 @@ use frost::Participant;
|
||||||
use sp_runtime::traits::Verify;
|
use sp_runtime::traits::Verify;
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::{SeraiAddress, Signature},
|
primitives::{SeraiAddress, Signature},
|
||||||
validator_sets::primitives::{ValidatorSet, KeyPair},
|
validator_sets::primitives::{ExternalValidatorSet, KeyPair},
|
||||||
};
|
};
|
||||||
|
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
@ -350,7 +350,7 @@ async fn dkg_test() {
|
||||||
async fn publish_set_keys(
|
async fn publish_set_keys(
|
||||||
&self,
|
&self,
|
||||||
_db: &(impl Sync + Get),
|
_db: &(impl Sync + Get),
|
||||||
set: ValidatorSet,
|
set: ExternalValidatorSet,
|
||||||
removed: Vec<SeraiAddress>,
|
removed: Vec<SeraiAddress>,
|
||||||
key_pair: KeyPair,
|
key_pair: KeyPair,
|
||||||
signature: Signature,
|
signature: Signature,
|
||||||
|
@ -362,7 +362,7 @@ async fn dkg_test() {
|
||||||
&*serai_client::validator_sets::primitives::set_keys_message(&set, &[], &key_pair),
|
&*serai_client::validator_sets::primitives::set_keys_message(&set, &[], &key_pair),
|
||||||
&serai_client::Public(
|
&serai_client::Public(
|
||||||
frost::dkg::musig::musig_key::<Ristretto>(
|
frost::dkg::musig::musig_key::<Ristretto>(
|
||||||
&serai_client::validator_sets::primitives::musig_context(set),
|
&serai_client::validator_sets::primitives::musig_context(set.into()),
|
||||||
&self.spec.validators().into_iter().map(|(validator, _)| validator).collect::<Vec<_>>()
|
&self.spec.validators().into_iter().map(|(validator, _)| validator).collect::<Vec<_>>()
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
|
@ -7,7 +7,7 @@ use ciphersuite::{group::Group, Ciphersuite, Ristretto};
|
||||||
use scale::{Encode, Decode};
|
use scale::{Encode, Decode};
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::{SeraiAddress, Signature},
|
primitives::{SeraiAddress, Signature},
|
||||||
validator_sets::primitives::{MAX_KEY_SHARES_PER_SET, ValidatorSet, KeyPair},
|
validator_sets::primitives::{ExternalValidatorSet, KeyPair, MAX_KEY_SHARES_PER_SET},
|
||||||
};
|
};
|
||||||
use processor_messages::coordinator::SubstrateSignableId;
|
use processor_messages::coordinator::SubstrateSignableId;
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ impl PublishSeraiTransaction for () {
|
||||||
async fn publish_set_keys(
|
async fn publish_set_keys(
|
||||||
&self,
|
&self,
|
||||||
_db: &(impl Sync + serai_db::Get),
|
_db: &(impl Sync + serai_db::Get),
|
||||||
_set: ValidatorSet,
|
_set: ExternalValidatorSet,
|
||||||
_removed: Vec<SeraiAddress>,
|
_removed: Vec<SeraiAddress>,
|
||||||
_key_pair: KeyPair,
|
_key_pair: KeyPair,
|
||||||
_signature: Signature,
|
_signature: Signature,
|
||||||
|
|
|
@ -6,7 +6,7 @@ use borsh::{BorshSerialize, BorshDeserialize};
|
||||||
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
||||||
use frost::Participant;
|
use frost::Participant;
|
||||||
|
|
||||||
use serai_client::validator_sets::primitives::{KeyPair, ValidatorSet};
|
use serai_client::validator_sets::primitives::{KeyPair, ExternalValidatorSet};
|
||||||
|
|
||||||
use processor_messages::coordinator::SubstrateSignableId;
|
use processor_messages::coordinator::SubstrateSignableId;
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ pub enum Accumulation {
|
||||||
create_db!(
|
create_db!(
|
||||||
Tributary {
|
Tributary {
|
||||||
SeraiBlockNumber: (hash: [u8; 32]) -> u64,
|
SeraiBlockNumber: (hash: [u8; 32]) -> u64,
|
||||||
SeraiDkgCompleted: (spec: ValidatorSet) -> [u8; 32],
|
SeraiDkgCompleted: (spec: ExternalValidatorSet) -> [u8; 32],
|
||||||
|
|
||||||
TributaryBlockNumber: (block: [u8; 32]) -> u32,
|
TributaryBlockNumber: (block: [u8; 32]) -> u32,
|
||||||
LastHandledBlock: (genesis: [u8; 32]) -> [u8; 32],
|
LastHandledBlock: (genesis: [u8; 32]) -> [u8; 32],
|
||||||
|
@ -80,7 +80,7 @@ create_db!(
|
||||||
SlashReports: (genesis: [u8; 32], signer: [u8; 32]) -> Vec<u32>,
|
SlashReports: (genesis: [u8; 32], signer: [u8; 32]) -> Vec<u32>,
|
||||||
SlashReported: (genesis: [u8; 32]) -> u16,
|
SlashReported: (genesis: [u8; 32]) -> u16,
|
||||||
SlashReportCutOff: (genesis: [u8; 32]) -> u64,
|
SlashReportCutOff: (genesis: [u8; 32]) -> u64,
|
||||||
SlashReport: (set: ValidatorSet) -> Vec<([u8; 32], u32)>,
|
SlashReport: (set: ExternalValidatorSet) -> Vec<([u8; 32], u32)>,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
||||||
|
|
||||||
use serai_client::validator_sets::primitives::ValidatorSet;
|
use serai_client::validator_sets::primitives::ExternalValidatorSet;
|
||||||
|
|
||||||
use tributary::{
|
use tributary::{
|
||||||
ReadWrite,
|
ReadWrite,
|
||||||
|
@ -40,7 +40,7 @@ pub fn removed_as_of_dkg_attempt(
|
||||||
|
|
||||||
pub fn removed_as_of_set_keys(
|
pub fn removed_as_of_set_keys(
|
||||||
getter: &impl Get,
|
getter: &impl Get,
|
||||||
set: ValidatorSet,
|
set: ExternalValidatorSet,
|
||||||
genesis: [u8; 32],
|
genesis: [u8; 32],
|
||||||
) -> Option<Vec<<Ristretto as Ciphersuite>::G>> {
|
) -> Option<Vec<<Ristretto as Ciphersuite>::G>> {
|
||||||
// SeraiDkgCompleted has the key placed on-chain.
|
// SeraiDkgCompleted has the key placed on-chain.
|
||||||
|
|
|
@ -10,7 +10,7 @@ use tokio::sync::broadcast;
|
||||||
use scale::{Encode, Decode};
|
use scale::{Encode, Decode};
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::{SeraiAddress, Signature},
|
primitives::{SeraiAddress, Signature},
|
||||||
validator_sets::primitives::{KeyPair, ValidatorSet},
|
validator_sets::primitives::{ExternalValidatorSet, KeyPair},
|
||||||
Serai,
|
Serai,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ pub enum RecognizedIdType {
|
||||||
pub trait RIDTrait {
|
pub trait RIDTrait {
|
||||||
async fn recognized_id(
|
async fn recognized_id(
|
||||||
&self,
|
&self,
|
||||||
set: ValidatorSet,
|
set: ExternalValidatorSet,
|
||||||
genesis: [u8; 32],
|
genesis: [u8; 32],
|
||||||
kind: RecognizedIdType,
|
kind: RecognizedIdType,
|
||||||
id: Vec<u8>,
|
id: Vec<u8>,
|
||||||
|
@ -47,12 +47,12 @@ pub trait RIDTrait {
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl<
|
impl<
|
||||||
FRid: Send + Future<Output = ()>,
|
FRid: Send + Future<Output = ()>,
|
||||||
F: Sync + Fn(ValidatorSet, [u8; 32], RecognizedIdType, Vec<u8>) -> FRid,
|
F: Sync + Fn(ExternalValidatorSet, [u8; 32], RecognizedIdType, Vec<u8>) -> FRid,
|
||||||
> RIDTrait for F
|
> RIDTrait for F
|
||||||
{
|
{
|
||||||
async fn recognized_id(
|
async fn recognized_id(
|
||||||
&self,
|
&self,
|
||||||
set: ValidatorSet,
|
set: ExternalValidatorSet,
|
||||||
genesis: [u8; 32],
|
genesis: [u8; 32],
|
||||||
kind: RecognizedIdType,
|
kind: RecognizedIdType,
|
||||||
id: Vec<u8>,
|
id: Vec<u8>,
|
||||||
|
@ -66,7 +66,7 @@ pub trait PublishSeraiTransaction {
|
||||||
async fn publish_set_keys(
|
async fn publish_set_keys(
|
||||||
&self,
|
&self,
|
||||||
db: &(impl Sync + Get),
|
db: &(impl Sync + Get),
|
||||||
set: ValidatorSet,
|
set: ExternalValidatorSet,
|
||||||
removed: Vec<SeraiAddress>,
|
removed: Vec<SeraiAddress>,
|
||||||
key_pair: KeyPair,
|
key_pair: KeyPair,
|
||||||
signature: Signature,
|
signature: Signature,
|
||||||
|
@ -86,7 +86,7 @@ mod impl_pst_for_serai {
|
||||||
async fn publish(
|
async fn publish(
|
||||||
serai: &Serai,
|
serai: &Serai,
|
||||||
db: &impl Get,
|
db: &impl Get,
|
||||||
set: ValidatorSet,
|
set: ExternalValidatorSet,
|
||||||
tx: serai_client::Transaction,
|
tx: serai_client::Transaction,
|
||||||
meta: $Meta,
|
meta: $Meta,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
@ -128,7 +128,7 @@ mod impl_pst_for_serai {
|
||||||
async fn publish_set_keys(
|
async fn publish_set_keys(
|
||||||
&self,
|
&self,
|
||||||
db: &(impl Sync + Get),
|
db: &(impl Sync + Get),
|
||||||
set: ValidatorSet,
|
set: ExternalValidatorSet,
|
||||||
removed: Vec<SeraiAddress>,
|
removed: Vec<SeraiAddress>,
|
||||||
key_pair: KeyPair,
|
key_pair: KeyPair,
|
||||||
signature: Signature,
|
signature: Signature,
|
||||||
|
@ -140,7 +140,7 @@ mod impl_pst_for_serai {
|
||||||
key_pair,
|
key_pair,
|
||||||
signature,
|
signature,
|
||||||
);
|
);
|
||||||
async fn check(serai: SeraiValidatorSets<'_>, set: ValidatorSet, (): ()) -> bool {
|
async fn check(serai: SeraiValidatorSets<'_>, set: ExternalValidatorSet, (): ()) -> bool {
|
||||||
if matches!(serai.keys(set).await, Ok(Some(_))) {
|
if matches!(serai.keys(set).await, Ok(Some(_))) {
|
||||||
log::info!("another coordinator set key pair for {:?}", set);
|
log::info!("another coordinator set key pair for {:?}", set);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -119,7 +119,7 @@ impl<T: DbTxn, C: Encode> SigningProtocol<'_, T, C> {
|
||||||
|
|
||||||
let algorithm = Schnorrkel::new(b"substrate");
|
let algorithm = Schnorrkel::new(b"substrate");
|
||||||
let keys: ThresholdKeys<Ristretto> =
|
let keys: ThresholdKeys<Ristretto> =
|
||||||
musig(&musig_context(self.spec.set()), self.key, participants)
|
musig(&musig_context(self.spec.set().into()), self.key, participants)
|
||||||
.expect("signing for a set we aren't in/validator present multiple times")
|
.expect("signing for a set we aren't in/validator present multiple times")
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ use frost::Participant;
|
||||||
use scale::Encode;
|
use scale::Encode;
|
||||||
use borsh::{BorshSerialize, BorshDeserialize};
|
use borsh::{BorshSerialize, BorshDeserialize};
|
||||||
|
|
||||||
use serai_client::{primitives::PublicKey, validator_sets::primitives::ValidatorSet};
|
use serai_client::{primitives::PublicKey, validator_sets::primitives::ExternalValidatorSet};
|
||||||
|
|
||||||
fn borsh_serialize_validators<W: io::Write>(
|
fn borsh_serialize_validators<W: io::Write>(
|
||||||
validators: &Vec<(<Ristretto as Ciphersuite>::G, u16)>,
|
validators: &Vec<(<Ristretto as Ciphersuite>::G, u16)>,
|
||||||
|
@ -43,7 +43,7 @@ fn borsh_deserialize_validators<R: io::Read>(
|
||||||
pub struct TributarySpec {
|
pub struct TributarySpec {
|
||||||
serai_block: [u8; 32],
|
serai_block: [u8; 32],
|
||||||
start_time: u64,
|
start_time: u64,
|
||||||
set: ValidatorSet,
|
set: ExternalValidatorSet,
|
||||||
#[borsh(
|
#[borsh(
|
||||||
serialize_with = "borsh_serialize_validators",
|
serialize_with = "borsh_serialize_validators",
|
||||||
deserialize_with = "borsh_deserialize_validators"
|
deserialize_with = "borsh_deserialize_validators"
|
||||||
|
@ -55,7 +55,7 @@ impl TributarySpec {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
serai_block: [u8; 32],
|
serai_block: [u8; 32],
|
||||||
start_time: u64,
|
start_time: u64,
|
||||||
set: ValidatorSet,
|
set: ExternalValidatorSet,
|
||||||
set_participants: Vec<(PublicKey, u16)>,
|
set_participants: Vec<(PublicKey, u16)>,
|
||||||
) -> TributarySpec {
|
) -> TributarySpec {
|
||||||
let mut validators = vec![];
|
let mut validators = vec![];
|
||||||
|
@ -68,7 +68,7 @@ impl TributarySpec {
|
||||||
Self { serai_block, start_time, set, validators }
|
Self { serai_block, start_time, set, validators }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(&self) -> ValidatorSet {
|
pub fn set(&self) -> ExternalValidatorSet {
|
||||||
self.set
|
self.set
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ parity-scale-codec = { version = "3", default-features = false, features = ["std
|
||||||
|
|
||||||
futures-util = { version = "0.3", default-features = false, features = ["std", "async-await-macro", "sink", "channel"] }
|
futures-util = { version = "0.3", default-features = false, features = ["std", "async-await-macro", "sink", "channel"] }
|
||||||
futures-channel = { version = "0.3", default-features = false, features = ["std", "sink"] }
|
futures-channel = { version = "0.3", default-features = false, features = ["std", "sink"] }
|
||||||
tokio = { version = "1", default-features = false, features = ["time"] }
|
patchable-async-sleep = { version = "0.1", path = "../../../common/patchable-async-sleep", default-features = false }
|
||||||
|
|
||||||
serai-db = { path = "../../../common/db", version = "0.1", default-features = false }
|
serai-db = { path = "../../../common/db", version = "0.1", default-features = false }
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ use futures_util::{
|
||||||
FutureExt, StreamExt, SinkExt,
|
FutureExt, StreamExt, SinkExt,
|
||||||
future::{self, Fuse},
|
future::{self, Fuse},
|
||||||
};
|
};
|
||||||
use tokio::time::sleep;
|
use patchable_async_sleep::sleep;
|
||||||
|
|
||||||
use serai_db::{Get, DbTxn, Db};
|
use serai_db::{Get, DbTxn, Db};
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures_util::{FutureExt, future};
|
use futures_util::{FutureExt, future};
|
||||||
use tokio::time::sleep;
|
use patchable_async_sleep::sleep;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
time::CanonicalInstant,
|
time::CanonicalInstant,
|
||||||
|
|
|
@ -10,6 +10,7 @@ ignore = [
|
||||||
"RUSTSEC-2020-0168", # mach is unmaintained
|
"RUSTSEC-2020-0168", # mach is unmaintained
|
||||||
"RUSTSEC-2021-0139", # https://github.com/serai-dex/serai/228
|
"RUSTSEC-2021-0139", # https://github.com/serai-dex/serai/228
|
||||||
"RUSTSEC-2022-0061", # https://github.com/serai-dex/serai/227
|
"RUSTSEC-2022-0061", # https://github.com/serai-dex/serai/227
|
||||||
|
"RUSTSEC-2024-0370", # proc-macro-error is unmaintained
|
||||||
]
|
]
|
||||||
|
|
||||||
[licenses]
|
[licenses]
|
||||||
|
@ -101,5 +102,5 @@ allow-git = [
|
||||||
"https://github.com/rust-lang-nursery/lazy-static.rs",
|
"https://github.com/rust-lang-nursery/lazy-static.rs",
|
||||||
"https://github.com/serai-dex/substrate-bip39",
|
"https://github.com/serai-dex/substrate-bip39",
|
||||||
"https://github.com/serai-dex/substrate",
|
"https://github.com/serai-dex/substrate",
|
||||||
"https://github.com/orcalabs/dockertest-rs",
|
"https://github.com/alloy-rs/core",
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,20 +5,20 @@ GEM
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
bigdecimal (3.1.8)
|
bigdecimal (3.1.8)
|
||||||
colorator (1.1.0)
|
colorator (1.1.0)
|
||||||
concurrent-ruby (1.3.3)
|
concurrent-ruby (1.3.4)
|
||||||
em-websocket (0.5.3)
|
em-websocket (0.5.3)
|
||||||
eventmachine (>= 0.12.9)
|
eventmachine (>= 0.12.9)
|
||||||
http_parser.rb (~> 0)
|
http_parser.rb (~> 0)
|
||||||
eventmachine (1.2.7)
|
eventmachine (1.2.7)
|
||||||
ffi (1.17.0-x86_64-linux-gnu)
|
ffi (1.17.0-x86_64-linux-gnu)
|
||||||
forwardable-extended (2.6.0)
|
forwardable-extended (2.6.0)
|
||||||
google-protobuf (4.27.3-x86_64-linux)
|
google-protobuf (4.28.2-x86_64-linux)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rake (>= 13)
|
rake (>= 13)
|
||||||
http_parser.rb (0.8.0)
|
http_parser.rb (0.8.0)
|
||||||
i18n (1.14.5)
|
i18n (1.14.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
jekyll (4.3.3)
|
jekyll (4.3.4)
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
colorator (~> 1.0)
|
colorator (~> 1.0)
|
||||||
em-websocket (~> 0.5)
|
em-websocket (~> 0.5)
|
||||||
|
@ -63,17 +63,15 @@ GEM
|
||||||
rb-fsevent (0.11.2)
|
rb-fsevent (0.11.2)
|
||||||
rb-inotify (0.11.1)
|
rb-inotify (0.11.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
rexml (3.3.4)
|
rexml (3.3.7)
|
||||||
strscan
|
rouge (4.4.0)
|
||||||
rouge (4.3.0)
|
|
||||||
safe_yaml (1.0.5)
|
safe_yaml (1.0.5)
|
||||||
sass-embedded (1.77.8-x86_64-linux-gnu)
|
sass-embedded (1.79.3-x86_64-linux-gnu)
|
||||||
google-protobuf (~> 4.26)
|
google-protobuf (~> 4.27)
|
||||||
strscan (3.1.0)
|
|
||||||
terminal-table (3.0.2)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (2.6.0)
|
||||||
webrick (1.8.1)
|
webrick (1.8.2)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
|
|
|
@ -6,7 +6,7 @@ pub(crate) use std::{
|
||||||
pub(crate) use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
pub(crate) use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
||||||
pub(crate) use schnorr_signatures::SchnorrSignature;
|
pub(crate) use schnorr_signatures::SchnorrSignature;
|
||||||
|
|
||||||
pub(crate) use serai_primitives::NetworkId;
|
pub(crate) use serai_primitives::ExternalNetworkId;
|
||||||
|
|
||||||
pub(crate) use tokio::{
|
pub(crate) use tokio::{
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
|
@ -193,10 +193,7 @@ async fn main() {
|
||||||
KEYS.write().unwrap().insert(service, key);
|
KEYS.write().unwrap().insert(service, key);
|
||||||
let mut queues = QUEUES.write().unwrap();
|
let mut queues = QUEUES.write().unwrap();
|
||||||
if service == Service::Coordinator {
|
if service == Service::Coordinator {
|
||||||
for network in serai_primitives::NETWORKS {
|
for network in serai_primitives::EXTERNAL_NETWORKS {
|
||||||
if network == NetworkId::Serai {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
queues.insert(
|
queues.insert(
|
||||||
(service, Service::Processor(network)),
|
(service, Service::Processor(network)),
|
||||||
RwLock::new(Queue(db.clone(), service, Service::Processor(network))),
|
RwLock::new(Queue(db.clone(), service, Service::Processor(network))),
|
||||||
|
@ -210,17 +207,13 @@ async fn main() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Make queues for each NetworkId, other than Serai
|
// Make queues for each ExternalNetworkId
|
||||||
for network in serai_primitives::NETWORKS {
|
for network in serai_primitives::EXTERNAL_NETWORKS {
|
||||||
if network == NetworkId::Serai {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Use a match so we error if the list of NetworkIds changes
|
// Use a match so we error if the list of NetworkIds changes
|
||||||
let Some(key) = read_key(match network {
|
let Some(key) = read_key(match network {
|
||||||
NetworkId::Serai => unreachable!(),
|
ExternalNetworkId::Bitcoin => "BITCOIN_KEY",
|
||||||
NetworkId::Bitcoin => "BITCOIN_KEY",
|
ExternalNetworkId::Ethereum => "ETHEREUM_KEY",
|
||||||
NetworkId::Ethereum => "ETHEREUM_KEY",
|
ExternalNetworkId::Monero => "MONERO_KEY",
|
||||||
NetworkId::Monero => "MONERO_KEY",
|
|
||||||
}) else {
|
}) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,11 +3,11 @@ use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
||||||
|
|
||||||
use borsh::{BorshSerialize, BorshDeserialize};
|
use borsh::{BorshSerialize, BorshDeserialize};
|
||||||
|
|
||||||
use serai_primitives::NetworkId;
|
use serai_primitives::ExternalNetworkId;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, BorshSerialize, BorshDeserialize)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, BorshSerialize, BorshDeserialize)]
|
||||||
pub enum Service {
|
pub enum Service {
|
||||||
Processor(NetworkId),
|
Processor(ExternalNetworkId),
|
||||||
Coordinator,
|
Coordinator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,6 @@ rand_core = { version = "0.6", default-features = false }
|
||||||
bitcoin = { version = "0.32", default-features = false }
|
bitcoin = { version = "0.32", default-features = false }
|
||||||
|
|
||||||
k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits"] }
|
k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits"] }
|
||||||
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", default-features = false, features = ["secp256k1"], optional = true }
|
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", default-features = false, features = ["secp256k1"], optional = true }
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, optional = true }
|
hex = { version = "0.4", default-features = false, optional = true }
|
||||||
|
@ -55,8 +53,6 @@ std = [
|
||||||
"bitcoin/serde",
|
"bitcoin/serde",
|
||||||
|
|
||||||
"k256/std",
|
"k256/std",
|
||||||
|
|
||||||
"transcript/std",
|
|
||||||
"frost",
|
"frost",
|
||||||
|
|
||||||
"hex/std",
|
"hex/std",
|
||||||
|
|
|
@ -40,14 +40,12 @@ mod frost_crypto {
|
||||||
|
|
||||||
use bitcoin::hashes::{HashEngine, Hash, sha256::Hash as Sha256};
|
use bitcoin::hashes::{HashEngine, Hash, sha256::Hash as Sha256};
|
||||||
|
|
||||||
use transcript::Transcript;
|
|
||||||
|
|
||||||
use k256::{elliptic_curve::ops::Reduce, U256, Scalar};
|
use k256::{elliptic_curve::ops::Reduce, U256, Scalar};
|
||||||
|
|
||||||
use frost::{
|
use frost::{
|
||||||
curve::{Ciphersuite, Secp256k1},
|
curve::{Ciphersuite, Secp256k1},
|
||||||
Participant, ThresholdKeys, ThresholdView, FrostError,
|
Participant, ThresholdKeys, ThresholdView, FrostError,
|
||||||
algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr},
|
algorithm::{Hram as HramTrait, Algorithm, IetfSchnorr as FrostSchnorr},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -82,16 +80,17 @@ mod frost_crypto {
|
||||||
///
|
///
|
||||||
/// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic.
|
/// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Schnorr<T: Sync + Clone + Debug + Transcript>(FrostSchnorr<Secp256k1, T, Hram>);
|
pub struct Schnorr(FrostSchnorr<Secp256k1, Hram>);
|
||||||
impl<T: Sync + Clone + Debug + Transcript> Schnorr<T> {
|
impl Schnorr {
|
||||||
/// Construct a Schnorr algorithm continuing the specified transcript.
|
/// Construct a Schnorr algorithm continuing the specified transcript.
|
||||||
pub fn new(transcript: T) -> Schnorr<T> {
|
#[allow(clippy::new_without_default)]
|
||||||
Schnorr(FrostSchnorr::new(transcript))
|
pub fn new() -> Schnorr {
|
||||||
|
Schnorr(FrostSchnorr::ietf())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Sync + Clone + Debug + Transcript> Algorithm<Secp256k1> for Schnorr<T> {
|
impl Algorithm<Secp256k1> for Schnorr {
|
||||||
type Transcript = T;
|
type Transcript = <FrostSchnorr<Secp256k1, Hram> as Algorithm<Secp256k1>>::Transcript;
|
||||||
type Addendum = ();
|
type Addendum = ();
|
||||||
type Signature = [u8; 64];
|
type Signature = [u8; 64];
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ use rand_core::OsRng;
|
||||||
use secp256k1::{Secp256k1 as BContext, Message, schnorr::Signature};
|
use secp256k1::{Secp256k1 as BContext, Message, schnorr::Signature};
|
||||||
|
|
||||||
use k256::Scalar;
|
use k256::Scalar;
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
use frost::{
|
use frost::{
|
||||||
curve::Secp256k1,
|
curve::Secp256k1,
|
||||||
Participant,
|
Participant,
|
||||||
|
@ -25,8 +24,7 @@ fn test_algorithm() {
|
||||||
*keys = keys.offset(Scalar::from(offset));
|
*keys = keys.offset(Scalar::from(offset));
|
||||||
}
|
}
|
||||||
|
|
||||||
let algo =
|
let algo = Schnorr::new();
|
||||||
Schnorr::<RecommendedTranscript>::new(RecommendedTranscript::new(b"bitcoin-serai sign test"));
|
|
||||||
let sig = sign(
|
let sig = sign(
|
||||||
&mut OsRng,
|
&mut OsRng,
|
||||||
&algo,
|
&algo,
|
||||||
|
|
|
@ -22,7 +22,7 @@ use bitcoin::{
|
||||||
Block,
|
Block,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use bitcoin::consensus::encode::Decodable;
|
use bitcoin::{hashes::Hash, consensus::encode::Decodable, TapTweakHash};
|
||||||
|
|
||||||
use crate::crypto::x_only;
|
use crate::crypto::x_only;
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
|
@ -33,12 +33,40 @@ mod send;
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
pub use send::*;
|
pub use send::*;
|
||||||
|
|
||||||
/// Tweak keys to ensure they're usable with Bitcoin.
|
/// Tweak keys to ensure they're usable with Bitcoin's Taproot upgrade.
|
||||||
///
|
///
|
||||||
/// Taproot keys, which these keys are used as, must be even. This offsets the keys until they're
|
/// This adds an unspendable script path to the key, preventing any outputs received to this key
|
||||||
/// even.
|
/// from being spent via a script. To have keys which have spendable script paths, further offsets
|
||||||
|
/// from this position must be used.
|
||||||
|
///
|
||||||
|
/// After adding an unspendable script path, the key is incremented until its even. This means the
|
||||||
|
/// existence of the unspendable script path may not provable, without an understanding of the
|
||||||
|
/// algorithm used here.
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
pub fn tweak_keys(keys: &ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
pub fn tweak_keys(keys: &ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
||||||
|
// Adds the unspendable script path per
|
||||||
|
// https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23
|
||||||
|
let keys = {
|
||||||
|
use k256::elliptic_curve::{
|
||||||
|
bigint::{Encoding, U256},
|
||||||
|
ops::Reduce,
|
||||||
|
group::GroupEncoding,
|
||||||
|
};
|
||||||
|
let tweak_hash = TapTweakHash::hash(&keys.group_key().to_bytes().as_slice()[1 ..]);
|
||||||
|
/*
|
||||||
|
https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#cite_ref-13-0 states how the
|
||||||
|
bias is negligible. This reduction shouldn't ever occur, yet if it did, the script path
|
||||||
|
would be unusable due to a check the script path hash is less than the order. That doesn't
|
||||||
|
impact us as we don't want the script path to be usable.
|
||||||
|
*/
|
||||||
|
keys.offset(<Secp256k1 as Ciphersuite>::F::reduce(U256::from_be_bytes(
|
||||||
|
*tweak_hash.to_raw_hash().as_ref(),
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
|
||||||
|
// This doesn't risk re-introducing a script path as you'd have to find a preimage for the tweak
|
||||||
|
// hash with whatever increment, or manipulate the key so that the tweak hash and increment
|
||||||
|
// equals the desired offset, yet manipulating the key would change the tweak hash
|
||||||
let (_, offset) = make_even(keys.group_key());
|
let (_, offset) = make_even(keys.group_key());
|
||||||
keys.offset(Scalar::from(offset))
|
keys.offset(Scalar::from(offset))
|
||||||
}
|
}
|
||||||
|
@ -142,6 +170,10 @@ impl Scanner {
|
||||||
///
|
///
|
||||||
/// This means offsets are surjective, not bijective, and the order offsets are registered in
|
/// This means offsets are surjective, not bijective, and the order offsets are registered in
|
||||||
/// may determine the validity of future offsets.
|
/// may determine the validity of future offsets.
|
||||||
|
///
|
||||||
|
/// The offsets registered must be securely generated. Arbitrary offsets may introduce a script
|
||||||
|
/// path into the output, allowing the output to be spent by satisfaction of an arbitrary script
|
||||||
|
/// (not by the signature of the key).
|
||||||
pub fn register_offset(&mut self, mut offset: Scalar) -> Option<Scalar> {
|
pub fn register_offset(&mut self, mut offset: Scalar) -> Option<Scalar> {
|
||||||
// This loop will terminate as soon as an even point is found, with any point having a ~50%
|
// This loop will terminate as soon as an even point is found, with any point having a ~50%
|
||||||
// chance of being even
|
// chance of being even
|
||||||
|
|
|
@ -7,9 +7,7 @@ use thiserror::Error;
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
use k256::Scalar;
|
||||||
|
|
||||||
use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar};
|
|
||||||
use frost::{curve::Secp256k1, Participant, ThresholdKeys, FrostError, sign::*};
|
use frost::{curve::Secp256k1, Participant, ThresholdKeys, FrostError, sign::*};
|
||||||
|
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
|
@ -46,7 +44,7 @@ pub enum TransactionError {
|
||||||
#[error("fee was too low to pass the default minimum fee rate")]
|
#[error("fee was too low to pass the default minimum fee rate")]
|
||||||
TooLowFee,
|
TooLowFee,
|
||||||
#[error("not enough funds for these payments")]
|
#[error("not enough funds for these payments")]
|
||||||
NotEnoughFunds,
|
NotEnoughFunds { inputs: u64, payments: u64, fee: u64 },
|
||||||
#[error("transaction was too large")]
|
#[error("transaction was too large")]
|
||||||
TooLargeTransaction,
|
TooLargeTransaction,
|
||||||
}
|
}
|
||||||
|
@ -215,7 +213,11 @@ impl SignableTransaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
if input_sat < (payment_sat + needed_fee) {
|
if input_sat < (payment_sat + needed_fee) {
|
||||||
Err(TransactionError::NotEnoughFunds)?;
|
Err(TransactionError::NotEnoughFunds {
|
||||||
|
inputs: input_sat,
|
||||||
|
payments: payment_sat,
|
||||||
|
fee: needed_fee,
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's a change address, check if there's change to give it
|
// If there's a change address, check if there's change to give it
|
||||||
|
@ -260,49 +262,23 @@ impl SignableTransaction {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the outputs this transaction will create.
|
/// Returns the transaction, sans witness, this will create if signed.
|
||||||
pub fn outputs(&self) -> &[TxOut] {
|
pub fn transaction(&self) -> &Transaction {
|
||||||
&self.tx.output
|
&self.tx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a multisig machine for this transaction.
|
/// Create a multisig machine for this transaction.
|
||||||
///
|
///
|
||||||
/// Returns None if the wrong keys are used.
|
/// Returns None if the wrong keys are used.
|
||||||
pub fn multisig(
|
pub fn multisig(self, keys: &ThresholdKeys<Secp256k1>) -> Option<TransactionMachine> {
|
||||||
self,
|
|
||||||
keys: &ThresholdKeys<Secp256k1>,
|
|
||||||
mut transcript: RecommendedTranscript,
|
|
||||||
) -> Option<TransactionMachine> {
|
|
||||||
transcript.domain_separate(b"bitcoin_transaction");
|
|
||||||
transcript.append_message(b"root_key", keys.group_key().to_encoded_point(true).as_bytes());
|
|
||||||
|
|
||||||
// Transcript the inputs and outputs
|
|
||||||
let tx = &self.tx;
|
|
||||||
for input in &tx.input {
|
|
||||||
transcript.append_message(b"input_hash", input.previous_output.txid);
|
|
||||||
transcript.append_message(b"input_output_index", input.previous_output.vout.to_le_bytes());
|
|
||||||
}
|
|
||||||
for payment in &tx.output {
|
|
||||||
transcript.append_message(b"output_script", payment.script_pubkey.as_bytes());
|
|
||||||
transcript.append_message(b"output_amount", payment.value.to_sat().to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut sigs = vec![];
|
let mut sigs = vec![];
|
||||||
for i in 0 .. tx.input.len() {
|
for i in 0 .. self.tx.input.len() {
|
||||||
let mut transcript = transcript.clone();
|
|
||||||
// This unwrap is safe since any transaction with this many inputs violates the maximum
|
|
||||||
// size allowed under standards, which this lib will error on creation of
|
|
||||||
transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes());
|
|
||||||
|
|
||||||
let offset = keys.clone().offset(self.offsets[i]);
|
let offset = keys.clone().offset(self.offsets[i]);
|
||||||
if p2tr_script_buf(offset.group_key())? != self.prevouts[i].script_pubkey {
|
if p2tr_script_buf(offset.group_key())? != self.prevouts[i].script_pubkey {
|
||||||
None?;
|
None?;
|
||||||
}
|
}
|
||||||
|
|
||||||
sigs.push(AlgorithmMachine::new(
|
sigs.push(AlgorithmMachine::new(Schnorr::new(), keys.clone().offset(self.offsets[i])));
|
||||||
Schnorr::new(transcript),
|
|
||||||
keys.clone().offset(self.offsets[i]),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(TransactionMachine { tx: self, sigs })
|
Some(TransactionMachine { tx: self, sigs })
|
||||||
|
@ -315,7 +291,7 @@ impl SignableTransaction {
|
||||||
/// This will panic if either `cache` is called or the message isn't empty.
|
/// This will panic if either `cache` is called or the message isn't empty.
|
||||||
pub struct TransactionMachine {
|
pub struct TransactionMachine {
|
||||||
tx: SignableTransaction,
|
tx: SignableTransaction,
|
||||||
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreprocessMachine for TransactionMachine {
|
impl PreprocessMachine for TransactionMachine {
|
||||||
|
@ -344,7 +320,7 @@ impl PreprocessMachine for TransactionMachine {
|
||||||
|
|
||||||
pub struct TransactionSignMachine {
|
pub struct TransactionSignMachine {
|
||||||
tx: SignableTransaction,
|
tx: SignableTransaction,
|
||||||
sigs: Vec<AlgorithmSignMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
sigs: Vec<AlgorithmSignMachine<Secp256k1, Schnorr>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignMachine<Transaction> for TransactionSignMachine {
|
impl SignMachine<Transaction> for TransactionSignMachine {
|
||||||
|
@ -424,7 +400,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
||||||
|
|
||||||
pub struct TransactionSignatureMachine {
|
pub struct TransactionSignatureMachine {
|
||||||
tx: Transaction,
|
tx: Transaction,
|
||||||
sigs: Vec<AlgorithmSignatureMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
sigs: Vec<AlgorithmSignatureMachine<Secp256k1, Schnorr>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignatureMachine<Transaction> for TransactionSignatureMachine {
|
impl SignatureMachine<Transaction> for TransactionSignatureMachine {
|
||||||
|
|
|
@ -2,8 +2,6 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use rand_core::{RngCore, OsRng};
|
use rand_core::{RngCore, OsRng};
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
|
|
||||||
use k256::{
|
use k256::{
|
||||||
elliptic_curve::{
|
elliptic_curve::{
|
||||||
group::{ff::Field, Group},
|
group::{ff::Field, Group},
|
||||||
|
@ -94,46 +92,11 @@ fn sign(
|
||||||
) -> Transaction {
|
) -> Transaction {
|
||||||
let mut machines = HashMap::new();
|
let mut machines = HashMap::new();
|
||||||
for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) {
|
for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) {
|
||||||
machines.insert(
|
machines.insert(i, tx.clone().multisig(&keys[&i].clone()).unwrap());
|
||||||
i,
|
|
||||||
tx.clone()
|
|
||||||
.multisig(&keys[&i].clone(), RecommendedTranscript::new(b"bitcoin-serai Test Transaction"))
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
sign_without_caching(&mut OsRng, machines, &[])
|
sign_without_caching(&mut OsRng, machines, &[])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_tweak_keys() {
|
|
||||||
let mut even = false;
|
|
||||||
let mut odd = false;
|
|
||||||
|
|
||||||
// Generate keys until we get an even set and an odd set
|
|
||||||
while !(even && odd) {
|
|
||||||
let mut keys = key_gen(&mut OsRng).drain().next().unwrap().1;
|
|
||||||
if is_even(keys.group_key()) {
|
|
||||||
// Tweaking should do nothing
|
|
||||||
assert_eq!(tweak_keys(&keys).group_key(), keys.group_key());
|
|
||||||
|
|
||||||
even = true;
|
|
||||||
} else {
|
|
||||||
let tweaked = tweak_keys(&keys).group_key();
|
|
||||||
assert_ne!(tweaked, keys.group_key());
|
|
||||||
// Tweaking should produce an even key
|
|
||||||
assert!(is_even(tweaked));
|
|
||||||
|
|
||||||
// Verify it uses the smallest possible offset
|
|
||||||
while keys.group_key().to_encoded_point(true).tag() == Tag::CompressedOddY {
|
|
||||||
keys = keys.offset(Scalar::ONE);
|
|
||||||
}
|
|
||||||
assert_eq!(tweaked, keys.group_key());
|
|
||||||
|
|
||||||
odd = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async_sequential! {
|
async_sequential! {
|
||||||
async fn test_scanner() {
|
async fn test_scanner() {
|
||||||
// Test Scanners are creatable for even keys.
|
// Test Scanners are creatable for even keys.
|
||||||
|
@ -232,10 +195,10 @@ async_sequential! {
|
||||||
Err(TransactionError::TooLowFee),
|
Err(TransactionError::TooLowFee),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
SignableTransaction::new(inputs.clone(), &[(addr(), inputs[0].value() * 2)], None, None, FEE),
|
SignableTransaction::new(inputs.clone(), &[(addr(), inputs[0].value() * 2)], None, None, FEE),
|
||||||
Err(TransactionError::NotEnoughFunds),
|
Err(TransactionError::NotEnoughFunds { .. }),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
SignableTransaction::new(inputs, &vec![(addr(), 1000); 10000], None, None, FEE),
|
SignableTransaction::new(inputs, &vec![(addr(), 1000); 10000], None, None, FEE),
|
||||||
|
|
|
@ -27,23 +27,23 @@ group = { version = "0.13", default-features = false }
|
||||||
k256 = { version = "^0.13.1", default-features = false, features = ["std", "ecdsa", "arithmetic"] }
|
k256 = { version = "^0.13.1", default-features = false, features = ["std", "ecdsa", "arithmetic"] }
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false, features = ["secp256k1"] }
|
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false, features = ["secp256k1"] }
|
||||||
|
|
||||||
alloy-core = { version = "0.7", default-features = false }
|
alloy-core = { version = "0.8", default-features = false }
|
||||||
alloy-sol-types = { version = "0.7", default-features = false, features = ["json"] }
|
alloy-sol-types = { version = "0.8", default-features = false, features = ["json"] }
|
||||||
alloy-consensus = { version = "0.1", default-features = false, features = ["k256"] }
|
alloy-consensus = { version = "0.3", default-features = false, features = ["k256"] }
|
||||||
alloy-network = { version = "0.1", default-features = false }
|
alloy-network = { version = "0.3", default-features = false }
|
||||||
alloy-rpc-types-eth = { version = "0.1", default-features = false }
|
alloy-rpc-types-eth = { version = "0.3", default-features = false }
|
||||||
alloy-rpc-client = { version = "0.1", default-features = false }
|
alloy-rpc-client = { version = "0.3", 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 = { version = "0.1", default-features = false }
|
alloy-provider = { version = "0.3", default-features = false }
|
||||||
|
|
||||||
alloy-node-bindings = { version = "0.1", default-features = false, optional = true }
|
alloy-node-bindings = { version = "0.3", 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 = { version = "0.1", default-features = false }
|
alloy-node-bindings = { version = "0.3", default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
tests = ["alloy-node-bindings", "frost/tests"]
|
tests = ["alloy-node-bindings", "frost/tests"]
|
||||||
|
|
|
@ -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 = { version = "0.1", default-features = false }
|
alloy-json-rpc = { version = "0.3", default-features = false }
|
||||||
alloy-transport = { version = "0.1", default-features = false }
|
alloy-transport = { version = "0.3", default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["tls"]
|
default = ["tls"]
|
||||||
|
|
|
@ -6,7 +6,7 @@ use k256::{Scalar, ProjectivePoint};
|
||||||
use frost::{curve::Secp256k1, Participant, ThresholdKeys, tests::key_gen as frost_key_gen};
|
use frost::{curve::Secp256k1, Participant, ThresholdKeys, tests::key_gen as frost_key_gen};
|
||||||
|
|
||||||
use alloy_core::{
|
use alloy_core::{
|
||||||
primitives::{Address, U256, Bytes, TxKind},
|
primitives::{Address, U256, Bytes, Signature, TxKind},
|
||||||
hex::FromHex,
|
hex::FromHex,
|
||||||
};
|
};
|
||||||
use alloy_consensus::{SignableTransaction, TxLegacy};
|
use alloy_consensus::{SignableTransaction, TxLegacy};
|
||||||
|
@ -69,7 +69,7 @@ pub async fn send(
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut bytes = vec![];
|
let mut bytes = vec![];
|
||||||
tx.encode_with_signature_fields(&sig.into(), &mut bytes);
|
tx.encode_with_signature_fields(&Signature::from(sig), &mut bytes);
|
||||||
let pending_tx = provider.send_raw_transaction(&bytes).await.ok()?;
|
let pending_tx = provider.send_raw_transaction(&bytes).await.ok()?;
|
||||||
pending_tx.get_receipt().await.ok()
|
pending_tx.get_receipt().await.ok()
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,6 @@ async fn latest_block_hash(client: &RootProvider<SimpleRequest>) -> [u8; 32] {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.header
|
.header
|
||||||
.hash
|
.hash
|
||||||
.unwrap()
|
|
||||||
.0
|
.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,5 +53,4 @@ std = [
|
||||||
]
|
]
|
||||||
|
|
||||||
compile-time-generators = ["curve25519-dalek/precomputed-tables", "monero-bulletproofs/compile-time-generators"]
|
compile-time-generators = ["curve25519-dalek/precomputed-tables", "monero-bulletproofs/compile-time-generators"]
|
||||||
multisig = ["monero-clsag/multisig", "std"]
|
|
||||||
default = ["std", "compile-time-generators"]
|
default = ["std", "compile-time-generators"]
|
||||||
|
|
|
@ -18,7 +18,6 @@ workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
||||||
|
|
||||||
async-trait = { version = "0.1", default-features = false }
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
thiserror = { version = "1", default-features = false, optional = true }
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||||
|
|
|
@ -16,8 +16,6 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { version = "0.1", default-features = false }
|
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
||||||
digest_auth = { version = "0.3", default-features = false }
|
digest_auth = { version = "0.3", default-features = false }
|
||||||
simple-request = { path = "../../../../common/request", version = "0.1", default-features = false, features = ["tls"] }
|
simple-request = { path = "../../../../common/request", version = "0.1", default-features = false, features = ["tls"] }
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
use core::future::Future;
|
||||||
use std::{sync::Arc, io::Read, time::Duration};
|
use std::{sync::Arc, io::Read, time::Duration};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use digest_auth::{WwwAuthenticateHeader, AuthContext};
|
use digest_auth::{WwwAuthenticateHeader, AuthContext};
|
||||||
|
@ -280,11 +279,16 @@ impl SimpleRequestRpc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Rpc for SimpleRequestRpc {
|
impl Rpc for SimpleRequestRpc {
|
||||||
async fn post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError> {
|
fn post(
|
||||||
tokio::time::timeout(self.request_timeout, self.inner_post(route, body))
|
&self,
|
||||||
.await
|
route: &str,
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?
|
body: Vec<u8>,
|
||||||
|
) -> impl Send + Future<Output = Result<Vec<u8>, RpcError>> {
|
||||||
|
async move {
|
||||||
|
tokio::time::timeout(self.request_timeout, self.inner_post(route, body))
|
||||||
|
.await
|
||||||
|
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -79,10 +79,13 @@ pub struct Block {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Block {
|
impl Block {
|
||||||
/// The zero-index position of this block within the blockchain.
|
/// The zero-indexed position of this block within the blockchain.
|
||||||
///
|
///
|
||||||
/// This information comes from the Block's miner transaction. If the miner transaction isn't
|
/// This information comes from the Block's miner transaction. If the miner transaction isn't
|
||||||
/// structed as expected, this will return None.
|
/// structed as expected, this will return None. This will return Some for any Block which would
|
||||||
|
/// pass the consensus rules.
|
||||||
|
// https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
|
||||||
|
// /src/cryptonote_core/blockchain.cpp#L1365-L1382
|
||||||
pub fn number(&self) -> Option<usize> {
|
pub fn number(&self) -> Option<usize> {
|
||||||
match &self.miner_transaction {
|
match &self.miner_transaction {
|
||||||
Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => {
|
Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => {
|
||||||
|
|
|
@ -11,6 +11,10 @@ rust-version = "1.80"
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
rust-version = "1.80"
|
||||||
|
|
||||||
|
[package.metadata.cargo-machete]
|
||||||
|
ignored = ["monero-clsag"]
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
@ -39,6 +43,7 @@ frost = { package = "modular-frost", path = "../../../crypto/frost", default-fea
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
||||||
|
|
||||||
|
monero-clsag = { path = "../ringct/clsag", default-features = false }
|
||||||
monero-serai = { path = "..", default-features = false }
|
monero-serai = { path = "..", default-features = false }
|
||||||
monero-rpc = { path = "../rpc", default-features = false }
|
monero-rpc = { path = "../rpc", default-features = false }
|
||||||
monero-address = { path = "./address", default-features = false }
|
monero-address = { path = "./address", default-features = false }
|
||||||
|
@ -66,10 +71,11 @@ std = [
|
||||||
"rand_chacha/std",
|
"rand_chacha/std",
|
||||||
"rand_distr/std",
|
"rand_distr/std",
|
||||||
|
|
||||||
|
"monero-clsag/std",
|
||||||
"monero-serai/std",
|
"monero-serai/std",
|
||||||
"monero-rpc/std",
|
"monero-rpc/std",
|
||||||
"monero-address/std",
|
"monero-address/std",
|
||||||
]
|
]
|
||||||
compile-time-generators = ["curve25519-dalek/precomputed-tables", "monero-serai/compile-time-generators"]
|
compile-time-generators = ["curve25519-dalek/precomputed-tables", "monero-serai/compile-time-generators"]
|
||||||
multisig = ["transcript", "group", "dalek-ff-group", "frost", "monero-serai/multisig", "std"]
|
multisig = ["std", "transcript", "group", "dalek-ff-group", "frost", "monero-clsag/multisig"]
|
||||||
default = ["std", "compile-time-generators"]
|
default = ["std", "compile-time-generators"]
|
||||||
|
|
|
@ -228,9 +228,6 @@ pub enum AddressError {
|
||||||
/// The Network embedded within the Address.
|
/// The Network embedded within the Address.
|
||||||
actual: Network,
|
actual: Network,
|
||||||
},
|
},
|
||||||
/// The view key was of small order despite being in a guaranteed address.
|
|
||||||
#[cfg_attr(feature = "std", error("small-order view key in guaranteed address"))]
|
|
||||||
SmallOrderView,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bytes used as prefixes when encoding addresses, variable to the network instance.
|
/// Bytes used as prefixes when encoding addresses, variable to the network instance.
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "polyseed"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Rust implementation of Polyseed"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet/polyseed"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
subtle = { version = "^2.4", default-features = false }
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
rand_core = { version = "0.6", default-features = false }
|
|
||||||
|
|
||||||
sha3 = { version = "0.10", default-features = false }
|
|
||||||
pbkdf2 = { version = "0.12", features = ["simple"], default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"thiserror",
|
|
||||||
|
|
||||||
"subtle/std",
|
|
||||||
"zeroize/std",
|
|
||||||
"rand_core/std",
|
|
||||||
|
|
||||||
"sha3/std",
|
|
||||||
"pbkdf2/std",
|
|
||||||
]
|
|
||||||
default = ["std"]
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,11 +0,0 @@
|
||||||
# Polyseed
|
|
||||||
|
|
||||||
Rust implementation of [Polyseed](https://github.com/tevador/polyseed).
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
|
@ -1,473 +0,0 @@
|
||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
use core::fmt;
|
|
||||||
use std_shims::{sync::LazyLock, string::String, collections::HashMap};
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
use subtle::ConstantTimeEq;
|
|
||||||
use zeroize::{Zeroize, Zeroizing, ZeroizeOnDrop};
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use sha3::Sha3_256;
|
|
||||||
use pbkdf2::pbkdf2_hmac;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
// Features
|
|
||||||
const FEATURE_BITS: u8 = 5;
|
|
||||||
#[allow(dead_code)]
|
|
||||||
const INTERNAL_FEATURES: u8 = 2;
|
|
||||||
const USER_FEATURES: u8 = 3;
|
|
||||||
|
|
||||||
const USER_FEATURES_MASK: u8 = (1 << USER_FEATURES) - 1;
|
|
||||||
const ENCRYPTED_MASK: u8 = 1 << 4;
|
|
||||||
const RESERVED_FEATURES_MASK: u8 = ((1 << FEATURE_BITS) - 1) ^ ENCRYPTED_MASK;
|
|
||||||
|
|
||||||
fn user_features(features: u8) -> u8 {
|
|
||||||
features & USER_FEATURES_MASK
|
|
||||||
}
|
|
||||||
|
|
||||||
fn polyseed_features_supported(features: u8) -> bool {
|
|
||||||
(features & RESERVED_FEATURES_MASK) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dates
|
|
||||||
const DATE_BITS: u8 = 10;
|
|
||||||
const DATE_MASK: u16 = (1u16 << DATE_BITS) - 1;
|
|
||||||
const POLYSEED_EPOCH: u64 = 1635768000; // 1st November 2021 12:00 UTC
|
|
||||||
const TIME_STEP: u64 = 2629746; // 30.436875 days = 1/12 of the Gregorian year
|
|
||||||
|
|
||||||
// After ~85 years, this will roll over.
|
|
||||||
fn birthday_encode(time: u64) -> u16 {
|
|
||||||
u16::try_from((time.saturating_sub(POLYSEED_EPOCH) / TIME_STEP) & u64::from(DATE_MASK))
|
|
||||||
.expect("value masked by 2**10 - 1 didn't fit into a u16")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn birthday_decode(birthday: u16) -> u64 {
|
|
||||||
POLYSEED_EPOCH + (u64::from(birthday) * TIME_STEP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Polyseed parameters
|
|
||||||
const SECRET_BITS: usize = 150;
|
|
||||||
|
|
||||||
const BITS_PER_BYTE: usize = 8;
|
|
||||||
const SECRET_SIZE: usize = SECRET_BITS.div_ceil(BITS_PER_BYTE); // 19
|
|
||||||
const CLEAR_BITS: usize = (SECRET_SIZE * BITS_PER_BYTE) - SECRET_BITS; // 2
|
|
||||||
|
|
||||||
// Polyseed calls this CLEAR_MASK and has a very complicated formula for this fundamental
|
|
||||||
// equivalency
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
|
||||||
const LAST_BYTE_SECRET_BITS_MASK: u8 = ((1 << (BITS_PER_BYTE - CLEAR_BITS)) - 1) as u8;
|
|
||||||
|
|
||||||
const SECRET_BITS_PER_WORD: usize = 10;
|
|
||||||
|
|
||||||
// The amount of words in a seed.
|
|
||||||
const POLYSEED_LENGTH: usize = 16;
|
|
||||||
// Amount of characters each word must have if trimmed
|
|
||||||
pub(crate) const PREFIX_LEN: usize = 4;
|
|
||||||
|
|
||||||
const POLY_NUM_CHECK_DIGITS: usize = 1;
|
|
||||||
const DATA_WORDS: usize = POLYSEED_LENGTH - POLY_NUM_CHECK_DIGITS;
|
|
||||||
|
|
||||||
// Polynomial
|
|
||||||
const GF_BITS: usize = 11;
|
|
||||||
const POLYSEED_MUL2_TABLE: [u16; 8] = [5, 7, 1, 3, 13, 15, 9, 11];
|
|
||||||
|
|
||||||
type Poly = [u16; POLYSEED_LENGTH];
|
|
||||||
|
|
||||||
fn elem_mul2(x: u16) -> u16 {
|
|
||||||
if x < 1024 {
|
|
||||||
return 2 * x;
|
|
||||||
}
|
|
||||||
POLYSEED_MUL2_TABLE[usize::from(x % 8)] + (16 * ((x - 1024) / 8))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poly_eval(poly: &Poly) -> u16 {
|
|
||||||
// Horner's method at x = 2
|
|
||||||
let mut result = poly[POLYSEED_LENGTH - 1];
|
|
||||||
for i in (0 .. (POLYSEED_LENGTH - 1)).rev() {
|
|
||||||
result = elem_mul2(result) ^ poly[i];
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key gen parameters
|
|
||||||
const POLYSEED_SALT: &[u8] = b"POLYSEED key";
|
|
||||||
const POLYSEED_KEYGEN_ITERATIONS: u32 = 10000;
|
|
||||||
|
|
||||||
// Polyseed technically supports multiple coins, and the value for Monero is 0
|
|
||||||
// See: https://github.com/tevador/polyseed/blob/dfb05d8edb682b0e8f743b1b70c9131712ff4157
|
|
||||||
// /include/polyseed.h#L57
|
|
||||||
const COIN: u16 = 0;
|
|
||||||
|
|
||||||
/// An error when working with a Polyseed.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
|
||||||
pub enum PolyseedError {
|
|
||||||
/// The seed was invalid.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid seed"))]
|
|
||||||
InvalidSeed,
|
|
||||||
/// The entropy was invalid.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid entropy"))]
|
|
||||||
InvalidEntropy,
|
|
||||||
/// The checksum did not match the data.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid checksum"))]
|
|
||||||
InvalidChecksum,
|
|
||||||
/// Unsupported feature bits were set.
|
|
||||||
#[cfg_attr(feature = "std", error("unsupported features"))]
|
|
||||||
UnsupportedFeatures,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Language options for Polyseed.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize)]
|
|
||||||
pub enum Language {
|
|
||||||
/// English language option.
|
|
||||||
English,
|
|
||||||
/// Spanish language option.
|
|
||||||
Spanish,
|
|
||||||
/// French language option.
|
|
||||||
French,
|
|
||||||
/// Italian language option.
|
|
||||||
Italian,
|
|
||||||
/// Japanese language option.
|
|
||||||
Japanese,
|
|
||||||
/// Korean language option.
|
|
||||||
Korean,
|
|
||||||
/// Czech language option.
|
|
||||||
Czech,
|
|
||||||
/// Portuguese language option.
|
|
||||||
Portuguese,
|
|
||||||
/// Simplified Chinese language option.
|
|
||||||
ChineseSimplified,
|
|
||||||
/// Traditional Chinese language option.
|
|
||||||
ChineseTraditional,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WordList {
|
|
||||||
words: &'static [&'static str],
|
|
||||||
has_prefix: bool,
|
|
||||||
has_accent: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WordList {
|
|
||||||
fn new(words: &'static [&'static str], has_prefix: bool, has_accent: bool) -> WordList {
|
|
||||||
let res = WordList { words, has_prefix, has_accent };
|
|
||||||
// This is needed for a later unwrap to not fails
|
|
||||||
assert!(words.len() < usize::from(u16::MAX));
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static LANGUAGES: LazyLock<HashMap<Language, WordList>> = LazyLock::new(|| {
|
|
||||||
HashMap::from([
|
|
||||||
(Language::Czech, WordList::new(include!("./words/cs.rs"), true, false)),
|
|
||||||
(Language::French, WordList::new(include!("./words/fr.rs"), true, true)),
|
|
||||||
(Language::Korean, WordList::new(include!("./words/ko.rs"), false, false)),
|
|
||||||
(Language::English, WordList::new(include!("./words/en.rs"), true, false)),
|
|
||||||
(Language::Italian, WordList::new(include!("./words/it.rs"), true, false)),
|
|
||||||
(Language::Spanish, WordList::new(include!("./words/es.rs"), true, true)),
|
|
||||||
(Language::Japanese, WordList::new(include!("./words/ja.rs"), false, false)),
|
|
||||||
(Language::Portuguese, WordList::new(include!("./words/pt.rs"), true, false)),
|
|
||||||
(
|
|
||||||
Language::ChineseSimplified,
|
|
||||||
WordList::new(include!("./words/zh_simplified.rs"), false, false),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Language::ChineseTraditional,
|
|
||||||
WordList::new(include!("./words/zh_traditional.rs"), false, false),
|
|
||||||
),
|
|
||||||
])
|
|
||||||
});
|
|
||||||
|
|
||||||
/// A Polyseed.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub struct Polyseed {
|
|
||||||
language: Language,
|
|
||||||
features: u8,
|
|
||||||
birthday: u16,
|
|
||||||
entropy: Zeroizing<[u8; 32]>,
|
|
||||||
checksum: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Polyseed {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.debug_struct("Polyseed").finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn valid_entropy(entropy: &Zeroizing<[u8; 32]>) -> bool {
|
|
||||||
// Last byte of the entropy should only use certain bits
|
|
||||||
let mut res =
|
|
||||||
entropy[SECRET_SIZE - 1].ct_eq(&(entropy[SECRET_SIZE - 1] & LAST_BYTE_SECRET_BITS_MASK));
|
|
||||||
// Last 13 bytes of the buffer should be unused
|
|
||||||
for b in SECRET_SIZE .. entropy.len() {
|
|
||||||
res &= entropy[b].ct_eq(&0);
|
|
||||||
}
|
|
||||||
res.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Polyseed {
|
|
||||||
// TODO: Clean this
|
|
||||||
fn to_poly(&self) -> Poly {
|
|
||||||
let mut extra_bits = u32::from(FEATURE_BITS + DATE_BITS);
|
|
||||||
let extra_val = (u16::from(self.features) << DATE_BITS) | self.birthday;
|
|
||||||
|
|
||||||
let mut entropy_idx = 0;
|
|
||||||
let mut secret_bits = BITS_PER_BYTE;
|
|
||||||
let mut seed_rem_bits = SECRET_BITS - BITS_PER_BYTE;
|
|
||||||
|
|
||||||
let mut poly = [0; POLYSEED_LENGTH];
|
|
||||||
for i in 0 .. DATA_WORDS {
|
|
||||||
extra_bits -= 1;
|
|
||||||
|
|
||||||
let mut word_bits = 0;
|
|
||||||
let mut word_val = 0;
|
|
||||||
while word_bits < SECRET_BITS_PER_WORD {
|
|
||||||
if secret_bits == 0 {
|
|
||||||
entropy_idx += 1;
|
|
||||||
secret_bits = seed_rem_bits.min(BITS_PER_BYTE);
|
|
||||||
seed_rem_bits -= secret_bits;
|
|
||||||
}
|
|
||||||
let chunk_bits = secret_bits.min(SECRET_BITS_PER_WORD - word_bits);
|
|
||||||
secret_bits -= chunk_bits;
|
|
||||||
word_bits += chunk_bits;
|
|
||||||
word_val <<= chunk_bits;
|
|
||||||
word_val |=
|
|
||||||
(u16::from(self.entropy[entropy_idx]) >> secret_bits) & ((1u16 << chunk_bits) - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
word_val <<= 1;
|
|
||||||
word_val |= (extra_val >> extra_bits) & 1;
|
|
||||||
poly[POLY_NUM_CHECK_DIGITS + i] = word_val;
|
|
||||||
}
|
|
||||||
|
|
||||||
poly
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_internal(
|
|
||||||
language: Language,
|
|
||||||
masked_features: u8,
|
|
||||||
encoded_birthday: u16,
|
|
||||||
entropy: Zeroizing<[u8; 32]>,
|
|
||||||
) -> Result<Polyseed, PolyseedError> {
|
|
||||||
if !polyseed_features_supported(masked_features) {
|
|
||||||
Err(PolyseedError::UnsupportedFeatures)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !valid_entropy(&entropy) {
|
|
||||||
Err(PolyseedError::InvalidEntropy)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut res = Polyseed {
|
|
||||||
language,
|
|
||||||
birthday: encoded_birthday,
|
|
||||||
features: masked_features,
|
|
||||||
entropy,
|
|
||||||
checksum: 0,
|
|
||||||
};
|
|
||||||
res.checksum = poly_eval(&res.to_poly());
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new `Polyseed` with specific internals.
|
|
||||||
///
|
|
||||||
/// `birthday` is defined in seconds since the epoch.
|
|
||||||
pub fn from(
|
|
||||||
language: Language,
|
|
||||||
features: u8,
|
|
||||||
birthday: u64,
|
|
||||||
entropy: Zeroizing<[u8; 32]>,
|
|
||||||
) -> Result<Polyseed, PolyseedError> {
|
|
||||||
Self::from_internal(language, user_features(features), birthday_encode(birthday), entropy)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new `Polyseed`.
|
|
||||||
///
|
|
||||||
/// This uses the system's time for the birthday, if available, else 0.
|
|
||||||
pub fn new<R: RngCore + CryptoRng>(rng: &mut R, language: Language) -> Polyseed {
|
|
||||||
// Get the birthday
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
let birthday =
|
|
||||||
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(core::time::Duration::ZERO).as_secs();
|
|
||||||
#[cfg(not(feature = "std"))]
|
|
||||||
let birthday = 0;
|
|
||||||
|
|
||||||
// Derive entropy
|
|
||||||
let mut entropy = Zeroizing::new([0; 32]);
|
|
||||||
rng.fill_bytes(entropy.as_mut());
|
|
||||||
entropy[SECRET_SIZE ..].fill(0);
|
|
||||||
entropy[SECRET_SIZE - 1] &= LAST_BYTE_SECRET_BITS_MASK;
|
|
||||||
|
|
||||||
Self::from(language, 0, birthday, entropy).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new `Polyseed` from a String.
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
pub fn from_string(lang: Language, seed: Zeroizing<String>) -> Result<Polyseed, PolyseedError> {
|
|
||||||
// Decode the seed into its polynomial coefficients
|
|
||||||
let mut poly = [0; POLYSEED_LENGTH];
|
|
||||||
|
|
||||||
// Validate words are in the lang word list
|
|
||||||
let lang_word_list: &WordList = &LANGUAGES[&lang];
|
|
||||||
for (i, word) in seed.split_whitespace().enumerate() {
|
|
||||||
// Find the word's index
|
|
||||||
fn check_if_matches<S: AsRef<str>, I: Iterator<Item = S>>(
|
|
||||||
has_prefix: bool,
|
|
||||||
mut lang_words: I,
|
|
||||||
word: &str,
|
|
||||||
) -> Option<usize> {
|
|
||||||
if has_prefix {
|
|
||||||
// Get the position of the word within the iterator
|
|
||||||
// Doesn't use starts_with and some words are substrs of others, leading to false
|
|
||||||
// positives
|
|
||||||
let mut get_position = || {
|
|
||||||
lang_words.position(|lang_word| {
|
|
||||||
let mut lang_word = lang_word.as_ref().chars();
|
|
||||||
let mut word = word.chars();
|
|
||||||
|
|
||||||
let mut res = true;
|
|
||||||
for _ in 0 .. PREFIX_LEN {
|
|
||||||
res &= lang_word.next() == word.next();
|
|
||||||
}
|
|
||||||
res
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let res = get_position();
|
|
||||||
// If another word has this prefix, don't call it a match
|
|
||||||
if get_position().is_some() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
res
|
|
||||||
} else {
|
|
||||||
lang_words.position(|lang_word| lang_word.as_ref() == word)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(coeff) = (if lang_word_list.has_accent {
|
|
||||||
let ascii = |word: &str| word.chars().filter(char::is_ascii).collect::<String>();
|
|
||||||
check_if_matches(
|
|
||||||
lang_word_list.has_prefix,
|
|
||||||
lang_word_list.words.iter().map(|lang_word| ascii(lang_word)),
|
|
||||||
&ascii(word),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
check_if_matches(lang_word_list.has_prefix, lang_word_list.words.iter(), word)
|
|
||||||
}) else {
|
|
||||||
Err(PolyseedError::InvalidSeed)?
|
|
||||||
};
|
|
||||||
|
|
||||||
// WordList asserts the word list length is less than u16::MAX
|
|
||||||
poly[i] = u16::try_from(coeff).expect("coeff exceeded u16");
|
|
||||||
}
|
|
||||||
|
|
||||||
// xor out the coin
|
|
||||||
poly[POLY_NUM_CHECK_DIGITS] ^= COIN;
|
|
||||||
|
|
||||||
// Validate the checksum
|
|
||||||
if poly_eval(&poly) != 0 {
|
|
||||||
Err(PolyseedError::InvalidChecksum)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the polynomial into entropy
|
|
||||||
let mut entropy = Zeroizing::new([0; 32]);
|
|
||||||
|
|
||||||
let mut extra = 0;
|
|
||||||
|
|
||||||
let mut entropy_idx = 0;
|
|
||||||
let mut entropy_bits = 0;
|
|
||||||
|
|
||||||
let checksum = poly[0];
|
|
||||||
for mut word_val in poly.into_iter().skip(POLY_NUM_CHECK_DIGITS) {
|
|
||||||
// Parse the bottom bit, which is one of the bits of extra
|
|
||||||
// This iterates for less than 16 iters, meaning this won't drop any bits
|
|
||||||
extra <<= 1;
|
|
||||||
extra |= word_val & 1;
|
|
||||||
word_val >>= 1;
|
|
||||||
|
|
||||||
// 10 bits per word creates a [8, 2], [6, 4], [4, 6], [2, 8] cycle
|
|
||||||
// 15 % 4 is 3, leaving 2 bits off, and 152 (19 * 8) - 2 is 150, the amount of bits in the
|
|
||||||
// secret
|
|
||||||
let mut word_bits = GF_BITS - 1;
|
|
||||||
while word_bits > 0 {
|
|
||||||
if entropy_bits == BITS_PER_BYTE {
|
|
||||||
entropy_idx += 1;
|
|
||||||
entropy_bits = 0;
|
|
||||||
}
|
|
||||||
let chunk_bits = word_bits.min(BITS_PER_BYTE - entropy_bits);
|
|
||||||
word_bits -= chunk_bits;
|
|
||||||
let chunk_mask = (1u16 << chunk_bits) - 1;
|
|
||||||
if chunk_bits < BITS_PER_BYTE {
|
|
||||||
entropy[entropy_idx] <<= chunk_bits;
|
|
||||||
}
|
|
||||||
entropy[entropy_idx] |=
|
|
||||||
u8::try_from((word_val >> word_bits) & chunk_mask).expect("chunk exceeded u8");
|
|
||||||
entropy_bits += chunk_bits;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let birthday = extra & DATE_MASK;
|
|
||||||
// extra is contained to u16, and DATE_BITS > 8
|
|
||||||
let features =
|
|
||||||
u8::try_from(extra >> DATE_BITS).expect("couldn't convert extra >> DATE_BITS to u8");
|
|
||||||
|
|
||||||
let res = Self::from_internal(lang, features, birthday, entropy);
|
|
||||||
if let Ok(res) = res.as_ref() {
|
|
||||||
debug_assert_eq!(res.checksum, checksum);
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When this seed was created, defined in seconds since the epoch.
|
|
||||||
pub fn birthday(&self) -> u64 {
|
|
||||||
birthday_decode(self.birthday)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This seed's features.
|
|
||||||
pub fn features(&self) -> u8 {
|
|
||||||
self.features
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This seed's entropy.
|
|
||||||
pub fn entropy(&self) -> &Zeroizing<[u8; 32]> {
|
|
||||||
&self.entropy
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The key derived from this seed.
|
|
||||||
pub fn key(&self) -> Zeroizing<[u8; 32]> {
|
|
||||||
let mut key = Zeroizing::new([0; 32]);
|
|
||||||
pbkdf2_hmac::<Sha3_256>(
|
|
||||||
self.entropy.as_slice(),
|
|
||||||
POLYSEED_SALT,
|
|
||||||
POLYSEED_KEYGEN_ITERATIONS,
|
|
||||||
key.as_mut(),
|
|
||||||
);
|
|
||||||
key
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The String representation of this seed.
|
|
||||||
pub fn to_string(&self) -> Zeroizing<String> {
|
|
||||||
// Encode the polynomial with the existing checksum
|
|
||||||
let mut poly = self.to_poly();
|
|
||||||
poly[0] = self.checksum;
|
|
||||||
|
|
||||||
// Embed the coin
|
|
||||||
poly[POLY_NUM_CHECK_DIGITS] ^= COIN;
|
|
||||||
|
|
||||||
// Output words
|
|
||||||
let mut seed = Zeroizing::new(String::new());
|
|
||||||
let words = &LANGUAGES[&self.language].words;
|
|
||||||
for i in 0 .. poly.len() {
|
|
||||||
seed.push_str(words[usize::from(poly[i])]);
|
|
||||||
if i < poly.len() - 1 {
|
|
||||||
seed.push(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seed
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,218 +0,0 @@
|
||||||
use zeroize::Zeroizing;
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_polyseed() {
|
|
||||||
struct Vector {
|
|
||||||
language: Language,
|
|
||||||
seed: String,
|
|
||||||
entropy: String,
|
|
||||||
birthday: u64,
|
|
||||||
has_prefix: bool,
|
|
||||||
has_accent: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
let vectors = [
|
|
||||||
Vector {
|
|
||||||
language: Language::English,
|
|
||||||
seed: "raven tail swear infant grief assist regular lamp \
|
|
||||||
duck valid someone little harsh puppy airport language"
|
|
||||||
.into(),
|
|
||||||
entropy: "dd76e7359a0ded37cd0ff0f3c829a5ae01673300000000000000000000000000".into(),
|
|
||||||
birthday: 1638446400,
|
|
||||||
has_prefix: true,
|
|
||||||
has_accent: false,
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Spanish,
|
|
||||||
seed: "eje fin parte célebre tabú pestaña lienzo puma \
|
|
||||||
prisión hora regalo lengua existir lápiz lote sonoro"
|
|
||||||
.into(),
|
|
||||||
entropy: "5a2b02df7db21fcbe6ec6df137d54c7b20fd2b00000000000000000000000000".into(),
|
|
||||||
birthday: 3118651200,
|
|
||||||
has_prefix: true,
|
|
||||||
has_accent: true,
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::French,
|
|
||||||
seed: "valable arracher décaler jeudi amusant dresser mener épaissir risible \
|
|
||||||
prouesse réserve ampleur ajuster muter caméra enchère"
|
|
||||||
.into(),
|
|
||||||
entropy: "11cfd870324b26657342c37360c424a14a050b00000000000000000000000000".into(),
|
|
||||||
birthday: 1679314966,
|
|
||||||
has_prefix: true,
|
|
||||||
has_accent: true,
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Italian,
|
|
||||||
seed: "caduco midollo copione meninge isotopo illogico riflesso tartaruga fermento \
|
|
||||||
olandese normale tristezza episodio voragine forbito achille"
|
|
||||||
.into(),
|
|
||||||
entropy: "7ecc57c9b4652d4e31428f62bec91cfd55500600000000000000000000000000".into(),
|
|
||||||
birthday: 1679316358,
|
|
||||||
has_prefix: true,
|
|
||||||
has_accent: false,
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Portuguese,
|
|
||||||
seed: "caverna custear azedo adeus senador apertada sedoso omitir \
|
|
||||||
sujeito aurora videira molho cartaz gesso dentista tapar"
|
|
||||||
.into(),
|
|
||||||
entropy: "45473063711376cae38f1b3eba18c874124e1d00000000000000000000000000".into(),
|
|
||||||
birthday: 1679316657,
|
|
||||||
has_prefix: true,
|
|
||||||
has_accent: false,
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Czech,
|
|
||||||
seed: "usmrtit nora dotaz komunita zavalit funkce mzda sotva akce \
|
|
||||||
vesta kabel herna stodola uvolnit ustrnout email"
|
|
||||||
.into(),
|
|
||||||
entropy: "7ac8a4efd62d9c3c4c02e350d32326df37821c00000000000000000000000000".into(),
|
|
||||||
birthday: 1679316898,
|
|
||||||
has_prefix: true,
|
|
||||||
has_accent: false,
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Korean,
|
|
||||||
seed: "전망 선풍기 국제 무궁화 설사 기름 이론적 해안 절망 예선 \
|
|
||||||
지우개 보관 절망 말기 시각 귀신"
|
|
||||||
.into(),
|
|
||||||
entropy: "684663fda420298f42ed94b2c512ed38ddf12b00000000000000000000000000".into(),
|
|
||||||
birthday: 1679317073,
|
|
||||||
has_prefix: false,
|
|
||||||
has_accent: false,
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Japanese,
|
|
||||||
seed: "うちあわせ ちつじょ つごう しはい けんこう とおる てみやげ はんとし たんとう \
|
|
||||||
といれ おさない おさえる むかう ぬぐう なふだ せまる"
|
|
||||||
.into(),
|
|
||||||
entropy: "94e6665518a6286c6e3ba508a2279eb62b771f00000000000000000000000000".into(),
|
|
||||||
birthday: 1679318722,
|
|
||||||
has_prefix: false,
|
|
||||||
has_accent: false,
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::ChineseTraditional,
|
|
||||||
seed: "亂 挖 斤 柄 代 圈 枝 轄 魯 論 函 開 勘 番 榮 壁".into(),
|
|
||||||
entropy: "b1594f585987ab0fd5a31da1f0d377dae5283f00000000000000000000000000".into(),
|
|
||||||
birthday: 1679426433,
|
|
||||||
has_prefix: false,
|
|
||||||
has_accent: false,
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::ChineseSimplified,
|
|
||||||
seed: "啊 百 族 府 票 划 伪 仓 叶 虾 借 溜 晨 左 等 鬼".into(),
|
|
||||||
entropy: "21cdd366f337b89b8d1bc1df9fe73047c22b0300000000000000000000000000".into(),
|
|
||||||
birthday: 1679426817,
|
|
||||||
has_prefix: false,
|
|
||||||
has_accent: false,
|
|
||||||
},
|
|
||||||
// The following seed requires the language specification in order to calculate
|
|
||||||
// a single valid checksum
|
|
||||||
Vector {
|
|
||||||
language: Language::Spanish,
|
|
||||||
seed: "impo sort usua cabi venu nobl oliv clim \
|
|
||||||
cont barr marc auto prod vaca torn fati"
|
|
||||||
.into(),
|
|
||||||
entropy: "dbfce25fe09b68a340e01c62417eeef43ad51800000000000000000000000000".into(),
|
|
||||||
birthday: 1701511650,
|
|
||||||
has_prefix: true,
|
|
||||||
has_accent: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for vector in vectors {
|
|
||||||
let add_whitespace = |mut seed: String| {
|
|
||||||
seed.push(' ');
|
|
||||||
seed
|
|
||||||
};
|
|
||||||
|
|
||||||
let seed_without_accents = |seed: &str| {
|
|
||||||
seed
|
|
||||||
.split_whitespace()
|
|
||||||
.map(|w| w.chars().filter(char::is_ascii).collect::<String>())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ")
|
|
||||||
};
|
|
||||||
|
|
||||||
let trim_seed = |seed: &str| {
|
|
||||||
let seed_to_trim =
|
|
||||||
if vector.has_accent { seed_without_accents(seed) } else { seed.to_string() };
|
|
||||||
seed_to_trim
|
|
||||||
.split_whitespace()
|
|
||||||
.map(|w| {
|
|
||||||
let mut ascii = 0;
|
|
||||||
let mut to_take = w.len();
|
|
||||||
for (i, char) in w.chars().enumerate() {
|
|
||||||
if char.is_ascii() {
|
|
||||||
ascii += 1;
|
|
||||||
}
|
|
||||||
if ascii == PREFIX_LEN {
|
|
||||||
// +1 to include this character, which put us at the prefix length
|
|
||||||
to_take = i + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.chars().take(to_take).collect::<String>()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ")
|
|
||||||
};
|
|
||||||
|
|
||||||
// String -> Seed
|
|
||||||
println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone());
|
|
||||||
let seed = Polyseed::from_string(vector.language, Zeroizing::new(vector.seed.clone())).unwrap();
|
|
||||||
let trim = trim_seed(&vector.seed);
|
|
||||||
let add_whitespace = add_whitespace(vector.seed.clone());
|
|
||||||
let seed_without_accents = seed_without_accents(&vector.seed);
|
|
||||||
|
|
||||||
// Make sure a version with added whitespace still works
|
|
||||||
let whitespaced_seed =
|
|
||||||
Polyseed::from_string(vector.language, Zeroizing::new(add_whitespace)).unwrap();
|
|
||||||
assert_eq!(seed, whitespaced_seed);
|
|
||||||
// Check trimmed versions works
|
|
||||||
if vector.has_prefix {
|
|
||||||
let trimmed_seed = Polyseed::from_string(vector.language, Zeroizing::new(trim)).unwrap();
|
|
||||||
assert_eq!(seed, trimmed_seed);
|
|
||||||
}
|
|
||||||
// Check versions without accents work
|
|
||||||
if vector.has_accent {
|
|
||||||
let seed_without_accents =
|
|
||||||
Polyseed::from_string(vector.language, Zeroizing::new(seed_without_accents)).unwrap();
|
|
||||||
assert_eq!(seed, seed_without_accents);
|
|
||||||
}
|
|
||||||
|
|
||||||
let entropy = Zeroizing::new(hex::decode(vector.entropy).unwrap().try_into().unwrap());
|
|
||||||
assert_eq!(*seed.entropy(), entropy);
|
|
||||||
assert!(seed.birthday().abs_diff(vector.birthday) < TIME_STEP);
|
|
||||||
|
|
||||||
// Entropy -> Seed
|
|
||||||
let from_entropy = Polyseed::from(vector.language, 0, seed.birthday(), entropy).unwrap();
|
|
||||||
assert_eq!(seed.to_string(), from_entropy.to_string());
|
|
||||||
|
|
||||||
// Check against ourselves
|
|
||||||
{
|
|
||||||
let seed = Polyseed::new(&mut OsRng, vector.language);
|
|
||||||
println!("{}. seed: {}", line!(), *seed.to_string());
|
|
||||||
assert_eq!(seed, Polyseed::from_string(vector.language, seed.to_string()).unwrap());
|
|
||||||
assert_eq!(
|
|
||||||
seed,
|
|
||||||
Polyseed::from(vector.language, 0, seed.birthday(), seed.entropy().clone(),).unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_polyseed() {
|
|
||||||
// This seed includes unsupported features bits and should error on decode
|
|
||||||
let seed = "include domain claim resemble urban hire lunch bird \
|
|
||||||
crucial fire best wife ring warm ignore model"
|
|
||||||
.into();
|
|
||||||
let res = Polyseed::from_string(Language::English, Zeroizing::new(seed));
|
|
||||||
assert_eq!(res, Err(PolyseedError::UnsupportedFeatures));
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,41 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "monero-seed"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Rust implementation of Monero's seed algorithm"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet/seed"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
rand_core = { version = "0.6", default-features = false }
|
|
||||||
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
|
||||||
monero-primitives = { path = "../../primitives", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"thiserror",
|
|
||||||
|
|
||||||
"zeroize/std",
|
|
||||||
"rand_core/std",
|
|
||||||
]
|
|
||||||
default = ["std"]
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,11 +0,0 @@
|
||||||
# Monero Seeds
|
|
||||||
|
|
||||||
Rust implementation of Monero's seed algorithm.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
|
@ -1,353 +0,0 @@
|
||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
use core::{ops::Deref, fmt};
|
|
||||||
use std_shims::{
|
|
||||||
sync::LazyLock,
|
|
||||||
vec,
|
|
||||||
vec::Vec,
|
|
||||||
string::{String, ToString},
|
|
||||||
collections::HashMap,
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use curve25519_dalek::scalar::Scalar;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
// The amount of words in a seed without a checksum.
|
|
||||||
const SEED_LENGTH: usize = 24;
|
|
||||||
// The amount of words in a seed with a checksum.
|
|
||||||
const SEED_LENGTH_WITH_CHECKSUM: usize = 25;
|
|
||||||
|
|
||||||
/// An error when working with a seed.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
|
||||||
pub enum SeedError {
|
|
||||||
#[cfg_attr(feature = "std", error("invalid seed"))]
|
|
||||||
/// The seed was invalid.
|
|
||||||
InvalidSeed,
|
|
||||||
/// The checksum did not match the data.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid checksum"))]
|
|
||||||
InvalidChecksum,
|
|
||||||
/// The deprecated English language option was used with a checksum.
|
|
||||||
///
|
|
||||||
/// The deprecated English language option did not include a checksum.
|
|
||||||
#[cfg_attr(feature = "std", error("deprecated English language option included a checksum"))]
|
|
||||||
DeprecatedEnglishWithChecksum,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Language options.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Zeroize)]
|
|
||||||
pub enum Language {
|
|
||||||
/// Chinese language option.
|
|
||||||
Chinese,
|
|
||||||
/// English language option.
|
|
||||||
English,
|
|
||||||
/// Dutch language option.
|
|
||||||
Dutch,
|
|
||||||
/// French language option.
|
|
||||||
French,
|
|
||||||
/// Spanish language option.
|
|
||||||
Spanish,
|
|
||||||
/// German language option.
|
|
||||||
German,
|
|
||||||
/// Italian language option.
|
|
||||||
Italian,
|
|
||||||
/// Portuguese language option.
|
|
||||||
Portuguese,
|
|
||||||
/// Japanese language option.
|
|
||||||
Japanese,
|
|
||||||
/// Russian language option.
|
|
||||||
Russian,
|
|
||||||
/// Esperanto language option.
|
|
||||||
Esperanto,
|
|
||||||
/// Lojban language option.
|
|
||||||
Lojban,
|
|
||||||
/// The original, and deprecated, English language.
|
|
||||||
DeprecatedEnglish,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trim(word: &str, len: usize) -> Zeroizing<String> {
|
|
||||||
Zeroizing::new(word.chars().take(len).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WordList {
|
|
||||||
word_list: &'static [&'static str],
|
|
||||||
word_map: HashMap<&'static str, usize>,
|
|
||||||
trimmed_word_map: HashMap<String, usize>,
|
|
||||||
unique_prefix_length: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WordList {
|
|
||||||
fn new(word_list: &'static [&'static str], prefix_length: usize) -> WordList {
|
|
||||||
let mut lang = WordList {
|
|
||||||
word_list,
|
|
||||||
word_map: HashMap::new(),
|
|
||||||
trimmed_word_map: HashMap::new(),
|
|
||||||
unique_prefix_length: prefix_length,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (i, word) in lang.word_list.iter().enumerate() {
|
|
||||||
lang.word_map.insert(word, i);
|
|
||||||
lang.trimmed_word_map.insert(trim(word, lang.unique_prefix_length).deref().clone(), i);
|
|
||||||
}
|
|
||||||
|
|
||||||
lang
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static LANGUAGES: LazyLock<HashMap<Language, WordList>> = LazyLock::new(|| {
|
|
||||||
HashMap::from([
|
|
||||||
(Language::Chinese, WordList::new(include!("./words/zh.rs"), 1)),
|
|
||||||
(Language::English, WordList::new(include!("./words/en.rs"), 3)),
|
|
||||||
(Language::Dutch, WordList::new(include!("./words/nl.rs"), 4)),
|
|
||||||
(Language::French, WordList::new(include!("./words/fr.rs"), 4)),
|
|
||||||
(Language::Spanish, WordList::new(include!("./words/es.rs"), 4)),
|
|
||||||
(Language::German, WordList::new(include!("./words/de.rs"), 4)),
|
|
||||||
(Language::Italian, WordList::new(include!("./words/it.rs"), 4)),
|
|
||||||
(Language::Portuguese, WordList::new(include!("./words/pt.rs"), 4)),
|
|
||||||
(Language::Japanese, WordList::new(include!("./words/ja.rs"), 3)),
|
|
||||||
(Language::Russian, WordList::new(include!("./words/ru.rs"), 4)),
|
|
||||||
(Language::Esperanto, WordList::new(include!("./words/eo.rs"), 4)),
|
|
||||||
(Language::Lojban, WordList::new(include!("./words/jbo.rs"), 4)),
|
|
||||||
(Language::DeprecatedEnglish, WordList::new(include!("./words/ang.rs"), 4)),
|
|
||||||
])
|
|
||||||
});
|
|
||||||
|
|
||||||
fn checksum_index(words: &[Zeroizing<String>], lang: &WordList) -> usize {
|
|
||||||
let mut trimmed_words = Zeroizing::new(String::new());
|
|
||||||
for w in words {
|
|
||||||
*trimmed_words += &trim(w, lang.unique_prefix_length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn crc32_table() -> [u32; 256] {
|
|
||||||
let poly = 0xedb88320u32;
|
|
||||||
|
|
||||||
let mut res = [0; 256];
|
|
||||||
let mut i = 0;
|
|
||||||
while i < 256 {
|
|
||||||
let mut entry = i;
|
|
||||||
let mut b = 0;
|
|
||||||
while b < 8 {
|
|
||||||
let trigger = entry & 1;
|
|
||||||
entry >>= 1;
|
|
||||||
if trigger == 1 {
|
|
||||||
entry ^= poly;
|
|
||||||
}
|
|
||||||
b += 1;
|
|
||||||
}
|
|
||||||
res[i as usize] = entry;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
res
|
|
||||||
}
|
|
||||||
const CRC32_TABLE: [u32; 256] = crc32_table();
|
|
||||||
|
|
||||||
let trimmed_words = trimmed_words.as_bytes();
|
|
||||||
let mut checksum = u32::MAX;
|
|
||||||
for i in 0 .. trimmed_words.len() {
|
|
||||||
checksum = CRC32_TABLE[usize::from(u8::try_from(checksum % 256).unwrap() ^ trimmed_words[i])] ^
|
|
||||||
(checksum >> 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
usize::try_from(!checksum).unwrap() % words.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert a private key to a seed
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
fn key_to_seed(lang: Language, key: Zeroizing<Scalar>) -> Seed {
|
|
||||||
let bytes = Zeroizing::new(key.to_bytes());
|
|
||||||
|
|
||||||
// get the language words
|
|
||||||
let words = &LANGUAGES[&lang].word_list;
|
|
||||||
let list_len = u64::try_from(words.len()).unwrap();
|
|
||||||
|
|
||||||
// To store the found words & add the checksum word later.
|
|
||||||
let mut seed = Vec::with_capacity(25);
|
|
||||||
|
|
||||||
// convert to words
|
|
||||||
// 4 bytes -> 3 words. 8 digits base 16 -> 3 digits base 1626
|
|
||||||
let mut segment = [0; 4];
|
|
||||||
let mut indices = [0; 4];
|
|
||||||
for i in 0 .. 8 {
|
|
||||||
// convert first 4 byte to u32 & get the word indices
|
|
||||||
let start = i * 4;
|
|
||||||
// convert 4 byte to u32
|
|
||||||
segment.copy_from_slice(&bytes[start .. (start + 4)]);
|
|
||||||
// Actually convert to a u64 so we can add without overflowing
|
|
||||||
indices[0] = u64::from(u32::from_le_bytes(segment));
|
|
||||||
indices[1] = indices[0];
|
|
||||||
indices[0] /= list_len;
|
|
||||||
indices[2] = indices[0] + indices[1];
|
|
||||||
indices[0] /= list_len;
|
|
||||||
indices[3] = indices[0] + indices[2];
|
|
||||||
|
|
||||||
// append words to seed
|
|
||||||
for i in indices.iter().skip(1) {
|
|
||||||
let word = usize::try_from(i % list_len).unwrap();
|
|
||||||
seed.push(Zeroizing::new(words[word].to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
segment.zeroize();
|
|
||||||
indices.zeroize();
|
|
||||||
|
|
||||||
// create a checksum word for all languages except old english
|
|
||||||
if lang != Language::DeprecatedEnglish {
|
|
||||||
let checksum = seed[checksum_index(&seed, &LANGUAGES[&lang])].clone();
|
|
||||||
seed.push(checksum);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut res = Zeroizing::new(String::new());
|
|
||||||
for (i, word) in seed.iter().enumerate() {
|
|
||||||
if i != 0 {
|
|
||||||
*res += " ";
|
|
||||||
}
|
|
||||||
*res += word;
|
|
||||||
}
|
|
||||||
Seed(lang, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert a seed to bytes
|
|
||||||
fn seed_to_bytes(lang: Language, words: &str) -> Result<Zeroizing<[u8; 32]>, SeedError> {
|
|
||||||
// get seed words
|
|
||||||
let words = words.split_whitespace().map(|w| Zeroizing::new(w.to_string())).collect::<Vec<_>>();
|
|
||||||
if (words.len() != SEED_LENGTH) && (words.len() != SEED_LENGTH_WITH_CHECKSUM) {
|
|
||||||
panic!("invalid seed passed to seed_to_bytes");
|
|
||||||
}
|
|
||||||
|
|
||||||
let has_checksum = words.len() == SEED_LENGTH_WITH_CHECKSUM;
|
|
||||||
if has_checksum && lang == Language::DeprecatedEnglish {
|
|
||||||
Err(SeedError::DeprecatedEnglishWithChecksum)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate words are in the language word list
|
|
||||||
let lang_word_list: &WordList = &LANGUAGES[&lang];
|
|
||||||
let matched_indices = (|| {
|
|
||||||
let has_checksum = words.len() == SEED_LENGTH_WITH_CHECKSUM;
|
|
||||||
let mut matched_indices = Zeroizing::new(vec![]);
|
|
||||||
|
|
||||||
// Iterate through all the words and see if they're all present
|
|
||||||
for word in &words {
|
|
||||||
let trimmed = trim(word, lang_word_list.unique_prefix_length);
|
|
||||||
let word = if has_checksum { &trimmed } else { word };
|
|
||||||
|
|
||||||
if let Some(index) = if has_checksum {
|
|
||||||
lang_word_list.trimmed_word_map.get(word.deref())
|
|
||||||
} else {
|
|
||||||
lang_word_list.word_map.get(&word.as_str())
|
|
||||||
} {
|
|
||||||
matched_indices.push(*index);
|
|
||||||
} else {
|
|
||||||
Err(SeedError::InvalidSeed)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if has_checksum {
|
|
||||||
// exclude the last word when calculating a checksum.
|
|
||||||
let last_word = words.last().unwrap().clone();
|
|
||||||
let checksum = words[checksum_index(&words[.. words.len() - 1], lang_word_list)].clone();
|
|
||||||
|
|
||||||
// check the trimmed checksum and trimmed last word line up
|
|
||||||
if trim(&checksum, lang_word_list.unique_prefix_length) !=
|
|
||||||
trim(&last_word, lang_word_list.unique_prefix_length)
|
|
||||||
{
|
|
||||||
Err(SeedError::InvalidChecksum)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(matched_indices)
|
|
||||||
})()?;
|
|
||||||
|
|
||||||
// convert to bytes
|
|
||||||
let mut res = Zeroizing::new([0; 32]);
|
|
||||||
let mut indices = Zeroizing::new([0; 4]);
|
|
||||||
for i in 0 .. 8 {
|
|
||||||
// read 3 indices at a time
|
|
||||||
let i3 = i * 3;
|
|
||||||
indices[1] = matched_indices[i3];
|
|
||||||
indices[2] = matched_indices[i3 + 1];
|
|
||||||
indices[3] = matched_indices[i3 + 2];
|
|
||||||
|
|
||||||
let inner = |i| {
|
|
||||||
let mut base = (lang_word_list.word_list.len() - indices[i] + indices[i + 1]) %
|
|
||||||
lang_word_list.word_list.len();
|
|
||||||
// Shift the index over
|
|
||||||
for _ in 0 .. i {
|
|
||||||
base *= lang_word_list.word_list.len();
|
|
||||||
}
|
|
||||||
base
|
|
||||||
};
|
|
||||||
// set the last index
|
|
||||||
indices[0] = indices[1] + inner(1) + inner(2);
|
|
||||||
if (indices[0] % lang_word_list.word_list.len()) != indices[1] {
|
|
||||||
Err(SeedError::InvalidSeed)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pos = i * 4;
|
|
||||||
let mut bytes = u32::try_from(indices[0]).unwrap().to_le_bytes();
|
|
||||||
res[pos .. (pos + 4)].copy_from_slice(&bytes);
|
|
||||||
bytes.zeroize();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Monero seed.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize)]
|
|
||||||
pub struct Seed(Language, Zeroizing<String>);
|
|
||||||
|
|
||||||
impl fmt::Debug for Seed {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.debug_struct("Seed").finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Seed {
|
|
||||||
/// Create a new seed.
|
|
||||||
pub fn new<R: RngCore + CryptoRng>(rng: &mut R, lang: Language) -> Seed {
|
|
||||||
let mut scalar_bytes = Zeroizing::new([0; 64]);
|
|
||||||
rng.fill_bytes(scalar_bytes.as_mut());
|
|
||||||
key_to_seed(lang, Zeroizing::new(Scalar::from_bytes_mod_order_wide(scalar_bytes.deref())))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a seed from a string.
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
pub fn from_string(lang: Language, words: Zeroizing<String>) -> Result<Seed, SeedError> {
|
|
||||||
let entropy = seed_to_bytes(lang, &words)?;
|
|
||||||
|
|
||||||
// Make sure this is a valid scalar
|
|
||||||
let scalar = Scalar::from_canonical_bytes(*entropy);
|
|
||||||
if scalar.is_none().into() {
|
|
||||||
Err(SeedError::InvalidSeed)?;
|
|
||||||
}
|
|
||||||
let mut scalar = scalar.unwrap();
|
|
||||||
scalar.zeroize();
|
|
||||||
|
|
||||||
// Call from_entropy so a trimmed seed becomes a full seed
|
|
||||||
Ok(Self::from_entropy(lang, entropy).unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a seed from entropy.
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
pub fn from_entropy(lang: Language, entropy: Zeroizing<[u8; 32]>) -> Option<Seed> {
|
|
||||||
Option::from(Scalar::from_canonical_bytes(*entropy))
|
|
||||||
.map(|scalar| key_to_seed(lang, Zeroizing::new(scalar)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a seed to a string.
|
|
||||||
pub fn to_string(&self) -> Zeroizing<String> {
|
|
||||||
self.1.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the entropy underlying this seed.
|
|
||||||
pub fn entropy(&self) -> Zeroizing<[u8; 32]> {
|
|
||||||
seed_to_bytes(self.0, &self.1).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,234 +0,0 @@
|
||||||
use zeroize::Zeroizing;
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use curve25519_dalek::scalar::Scalar;
|
|
||||||
|
|
||||||
use monero_primitives::keccak256;
|
|
||||||
|
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_original_seed() {
|
|
||||||
struct Vector {
|
|
||||||
language: Language,
|
|
||||||
seed: String,
|
|
||||||
spend: String,
|
|
||||||
view: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
let vectors = [
|
|
||||||
Vector {
|
|
||||||
language: Language::Chinese,
|
|
||||||
seed: "摇 曲 艺 武 滴 然 效 似 赏 式 祥 歌 买 疑 小 碧 堆 博 键 房 鲜 悲 付 喷 武".into(),
|
|
||||||
spend: "a5e4fff1706ef9212993a69f246f5c95ad6d84371692d63e9bb0ea112a58340d".into(),
|
|
||||||
view: "1176c43ce541477ea2f3ef0b49b25112b084e26b8a843e1304ac4677b74cdf02".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::English,
|
|
||||||
seed: "washing thirsty occur lectures tuesday fainted toxic adapt \
|
|
||||||
abnormal memoir nylon mostly building shrugged online ember northern \
|
|
||||||
ruby woes dauntless boil family illness inroads northern"
|
|
||||||
.into(),
|
|
||||||
spend: "c0af65c0dd837e666b9d0dfed62745f4df35aed7ea619b2798a709f0fe545403".into(),
|
|
||||||
view: "513ba91c538a5a9069e0094de90e927c0cd147fa10428ce3ac1afd49f63e3b01".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Dutch,
|
|
||||||
seed: "setwinst riphagen vimmetje extase blief tuitelig fuiven meifeest \
|
|
||||||
ponywagen zesmaal ripdeal matverf codetaal leut ivoor rotten \
|
|
||||||
wisgerhof winzucht typograaf atrium rein zilt traktaat verzaagd setwinst"
|
|
||||||
.into(),
|
|
||||||
spend: "e2d2873085c447c2bc7664222ac8f7d240df3aeac137f5ff2022eaa629e5b10a".into(),
|
|
||||||
view: "eac30b69477e3f68093d131c7fd961564458401b07f8c87ff8f6030c1a0c7301".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::French,
|
|
||||||
seed: "poids vaseux tarte bazar poivre effet entier nuance \
|
|
||||||
sensuel ennui pacte osselet poudre battre alibi mouton \
|
|
||||||
stade paquet pliage gibier type question position projet pliage"
|
|
||||||
.into(),
|
|
||||||
spend: "2dd39ff1a4628a94b5c2ec3e42fb3dfe15c2b2f010154dc3b3de6791e805b904".into(),
|
|
||||||
view: "6725b32230400a1032f31d622b44c3a227f88258939b14a7c72e00939e7bdf0e".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Spanish,
|
|
||||||
seed: "minero ocupar mirar evadir octubre cal logro miope \
|
|
||||||
opaco disco ancla litio clase cuello nasal clase \
|
|
||||||
fiar avance deseo mente grumo negro cordón croqueta clase"
|
|
||||||
.into(),
|
|
||||||
spend: "ae2c9bebdddac067d73ec0180147fc92bdf9ac7337f1bcafbbe57dd13558eb02".into(),
|
|
||||||
view: "18deafb34d55b7a43cae2c1c1c206a3c80c12cc9d1f84640b484b95b7fec3e05".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::German,
|
|
||||||
seed: "Kaliber Gabelung Tapir Liveband Favorit Specht Enklave Nabel \
|
|
||||||
Jupiter Foliant Chronik nisten löten Vase Aussage Rekord \
|
|
||||||
Yeti Gesetz Eleganz Alraune Künstler Almweide Jahr Kastanie Almweide"
|
|
||||||
.into(),
|
|
||||||
spend: "79801b7a1b9796856e2397d862a113862e1fdc289a205e79d8d70995b276db06".into(),
|
|
||||||
view: "99f0ec556643bd9c038a4ed86edcb9c6c16032c4622ed2e000299d527a792701".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Italian,
|
|
||||||
seed: "cavo pancetta auto fulmine alleanza filmato diavolo prato \
|
|
||||||
forzare meritare litigare lezione segreto evasione votare buio \
|
|
||||||
licenza cliente dorso natale crescere vento tutelare vetta evasione"
|
|
||||||
.into(),
|
|
||||||
spend: "5e7fd774eb00fa5877e2a8b4dc9c7ffe111008a3891220b56a6e49ac816d650a".into(),
|
|
||||||
view: "698a1dce6018aef5516e82ca0cb3e3ec7778d17dfb41a137567bfa2e55e63a03".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Portuguese,
|
|
||||||
seed: "agito eventualidade onus itrio holograma sodomizar objetos dobro \
|
|
||||||
iugoslavo bcrepuscular odalisca abjeto iuane darwinista eczema acetona \
|
|
||||||
cibernetico hoquei gleba driver buffer azoto megera nogueira agito"
|
|
||||||
.into(),
|
|
||||||
spend: "13b3115f37e35c6aa1db97428b897e584698670c1b27854568d678e729200c0f".into(),
|
|
||||||
view: "ad1b4fd35270f5f36c4da7166672b347e75c3f4d41346ec2a06d1d0193632801".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Japanese,
|
|
||||||
seed: "ぜんぶ どうぐ おたがい せんきょ おうじ そんちょう じゅしん いろえんぴつ \
|
|
||||||
かほう つかれる えらぶ にちじょう くのう にちようび ぬまえび さんきゃく \
|
|
||||||
おおや ちぬき うすめる いがく せつでん さうな すいえい せつだん おおや"
|
|
||||||
.into(),
|
|
||||||
spend: "c56e895cdb13007eda8399222974cdbab493640663804b93cbef3d8c3df80b0b".into(),
|
|
||||||
view: "6c3634a313ec2ee979d565c33888fd7c3502d696ce0134a8bc1a2698c7f2c508".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Russian,
|
|
||||||
seed: "шатер икра нация ехать получать инерция доза реальный \
|
|
||||||
рыжий таможня лопата душа веселый клетка атлас лекция \
|
|
||||||
обгонять паек наивный лыжный дурак стать ежик задача паек"
|
|
||||||
.into(),
|
|
||||||
spend: "7cb5492df5eb2db4c84af20766391cd3e3662ab1a241c70fc881f3d02c381f05".into(),
|
|
||||||
view: "fcd53e41ec0df995ab43927f7c44bc3359c93523d5009fb3f5ba87431d545a03".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Esperanto,
|
|
||||||
seed: "ukazo klini peco etikedo fabriko imitado onklino urino \
|
|
||||||
pudro incidento kumuluso ikono smirgi hirundo uretro krii \
|
|
||||||
sparkado super speciala pupo alpinisto cvana vokegi zombio fabriko"
|
|
||||||
.into(),
|
|
||||||
spend: "82ebf0336d3b152701964ed41df6b6e9a035e57fc98b84039ed0bd4611c58904".into(),
|
|
||||||
view: "cd4d120e1ea34360af528f6a3e6156063312d9cefc9aa6b5218d366c0ed6a201".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Lojban,
|
|
||||||
seed: "jetnu vensa julne xrotu xamsi julne cutci dakli \
|
|
||||||
mlatu xedja muvgau palpi xindo sfubu ciste cinri \
|
|
||||||
blabi darno dembi janli blabi fenki bukpu burcu blabi"
|
|
||||||
.into(),
|
|
||||||
spend: "e4f8c6819ab6cf792cebb858caabac9307fd646901d72123e0367ebc0a79c200".into(),
|
|
||||||
view: "c806ce62bafaa7b2d597f1a1e2dbe4a2f96bfd804bf6f8420fc7f4a6bd700c00".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::DeprecatedEnglish,
|
|
||||||
seed: "glorious especially puff son moment add youth nowhere \
|
|
||||||
throw glide grip wrong rhythm consume very swear \
|
|
||||||
bitter heavy eventually begin reason flirt type unable"
|
|
||||||
.into(),
|
|
||||||
spend: "647f4765b66b636ff07170ab6280a9a6804dfbaf19db2ad37d23be024a18730b".into(),
|
|
||||||
view: "045da65316a906a8c30046053119c18020b07a7a3a6ef5c01ab2a8755416bd02".into(),
|
|
||||||
},
|
|
||||||
// The following seeds require the language specification in order to calculate
|
|
||||||
// a single valid checksum
|
|
||||||
Vector {
|
|
||||||
language: Language::Spanish,
|
|
||||||
seed: "pluma laico atraer pintor peor cerca balde buscar \
|
|
||||||
lancha batir nulo reloj resto gemelo nevera poder columna gol \
|
|
||||||
oveja latir amplio bolero feliz fuerza nevera"
|
|
||||||
.into(),
|
|
||||||
spend: "30303983fc8d215dd020cc6b8223793318d55c466a86e4390954f373fdc7200a".into(),
|
|
||||||
view: "97c649143f3c147ba59aa5506cc09c7992c5c219bb26964442142bf97980800e".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Spanish,
|
|
||||||
seed: "pluma pluma pluma pluma pluma pluma pluma pluma \
|
|
||||||
pluma pluma pluma pluma pluma pluma pluma pluma \
|
|
||||||
pluma pluma pluma pluma pluma pluma pluma pluma pluma"
|
|
||||||
.into(),
|
|
||||||
spend: "b4050000b4050000b4050000b4050000b4050000b4050000b4050000b4050000".into(),
|
|
||||||
view: "d73534f7912b395eb70ef911791a2814eb6df7ce56528eaaa83ff2b72d9f5e0f".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::English,
|
|
||||||
seed: "plus plus plus plus plus plus plus plus \
|
|
||||||
plus plus plus plus plus plus plus plus \
|
|
||||||
plus plus plus plus plus plus plus plus plus"
|
|
||||||
.into(),
|
|
||||||
spend: "3b0400003b0400003b0400003b0400003b0400003b0400003b0400003b040000".into(),
|
|
||||||
view: "43a8a7715eed11eff145a2024ddcc39740255156da7bbd736ee66a0838053a02".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::Spanish,
|
|
||||||
seed: "audio audio audio audio audio audio audio audio \
|
|
||||||
audio audio audio audio audio audio audio audio \
|
|
||||||
audio audio audio audio audio audio audio audio audio"
|
|
||||||
.into(),
|
|
||||||
spend: "ba000000ba000000ba000000ba000000ba000000ba000000ba000000ba000000".into(),
|
|
||||||
view: "1437256da2c85d029b293d8c6b1d625d9374969301869b12f37186e3f906c708".into(),
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
language: Language::English,
|
|
||||||
seed: "audio audio audio audio audio audio audio audio \
|
|
||||||
audio audio audio audio audio audio audio audio \
|
|
||||||
audio audio audio audio audio audio audio audio audio"
|
|
||||||
.into(),
|
|
||||||
spend: "7900000079000000790000007900000079000000790000007900000079000000".into(),
|
|
||||||
view: "20bec797ab96780ae6a045dd816676ca7ed1d7c6773f7022d03ad234b581d600".into(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for vector in vectors {
|
|
||||||
fn trim_by_lang(word: &str, lang: Language) -> String {
|
|
||||||
if lang != Language::DeprecatedEnglish {
|
|
||||||
word.chars().take(LANGUAGES[&lang].unique_prefix_length).collect()
|
|
||||||
} else {
|
|
||||||
word.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let trim_seed = |seed: &str| {
|
|
||||||
seed
|
|
||||||
.split_whitespace()
|
|
||||||
.map(|word| trim_by_lang(word, vector.language))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ")
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test against Monero
|
|
||||||
{
|
|
||||||
println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone());
|
|
||||||
let seed = Seed::from_string(vector.language, Zeroizing::new(vector.seed.clone())).unwrap();
|
|
||||||
let trim = trim_seed(&vector.seed);
|
|
||||||
assert_eq!(seed, Seed::from_string(vector.language, Zeroizing::new(trim)).unwrap());
|
|
||||||
|
|
||||||
let spend: [u8; 32] = hex::decode(vector.spend).unwrap().try_into().unwrap();
|
|
||||||
// For originalal seeds, Monero directly uses the entropy as a spend key
|
|
||||||
assert_eq!(
|
|
||||||
Option::<Scalar>::from(Scalar::from_canonical_bytes(*seed.entropy())),
|
|
||||||
Option::<Scalar>::from(Scalar::from_canonical_bytes(spend)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let view: [u8; 32] = hex::decode(vector.view).unwrap().try_into().unwrap();
|
|
||||||
// Monero then derives the view key as H(spend)
|
|
||||||
assert_eq!(
|
|
||||||
Scalar::from_bytes_mod_order(keccak256(spend)),
|
|
||||||
Scalar::from_canonical_bytes(view).unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(Seed::from_entropy(vector.language, Zeroizing::new(spend)).unwrap(), seed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test against ourselves
|
|
||||||
{
|
|
||||||
let seed = Seed::new(&mut OsRng, vector.language);
|
|
||||||
println!("{}. seed: {}", line!(), *seed.to_string());
|
|
||||||
let trim = trim_seed(&seed.to_string());
|
|
||||||
assert_eq!(seed, Seed::from_string(vector.language, Zeroizing::new(trim)).unwrap());
|
|
||||||
assert_eq!(seed, Seed::from_entropy(vector.language, seed.entropy()).unwrap());
|
|
||||||
assert_eq!(seed, Seed::from_string(vector.language, seed.to_string()).unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -302,7 +302,8 @@ impl Extra {
|
||||||
// `fill_buf` returns the current buffer, filled if empty, only empty if the reader is
|
// `fill_buf` returns the current buffer, filled if empty, only empty if the reader is
|
||||||
// exhausted
|
// exhausted
|
||||||
while !r.fill_buf()?.is_empty() {
|
while !r.fill_buf()?.is_empty() {
|
||||||
res.0.push(ExtraField::read(r)?);
|
let Ok(field) = ExtraField::read(r) else { break };
|
||||||
|
res.0.push(field);
|
||||||
}
|
}
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ pub use monero_rpc as rpc;
|
||||||
pub use monero_address as address;
|
pub use monero_address as address;
|
||||||
|
|
||||||
mod view_pair;
|
mod view_pair;
|
||||||
pub use view_pair::{ViewPair, GuaranteedViewPair};
|
pub use view_pair::{ViewPairError, ViewPair, GuaranteedViewPair};
|
||||||
|
|
||||||
/// Structures and functionality for working with transactions' extra fields.
|
/// Structures and functionality for working with transactions' extra fields.
|
||||||
pub mod extra;
|
pub mod extra;
|
||||||
|
@ -33,7 +33,7 @@ pub(crate) mod output;
|
||||||
pub use output::WalletOutput;
|
pub use output::WalletOutput;
|
||||||
|
|
||||||
mod scan;
|
mod scan;
|
||||||
pub use scan::{Scanner, GuaranteedScanner};
|
pub use scan::{Timelocked, ScanError, Scanner, GuaranteedScanner};
|
||||||
|
|
||||||
mod decoys;
|
mod decoys;
|
||||||
pub use decoys::OutputWithDecoys;
|
pub use decoys::OutputWithDecoys;
|
||||||
|
@ -137,15 +137,13 @@ impl SharedKeyDerivations {
|
||||||
|
|
||||||
fn decrypt(&self, enc_amount: &EncryptedAmount) -> Commitment {
|
fn decrypt(&self, enc_amount: &EncryptedAmount) -> Commitment {
|
||||||
match enc_amount {
|
match enc_amount {
|
||||||
// TODO: Add a test vector for this
|
|
||||||
EncryptedAmount::Original { mask, amount } => {
|
EncryptedAmount::Original { mask, amount } => {
|
||||||
let mask_shared_sec = keccak256(self.shared_key.as_bytes());
|
let mask_shared_sec_scalar = keccak256_to_scalar(self.shared_key.as_bytes());
|
||||||
let mask =
|
let amount_shared_sec_scalar = keccak256_to_scalar(mask_shared_sec_scalar.as_bytes());
|
||||||
Scalar::from_bytes_mod_order(*mask) - Scalar::from_bytes_mod_order(mask_shared_sec);
|
|
||||||
|
let mask = Scalar::from_bytes_mod_order(*mask) - mask_shared_sec_scalar;
|
||||||
|
let amount_scalar = Scalar::from_bytes_mod_order(*amount) - amount_shared_sec_scalar;
|
||||||
|
|
||||||
let amount_shared_sec = keccak256(mask_shared_sec);
|
|
||||||
let amount_scalar =
|
|
||||||
Scalar::from_bytes_mod_order(*amount) - Scalar::from_bytes_mod_order(amount_shared_sec);
|
|
||||||
// d2b from rctTypes.cpp
|
// d2b from rctTypes.cpp
|
||||||
let amount = u64::from_le_bytes(amount_scalar.to_bytes()[0 .. 8].try_into().unwrap());
|
let amount = u64::from_le_bytes(amount_scalar.to_bytes()[0 .. 8].try_into().unwrap());
|
||||||
|
|
||||||
|
|
|
@ -301,12 +301,28 @@ impl WalletOutput {
|
||||||
|
|
||||||
/// The payment ID included with this output.
|
/// The payment ID included with this output.
|
||||||
///
|
///
|
||||||
/// This field may be `Some` even if wallet2 would not return a payment ID. This will happen if
|
/// This field may be `Some` even if wallet2 would not return a payment ID. wallet2 will only
|
||||||
/// the scanned output belongs to the subaddress which spent Monero within the transaction which
|
/// decrypt a payment ID if either:
|
||||||
/// created the output. If multiple subaddresses spent Monero within this transactions, the key
|
///
|
||||||
/// image with the highest index is determined to be the subaddress considered as the one
|
/// A) The transaction wasn't made by the wallet (via checking if any key images are recognized)
|
||||||
/// spending.
|
/// B) For the highest-indexed input with a recognized key image, it spends an output with
|
||||||
// TODO: Clarify and cite for point A ("highest index spent key image"??)
|
/// subaddress account `(a, _)` which is distinct from this output's subaddress account
|
||||||
|
///
|
||||||
|
/// Neither of these cases are handled by `monero-wallet` as scanning doesn't have the context
|
||||||
|
/// of key images.
|
||||||
|
//
|
||||||
|
// Identification of the subaddress account for the highest-indexed input with a recognized key
|
||||||
|
// image:
|
||||||
|
// https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
|
||||||
|
// /src/wallet/wallet2.cpp/#L2637-L2670
|
||||||
|
//
|
||||||
|
// Removal of 'transfers' received to this account:
|
||||||
|
// https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
|
||||||
|
// /src/wallet/wallet2.cpp/#L2782-L2794
|
||||||
|
//
|
||||||
|
// Payment IDs only being decrypted for the remaining transfers:
|
||||||
|
// https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
|
||||||
|
// /src/wallet/wallet2.cpp/#L2796-L2844
|
||||||
pub fn payment_id(&self) -> Option<PaymentId> {
|
pub fn payment_id(&self) -> Option<PaymentId> {
|
||||||
self.metadata.payment_id
|
self.metadata.payment_id
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
use std_shims::{alloc::format, vec, vec::Vec, string::ToString, collections::HashMap};
|
use std_shims::{vec, vec::Vec, collections::HashMap};
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||||
|
|
||||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY};
|
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY};
|
||||||
|
|
||||||
use monero_rpc::{RpcError, Rpc};
|
use monero_rpc::ScannableBlock;
|
||||||
use monero_serai::{
|
use monero_serai::{
|
||||||
io::*,
|
io::*,
|
||||||
primitives::Commitment,
|
primitives::Commitment,
|
||||||
transaction::{Timelock, Pruned, Transaction},
|
transaction::{Timelock, Pruned, Transaction},
|
||||||
block::Block,
|
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
address::SubaddressIndex, ViewPair, GuaranteedViewPair, output::*, PaymentId, Extra,
|
address::SubaddressIndex, ViewPair, GuaranteedViewPair, output::*, PaymentId, Extra,
|
||||||
|
@ -42,9 +41,9 @@ impl Timelocked {
|
||||||
///
|
///
|
||||||
/// `block` is the block number of the block the additional timelock must be satsified by.
|
/// `block` is the block number of the block the additional timelock must be satsified by.
|
||||||
///
|
///
|
||||||
/// `time` is represented in seconds since the epoch. Please note Monero uses an on-chain
|
/// `time` is represented in seconds since the epoch and is in terms of Monero's on-chain clock.
|
||||||
/// deterministic clock for time which is subject to variance from the real world time. This time
|
/// That means outputs whose additional timelocks are statisfied by `Instant::now()` (the time
|
||||||
/// argument will be evaluated against Monero's clock, not the local system's clock.
|
/// according to the local system clock) may still be locked due to variance with Monero's clock.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn additional_timelock_satisfied_by(self, block: usize, time: u64) -> Vec<WalletOutput> {
|
pub fn additional_timelock_satisfied_by(self, block: usize, time: u64) -> Vec<WalletOutput> {
|
||||||
let mut res = vec![];
|
let mut res = vec![];
|
||||||
|
@ -67,6 +66,18 @@ impl Timelocked {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Errors when scanning a block.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||||
|
pub enum ScanError {
|
||||||
|
/// The block was for an unsupported protocol version.
|
||||||
|
#[cfg_attr(feature = "std", error("unsupported protocol version ({0})"))]
|
||||||
|
UnsupportedProtocol(u8),
|
||||||
|
/// The ScannableBlock was invalid.
|
||||||
|
#[cfg_attr(feature = "std", error("invalid scannable block ({0})"))]
|
||||||
|
InvalidScannableBlock(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct InternalScanner {
|
struct InternalScanner {
|
||||||
pair: ViewPair,
|
pair: ViewPair,
|
||||||
|
@ -107,10 +118,10 @@ impl InternalScanner {
|
||||||
|
|
||||||
fn scan_transaction(
|
fn scan_transaction(
|
||||||
&self,
|
&self,
|
||||||
tx_start_index_on_blockchain: u64,
|
output_index_for_first_ringct_output: u64,
|
||||||
tx_hash: [u8; 32],
|
tx_hash: [u8; 32],
|
||||||
tx: &Transaction<Pruned>,
|
tx: &Transaction<Pruned>,
|
||||||
) -> Result<Timelocked, RpcError> {
|
) -> Result<Timelocked, ScanError> {
|
||||||
// Only scan TXs creating RingCT outputs
|
// Only scan TXs creating RingCT outputs
|
||||||
// For the full details on why this check is equivalent, please see the documentation in `scan`
|
// For the full details on why this check is equivalent, please see the documentation in `scan`
|
||||||
if tx.version() != 2 {
|
if tx.version() != 2 {
|
||||||
|
@ -197,14 +208,14 @@ impl InternalScanner {
|
||||||
} else {
|
} else {
|
||||||
let Transaction::V2 { proofs: Some(ref proofs), .. } = &tx else {
|
let Transaction::V2 { proofs: Some(ref proofs), .. } = &tx else {
|
||||||
// Invalid transaction, as of consensus rules at the time of writing this code
|
// Invalid transaction, as of consensus rules at the time of writing this code
|
||||||
Err(RpcError::InvalidNode("non-miner v2 transaction without RCT proofs".to_string()))?
|
Err(ScanError::InvalidScannableBlock("non-miner v2 transaction without RCT proofs"))?
|
||||||
};
|
};
|
||||||
|
|
||||||
commitment = match proofs.base.encrypted_amounts.get(o) {
|
commitment = match proofs.base.encrypted_amounts.get(o) {
|
||||||
Some(amount) => output_derivations.decrypt(amount),
|
Some(amount) => output_derivations.decrypt(amount),
|
||||||
// Invalid transaction, as of consensus rules at the time of writing this code
|
// Invalid transaction, as of consensus rules at the time of writing this code
|
||||||
None => Err(RpcError::InvalidNode(
|
None => Err(ScanError::InvalidScannableBlock(
|
||||||
"RCT proofs without an encrypted amount per output".to_string(),
|
"RCT proofs without an encrypted amount per output",
|
||||||
))?,
|
))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -223,7 +234,7 @@ impl InternalScanner {
|
||||||
index_in_transaction: o.try_into().unwrap(),
|
index_in_transaction: o.try_into().unwrap(),
|
||||||
},
|
},
|
||||||
relative_id: RelativeId {
|
relative_id: RelativeId {
|
||||||
index_on_blockchain: tx_start_index_on_blockchain + u64::try_from(o).unwrap(),
|
index_on_blockchain: output_index_for_first_ringct_output + u64::try_from(o).unwrap(),
|
||||||
},
|
},
|
||||||
data: OutputData { key: output_key, key_offset, commitment },
|
data: OutputData { key: output_key, key_offset, commitment },
|
||||||
metadata: Metadata {
|
metadata: Metadata {
|
||||||
|
@ -243,12 +254,22 @@ impl InternalScanner {
|
||||||
Ok(Timelocked(res))
|
Ok(Timelocked(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result<Timelocked, RpcError> {
|
fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
|
||||||
|
// This is the output index for the first RingCT output within the block
|
||||||
|
// We mutate it to be the output index for the first RingCT for each transaction
|
||||||
|
let ScannableBlock { block, transactions, output_index_for_first_ringct_output } = block;
|
||||||
|
if block.transactions.len() != transactions.len() {
|
||||||
|
Err(ScanError::InvalidScannableBlock(
|
||||||
|
"scanning a ScannableBlock with more/less transactions than it should have",
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
let Some(mut output_index_for_first_ringct_output) = output_index_for_first_ringct_output
|
||||||
|
else {
|
||||||
|
return Ok(Timelocked(vec![]));
|
||||||
|
};
|
||||||
|
|
||||||
if block.header.hardfork_version > 16 {
|
if block.header.hardfork_version > 16 {
|
||||||
Err(RpcError::InternalError(format!(
|
Err(ScanError::UnsupportedProtocol(block.header.hardfork_version))?;
|
||||||
"scanning a hardfork {} block, when we only support up to 16",
|
|
||||||
block.header.hardfork_version
|
|
||||||
)))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We obtain all TXs in full
|
// We obtain all TXs in full
|
||||||
|
@ -256,80 +277,17 @@ impl InternalScanner {
|
||||||
block.miner_transaction.hash(),
|
block.miner_transaction.hash(),
|
||||||
Transaction::<Pruned>::from(block.miner_transaction.clone()),
|
Transaction::<Pruned>::from(block.miner_transaction.clone()),
|
||||||
)];
|
)];
|
||||||
let txs = rpc.get_pruned_transactions(&block.transactions).await?;
|
for (hash, tx) in block.transactions.iter().zip(transactions) {
|
||||||
for (hash, tx) in block.transactions.iter().zip(txs) {
|
|
||||||
txs_with_hashes.push((*hash, tx));
|
txs_with_hashes.push((*hash, tx));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Requesting the output index for each output we sucessfully scan would cause a loss of privacy
|
|
||||||
We could instead request the output indexes for all outputs we scan, yet this would notably
|
|
||||||
increase the amount of RPC calls we make.
|
|
||||||
|
|
||||||
We solve this by requesting the output index for the first RingCT output in the block, which
|
|
||||||
should be within the miner transaction. Then, as we scan transactions, we update the output
|
|
||||||
index ourselves.
|
|
||||||
|
|
||||||
Please note we only will scan RingCT outputs so we only need to track the RingCT output
|
|
||||||
index. This decision was made due to spending CN outputs potentially having burdensome
|
|
||||||
requirements (the need to make a v1 TX due to insufficient decoys).
|
|
||||||
|
|
||||||
We bound ourselves to only scanning RingCT outputs by only scanning v2 transactions. This is
|
|
||||||
safe and correct since:
|
|
||||||
|
|
||||||
1) v1 transactions cannot create RingCT outputs.
|
|
||||||
|
|
||||||
https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
|
||||||
/src/cryptonote_basic/cryptonote_format_utils.cpp#L866-L869
|
|
||||||
|
|
||||||
2) v2 miner transactions implicitly create RingCT outputs.
|
|
||||||
|
|
||||||
https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
|
||||||
/src/blockchain_db/blockchain_db.cpp#L232-L241
|
|
||||||
|
|
||||||
3) v2 transactions must create RingCT outputs.
|
|
||||||
|
|
||||||
https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45
|
|
||||||
/src/cryptonote_core/blockchain.cpp#L3055-L3065
|
|
||||||
|
|
||||||
That does bound on the hard fork version being >= 3, yet all v2 TXs have a hard fork
|
|
||||||
version > 3.
|
|
||||||
|
|
||||||
https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
|
||||||
/src/cryptonote_core/blockchain.cpp#L3417
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Get the starting index
|
|
||||||
let mut tx_start_index_on_blockchain = {
|
|
||||||
let mut tx_start_index_on_blockchain = None;
|
|
||||||
for (hash, tx) in &txs_with_hashes {
|
|
||||||
// If this isn't a RingCT output, or there are no outputs, move to the next TX
|
|
||||||
if (!matches!(tx, Transaction::V2 { .. })) || tx.prefix().outputs.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = *rpc.get_o_indexes(*hash).await?.first().ok_or_else(|| {
|
|
||||||
RpcError::InvalidNode(
|
|
||||||
"requested output indexes for a TX with outputs and got none".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
tx_start_index_on_blockchain = Some(index);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let Some(tx_start_index_on_blockchain) = tx_start_index_on_blockchain else {
|
|
||||||
// Block had no RingCT outputs
|
|
||||||
return Ok(Timelocked(vec![]));
|
|
||||||
};
|
|
||||||
tx_start_index_on_blockchain
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut res = Timelocked(vec![]);
|
let mut res = Timelocked(vec![]);
|
||||||
for (hash, tx) in txs_with_hashes {
|
for (hash, tx) in txs_with_hashes {
|
||||||
// Push all outputs into our result
|
// Push all outputs into our result
|
||||||
{
|
{
|
||||||
let mut this_txs_outputs = vec![];
|
let mut this_txs_outputs = vec![];
|
||||||
core::mem::swap(
|
core::mem::swap(
|
||||||
&mut self.scan_transaction(tx_start_index_on_blockchain, hash, &tx)?.0,
|
&mut self.scan_transaction(output_index_for_first_ringct_output, hash, &tx)?.0,
|
||||||
&mut this_txs_outputs,
|
&mut this_txs_outputs,
|
||||||
);
|
);
|
||||||
res.0.extend(this_txs_outputs);
|
res.0.extend(this_txs_outputs);
|
||||||
|
@ -337,7 +295,7 @@ impl InternalScanner {
|
||||||
|
|
||||||
// Update the RingCT starting index for the next TX
|
// Update the RingCT starting index for the next TX
|
||||||
if matches!(tx, Transaction::V2 { .. }) {
|
if matches!(tx, Transaction::V2 { .. }) {
|
||||||
tx_start_index_on_blockchain += u64::try_from(tx.prefix().outputs.len()).unwrap()
|
output_index_for_first_ringct_output += u64::try_from(tx.prefix().outputs.len()).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,8 +342,8 @@ impl Scanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scan a block.
|
/// Scan a block.
|
||||||
pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result<Timelocked, RpcError> {
|
pub fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
|
||||||
self.0.scan(rpc, block).await
|
self.0.scan(block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,7 +371,7 @@ impl GuaranteedScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scan a block.
|
/// Scan a block.
|
||||||
pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result<Timelocked, RpcError> {
|
pub fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
|
||||||
self.0.scan(rpc, block).await
|
self.0.scan(block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
mod tx_keys;
|
mod tx_keys;
|
||||||
|
pub use tx_keys::TransactionKeys;
|
||||||
mod tx;
|
mod tx;
|
||||||
mod eventuality;
|
mod eventuality;
|
||||||
pub use eventuality::Eventuality;
|
pub use eventuality::Eventuality;
|
||||||
|
@ -100,10 +101,11 @@ impl Change {
|
||||||
///
|
///
|
||||||
/// 1) The change in the TX is shunted to the fee (making it fingerprintable).
|
/// 1) The change in the TX is shunted to the fee (making it fingerprintable).
|
||||||
///
|
///
|
||||||
/// 2) If there are two outputs in the TX, Monero would create a payment ID for the non-change
|
/// 2) In two-output transactions, where the payment address doesn't have a payment ID, wallet2
|
||||||
/// output so an observer can't tell apart TXs with a payment ID from TXs without a payment
|
/// includes an encrypted dummy payment ID for the non-change output in order to not allow
|
||||||
/// ID. monero-wallet will simply not create a payment ID in this case, revealing it's a
|
/// differentiating if transactions send to addresses with payment IDs or not. monero-wallet
|
||||||
/// monero-wallet TX without change.
|
/// includes a dummy payment ID which at least one recipient will identify as not the expected
|
||||||
|
/// dummy payment ID, revealing to the recipient(s) the sender is using non-wallet2 software.
|
||||||
pub fn fingerprintable(address: Option<MoneroAddress>) -> Change {
|
pub fn fingerprintable(address: Option<MoneroAddress>) -> Change {
|
||||||
if let Some(address) = address {
|
if let Some(address) = address {
|
||||||
Change(Some(ChangeEnum::AddressOnly(address)))
|
Change(Some(ChangeEnum::AddressOnly(address)))
|
||||||
|
|
|
@ -76,10 +76,18 @@ impl SignableTransaction {
|
||||||
PaymentId::Encrypted(id).write(&mut id_vec).unwrap();
|
PaymentId::Encrypted(id).write(&mut id_vec).unwrap();
|
||||||
extra.push_nonce(id_vec);
|
extra.push_nonce(id_vec);
|
||||||
} else {
|
} else {
|
||||||
// If there's no payment ID, we push a dummy (as wallet2 does) if there's only one payment
|
/*
|
||||||
if (self.payments.len() == 2) &&
|
If there's no payment ID, we push a dummy (as wallet2 does) to the first payment.
|
||||||
self.payments.iter().any(|payment| matches!(payment, InternalPayment::Change(_)))
|
|
||||||
{
|
This does cause a random payment ID for the other recipient (a documented fingerprint).
|
||||||
|
Functionally, random payment IDs should be fine as wallet2 will trigger this same behavior
|
||||||
|
(a random payment ID being seen by the recipient) with a batch send if one of the recipient
|
||||||
|
addresses has a payment ID.
|
||||||
|
|
||||||
|
The alternative would be to not include any payment ID, fingerprinting to the entire
|
||||||
|
blockchain this is non-standard wallet software (instead of just a single recipient).
|
||||||
|
*/
|
||||||
|
if self.payments.len() == 2 {
|
||||||
let (_, payment_id_xor) = self
|
let (_, payment_id_xor) = self
|
||||||
.payments
|
.payments
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
use std_shims::{vec, vec::Vec};
|
use std_shims::{vec, vec::Vec};
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
use rand_core::SeedableRng;
|
use rand_core::SeedableRng;
|
||||||
use rand_chacha::ChaCha20Rng;
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
@ -15,28 +15,61 @@ use crate::{
|
||||||
send::{ChangeEnum, InternalPayment, SignableTransaction, key_image_sort},
|
send::{ChangeEnum, InternalPayment, SignableTransaction, key_image_sort},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn seeded_rng(
|
||||||
|
dst: &'static [u8],
|
||||||
|
outgoing_view_key: &[u8; 32],
|
||||||
|
mut input_keys: Vec<EdwardsPoint>,
|
||||||
|
) -> ChaCha20Rng {
|
||||||
|
// Apply the DST
|
||||||
|
let mut transcript = Zeroizing::new(vec![u8::try_from(dst.len()).unwrap()]);
|
||||||
|
transcript.extend(dst);
|
||||||
|
|
||||||
|
// Bind to the outgoing view key to prevent foreign entities from rebuilding the transcript
|
||||||
|
transcript.extend(outgoing_view_key);
|
||||||
|
|
||||||
|
// We sort the inputs here to ensure a consistent order
|
||||||
|
// We use the key image sort as it's applicable and well-defined, not because these are key
|
||||||
|
// images
|
||||||
|
input_keys.sort_by(key_image_sort);
|
||||||
|
|
||||||
|
// Ensure uniqueness across transactions by binding to a use-once object
|
||||||
|
// The keys for the inputs is binding to their key images, making them use-once
|
||||||
|
for key in input_keys {
|
||||||
|
transcript.extend(key.compress().to_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = ChaCha20Rng::from_seed(keccak256(&transcript));
|
||||||
|
transcript.zeroize();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator yielding an endless amount of ephemeral keys to use within a transaction.
|
||||||
|
///
|
||||||
|
/// This is used when sending and can be used after sending to re-derive the keys used, as
|
||||||
|
/// necessary for payment proofs.
|
||||||
|
pub struct TransactionKeys(ChaCha20Rng);
|
||||||
|
impl TransactionKeys {
|
||||||
|
/// Construct a new `TransactionKeys`.
|
||||||
|
///
|
||||||
|
/// `input_keys` is the list of keys from the outputs spent within this transaction.
|
||||||
|
pub fn new(outgoing_view_key: &Zeroizing<[u8; 32]>, input_keys: Vec<EdwardsPoint>) -> Self {
|
||||||
|
Self(seeded_rng(b"transaction_keys", outgoing_view_key, input_keys))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Iterator for TransactionKeys {
|
||||||
|
type Item = Zeroizing<Scalar>;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
Some(Zeroizing::new(Scalar::random(&mut self.0)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SignableTransaction {
|
impl SignableTransaction {
|
||||||
|
fn input_keys(&self) -> Vec<EdwardsPoint> {
|
||||||
|
self.inputs.iter().map(OutputWithDecoys::key).collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn seeded_rng(&self, dst: &'static [u8]) -> ChaCha20Rng {
|
pub(crate) fn seeded_rng(&self, dst: &'static [u8]) -> ChaCha20Rng {
|
||||||
// Apply the DST
|
seeded_rng(dst, &self.outgoing_view_key, self.input_keys())
|
||||||
let mut transcript = Zeroizing::new(vec![u8::try_from(dst.len()).unwrap()]);
|
|
||||||
transcript.extend(dst);
|
|
||||||
|
|
||||||
// Bind to the outgoing view key to prevent foreign entities from rebuilding the transcript
|
|
||||||
transcript.extend(self.outgoing_view_key.as_slice());
|
|
||||||
|
|
||||||
// Ensure uniqueness across transactions by binding to a use-once object
|
|
||||||
// The keys for the inputs is binding to their key images, making them use-once
|
|
||||||
let mut input_keys = self.inputs.iter().map(OutputWithDecoys::key).collect::<Vec<_>>();
|
|
||||||
// We sort the inputs mid-way through TX construction, so apply our own sort to ensure a
|
|
||||||
// consistent order
|
|
||||||
// We use the key image sort as it's applicable and well-defined, not because these are key
|
|
||||||
// images
|
|
||||||
input_keys.sort_by(key_image_sort);
|
|
||||||
for key in input_keys {
|
|
||||||
transcript.extend(key.compress().to_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
ChaCha20Rng::from_seed(keccak256(&transcript))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_payments_to_subaddresses(&self) -> bool {
|
fn has_payments_to_subaddresses(&self) -> bool {
|
||||||
|
@ -81,14 +114,14 @@ impl SignableTransaction {
|
||||||
|
|
||||||
// Calculate the transaction keys used as randomness.
|
// Calculate the transaction keys used as randomness.
|
||||||
fn transaction_keys(&self) -> (Zeroizing<Scalar>, Vec<Zeroizing<Scalar>>) {
|
fn transaction_keys(&self) -> (Zeroizing<Scalar>, Vec<Zeroizing<Scalar>>) {
|
||||||
let mut rng = self.seeded_rng(b"transaction_keys");
|
let mut tx_keys = TransactionKeys::new(&self.outgoing_view_key, self.input_keys());
|
||||||
|
|
||||||
let tx_key = Zeroizing::new(Scalar::random(&mut rng));
|
let tx_key = tx_keys.next().unwrap();
|
||||||
|
|
||||||
let mut additional_keys = vec![];
|
let mut additional_keys = vec![];
|
||||||
if self.should_use_additional_keys() {
|
if self.should_use_additional_keys() {
|
||||||
for _ in 0 .. self.payments.len() {
|
for _ in 0 .. self.payments.len() {
|
||||||
additional_keys.push(Zeroizing::new(Scalar::random(&mut rng)));
|
additional_keys.push(tx_keys.next().unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(tx_key, additional_keys)
|
(tx_key, additional_keys)
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
// Tests derived from
|
// Tests derived from
|
||||||
// https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/
|
// https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/
|
||||||
// tests/unit_tests/test_tx_utils.cpp
|
// tests/unit_tests/test_tx_utils.cpp
|
||||||
// which is licensed
|
// which is licensed as follows:
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
/*
|
/*
|
||||||
Copyright (c) 2014-2022, The Monero Project
|
Copyright (c) 2014-2022, The Monero Project
|
||||||
|
@ -105,13 +105,15 @@ fn padding_only_max_size() {
|
||||||
#[test]
|
#[test]
|
||||||
fn padding_only_exceed_max_size() {
|
fn padding_only_exceed_max_size() {
|
||||||
let buf: Vec<u8> = vec![0; MAX_TX_EXTRA_PADDING_COUNT + 1];
|
let buf: Vec<u8> = vec![0; MAX_TX_EXTRA_PADDING_COUNT + 1];
|
||||||
Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err();
|
let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap();
|
||||||
|
assert!(extra.0.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_padding_only() {
|
fn invalid_padding_only() {
|
||||||
let buf: Vec<u8> = vec![0, 42];
|
let buf: Vec<u8> = vec![0, 42];
|
||||||
Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err();
|
let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap();
|
||||||
|
assert!(extra.0.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -135,7 +137,8 @@ fn extra_nonce_only_wrong_size() {
|
||||||
let mut buf: Vec<u8> = vec![0; 20];
|
let mut buf: Vec<u8> = vec![0; 20];
|
||||||
buf[0] = 2;
|
buf[0] = 2;
|
||||||
buf[1] = 255;
|
buf[1] = 255;
|
||||||
Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err();
|
let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap();
|
||||||
|
assert!(extra.0.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -155,7 +158,8 @@ fn pub_key_and_padding() {
|
||||||
fn pub_key_and_invalid_padding() {
|
fn pub_key_and_invalid_padding() {
|
||||||
let mut buf: Vec<u8> = PUB_KEY_BYTES.to_vec();
|
let mut buf: Vec<u8> = PUB_KEY_BYTES.to_vec();
|
||||||
buf.extend([0, 1]);
|
buf.extend([0, 1]);
|
||||||
Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err();
|
let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap();
|
||||||
|
assert_eq!(extra.0, vec![ExtraField::PublicKey(pub_key())]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -181,7 +185,8 @@ fn extra_mysterious_minergate_only_wrong_size() {
|
||||||
let mut buf: Vec<u8> = vec![0; 20];
|
let mut buf: Vec<u8> = vec![0; 20];
|
||||||
buf[0] = 222;
|
buf[0] = 222;
|
||||||
buf[1] = 255;
|
buf[1] = 255;
|
||||||
Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err();
|
let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap();
|
||||||
|
assert!(extra.0.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
mod extra;
|
mod extra;
|
||||||
|
mod scan;
|
||||||
|
|
168
networks/monero/wallet/src/tests/scan.rs
Normal file
168
networks/monero/wallet/src/tests/scan.rs
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
use monero_rpc::ScannableBlock;
|
||||||
|
use crate::{
|
||||||
|
transaction::{Pruned, Transaction},
|
||||||
|
block::Block,
|
||||||
|
ViewPair, Scanner, WalletOutput,
|
||||||
|
output::{AbsoluteId, RelativeId, OutputData, Metadata},
|
||||||
|
Commitment,
|
||||||
|
PaymentId::Encrypted,
|
||||||
|
transaction::Timelock,
|
||||||
|
ringct::EncryptedAmount,
|
||||||
|
};
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
use curve25519_dalek::{Scalar, constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY};
|
||||||
|
|
||||||
|
const SPEND_KEY: &str = "ccf0ea10e1ea64354f42fa710c2b318e581969cf49046d809d1f0aadb3fc7a02";
|
||||||
|
const VIEW_KEY: &str = "a28b4b2085592881df94ee95da332c16b5bb773eb8bb74730208cbb236c73806";
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const PRUNED_TX_WITH_LONG_ENCRYPTED_AMOUNT: &str = "020001020003060101cf60390bb71aa15eb24037772012d59dc68cb4b6211e1c93206db09a6c346261020002ee8ca293511571c0005e1c144e49d09b8ff03046dbafb3e064a34cb9fc1994b600029e2e5cd08c8681dbcf2ce66071467e835f7e86613fbfed3c4fb170127b94e1072c01d3ce2a622c6e06ed465f81017dd6188c3a6e3d8e65a846f9c98416da0e150a82020901c553d35e54111bd001e0bbcbf289d701ce90e309ead2b487ec1d4d8af5d649543eb99a7620f6b54e532898527be29704f050e6f06de61e5967b2ddd506b4d6d36546065d6aae156ac7bec18c99580c07867fb98cb29853edbafec91af2df605c12f9aaa81a9165625afb6649f5a652012c5ba6612351140e1fb4a8463cc765d0a9bb7d999ba35750f365c5285d77230b76c7a612784f4845812a2899f2ca6a304fee61362db59b263115c27d2ce78af6b1d9e939c1f4036c7707851f41abe6458cf1c748353e593469ebf43536a939f7";
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const BLOCK: &str = "0202e8e28efe04db09e2fc4d57854786220bd33e0169ff692440d27ae3932b9219df9ab1d7260b00000000014101ff050580d0acf30e02704972eb1878e94686b62fa4c0202f3e7e3a263073bd6edd751990ea769494ee80c0fc82aa0202edac72ab7c5745d4acaa95f76a3b76e238a55743cd51efb586f968e09821788d80d0dbc3f40202f9b4cf3141aac4203a1aaed01f09326615544997d1b68964928d9aafd07e38e580a0e5b9c29101023405e3aa75b1b7adf04e8c7faa3c3d45616ae740a8b11fb7cc1555dd8b9e4c9180c0dfda8ee90602d2b78accfe1c2ae57bed4fe3385f7735a988f160ef3bbc1f9d7a0c911c26ffd92101d2d55b5066d247a97696be4a84bf70873e4f149687f57e606eb6682f11650e1701b74773bbea995079805398052da9b69244bda034b089b50e4d9151dedb59a12f";
|
||||||
|
|
||||||
|
const OUTPUT_INDEX_FOR_FIRST_RINGCT_OUTPUT: u64 = 0; // note the miner tx is a v1 tx
|
||||||
|
|
||||||
|
fn wallet_output0() -> WalletOutput {
|
||||||
|
WalletOutput {
|
||||||
|
absolute_id: AbsoluteId {
|
||||||
|
transaction: hex::decode("b74773bbea995079805398052da9b69244bda034b089b50e4d9151dedb59a12f")
|
||||||
|
.unwrap()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
index_in_transaction: 0,
|
||||||
|
},
|
||||||
|
relative_id: RelativeId { index_on_blockchain: OUTPUT_INDEX_FOR_FIRST_RINGCT_OUTPUT },
|
||||||
|
data: OutputData {
|
||||||
|
key: CompressedEdwardsY(
|
||||||
|
hex::decode("ee8ca293511571c0005e1c144e49d09b8ff03046dbafb3e064a34cb9fc1994b6")
|
||||||
|
.unwrap()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.decompress()
|
||||||
|
.unwrap(),
|
||||||
|
key_offset: Scalar::from_canonical_bytes(
|
||||||
|
hex::decode("f1d21a76ea0bb228fbc5f0dece0597a8ffb59de7a04b29f70b7c0310446ea905")
|
||||||
|
.unwrap()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
commitment: Commitment {
|
||||||
|
amount: 10000,
|
||||||
|
mask: Scalar::from_canonical_bytes(
|
||||||
|
hex::decode("05c2f142aaf3054cbff0a022f6c7cb75403fd92af0f9441c072ade3f273f7706")
|
||||||
|
.unwrap()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metadata: Metadata {
|
||||||
|
additional_timelock: Timelock::None,
|
||||||
|
subaddress: None,
|
||||||
|
payment_id: Some(Encrypted([0, 0, 0, 0, 0, 0, 0, 0])),
|
||||||
|
arbitrary_data: [].to_vec(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wallet_output1() -> WalletOutput {
|
||||||
|
WalletOutput {
|
||||||
|
absolute_id: AbsoluteId {
|
||||||
|
transaction: hex::decode("b74773bbea995079805398052da9b69244bda034b089b50e4d9151dedb59a12f")
|
||||||
|
.unwrap()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
index_in_transaction: 1,
|
||||||
|
},
|
||||||
|
relative_id: RelativeId { index_on_blockchain: OUTPUT_INDEX_FOR_FIRST_RINGCT_OUTPUT + 1 },
|
||||||
|
data: OutputData {
|
||||||
|
key: CompressedEdwardsY(
|
||||||
|
hex::decode("9e2e5cd08c8681dbcf2ce66071467e835f7e86613fbfed3c4fb170127b94e107")
|
||||||
|
.unwrap()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.decompress()
|
||||||
|
.unwrap(),
|
||||||
|
key_offset: Scalar::from_canonical_bytes(
|
||||||
|
hex::decode("c5189738c1cb40e68d464f1a1848a85f6ab2c09652a31849213dc0fefd212806")
|
||||||
|
.unwrap()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
commitment: Commitment {
|
||||||
|
amount: 10000,
|
||||||
|
mask: Scalar::from_canonical_bytes(
|
||||||
|
hex::decode("c8922ce32cb2bf454a6b77bc91423ba7a18412b71fa39a97a2a743c1fe0bad04")
|
||||||
|
.unwrap()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metadata: Metadata {
|
||||||
|
additional_timelock: Timelock::None,
|
||||||
|
subaddress: None,
|
||||||
|
payment_id: Some(Encrypted([0, 0, 0, 0, 0, 0, 0, 0])),
|
||||||
|
arbitrary_data: [].to_vec(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_long_encrypted_amount() {
|
||||||
|
// Parse strings
|
||||||
|
let spend_key_buf = hex::decode(SPEND_KEY).unwrap();
|
||||||
|
let spend_key =
|
||||||
|
Zeroizing::new(Scalar::from_canonical_bytes(spend_key_buf.try_into().unwrap()).unwrap());
|
||||||
|
|
||||||
|
let view_key_buf = hex::decode(VIEW_KEY).unwrap();
|
||||||
|
let view_key =
|
||||||
|
Zeroizing::new(Scalar::from_canonical_bytes(view_key_buf.try_into().unwrap()).unwrap());
|
||||||
|
|
||||||
|
let tx_buf = hex::decode(PRUNED_TX_WITH_LONG_ENCRYPTED_AMOUNT).unwrap();
|
||||||
|
let tx = Transaction::<Pruned>::read::<&[u8]>(&mut tx_buf.as_ref()).unwrap();
|
||||||
|
|
||||||
|
let block_buf = hex::decode(BLOCK).unwrap();
|
||||||
|
let block = Block::read::<&[u8]>(&mut block_buf.as_ref()).unwrap();
|
||||||
|
|
||||||
|
// Confirm tx has long form encrypted amounts
|
||||||
|
match &tx {
|
||||||
|
Transaction::V2 { prefix: _, proofs } => {
|
||||||
|
let proofs = proofs.clone().unwrap();
|
||||||
|
assert_eq!(proofs.base.encrypted_amounts.len(), 2);
|
||||||
|
assert!(proofs
|
||||||
|
.base
|
||||||
|
.encrypted_amounts
|
||||||
|
.iter()
|
||||||
|
.all(|o| matches!(o, EncryptedAmount::Original { .. })));
|
||||||
|
}
|
||||||
|
_ => panic!("Unexpected tx version"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare scanner
|
||||||
|
let spend_pub = &*spend_key * ED25519_BASEPOINT_TABLE;
|
||||||
|
let view: ViewPair = ViewPair::new(spend_pub, view_key).unwrap();
|
||||||
|
let mut scanner = Scanner::new(view);
|
||||||
|
|
||||||
|
// Prepare scannable block
|
||||||
|
let txs: Vec<Transaction<Pruned>> = vec![tx];
|
||||||
|
let scannable_block = ScannableBlock {
|
||||||
|
block,
|
||||||
|
transactions: txs,
|
||||||
|
output_index_for_first_ringct_output: Some(OUTPUT_INDEX_FOR_FIRST_RINGCT_OUTPUT),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Scan the block
|
||||||
|
let outputs = scanner.scan(scannable_block).unwrap().not_additionally_locked();
|
||||||
|
|
||||||
|
assert_eq!(outputs.len(), 2);
|
||||||
|
assert_eq!(outputs[0], wallet_output0());
|
||||||
|
assert_eq!(outputs[1], wallet_output1());
|
||||||
|
}
|
|
@ -1,8 +1,12 @@
|
||||||
use monero_serai::transaction::Transaction;
|
use monero_serai::transaction::Transaction;
|
||||||
|
use monero_simple_request_rpc::SimpleRequestRpc;
|
||||||
use monero_wallet::{rpc::Rpc, extra::MAX_ARBITRARY_DATA_SIZE, send::SendError};
|
use monero_wallet::{rpc::Rpc, extra::MAX_ARBITRARY_DATA_SIZE, send::SendError};
|
||||||
|
|
||||||
mod runner;
|
mod runner;
|
||||||
|
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
type SRR = SimpleRequestRpc;
|
||||||
|
|
||||||
test!(
|
test!(
|
||||||
add_single_data_less_than_max,
|
add_single_data_less_than_max,
|
||||||
(
|
(
|
||||||
|
@ -15,9 +19,8 @@ test!(
|
||||||
builder.add_payment(addr, 5);
|
builder.add_payment(addr, 5);
|
||||||
(builder.build().unwrap(), (arbitrary_data,))
|
(builder.build().unwrap(), (arbitrary_data,))
|
||||||
},
|
},
|
||||||
|rpc, block, tx: Transaction, mut scanner: Scanner, data: (Vec<u8>,)| async move {
|
|_rpc: SRR, block, tx: Transaction, mut scanner: Scanner, data: (Vec<u8>,)| async move {
|
||||||
let output =
|
let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
|
||||||
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
|
|
||||||
assert_eq!(output.transaction(), tx.hash());
|
assert_eq!(output.transaction(), tx.hash());
|
||||||
assert_eq!(output.commitment().amount, 5);
|
assert_eq!(output.commitment().amount, 5);
|
||||||
assert_eq!(output.arbitrary_data()[0], data.0);
|
assert_eq!(output.arbitrary_data()[0], data.0);
|
||||||
|
@ -42,9 +45,8 @@ test!(
|
||||||
builder.add_payment(addr, 5);
|
builder.add_payment(addr, 5);
|
||||||
(builder.build().unwrap(), data)
|
(builder.build().unwrap(), data)
|
||||||
},
|
},
|
||||||
|rpc, block, tx: Transaction, mut scanner: Scanner, data: Vec<Vec<u8>>| async move {
|
|_rpc: SRR, block, tx: Transaction, mut scanner: Scanner, data: Vec<Vec<u8>>| async move {
|
||||||
let output =
|
let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
|
||||||
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
|
|
||||||
assert_eq!(output.transaction(), tx.hash());
|
assert_eq!(output.transaction(), tx.hash());
|
||||||
assert_eq!(output.commitment().amount, 5);
|
assert_eq!(output.commitment().amount, 5);
|
||||||
assert_eq!(output.arbitrary_data(), data);
|
assert_eq!(output.arbitrary_data(), data);
|
||||||
|
@ -70,9 +72,8 @@ test!(
|
||||||
builder.add_payment(addr, 5);
|
builder.add_payment(addr, 5);
|
||||||
(builder.build().unwrap(), data)
|
(builder.build().unwrap(), data)
|
||||||
},
|
},
|
||||||
|rpc, block, tx: Transaction, mut scanner: Scanner, data: Vec<u8>| async move {
|
|_rpc: SRR, block, tx: Transaction, mut scanner: Scanner, data: Vec<u8>| async move {
|
||||||
let output =
|
let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
|
||||||
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
|
|
||||||
assert_eq!(output.transaction(), tx.hash());
|
assert_eq!(output.transaction(), tx.hash());
|
||||||
assert_eq!(output.commitment().amount, 5);
|
assert_eq!(output.commitment().amount, 5);
|
||||||
assert_eq!(output.arbitrary_data(), vec![data]);
|
assert_eq!(output.arbitrary_data(), vec![data]);
|
||||||
|
|
|
@ -16,9 +16,8 @@ test!(
|
||||||
builder.add_payment(addr, 2000000000000);
|
builder.add_payment(addr, 2000000000000);
|
||||||
(builder.build().unwrap(), ())
|
(builder.build().unwrap(), ())
|
||||||
},
|
},
|
||||||
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
|
|_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
|
||||||
let output =
|
let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
|
||||||
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
|
|
||||||
assert_eq!(output.transaction(), tx.hash());
|
assert_eq!(output.transaction(), tx.hash());
|
||||||
assert_eq!(output.commitment().amount, 2000000000000);
|
assert_eq!(output.commitment().amount, 2000000000000);
|
||||||
output
|
output
|
||||||
|
@ -94,9 +93,8 @@ test!(
|
||||||
builder.add_payment(addr, 2000000000000);
|
builder.add_payment(addr, 2000000000000);
|
||||||
(builder.build().unwrap(), ())
|
(builder.build().unwrap(), ())
|
||||||
},
|
},
|
||||||
|rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
|
|_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
|
||||||
let output =
|
let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
|
||||||
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
|
|
||||||
assert_eq!(output.transaction(), tx.hash());
|
assert_eq!(output.transaction(), tx.hash());
|
||||||
assert_eq!(output.commitment().amount, 2000000000000);
|
assert_eq!(output.commitment().amount, 2000000000000);
|
||||||
output
|
output
|
||||||
|
|
|
@ -105,7 +105,11 @@ pub async fn get_miner_tx_output(rpc: &SimpleRequestRpc, view: &ViewPair) -> Wal
|
||||||
rpc.generate_blocks(&view.legacy_address(Network::Mainnet), 60).await.unwrap();
|
rpc.generate_blocks(&view.legacy_address(Network::Mainnet), 60).await.unwrap();
|
||||||
|
|
||||||
let block = rpc.get_block_by_number(start).await.unwrap();
|
let block = rpc.get_block_by_number(start).await.unwrap();
|
||||||
scanner.scan(rpc, &block).await.unwrap().ignore_additional_timelock().swap_remove(0)
|
scanner
|
||||||
|
.scan(rpc.get_scannable_block(block).await.unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.ignore_additional_timelock()
|
||||||
|
.swap_remove(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make sure the weight and fee match the expected calculation.
|
/// Make sure the weight and fee match the expected calculation.
|
||||||
|
@ -315,6 +319,7 @@ macro_rules! test {
|
||||||
rpc.publish_transaction(&signed).await.unwrap();
|
rpc.publish_transaction(&signed).await.unwrap();
|
||||||
let block =
|
let block =
|
||||||
mine_until_unlocked(&rpc, &random_address().2, signed.hash()).await;
|
mine_until_unlocked(&rpc, &random_address().2, signed.hash()).await;
|
||||||
|
let block = rpc.get_scannable_block(block).await.unwrap();
|
||||||
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
|
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
|
||||||
check_weight_and_fee(&tx, fee_rate);
|
check_weight_and_fee(&tx, fee_rate);
|
||||||
let scanner = Scanner::new(view.clone());
|
let scanner = Scanner::new(view.clone());
|
||||||
|
@ -336,6 +341,7 @@ macro_rules! test {
|
||||||
rpc.publish_transaction(&signed).await.unwrap();
|
rpc.publish_transaction(&signed).await.unwrap();
|
||||||
let block =
|
let block =
|
||||||
mine_until_unlocked(&rpc, &random_address().2, signed.hash()).await;
|
mine_until_unlocked(&rpc, &random_address().2, signed.hash()).await;
|
||||||
|
let block = rpc.get_scannable_block(block).await.unwrap();
|
||||||
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
|
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
|
||||||
if stringify!($name) != "spend_one_input_to_two_outputs_no_change" {
|
if stringify!($name) != "spend_one_input_to_two_outputs_no_change" {
|
||||||
// Skip weight and fee check for the above test because when there is no change,
|
// Skip weight and fee check for the above test because when there is no change,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue