mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-22 19:49:22 +00:00
Monero processor primitives
This commit is contained in:
parent
0d4c8cf032
commit
f2cf03cedf
16 changed files with 873 additions and 147 deletions
36
Cargo.lock
generated
36
Cargo.lock
generated
|
@ -8129,7 +8129,6 @@ dependencies = [
|
||||||
"borsh",
|
"borsh",
|
||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
"dkg",
|
"dkg",
|
||||||
"env_logger",
|
|
||||||
"flexible-transcript",
|
"flexible-transcript",
|
||||||
"hex",
|
"hex",
|
||||||
"log",
|
"log",
|
||||||
|
@ -8139,11 +8138,8 @@ dependencies = [
|
||||||
"secp256k1",
|
"secp256k1",
|
||||||
"serai-client",
|
"serai-client",
|
||||||
"serai-db",
|
"serai-db",
|
||||||
"serai-env",
|
|
||||||
"serai-message-queue",
|
|
||||||
"serai-processor-bin",
|
"serai-processor-bin",
|
||||||
"serai-processor-key-gen",
|
"serai-processor-key-gen",
|
||||||
"serai-processor-messages",
|
|
||||||
"serai-processor-primitives",
|
"serai-processor-primitives",
|
||||||
"serai-processor-scanner",
|
"serai-processor-scanner",
|
||||||
"serai-processor-scheduler-primitives",
|
"serai-processor-scheduler-primitives",
|
||||||
|
@ -8152,7 +8148,6 @@ dependencies = [
|
||||||
"serai-processor-utxo-scheduler-primitives",
|
"serai-processor-utxo-scheduler-primitives",
|
||||||
"tokio",
|
"tokio",
|
||||||
"zalloc",
|
"zalloc",
|
||||||
"zeroize",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -8170,7 +8165,7 @@ dependencies = [
|
||||||
"frost-schnorrkel",
|
"frost-schnorrkel",
|
||||||
"hex",
|
"hex",
|
||||||
"modular-frost",
|
"modular-frost",
|
||||||
"monero-wallet",
|
"monero-address",
|
||||||
"multiaddr",
|
"multiaddr",
|
||||||
"parity-scale-codec",
|
"parity-scale-codec",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
|
@ -8522,19 +8517,26 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"borsh",
|
"borsh",
|
||||||
"const-hex",
|
"ciphersuite",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
"env_logger",
|
"dkg",
|
||||||
|
"flexible-transcript",
|
||||||
"hex",
|
"hex",
|
||||||
"log",
|
"log",
|
||||||
"monero-simple-request-rpc",
|
"modular-frost",
|
||||||
"monero-wallet",
|
"monero-wallet",
|
||||||
"parity-scale-codec",
|
"parity-scale-codec",
|
||||||
|
"rand_core",
|
||||||
|
"serai-client",
|
||||||
"serai-db",
|
"serai-db",
|
||||||
"serai-env",
|
"serai-processor-bin",
|
||||||
"serai-message-queue",
|
"serai-processor-key-gen",
|
||||||
"serai-processor-messages",
|
"serai-processor-primitives",
|
||||||
"serde_json",
|
"serai-processor-scanner",
|
||||||
|
"serai-processor-scheduler-primitives",
|
||||||
|
"serai-processor-signers",
|
||||||
|
"serai-processor-utxo-scheduler",
|
||||||
|
"serai-processor-utxo-scheduler-primitives",
|
||||||
"tokio",
|
"tokio",
|
||||||
"zalloc",
|
"zalloc",
|
||||||
]
|
]
|
||||||
|
@ -8643,18 +8645,13 @@ name = "serai-processor-bin"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bitcoin-serai",
|
|
||||||
"borsh",
|
"borsh",
|
||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
"dkg",
|
"dkg",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"flexible-transcript",
|
|
||||||
"hex",
|
"hex",
|
||||||
"log",
|
"log",
|
||||||
"modular-frost",
|
|
||||||
"parity-scale-codec",
|
"parity-scale-codec",
|
||||||
"rand_core",
|
|
||||||
"secp256k1",
|
|
||||||
"serai-client",
|
"serai-client",
|
||||||
"serai-db",
|
"serai-db",
|
||||||
"serai-env",
|
"serai-env",
|
||||||
|
@ -8665,10 +8662,7 @@ dependencies = [
|
||||||
"serai-processor-scanner",
|
"serai-processor-scanner",
|
||||||
"serai-processor-scheduler-primitives",
|
"serai-processor-scheduler-primitives",
|
||||||
"serai-processor-signers",
|
"serai-processor-signers",
|
||||||
"serai-processor-transaction-chaining-scheduler",
|
|
||||||
"serai-processor-utxo-scheduler-primitives",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"zalloc",
|
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -19,29 +19,22 @@ workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { version = "0.1", default-features = false }
|
async-trait = { version = "0.1", default-features = false }
|
||||||
zeroize = { version = "1", default-features = false, features = ["std"] }
|
zeroize = { version = "1", default-features = false, features = ["std"] }
|
||||||
rand_core = { version = "0.6", default-features = false }
|
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
hex = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] }
|
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] }
|
||||||
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
|
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
|
||||||
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", default-features = false, features = ["std", "recommended"] }
|
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std"] }
|
||||||
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "secp256k1"] }
|
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-ristretto"] }
|
||||||
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-secp256k1"] }
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false }
|
|
||||||
|
|
||||||
secp256k1 = { version = "0.29", default-features = false, features = ["std", "global-context", "rand-std"] }
|
serai-client = { path = "../../substrate/client", default-features = false, features = ["bitcoin"] }
|
||||||
bitcoin-serai = { path = "../../networks/bitcoin", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
log = { version = "0.4", default-features = false, features = ["std"] }
|
log = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
env_logger = { version = "0.10", default-features = false, features = ["humantime"] }
|
env_logger = { version = "0.10", default-features = false, features = ["humantime"] }
|
||||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] }
|
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] }
|
||||||
|
|
||||||
zalloc = { path = "../../common/zalloc" }
|
|
||||||
serai-db = { path = "../../common/db" }
|
|
||||||
serai-env = { path = "../../common/env" }
|
serai-env = { path = "../../common/env" }
|
||||||
|
serai-db = { path = "../../common/db" }
|
||||||
serai-client = { path = "../../substrate/client", default-features = false, features = ["bitcoin"] }
|
|
||||||
|
|
||||||
messages = { package = "serai-processor-messages", path = "../messages" }
|
messages = { package = "serai-processor-messages", path = "../messages" }
|
||||||
key-gen = { package = "serai-processor-key-gen", path = "../key-gen" }
|
key-gen = { package = "serai-processor-key-gen", path = "../key-gen" }
|
||||||
|
@ -49,8 +42,6 @@ key-gen = { package = "serai-processor-key-gen", path = "../key-gen" }
|
||||||
primitives = { package = "serai-processor-primitives", path = "../primitives" }
|
primitives = { package = "serai-processor-primitives", path = "../primitives" }
|
||||||
scheduler = { package = "serai-processor-scheduler-primitives", path = "../scheduler/primitives" }
|
scheduler = { package = "serai-processor-scheduler-primitives", path = "../scheduler/primitives" }
|
||||||
scanner = { package = "serai-processor-scanner", path = "../scanner" }
|
scanner = { package = "serai-processor-scanner", path = "../scanner" }
|
||||||
utxo-scheduler = { package = "serai-processor-utxo-scheduler-primitives", path = "../scheduler/utxo/primitives" }
|
|
||||||
transaction-chaining-scheduler = { package = "serai-processor-transaction-chaining-scheduler", path = "../scheduler/utxo/transaction-chaining" }
|
|
||||||
signers = { package = "serai-processor-signers", path = "../signers" }
|
signers = { package = "serai-processor-signers", path = "../signers" }
|
||||||
|
|
||||||
message-queue = { package = "serai-message-queue", path = "../../message-queue" }
|
message-queue = { package = "serai-message-queue", path = "../../message-queue" }
|
||||||
|
|
|
@ -69,6 +69,7 @@ impl Coordinator {
|
||||||
"monero" => NetworkId::Monero,
|
"monero" => NetworkId::Monero,
|
||||||
_ => panic!("unrecognized network"),
|
_ => panic!("unrecognized network"),
|
||||||
};
|
};
|
||||||
|
// TODO: Read this from ScannerFeed
|
||||||
let service = Service::Processor(network_id);
|
let service = Service::Processor(network_id);
|
||||||
let message_queue = Arc::new(MessageQueue::from_env(service));
|
let message_queue = Arc::new(MessageQueue::from_env(service));
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { version = "0.1", default-features = false }
|
async-trait = { version = "0.1", default-features = false }
|
||||||
zeroize = { version = "1", default-features = false, features = ["std"] }
|
|
||||||
rand_core = { version = "0.6", default-features = false }
|
rand_core = { version = "0.6", default-features = false }
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
hex = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
|
@ -33,17 +32,14 @@ frost = { package = "modular-frost", path = "../../crypto/frost", default-featur
|
||||||
secp256k1 = { version = "0.29", default-features = false, features = ["std", "global-context", "rand-std"] }
|
secp256k1 = { version = "0.29", default-features = false, features = ["std", "global-context", "rand-std"] }
|
||||||
bitcoin-serai = { path = "../../networks/bitcoin", default-features = false, features = ["std"] }
|
bitcoin-serai = { path = "../../networks/bitcoin", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
log = { version = "0.4", default-features = false, features = ["std"] }
|
|
||||||
env_logger = { version = "0.10", default-features = false, features = ["humantime"] }
|
|
||||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] }
|
|
||||||
|
|
||||||
zalloc = { path = "../../common/zalloc" }
|
|
||||||
serai-db = { path = "../../common/db" }
|
|
||||||
serai-env = { path = "../../common/env" }
|
|
||||||
|
|
||||||
serai-client = { path = "../../substrate/client", default-features = false, features = ["bitcoin"] }
|
serai-client = { path = "../../substrate/client", default-features = false, features = ["bitcoin"] }
|
||||||
|
|
||||||
messages = { package = "serai-processor-messages", path = "../messages" }
|
zalloc = { path = "../../common/zalloc" }
|
||||||
|
log = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
|
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] }
|
||||||
|
|
||||||
|
serai-db = { path = "../../common/db" }
|
||||||
|
|
||||||
key-gen = { package = "serai-processor-key-gen", path = "../key-gen" }
|
key-gen = { package = "serai-processor-key-gen", path = "../key-gen" }
|
||||||
|
|
||||||
primitives = { package = "serai-processor-primitives", path = "../primitives" }
|
primitives = { package = "serai-processor-primitives", path = "../primitives" }
|
||||||
|
@ -55,8 +51,6 @@ signers = { package = "serai-processor-signers", path = "../signers" }
|
||||||
|
|
||||||
bin = { package = "serai-processor-bin", path = "../bin" }
|
bin = { package = "serai-processor-bin", path = "../bin" }
|
||||||
|
|
||||||
message-queue = { package = "serai-message-queue", path = "../../message-queue" }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
parity-db = ["bin/parity-db"]
|
parity-db = ["bin/parity-db"]
|
||||||
rocksdb = ["bin/rocksdb"]
|
rocksdb = ["bin/rocksdb"]
|
||||||
|
|
|
@ -18,29 +18,39 @@ workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { version = "0.1", default-features = false }
|
async-trait = { version = "0.1", default-features = false }
|
||||||
|
rand_core = { version = "0.6", default-features = false }
|
||||||
|
|
||||||
const-hex = { version = "1", default-features = false }
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
hex = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] }
|
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] }
|
||||||
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
|
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
|
||||||
serde_json = { version = "1", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false, features = ["std"], optional = true }
|
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", default-features = false, features = ["std", "recommended"] }
|
||||||
monero-simple-request-rpc = { path = "../../networks/monero/rpc/simple-request", default-features = false, optional = true }
|
dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false, features = ["std"] }
|
||||||
monero-wallet = { path = "../../networks/monero/wallet", default-features = false, features = ["std", "multisig", "compile-time-generators"], optional = true }
|
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "ed25519"] }
|
||||||
|
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-ed25519"] }
|
||||||
|
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false }
|
||||||
|
|
||||||
log = { version = "0.4", default-features = false, features = ["std"] }
|
monero-wallet = { path = "../../networks/monero/wallet", default-features = false, features = ["std", "multisig"] }
|
||||||
env_logger = { version = "0.10", default-features = false, features = ["humantime"] }
|
|
||||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] }
|
serai-client = { path = "../../substrate/client", default-features = false, features = ["monero"] }
|
||||||
|
|
||||||
zalloc = { path = "../../common/zalloc" }
|
zalloc = { path = "../../common/zalloc" }
|
||||||
|
log = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
|
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] }
|
||||||
|
|
||||||
serai-db = { path = "../../common/db" }
|
serai-db = { path = "../../common/db" }
|
||||||
serai-env = { path = "../../common/env" }
|
|
||||||
|
|
||||||
messages = { package = "serai-processor-messages", path = "../messages" }
|
key-gen = { package = "serai-processor-key-gen", path = "../key-gen" }
|
||||||
|
|
||||||
message-queue = { package = "serai-message-queue", path = "../../message-queue" }
|
primitives = { package = "serai-processor-primitives", path = "../primitives" }
|
||||||
|
scheduler = { package = "serai-processor-scheduler-primitives", path = "../scheduler/primitives" }
|
||||||
|
scanner = { package = "serai-processor-scanner", path = "../scanner" }
|
||||||
|
utxo-scheduler = { package = "serai-processor-utxo-scheduler-primitives", path = "../scheduler/utxo/primitives" }
|
||||||
|
utxo-standard-scheduler = { package = "serai-processor-utxo-scheduler", path = "../scheduler/utxo/standard" }
|
||||||
|
signers = { package = "serai-processor-signers", path = "../signers" }
|
||||||
|
|
||||||
|
bin = { package = "serai-processor-bin", path = "../bin" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
parity-db = ["serai-db/parity-db"]
|
parity-db = ["bin/parity-db"]
|
||||||
rocksdb = ["serai-db/rocksdb"]
|
rocksdb = ["bin/rocksdb"]
|
||||||
|
|
11
processor/monero/src/key_gen.rs
Normal file
11
processor/monero/src/key_gen.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ed25519};
|
||||||
|
use frost::ThresholdKeys;
|
||||||
|
|
||||||
|
pub(crate) struct KeyGenParams;
|
||||||
|
impl key_gen::KeyGenParams for KeyGenParams {
|
||||||
|
const ID: &'static str = "Monero";
|
||||||
|
|
||||||
|
type ExternalNetworkCiphersuite = Ed25519;
|
||||||
|
|
||||||
|
fn tweak_keys(keys: &mut ThresholdKeys<Self::ExternalNetworkCiphersuite>) {}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
/*
|
||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
@ -809,3 +810,4 @@ impl UtxoNetwork for Monero {
|
||||||
// TODO: Test creating a TX this big
|
// TODO: Test creating a TX this big
|
||||||
const MAX_INPUTS: usize = 120;
|
const MAX_INPUTS: usize = 120;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
43
processor/monero/src/main.rs
Normal file
43
processor/monero/src/main.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOCATOR: zalloc::ZeroizingAlloc<std::alloc::System> =
|
||||||
|
zalloc::ZeroizingAlloc(std::alloc::System);
|
||||||
|
|
||||||
|
use monero_wallet::rpc::Rpc as MRpc;
|
||||||
|
|
||||||
|
mod primitives;
|
||||||
|
pub(crate) use crate::primitives::*;
|
||||||
|
|
||||||
|
/*
|
||||||
|
mod key_gen;
|
||||||
|
use crate::key_gen::KeyGenParams;
|
||||||
|
mod rpc;
|
||||||
|
use rpc::Rpc;
|
||||||
|
mod scheduler;
|
||||||
|
use scheduler::Scheduler;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let db = bin::init();
|
||||||
|
let feed = Rpc {
|
||||||
|
db: db.clone(),
|
||||||
|
rpc: loop {
|
||||||
|
match MRpc::new(bin::url()).await {
|
||||||
|
Ok(rpc) => break rpc,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("couldn't connect to the Monero node: {e:?}");
|
||||||
|
tokio::time::sleep(core::time::Duration::from_secs(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
bin::main_loop::<_, KeyGenParams, Scheduler<_>, Rpc<bin::Db>>(db, feed.clone(), feed).await;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {}
|
54
processor/monero/src/primitives/block.rs
Normal file
54
processor/monero/src/primitives/block.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use ciphersuite::{Ciphersuite, Ed25519};
|
||||||
|
|
||||||
|
use monero_wallet::{transaction::Transaction, block::Block as MBlock};
|
||||||
|
|
||||||
|
use serai_client::networks::monero::Address;
|
||||||
|
|
||||||
|
use primitives::{ReceivedOutput, EventualityTracker};
|
||||||
|
|
||||||
|
use crate::{output::Output, transaction::Eventuality};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct BlockHeader(pub(crate) MBlock);
|
||||||
|
impl primitives::BlockHeader for BlockHeader {
|
||||||
|
fn id(&self) -> [u8; 32] {
|
||||||
|
self.0.hash()
|
||||||
|
}
|
||||||
|
fn parent(&self) -> [u8; 32] {
|
||||||
|
self.0.header.previous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Block(pub(crate) MBlock, Vec<Transaction>);
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl primitives::Block for Block {
|
||||||
|
type Header = BlockHeader;
|
||||||
|
|
||||||
|
type Key = <Ed25519 as Ciphersuite>::G;
|
||||||
|
type Address = Address;
|
||||||
|
type Output = Output;
|
||||||
|
type Eventuality = Eventuality;
|
||||||
|
|
||||||
|
fn id(&self) -> [u8; 32] {
|
||||||
|
self.0.hash()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_for_outputs_unordered(&self, key: Self::Key) -> Vec<Self::Output> {
|
||||||
|
todo!("TODO")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn check_for_eventuality_resolutions(
|
||||||
|
&self,
|
||||||
|
eventualities: &mut EventualityTracker<Self::Eventuality>,
|
||||||
|
) -> HashMap<
|
||||||
|
<Self::Output as ReceivedOutput<Self::Key, Self::Address>>::TransactionId,
|
||||||
|
Self::Eventuality,
|
||||||
|
> {
|
||||||
|
todo!("TODO")
|
||||||
|
}
|
||||||
|
}
|
3
processor/monero/src/primitives/mod.rs
Normal file
3
processor/monero/src/primitives/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub(crate) mod output;
|
||||||
|
pub(crate) mod transaction;
|
||||||
|
pub(crate) mod block;
|
86
processor/monero/src/primitives/output.rs
Normal file
86
processor/monero/src/primitives/output.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use ciphersuite::{group::Group, Ciphersuite, Ed25519};
|
||||||
|
|
||||||
|
use monero_wallet::WalletOutput;
|
||||||
|
|
||||||
|
use scale::{Encode, Decode};
|
||||||
|
use borsh::{BorshSerialize, BorshDeserialize};
|
||||||
|
|
||||||
|
use serai_client::{
|
||||||
|
primitives::{Coin, Amount, Balance},
|
||||||
|
networks::monero::Address,
|
||||||
|
};
|
||||||
|
|
||||||
|
use primitives::{OutputType, ReceivedOutput};
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, PartialEq, Eq, Default, Hash, Debug, Encode, Decode, BorshSerialize, BorshDeserialize,
|
||||||
|
)]
|
||||||
|
pub(crate) struct OutputId(pub(crate) [u8; 32]);
|
||||||
|
impl AsRef<[u8]> for OutputId {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl AsMut<[u8]> for OutputId {
|
||||||
|
fn as_mut(&mut self) -> &mut [u8] {
|
||||||
|
self.0.as_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub(crate) struct Output(WalletOutput);
|
||||||
|
|
||||||
|
impl Output {
|
||||||
|
pub(crate) fn new(output: WalletOutput) -> Self {
|
||||||
|
Self(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReceivedOutput<<Ed25519 as Ciphersuite>::G, Address> for Output {
|
||||||
|
type Id = OutputId;
|
||||||
|
type TransactionId = [u8; 32];
|
||||||
|
|
||||||
|
fn kind(&self) -> OutputType {
|
||||||
|
todo!("TODO")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Self::Id {
|
||||||
|
OutputId(self.0.key().compress().to_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_id(&self) -> Self::TransactionId {
|
||||||
|
self.0.transaction()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key(&self) -> <Ed25519 as Ciphersuite>::G {
|
||||||
|
// The spend key will be a key we generated, so it'll be in the prime-order subgroup
|
||||||
|
// The output's key is the spend key + (key_offset * G), so it's in the prime-order subgroup if
|
||||||
|
// the spend key is
|
||||||
|
dalek_ff_group::EdwardsPoint(
|
||||||
|
self.0.key() - (*<Ed25519 as Ciphersuite>::G::generator() * self.0.key_offset()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn presumed_origin(&self) -> Option<Address> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn balance(&self) -> Balance {
|
||||||
|
Balance { coin: Coin::Monero, amount: Amount(self.0.commitment().amount) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(&self) -> &[u8] {
|
||||||
|
self.0.arbitrary_data().first().map_or(&[], Vec::as_slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||||
|
self.0.write(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||||
|
WalletOutput::read(reader).map(Self)
|
||||||
|
}
|
||||||
|
}
|
137
processor/monero/src/primitives/transaction.rs
Normal file
137
processor/monero/src/primitives/transaction.rs
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use ciphersuite::Ed25519;
|
||||||
|
use frost::{dkg::ThresholdKeys, sign::PreprocessMachine};
|
||||||
|
|
||||||
|
use monero_wallet::{
|
||||||
|
transaction::Transaction as MTransaction,
|
||||||
|
send::{
|
||||||
|
SignableTransaction as MSignableTransaction, TransactionMachine, Eventuality as MEventuality,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::output::OutputId;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Transaction(pub(crate) MTransaction);
|
||||||
|
|
||||||
|
impl From<MTransaction> for Transaction {
|
||||||
|
fn from(tx: MTransaction) -> Self {
|
||||||
|
Self(tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl scheduler::Transaction for Transaction {
|
||||||
|
fn read(reader: &mut impl io::Read) -> io::Result<Self> {
|
||||||
|
MTransaction::read(reader).map(Self)
|
||||||
|
}
|
||||||
|
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
|
||||||
|
self.0.write(writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct SignableTransaction {
|
||||||
|
id: [u8; 32],
|
||||||
|
signable: MSignableTransaction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct ClonableTransctionMachine(MSignableTransaction, ThresholdKeys<Ed25519>);
|
||||||
|
impl PreprocessMachine for ClonableTransctionMachine {
|
||||||
|
type Preprocess = <TransactionMachine as PreprocessMachine>::Preprocess;
|
||||||
|
type Signature = <TransactionMachine as PreprocessMachine>::Signature;
|
||||||
|
type SignMachine = <TransactionMachine as PreprocessMachine>::SignMachine;
|
||||||
|
|
||||||
|
fn preprocess<R: RngCore + CryptoRng>(
|
||||||
|
self,
|
||||||
|
rng: &mut R,
|
||||||
|
) -> (Self::SignMachine, Self::Preprocess) {
|
||||||
|
self.0.multisig(self.1).expect("incorrect keys used for SignableTransaction").preprocess(rng)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl scheduler::SignableTransaction for SignableTransaction {
|
||||||
|
type Transaction = Transaction;
|
||||||
|
type Ciphersuite = Ed25519;
|
||||||
|
type PreprocessMachine = ClonableTransctionMachine;
|
||||||
|
|
||||||
|
fn read(reader: &mut impl io::Read) -> io::Result<Self> {
|
||||||
|
let mut id = [0; 32];
|
||||||
|
reader.read_exact(&mut id)?;
|
||||||
|
|
||||||
|
let signable = MSignableTransaction::read(reader)?;
|
||||||
|
Ok(SignableTransaction { id, signable })
|
||||||
|
}
|
||||||
|
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
|
||||||
|
writer.write_all(&self.id)?;
|
||||||
|
self.signable.write(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> [u8; 32] {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign(self, keys: ThresholdKeys<Self::Ciphersuite>) -> Self::PreprocessMachine {
|
||||||
|
ClonableTransctionMachine(self.signable, keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub(crate) struct Eventuality {
|
||||||
|
id: [u8; 32],
|
||||||
|
singular_spent_output: Option<OutputId>,
|
||||||
|
eventuality: MEventuality,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl primitives::Eventuality for Eventuality {
|
||||||
|
type OutputId = OutputId;
|
||||||
|
|
||||||
|
fn id(&self) -> [u8; 32] {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// We define the lookup as our ID since the resolving transaction only has a singular possible ID
|
||||||
|
fn lookup(&self) -> Vec<u8> {
|
||||||
|
self.eventuality.extra()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn singular_spent_output(&self) -> Option<Self::OutputId> {
|
||||||
|
self.singular_spent_output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(reader: &mut impl io::Read) -> io::Result<Self> {
|
||||||
|
let mut id = [0; 32];
|
||||||
|
reader.read_exact(&mut id)?;
|
||||||
|
|
||||||
|
let singular_spent_output = {
|
||||||
|
let mut singular_spent_output_opt = [0xff];
|
||||||
|
reader.read_exact(&mut singular_spent_output_opt)?;
|
||||||
|
assert!(singular_spent_output_opt[0] <= 1);
|
||||||
|
(singular_spent_output_opt[0] == 1)
|
||||||
|
.then(|| -> io::Result<_> {
|
||||||
|
let mut singular_spent_output = [0; 32];
|
||||||
|
reader.read_exact(&mut singular_spent_output)?;
|
||||||
|
Ok(OutputId(singular_spent_output))
|
||||||
|
})
|
||||||
|
.transpose()?
|
||||||
|
};
|
||||||
|
|
||||||
|
let eventuality = MEventuality::read(reader)?;
|
||||||
|
Ok(Self { id, singular_spent_output, eventuality })
|
||||||
|
}
|
||||||
|
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
|
||||||
|
writer.write_all(&self.id)?;
|
||||||
|
|
||||||
|
if let Some(singular_spent_output) = self.singular_spent_output {
|
||||||
|
writer.write_all(&[1])?;
|
||||||
|
writer.write_all(singular_spent_output.as_ref())?;
|
||||||
|
} else {
|
||||||
|
writer.write_all(&[0])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.eventuality.write(writer)
|
||||||
|
}
|
||||||
|
}
|
156
processor/monero/src/rpc.rs
Normal file
156
processor/monero/src/rpc.rs
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
use bitcoin_serai::rpc::{RpcError, Rpc as BRpc};
|
||||||
|
|
||||||
|
use serai_client::primitives::{NetworkId, Coin, Amount};
|
||||||
|
|
||||||
|
use serai_db::Db;
|
||||||
|
use scanner::ScannerFeed;
|
||||||
|
use signers::TransactionPublisher;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db,
|
||||||
|
transaction::Transaction,
|
||||||
|
block::{BlockHeader, Block},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct Rpc<D: Db> {
|
||||||
|
pub(crate) db: D,
|
||||||
|
pub(crate) rpc: BRpc,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<D: Db> ScannerFeed for Rpc<D> {
|
||||||
|
const NETWORK: NetworkId = NetworkId::Bitcoin;
|
||||||
|
const CONFIRMATIONS: u64 = 6;
|
||||||
|
const WINDOW_LENGTH: u64 = 6;
|
||||||
|
|
||||||
|
const TEN_MINUTES: u64 = 1;
|
||||||
|
|
||||||
|
type Block = Block<D>;
|
||||||
|
|
||||||
|
type EphemeralError = RpcError;
|
||||||
|
|
||||||
|
async fn latest_finalized_block_number(&self) -> Result<u64, Self::EphemeralError> {
|
||||||
|
db::LatestBlockToYieldAsFinalized::get(&self.db).ok_or(RpcError::ConnectionError)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn time_of_block(&self, number: u64) -> Result<u64, Self::EphemeralError> {
|
||||||
|
let number = usize::try_from(number).unwrap();
|
||||||
|
|
||||||
|
/*
|
||||||
|
The block time isn't guaranteed to be monotonic. It is guaranteed to be greater than the
|
||||||
|
median time of prior blocks, as detailed in BIP-0113 (a BIP which used that fact to improve
|
||||||
|
CLTV). This creates a monotonic median time which we use as the block time.
|
||||||
|
*/
|
||||||
|
// This implements `GetMedianTimePast`
|
||||||
|
let median = {
|
||||||
|
const MEDIAN_TIMESPAN: usize = 11;
|
||||||
|
let mut timestamps = Vec::with_capacity(MEDIAN_TIMESPAN);
|
||||||
|
for i in number.saturating_sub(MEDIAN_TIMESPAN) .. number {
|
||||||
|
timestamps.push(self.rpc.get_block(&self.rpc.get_block_hash(i).await?).await?.header.time);
|
||||||
|
}
|
||||||
|
timestamps.sort();
|
||||||
|
timestamps[timestamps.len() / 2]
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
This block's timestamp is guaranteed to be greater than this median:
|
||||||
|
https://github.com/bitcoin/bitcoin/blob/0725a374941355349bb4bc8a79dad1affb27d3b9
|
||||||
|
/src/validation.cpp#L4182-L4184
|
||||||
|
|
||||||
|
This does not guarantee the median always increases however. Take the following trivial
|
||||||
|
example, as the window is initially built:
|
||||||
|
|
||||||
|
0 block has time 0 // Prior blocks: []
|
||||||
|
1 block has time 1 // Prior blocks: [0]
|
||||||
|
2 block has time 2 // Prior blocks: [0, 1]
|
||||||
|
3 block has time 2 // Prior blocks: [0, 1, 2]
|
||||||
|
|
||||||
|
These two blocks have the same time (both greater than the median of their prior blocks) and
|
||||||
|
the same median.
|
||||||
|
|
||||||
|
The median will never decrease however. The values pushed onto the window will always be
|
||||||
|
greater than the median. If a value greater than the median is popped, the median will remain
|
||||||
|
the same (due to the counterbalance of the pushed value). If a value less than the median is
|
||||||
|
popped, the median will increase (either to another instance of the same value, yet one
|
||||||
|
closer to the end of the repeating sequence, or to a higher value).
|
||||||
|
*/
|
||||||
|
Ok(median.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unchecked_block_header_by_number(
|
||||||
|
&self,
|
||||||
|
number: u64,
|
||||||
|
) -> Result<<Self::Block as primitives::Block>::Header, Self::EphemeralError> {
|
||||||
|
Ok(BlockHeader(
|
||||||
|
self.rpc.get_block(&self.rpc.get_block_hash(number.try_into().unwrap()).await?).await?.header,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unchecked_block_by_number(
|
||||||
|
&self,
|
||||||
|
number: u64,
|
||||||
|
) -> Result<Self::Block, Self::EphemeralError> {
|
||||||
|
Ok(Block(
|
||||||
|
self.db.clone(),
|
||||||
|
self.rpc.get_block(&self.rpc.get_block_hash(number.try_into().unwrap()).await?).await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dust(coin: Coin) -> Amount {
|
||||||
|
assert_eq!(coin, Coin::Bitcoin);
|
||||||
|
|
||||||
|
/*
|
||||||
|
A Taproot input is:
|
||||||
|
- 36 bytes for the OutPoint
|
||||||
|
- 0 bytes for the script (+1 byte for the length)
|
||||||
|
- 4 bytes for the sequence
|
||||||
|
Per https://developer.bitcoin.org/reference/transactions.html#raw-transaction-format
|
||||||
|
|
||||||
|
There's also:
|
||||||
|
- 1 byte for the witness length
|
||||||
|
- 1 byte for the signature length
|
||||||
|
- 64 bytes for the signature
|
||||||
|
which have the SegWit discount.
|
||||||
|
|
||||||
|
(4 * (36 + 1 + 4)) + (1 + 1 + 64) = 164 + 66 = 230 weight units
|
||||||
|
230 ceil div 4 = 57 vbytes
|
||||||
|
|
||||||
|
Bitcoin defines multiple minimum feerate constants *per kilo-vbyte*. Currently, these are:
|
||||||
|
- 1000 sat/kilo-vbyte for a transaction to be relayed
|
||||||
|
- Each output's value must exceed the fee of the TX spending it at 3000 sat/kilo-vbyte
|
||||||
|
The DUST constant needs to be determined by the latter.
|
||||||
|
Since these are solely relay rules, and may be raised, we require all outputs be spendable
|
||||||
|
under a 5000 sat/kilo-vbyte fee rate.
|
||||||
|
|
||||||
|
5000 sat/kilo-vbyte = 5 sat/vbyte
|
||||||
|
5 * 57 = 285 sats/spent-output
|
||||||
|
|
||||||
|
Even if an output took 100 bytes (it should be just ~29-43), taking 400 weight units, adding
|
||||||
|
100 vbytes, tripling the transaction size, then the sats/tx would be < 1000.
|
||||||
|
|
||||||
|
Increase by an order of magnitude, in order to ensure this is actually worth our time, and we
|
||||||
|
get 10,000 satoshis. This is $5 if 1 BTC = 50,000 USD.
|
||||||
|
*/
|
||||||
|
Amount(10_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cost_to_aggregate(
|
||||||
|
&self,
|
||||||
|
coin: Coin,
|
||||||
|
_reference_block: &Self::Block,
|
||||||
|
) -> Result<Amount, Self::EphemeralError> {
|
||||||
|
assert_eq!(coin, Coin::Bitcoin);
|
||||||
|
// TODO
|
||||||
|
Ok(Amount(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<D: Db> TransactionPublisher<Transaction> for Rpc<D> {
|
||||||
|
type EphemeralError = RpcError;
|
||||||
|
|
||||||
|
async fn publish(&self, tx: Transaction) -> Result<(), Self::EphemeralError> {
|
||||||
|
self.rpc.send_raw_transaction(&tx.0).await.map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
205
processor/monero/src/scheduler.rs
Normal file
205
processor/monero/src/scheduler.rs
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
use ciphersuite::{Ciphersuite, Secp256k1};
|
||||||
|
|
||||||
|
use bitcoin_serai::{
|
||||||
|
bitcoin::ScriptBuf,
|
||||||
|
wallet::{TransactionError, SignableTransaction as BSignableTransaction, p2tr_script_buf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serai_client::{
|
||||||
|
primitives::{Coin, Amount},
|
||||||
|
networks::bitcoin::Address,
|
||||||
|
};
|
||||||
|
|
||||||
|
use serai_db::Db;
|
||||||
|
use primitives::{OutputType, ReceivedOutput, Payment};
|
||||||
|
use scanner::{KeyFor, AddressFor, OutputFor, BlockFor};
|
||||||
|
use utxo_scheduler::{PlannedTransaction, TransactionPlanner};
|
||||||
|
use transaction_chaining_scheduler::{EffectedReceivedOutputs, Scheduler as GenericScheduler};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
scan::{offsets_for_key, scanner},
|
||||||
|
output::Output,
|
||||||
|
transaction::{SignableTransaction, Eventuality},
|
||||||
|
rpc::Rpc,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn address_from_serai_key(key: <Secp256k1 as Ciphersuite>::G, kind: OutputType) -> Address {
|
||||||
|
let offset = <Secp256k1 as Ciphersuite>::G::GENERATOR * offsets_for_key(key)[&kind];
|
||||||
|
Address::new(
|
||||||
|
p2tr_script_buf(key + offset)
|
||||||
|
.expect("creating address from Serai key which wasn't properly tweaked"),
|
||||||
|
)
|
||||||
|
.expect("couldn't create Serai-representable address for P2TR script")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signable_transaction<D: Db>(
|
||||||
|
fee_per_vbyte: u64,
|
||||||
|
inputs: Vec<OutputFor<Rpc<D>>>,
|
||||||
|
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
|
||||||
|
change: Option<KeyFor<Rpc<D>>>,
|
||||||
|
) -> Result<(SignableTransaction, BSignableTransaction), TransactionError> {
|
||||||
|
assert!(
|
||||||
|
inputs.len() <
|
||||||
|
<Planner as TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>>>::MAX_INPUTS
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(payments.len() + usize::from(u8::from(change.is_some()))) <
|
||||||
|
<Planner as TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>>>::MAX_OUTPUTS
|
||||||
|
);
|
||||||
|
|
||||||
|
let inputs = inputs.into_iter().map(|input| input.output).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut payments = payments
|
||||||
|
.into_iter()
|
||||||
|
.map(|payment| {
|
||||||
|
(payment.address().clone(), {
|
||||||
|
let balance = payment.balance();
|
||||||
|
assert_eq!(balance.coin, Coin::Bitcoin);
|
||||||
|
balance.amount.0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
/*
|
||||||
|
Push a payment to a key with a known private key which anyone can spend. If this transaction
|
||||||
|
gets stuck, this lets anyone create a child transaction spending this output, raising the fee,
|
||||||
|
getting the transaction unstuck (via CPFP).
|
||||||
|
*/
|
||||||
|
payments.push((
|
||||||
|
// The generator is even so this is valid
|
||||||
|
Address::new(p2tr_script_buf(<Secp256k1 as Ciphersuite>::G::GENERATOR).unwrap()).unwrap(),
|
||||||
|
// This uses the minimum output value allowed, as defined as a constant in bitcoin-serai
|
||||||
|
// TODO: Add a test for this comparing to bitcoin's `minimal_non_dust`
|
||||||
|
bitcoin_serai::wallet::DUST,
|
||||||
|
));
|
||||||
|
|
||||||
|
let change = change
|
||||||
|
.map(<Planner as TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>>>::change_address);
|
||||||
|
|
||||||
|
BSignableTransaction::new(
|
||||||
|
inputs.clone(),
|
||||||
|
&payments
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|(address, amount)| (ScriptBuf::from(address), amount))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
change.clone().map(ScriptBuf::from),
|
||||||
|
None,
|
||||||
|
fee_per_vbyte,
|
||||||
|
)
|
||||||
|
.map(|bst| (SignableTransaction { inputs, payments, change, fee_per_vbyte }, bst))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Planner;
|
||||||
|
impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Planner {
|
||||||
|
type FeeRate = u64;
|
||||||
|
|
||||||
|
type SignableTransaction = SignableTransaction;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Bitcoin has a max weight of 400,000 (MAX_STANDARD_TX_WEIGHT).
|
||||||
|
|
||||||
|
A non-SegWit TX will have 4 weight units per byte, leaving a max size of 100,000 bytes. While
|
||||||
|
our inputs are entirely SegWit, such fine tuning is not necessary and could create issues in
|
||||||
|
the future (if the size decreases or we misevaluate it). It also offers a minimal amount of
|
||||||
|
benefit when we are able to logarithmically accumulate inputs/fulfill payments.
|
||||||
|
|
||||||
|
For 128-byte inputs (36-byte output specification, 64-byte signature, whatever overhead) and
|
||||||
|
64-byte outputs (40-byte script, 8-byte amount, whatever overhead), they together take up 192
|
||||||
|
bytes.
|
||||||
|
|
||||||
|
100,000 / 192 = 520
|
||||||
|
520 * 192 leaves 160 bytes of overhead for the transaction structure itself.
|
||||||
|
*/
|
||||||
|
const MAX_INPUTS: usize = 520;
|
||||||
|
// We always reserve one output to create an anyone-can-spend output enabling anyone to use CPFP
|
||||||
|
// to unstick any transactions which had too low of a fee.
|
||||||
|
const MAX_OUTPUTS: usize = 519;
|
||||||
|
|
||||||
|
fn fee_rate(block: &BlockFor<Rpc<D>>, coin: Coin) -> Self::FeeRate {
|
||||||
|
assert_eq!(coin, Coin::Bitcoin);
|
||||||
|
// TODO
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn branch_address(key: KeyFor<Rpc<D>>) -> AddressFor<Rpc<D>> {
|
||||||
|
address_from_serai_key(key, OutputType::Branch)
|
||||||
|
}
|
||||||
|
fn change_address(key: KeyFor<Rpc<D>>) -> AddressFor<Rpc<D>> {
|
||||||
|
address_from_serai_key(key, OutputType::Change)
|
||||||
|
}
|
||||||
|
fn forwarding_address(key: KeyFor<Rpc<D>>) -> AddressFor<Rpc<D>> {
|
||||||
|
address_from_serai_key(key, OutputType::Forwarded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_fee(
|
||||||
|
fee_rate: Self::FeeRate,
|
||||||
|
inputs: Vec<OutputFor<Rpc<D>>>,
|
||||||
|
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
|
||||||
|
change: Option<KeyFor<Rpc<D>>>,
|
||||||
|
) -> Amount {
|
||||||
|
match signable_transaction::<D>(fee_rate, inputs, payments, change) {
|
||||||
|
Ok(tx) => Amount(tx.1.needed_fee()),
|
||||||
|
Err(
|
||||||
|
TransactionError::NoInputs | TransactionError::NoOutputs | TransactionError::DustPayment,
|
||||||
|
) => panic!("malformed arguments to calculate_fee"),
|
||||||
|
// No data, we have a minimum fee rate, we checked the amount of inputs/outputs
|
||||||
|
Err(
|
||||||
|
TransactionError::TooMuchData |
|
||||||
|
TransactionError::TooLowFee |
|
||||||
|
TransactionError::TooLargeTransaction,
|
||||||
|
) => unreachable!(),
|
||||||
|
Err(TransactionError::NotEnoughFunds { fee, .. }) => Amount(fee),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plan(
|
||||||
|
fee_rate: Self::FeeRate,
|
||||||
|
inputs: Vec<OutputFor<Rpc<D>>>,
|
||||||
|
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
|
||||||
|
change: Option<KeyFor<Rpc<D>>>,
|
||||||
|
) -> PlannedTransaction<Rpc<D>, Self::SignableTransaction, EffectedReceivedOutputs<Rpc<D>>> {
|
||||||
|
let key = inputs.first().unwrap().key();
|
||||||
|
for input in &inputs {
|
||||||
|
assert_eq!(key, input.key());
|
||||||
|
}
|
||||||
|
|
||||||
|
let singular_spent_output = (inputs.len() == 1).then(|| inputs[0].id());
|
||||||
|
match signable_transaction::<D>(fee_rate, inputs.clone(), payments, change) {
|
||||||
|
Ok(tx) => PlannedTransaction {
|
||||||
|
signable: tx.0,
|
||||||
|
eventuality: Eventuality { txid: tx.1.txid(), singular_spent_output },
|
||||||
|
auxilliary: EffectedReceivedOutputs({
|
||||||
|
let tx = tx.1.transaction();
|
||||||
|
let scanner = scanner(key);
|
||||||
|
|
||||||
|
let mut res = vec![];
|
||||||
|
for output in scanner.scan_transaction(tx) {
|
||||||
|
res.push(Output::new_with_presumed_origin(
|
||||||
|
key,
|
||||||
|
tx,
|
||||||
|
// It shouldn't matter if this is wrong as we should never try to return these
|
||||||
|
// We still provide an accurate value to ensure a lack of discrepancies
|
||||||
|
Some(Address::new(inputs[0].output.output().script_pubkey.clone()).unwrap()),
|
||||||
|
output,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
Err(
|
||||||
|
TransactionError::NoInputs | TransactionError::NoOutputs | TransactionError::DustPayment,
|
||||||
|
) => panic!("malformed arguments to plan"),
|
||||||
|
// No data, we have a minimum fee rate, we checked the amount of inputs/outputs
|
||||||
|
Err(
|
||||||
|
TransactionError::TooMuchData |
|
||||||
|
TransactionError::TooLowFee |
|
||||||
|
TransactionError::TooLargeTransaction,
|
||||||
|
) => unreachable!(),
|
||||||
|
Err(TransactionError::NotEnoughFunds { .. }) => {
|
||||||
|
panic!("plan called for a transaction without enough funds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type Scheduler<D> = GenericScheduler<Rpc<D>, Planner>;
|
|
@ -42,7 +42,7 @@ simple-request = { path = "../../common/request", version = "0.1", optional = tr
|
||||||
bitcoin = { version = "0.32", optional = true }
|
bitcoin = { version = "0.32", optional = true }
|
||||||
|
|
||||||
ciphersuite = { path = "../../crypto/ciphersuite", version = "0.4", optional = true }
|
ciphersuite = { path = "../../crypto/ciphersuite", version = "0.4", optional = true }
|
||||||
monero-wallet = { path = "../../networks/monero/wallet", version = "0.1.0", default-features = false, features = ["std"], optional = true }
|
monero-address = { path = "../../networks/monero/wallet/address", version = "0.1.0", default-features = false, features = ["std"], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand_core = "0.6"
|
rand_core = "0.6"
|
||||||
|
@ -65,7 +65,7 @@ borsh = ["serai-abi/borsh"]
|
||||||
|
|
||||||
networks = []
|
networks = []
|
||||||
bitcoin = ["networks", "dep:bitcoin"]
|
bitcoin = ["networks", "dep:bitcoin"]
|
||||||
monero = ["networks", "ciphersuite/ed25519", "monero-wallet"]
|
monero = ["networks", "ciphersuite/ed25519", "monero-address"]
|
||||||
|
|
||||||
# Assumes the default usage is to use Serai as a DEX, which doesn't actually
|
# Assumes the default usage is to use Serai as a DEX, which doesn't actually
|
||||||
# require connecting to a Serai node
|
# require connecting to a Serai node
|
||||||
|
|
|
@ -1,102 +1,141 @@
|
||||||
use core::{str::FromStr, fmt};
|
use core::{str::FromStr, fmt};
|
||||||
|
|
||||||
use scale::{Encode, Decode};
|
|
||||||
|
|
||||||
use ciphersuite::{Ciphersuite, Ed25519};
|
use ciphersuite::{Ciphersuite, Ed25519};
|
||||||
|
|
||||||
use monero_wallet::address::{AddressError, Network, AddressType, MoneroAddress};
|
use monero_address::{Network, AddressType as MoneroAddressType, MoneroAddress};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
use crate::primitives::ExternalAddress;
|
||||||
pub struct Address(MoneroAddress);
|
|
||||||
impl Address {
|
|
||||||
pub fn new(address: MoneroAddress) -> Option<Address> {
|
|
||||||
if address.payment_id().is_some() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(Address(address))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Address {
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
type Err = AddressError;
|
enum AddressType {
|
||||||
fn from_str(str: &str) -> Result<Address, AddressError> {
|
|
||||||
MoneroAddress::from_str(Network::Mainnet, str).map(Address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Address {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SCALE-encoded variant of Monero addresses.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
|
|
||||||
enum EncodedAddressType {
|
|
||||||
Legacy,
|
Legacy,
|
||||||
Subaddress,
|
Subaddress,
|
||||||
Featured(u8),
|
Featured(u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
|
/// A representation of a Monero address.
|
||||||
struct EncodedAddress {
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
kind: EncodedAddressType,
|
pub struct Address {
|
||||||
spend: [u8; 32],
|
kind: AddressType,
|
||||||
view: [u8; 32],
|
spend: <Ed25519 as Ciphersuite>::G,
|
||||||
|
view: <Ed25519 as Ciphersuite>::G,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<Vec<u8>> for Address {
|
fn byte_for_kind(kind: AddressType) -> u8 {
|
||||||
type Error = ();
|
// We use the second and third highest bits for the type
|
||||||
fn try_from(data: Vec<u8>) -> Result<Address, ()> {
|
// This leaves the top bit open for interpretation as a VarInt later
|
||||||
// Decode as SCALE
|
match kind {
|
||||||
let addr = EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())?;
|
AddressType::Legacy => 0,
|
||||||
// Convert over
|
AddressType::Subaddress => 1 << 5,
|
||||||
Ok(Address(MoneroAddress::new(
|
AddressType::Featured(flags) => {
|
||||||
Network::Mainnet,
|
// The flags only take up the low three bits
|
||||||
match addr.kind {
|
debug_assert!(flags <= 0b111);
|
||||||
EncodedAddressType::Legacy => AddressType::Legacy,
|
(2 << 5) | flags
|
||||||
EncodedAddressType::Subaddress => AddressType::Subaddress,
|
|
||||||
EncodedAddressType::Featured(flags) => {
|
|
||||||
let subaddress = (flags & 1) != 0;
|
|
||||||
let integrated = (flags & (1 << 1)) != 0;
|
|
||||||
let guaranteed = (flags & (1 << 2)) != 0;
|
|
||||||
if integrated {
|
|
||||||
Err(())?;
|
|
||||||
}
|
|
||||||
AddressType::Featured { subaddress, payment_id: None, guaranteed }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ed25519::read_G::<&[u8]>(&mut addr.spend.as_ref()).map_err(|_| ())?.0,
|
|
||||||
Ed25519::read_G::<&[u8]>(&mut addr.view.as_ref()).map_err(|_| ())?.0,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::from_over_into)]
|
|
||||||
impl Into<MoneroAddress> for Address {
|
|
||||||
fn into(self) -> MoneroAddress {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::from_over_into)]
|
|
||||||
impl Into<Vec<u8>> for Address {
|
|
||||||
fn into(self) -> Vec<u8> {
|
|
||||||
EncodedAddress {
|
|
||||||
kind: match self.0.kind() {
|
|
||||||
AddressType::Legacy => EncodedAddressType::Legacy,
|
|
||||||
AddressType::LegacyIntegrated(_) => {
|
|
||||||
panic!("integrated address became Serai Monero address")
|
|
||||||
}
|
|
||||||
AddressType::Subaddress => EncodedAddressType::Subaddress,
|
|
||||||
AddressType::Featured { subaddress, payment_id, guaranteed } => {
|
|
||||||
debug_assert!(payment_id.is_none());
|
|
||||||
EncodedAddressType::Featured(u8::from(*subaddress) + (u8::from(*guaranteed) << 2))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
spend: self.0.spend().compress().0,
|
|
||||||
view: self.0.view().compress().0,
|
|
||||||
}
|
}
|
||||||
.encode()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl borsh::BorshSerialize for Address {
|
||||||
|
fn serialize<W: borsh::io::Write>(&self, writer: &mut W) -> borsh::io::Result<()> {
|
||||||
|
writer.write_all(&[byte_for_kind(self.kind)])?;
|
||||||
|
writer.write_all(&self.spend.compress().to_bytes())?;
|
||||||
|
writer.write_all(&self.view.compress().to_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl borsh::BorshDeserialize for Address {
|
||||||
|
fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
|
||||||
|
let mut kind_byte = [0xff];
|
||||||
|
reader.read_exact(&mut kind_byte)?;
|
||||||
|
let kind_byte = kind_byte[0];
|
||||||
|
let kind = match kind_byte >> 5 {
|
||||||
|
0 => AddressType::Legacy,
|
||||||
|
1 => AddressType::Subaddress,
|
||||||
|
2 => AddressType::Featured(kind_byte & 0b111),
|
||||||
|
_ => Err(borsh::io::Error::other("unrecognized type"))?,
|
||||||
|
};
|
||||||
|
// Check this wasn't malleated
|
||||||
|
if byte_for_kind(kind) != kind_byte {
|
||||||
|
Err(borsh::io::Error::other("malleated type byte"))?;
|
||||||
|
}
|
||||||
|
let spend = Ed25519::read_G(reader)?;
|
||||||
|
let view = Ed25519::read_G(reader)?;
|
||||||
|
Ok(Self { kind, spend, view })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<MoneroAddress> for Address {
|
||||||
|
type Error = ();
|
||||||
|
fn try_from(address: MoneroAddress) -> Result<Self, ()> {
|
||||||
|
let spend = address.spend().compress().to_bytes();
|
||||||
|
let view = address.view().compress().to_bytes();
|
||||||
|
let kind = match address.kind() {
|
||||||
|
MoneroAddressType::Legacy => AddressType::Legacy,
|
||||||
|
MoneroAddressType::LegacyIntegrated(_) => Err(())?,
|
||||||
|
MoneroAddressType::Subaddress => AddressType::Subaddress,
|
||||||
|
MoneroAddressType::Featured { subaddress, payment_id, guaranteed } => {
|
||||||
|
if payment_id.is_some() {
|
||||||
|
Err(())?
|
||||||
|
}
|
||||||
|
// This maintains the same bit layout as featured addresses use
|
||||||
|
AddressType::Featured(u8::from(*subaddress) + (u8::from(*guaranteed) << 2))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Address {
|
||||||
|
kind,
|
||||||
|
spend: Ed25519::read_G(&mut spend.as_slice()).map_err(|_| ())?,
|
||||||
|
view: Ed25519::read_G(&mut view.as_slice()).map_err(|_| ())?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Address> for MoneroAddress {
|
||||||
|
fn from(address: Address) -> MoneroAddress {
|
||||||
|
let kind = match address.kind {
|
||||||
|
AddressType::Legacy => MoneroAddressType::Legacy,
|
||||||
|
AddressType::Subaddress => MoneroAddressType::Subaddress,
|
||||||
|
AddressType::Featured(features) => {
|
||||||
|
debug_assert!(features <= 0b111);
|
||||||
|
let subaddress = (features & 1) != 0;
|
||||||
|
let integrated = (features & (1 << 1)) != 0;
|
||||||
|
debug_assert!(!integrated);
|
||||||
|
let guaranteed = (features & (1 << 2)) != 0;
|
||||||
|
MoneroAddressType::Featured { subaddress, payment_id: None, guaranteed }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MoneroAddress::new(Network::Mainnet, kind, address.spend.0, address.view.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<ExternalAddress> for Address {
|
||||||
|
type Error = ();
|
||||||
|
fn try_from(data: ExternalAddress) -> Result<Address, ()> {
|
||||||
|
// Decode as an Address
|
||||||
|
let mut data = data.as_ref();
|
||||||
|
let address =
|
||||||
|
<Address as borsh::BorshDeserialize>::deserialize_reader(&mut data).map_err(|_| ())?;
|
||||||
|
if !data.is_empty() {
|
||||||
|
Err(())?
|
||||||
|
}
|
||||||
|
Ok(address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Address> for ExternalAddress {
|
||||||
|
fn from(address: Address) -> ExternalAddress {
|
||||||
|
// This is 65 bytes which is less than MAX_ADDRESS_LEN
|
||||||
|
ExternalAddress::new(borsh::to_vec(&address).unwrap()).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Address {
|
||||||
|
type Err = ();
|
||||||
|
fn from_str(str: &str) -> Result<Address, ()> {
|
||||||
|
let Ok(address) = MoneroAddress::from_str(Network::Mainnet, str) else { Err(())? };
|
||||||
|
Address::try_from(address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Address {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
MoneroAddress::from(*self).fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue