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",
|
||||
"ciphersuite",
|
||||
"dkg",
|
||||
"env_logger",
|
||||
"flexible-transcript",
|
||||
"hex",
|
||||
"log",
|
||||
|
@ -8139,11 +8138,8 @@ dependencies = [
|
|||
"secp256k1",
|
||||
"serai-client",
|
||||
"serai-db",
|
||||
"serai-env",
|
||||
"serai-message-queue",
|
||||
"serai-processor-bin",
|
||||
"serai-processor-key-gen",
|
||||
"serai-processor-messages",
|
||||
"serai-processor-primitives",
|
||||
"serai-processor-scanner",
|
||||
"serai-processor-scheduler-primitives",
|
||||
|
@ -8152,7 +8148,6 @@ dependencies = [
|
|||
"serai-processor-utxo-scheduler-primitives",
|
||||
"tokio",
|
||||
"zalloc",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -8170,7 +8165,7 @@ dependencies = [
|
|||
"frost-schnorrkel",
|
||||
"hex",
|
||||
"modular-frost",
|
||||
"monero-wallet",
|
||||
"monero-address",
|
||||
"multiaddr",
|
||||
"parity-scale-codec",
|
||||
"rand_core",
|
||||
|
@ -8522,19 +8517,26 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"async-trait",
|
||||
"borsh",
|
||||
"const-hex",
|
||||
"ciphersuite",
|
||||
"dalek-ff-group",
|
||||
"env_logger",
|
||||
"dkg",
|
||||
"flexible-transcript",
|
||||
"hex",
|
||||
"log",
|
||||
"monero-simple-request-rpc",
|
||||
"modular-frost",
|
||||
"monero-wallet",
|
||||
"parity-scale-codec",
|
||||
"rand_core",
|
||||
"serai-client",
|
||||
"serai-db",
|
||||
"serai-env",
|
||||
"serai-message-queue",
|
||||
"serai-processor-messages",
|
||||
"serde_json",
|
||||
"serai-processor-bin",
|
||||
"serai-processor-key-gen",
|
||||
"serai-processor-primitives",
|
||||
"serai-processor-scanner",
|
||||
"serai-processor-scheduler-primitives",
|
||||
"serai-processor-signers",
|
||||
"serai-processor-utxo-scheduler",
|
||||
"serai-processor-utxo-scheduler-primitives",
|
||||
"tokio",
|
||||
"zalloc",
|
||||
]
|
||||
|
@ -8643,18 +8645,13 @@ name = "serai-processor-bin"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bitcoin-serai",
|
||||
"borsh",
|
||||
"ciphersuite",
|
||||
"dkg",
|
||||
"env_logger",
|
||||
"flexible-transcript",
|
||||
"hex",
|
||||
"log",
|
||||
"modular-frost",
|
||||
"parity-scale-codec",
|
||||
"rand_core",
|
||||
"secp256k1",
|
||||
"serai-client",
|
||||
"serai-db",
|
||||
"serai-env",
|
||||
|
@ -8665,10 +8662,7 @@ dependencies = [
|
|||
"serai-processor-scanner",
|
||||
"serai-processor-scheduler-primitives",
|
||||
"serai-processor-signers",
|
||||
"serai-processor-transaction-chaining-scheduler",
|
||||
"serai-processor-utxo-scheduler-primitives",
|
||||
"tokio",
|
||||
"zalloc",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
|
|
|
@ -19,29 +19,22 @@ workspace = true
|
|||
[dependencies]
|
||||
async-trait = { version = "0.1", default-features = false }
|
||||
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"] }
|
||||
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] }
|
||||
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", "secp256k1"] }
|
||||
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-secp256k1"] }
|
||||
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false }
|
||||
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std"] }
|
||||
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-ristretto"] }
|
||||
|
||||
secp256k1 = { version = "0.29", default-features = false, features = ["std", "global-context", "rand-std"] }
|
||||
bitcoin-serai = { path = "../../networks/bitcoin", default-features = false, features = ["std"] }
|
||||
serai-client = { path = "../../substrate/client", default-features = false, features = ["bitcoin"] }
|
||||
|
||||
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-db = { path = "../../common/db" }
|
||||
|
||||
messages = { package = "serai-processor-messages", path = "../messages" }
|
||||
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" }
|
||||
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" }
|
||||
transaction-chaining-scheduler = { package = "serai-processor-transaction-chaining-scheduler", path = "../scheduler/utxo/transaction-chaining" }
|
||||
signers = { package = "serai-processor-signers", path = "../signers" }
|
||||
|
||||
message-queue = { package = "serai-message-queue", path = "../../message-queue" }
|
||||
|
|
|
@ -69,6 +69,7 @@ impl Coordinator {
|
|||
"monero" => NetworkId::Monero,
|
||||
_ => panic!("unrecognized network"),
|
||||
};
|
||||
// TODO: Read this from ScannerFeed
|
||||
let service = Service::Processor(network_id);
|
||||
let message_queue = Arc::new(MessageQueue::from_env(service));
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
async-trait = { version = "0.1", default-features = false }
|
||||
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"] }
|
||||
|
@ -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"] }
|
||||
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"] }
|
||||
|
||||
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" }
|
||||
|
||||
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" }
|
||||
|
||||
message-queue = { package = "serai-message-queue", path = "../../message-queue" }
|
||||
|
||||
[features]
|
||||
parity-db = ["bin/parity-db"]
|
||||
rocksdb = ["bin/rocksdb"]
|
||||
|
|
|
@ -18,29 +18,39 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
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"] }
|
||||
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] }
|
||||
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 }
|
||||
monero-simple-request-rpc = { path = "../../networks/monero/rpc/simple-request", default-features = false, optional = true }
|
||||
monero-wallet = { path = "../../networks/monero/wallet", default-features = false, features = ["std", "multisig", "compile-time-generators"], optional = true }
|
||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", default-features = false, features = ["std", "recommended"] }
|
||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false, features = ["std"] }
|
||||
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"] }
|
||||
env_logger = { version = "0.10", default-features = false, features = ["humantime"] }
|
||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] }
|
||||
monero-wallet = { path = "../../networks/monero/wallet", default-features = false, features = ["std", "multisig"] }
|
||||
|
||||
serai-client = { path = "../../substrate/client", default-features = false, features = ["monero"] }
|
||||
|
||||
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-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]
|
||||
parity-db = ["serai-db/parity-db"]
|
||||
rocksdb = ["serai-db/rocksdb"]
|
||||
parity-db = ["bin/parity-db"]
|
||||
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))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
|
@ -809,3 +810,4 @@ impl UtxoNetwork for Monero {
|
|||
// TODO: Test creating a TX this big
|
||||
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 }
|
||||
|
||||
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]
|
||||
rand_core = "0.6"
|
||||
|
@ -65,7 +65,7 @@ borsh = ["serai-abi/borsh"]
|
|||
|
||||
networks = []
|
||||
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
|
||||
# require connecting to a Serai node
|
||||
|
|
|
@ -1,102 +1,141 @@
|
|||
use core::{str::FromStr, fmt};
|
||||
|
||||
use scale::{Encode, Decode};
|
||||
|
||||
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)]
|
||||
pub struct Address(MoneroAddress);
|
||||
impl Address {
|
||||
pub fn new(address: MoneroAddress) -> Option<Address> {
|
||||
if address.payment_id().is_some() {
|
||||
return None;
|
||||
}
|
||||
Some(Address(address))
|
||||
}
|
||||
}
|
||||
use crate::primitives::ExternalAddress;
|
||||
|
||||
impl FromStr for Address {
|
||||
type Err = AddressError;
|
||||
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 {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum AddressType {
|
||||
Legacy,
|
||||
Subaddress,
|
||||
Featured(u8),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
|
||||
struct EncodedAddress {
|
||||
kind: EncodedAddressType,
|
||||
spend: [u8; 32],
|
||||
view: [u8; 32],
|
||||
/// A representation of a Monero address.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct Address {
|
||||
kind: AddressType,
|
||||
spend: <Ed25519 as Ciphersuite>::G,
|
||||
view: <Ed25519 as Ciphersuite>::G,
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for Address {
|
||||
type Error = ();
|
||||
fn try_from(data: Vec<u8>) -> Result<Address, ()> {
|
||||
// Decode as SCALE
|
||||
let addr = EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())?;
|
||||
// Convert over
|
||||
Ok(Address(MoneroAddress::new(
|
||||
Network::Mainnet,
|
||||
match addr.kind {
|
||||
EncodedAddressType::Legacy => AddressType::Legacy,
|
||||
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,
|
||||
fn byte_for_kind(kind: AddressType) -> u8 {
|
||||
// We use the second and third highest bits for the type
|
||||
// This leaves the top bit open for interpretation as a VarInt later
|
||||
match kind {
|
||||
AddressType::Legacy => 0,
|
||||
AddressType::Subaddress => 1 << 5,
|
||||
AddressType::Featured(flags) => {
|
||||
// The flags only take up the low three bits
|
||||
debug_assert!(flags <= 0b111);
|
||||
(2 << 5) | flags
|
||||
}
|
||||
.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