serai/tests/processor/src/networks.rs

412 lines
13 KiB
Rust

use std::collections::HashSet;
use zeroize::Zeroizing;
use rand_core::{RngCore, OsRng};
use scale::Encode;
use serai_client::{
primitives::{Amount, NetworkId, Coin, Balance, ExternalAddress},
validator_sets::primitives::ExternalKey,
in_instructions::primitives::{InInstruction, RefundableInInstruction, Shorthand},
};
use dockertest::{PullPolicy, Image, StartPolicy, Composition, DockerOperations};
use crate::*;
pub const RPC_USER: &str = "serai";
pub const RPC_PASS: &str = "seraidex";
pub const BTC_PORT: u32 = 8332;
pub const XMR_PORT: u32 = 18081;
pub fn bitcoin_instance() -> (Composition, u32) {
serai_docker_tests::build("bitcoin".to_string());
let mut composition = Composition::with_image(
Image::with_repository("serai-dev-bitcoin").pull_policy(PullPolicy::Never),
)
.with_cmd(vec![
"bitcoind".to_string(),
"-txindex".to_string(),
"-regtest".to_string(),
format!("-rpcuser={RPC_USER}"),
format!("-rpcpassword={RPC_PASS}"),
"-rpcbind=0.0.0.0".to_string(),
"-rpcallowip=0.0.0.0/0".to_string(),
"-rpcport=8332".to_string(),
]);
composition.publish_all_ports();
(composition, BTC_PORT)
}
pub fn monero_instance() -> (Composition, u32) {
serai_docker_tests::build("monero".to_string());
let mut composition = Composition::with_image(
Image::with_repository("serai-dev-monero").pull_policy(PullPolicy::Never),
)
.with_cmd(vec![
"monerod".to_string(),
"--regtest".to_string(),
"--offline".to_string(),
"--fixed-difficulty=1".to_string(),
"--rpc-bind-ip=0.0.0.0".to_string(),
format!("--rpc-login={RPC_USER}:{RPC_PASS}"),
"--rpc-access-control-origins=*".to_string(),
"--confirm-external-bind".to_string(),
"--non-interactive".to_string(),
])
.with_start_policy(StartPolicy::Strict);
composition.publish_all_ports();
(composition, XMR_PORT)
}
pub fn network_instance(network: NetworkId) -> (Composition, u32) {
match network {
NetworkId::Bitcoin => bitcoin_instance(),
NetworkId::Ethereum => todo!(),
NetworkId::Monero => monero_instance(),
NetworkId::Serai => {
panic!("Serai is not a valid network to spawn an instance of for a processor")
}
}
}
pub fn network_rpc(network: NetworkId, ops: &DockerOperations, handle: &str) -> String {
let (ip, port) = ops
.handle(handle)
.host_port(match network {
NetworkId::Bitcoin => BTC_PORT,
NetworkId::Ethereum => todo!(),
NetworkId::Monero => XMR_PORT,
NetworkId::Serai => panic!("getting port for external network yet it was Serai"),
})
.unwrap();
format!("http://{RPC_USER}:{RPC_PASS}@{ip}:{port}")
}
pub fn confirmations(network: NetworkId) -> usize {
use processor::networks::*;
match network {
NetworkId::Bitcoin => Bitcoin::CONFIRMATIONS,
NetworkId::Ethereum => todo!(),
NetworkId::Monero => Monero::CONFIRMATIONS,
NetworkId::Serai => panic!("getting confirmations required for Serai"),
}
}
#[derive(Clone)]
pub enum Wallet {
Bitcoin {
private_key: bitcoin_serai::bitcoin::PrivateKey,
public_key: bitcoin_serai::bitcoin::PublicKey,
input_tx: bitcoin_serai::bitcoin::Transaction,
},
Monero {
handle: String,
spend_key: Zeroizing<curve25519_dalek::scalar::Scalar>,
view_pair: monero_serai::wallet::ViewPair,
inputs: Vec<monero_serai::wallet::ReceivedOutput>,
},
}
// TODO: Merge these functions with the processor's tests, which offers very similar functionality
impl Wallet {
pub async fn new(network: NetworkId, ops: &DockerOperations, handle: String) -> Wallet {
let rpc_url = network_rpc(network, ops, &handle);
match network {
NetworkId::Bitcoin => {
use bitcoin_serai::{
bitcoin::{
secp256k1::{SECP256K1, SecretKey},
PrivateKey, PublicKey, ScriptBuf, Network, Address,
},
rpc::Rpc,
};
let secret_key = SecretKey::new(&mut rand_core::OsRng);
let private_key = PrivateKey::new(secret_key, Network::Regtest);
let public_key = PublicKey::from_private_key(SECP256K1, &private_key);
let main_addr = Address::p2pkh(&public_key, Network::Regtest);
let rpc = Rpc::new(rpc_url).await.expect("couldn't connect to the Bitcoin RPC");
let new_block = rpc.get_latest_block_number().await.unwrap() + 1;
rpc
.rpc_call::<Vec<String>>("generatetoaddress", serde_json::json!([1, main_addr]))
.await
.unwrap();
// Mine it to maturity
rpc
.rpc_call::<Vec<String>>(
"generatetoaddress",
serde_json::json!([100, Address::p2sh(&ScriptBuf::new(), Network::Regtest).unwrap()]),
)
.await
.unwrap();
let funds = rpc
.get_block(&rpc.get_block_hash(new_block).await.unwrap())
.await
.unwrap()
.txdata
.swap_remove(0);
Wallet::Bitcoin { private_key, public_key, input_tx: funds }
}
NetworkId::Ethereum => todo!(),
NetworkId::Monero => {
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar};
use monero_serai::{
wallet::{
ViewPair, Scanner,
address::{Network, AddressSpec},
},
rpc::HttpRpc,
};
let mut bytes = [0; 64];
OsRng.fill_bytes(&mut bytes);
let spend_key = Scalar::from_bytes_mod_order_wide(&bytes);
OsRng.fill_bytes(&mut bytes);
let view_key = Scalar::from_bytes_mod_order_wide(&bytes);
let view_pair =
ViewPair::new(ED25519_BASEPOINT_POINT * spend_key, Zeroizing::new(view_key));
let rpc = HttpRpc::new(rpc_url).expect("couldn't connect to the Monero RPC");
let height = rpc.get_height().await.unwrap();
// Mines 200 blocks so sufficient decoys exist, as only 60 is needed for maturity
let _: EmptyResponse = rpc
.json_rpc_call(
"generateblocks",
Some(serde_json::json!({
"wallet_address": view_pair.address(
Network::Mainnet,
AddressSpec::Standard
).to_string(),
"amount_of_blocks": 200,
})),
)
.await
.unwrap();
let block = rpc.get_block(rpc.get_block_hash(height).await.unwrap()).await.unwrap();
let output = Scanner::from_view(view_pair.clone(), Some(HashSet::new()))
.scan(&rpc, &block)
.await
.unwrap()
.remove(0)
.ignore_timelock()
.remove(0);
Wallet::Monero {
handle,
spend_key: Zeroizing::new(spend_key),
view_pair,
inputs: vec![output.output.clone()],
}
}
NetworkId::Serai => panic!("creating a wallet for for Serai"),
}
}
pub async fn send_to_address(
&mut self,
ops: &DockerOperations,
to: &ExternalKey,
instruction: Option<InInstruction>,
) -> (Vec<u8>, Balance) {
match self {
Wallet::Bitcoin { private_key, public_key, ref mut input_tx } => {
use bitcoin_serai::bitcoin::{
secp256k1::{SECP256K1, Message},
key::{XOnlyPublicKey, TweakedPublicKey},
consensus::Encodable,
sighash::{EcdsaSighashType, SighashCache},
script::{PushBytesBuf, Script, ScriptBuf, Builder},
address::Payload,
OutPoint, Sequence, Witness, TxIn, TxOut,
absolute::LockTime,
Transaction,
};
const AMOUNT: u64 = 100000000;
let mut tx = Transaction {
version: 2,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint { txid: input_tx.txid(), vout: 0 },
script_sig: Script::empty().into(),
sequence: Sequence(u32::MAX),
witness: Witness::default(),
}],
output: vec![
TxOut {
value: input_tx.output[0].value - AMOUNT - 10000,
script_pubkey: input_tx.output[0].script_pubkey.clone(),
},
TxOut {
value: AMOUNT,
script_pubkey: Payload::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(
XOnlyPublicKey::from_slice(&to[1 ..]).unwrap(),
))
.script_pubkey(),
},
],
};
if let Some(instruction) = instruction {
tx.output.push(TxOut {
value: 0,
script_pubkey: ScriptBuf::new_op_return(
&PushBytesBuf::try_from(
Shorthand::Raw(RefundableInInstruction { origin: None, instruction }).encode(),
)
.unwrap(),
),
});
}
let mut der = SECP256K1
.sign_ecdsa_low_r(
&Message::from(
SighashCache::new(&tx)
.legacy_signature_hash(
0,
&input_tx.output[0].script_pubkey,
EcdsaSighashType::All.to_u32(),
)
.unwrap()
.to_raw_hash(),
),
&private_key.inner,
)
.serialize_der()
.to_vec();
der.push(1);
tx.input[0].script_sig = Builder::new()
.push_slice(PushBytesBuf::try_from(der).unwrap())
.push_key(public_key)
.into_script();
let mut buf = vec![];
tx.consensus_encode(&mut buf).unwrap();
*input_tx = tx;
(buf, Balance { coin: Coin::Bitcoin, amount: Amount(AMOUNT) })
}
Wallet::Monero { handle, ref spend_key, ref view_pair, ref mut inputs } => {
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, edwards::CompressedEdwardsY};
use monero_serai::{
Protocol,
wallet::{
address::{Network, AddressType, AddressMeta, Address},
SpendableOutput, Decoys, Change, FeePriority, Scanner, SignableTransaction,
},
rpc::HttpRpc,
};
use processor::{additional_key, networks::Monero};
let rpc_url = network_rpc(NetworkId::Monero, ops, handle);
let rpc = HttpRpc::new(rpc_url).expect("couldn't connect to the Monero RPC");
// Prepare inputs
let outputs = std::mem::take(inputs);
let mut these_inputs = vec![];
for output in outputs {
these_inputs.push(
SpendableOutput::from(&rpc, output)
.await
.expect("prior transaction was never published"),
);
}
let mut decoys = Decoys::select(
&mut OsRng,
&rpc,
Protocol::v16.ring_len(),
rpc.get_height().await.unwrap() - 1,
&these_inputs,
)
.await
.unwrap();
let to_spend_key =
CompressedEdwardsY(<[u8; 32]>::try_from(to.as_ref()).unwrap()).decompress().unwrap();
let to_view_key = additional_key::<Monero>(0);
let to_addr = Address::new(
AddressMeta::new(
Network::Mainnet,
AddressType::Featured { subaddress: false, payment_id: None, guaranteed: true },
),
to_spend_key,
ED25519_BASEPOINT_POINT * to_view_key.0,
);
// Create and sign the TX
const AMOUNT: u64 = 1_000_000_000_000;
let mut data = vec![];
if let Some(instruction) = instruction {
data.push(Shorthand::Raw(RefundableInInstruction { origin: None, instruction }).encode());
}
let tx = SignableTransaction::new(
Protocol::v16,
None,
these_inputs.drain(..).zip(decoys.drain(..)).collect(),
vec![(to_addr, AMOUNT)],
Some(Change::new(view_pair, false)),
data,
rpc.get_fee(Protocol::v16, FeePriority::Low).await.unwrap(),
)
.unwrap()
.sign(&mut OsRng, spend_key)
.unwrap();
// Push the change output
inputs.push(
Scanner::from_view(view_pair.clone(), Some(HashSet::new()))
.scan_transaction(&tx)
.ignore_timelock()
.remove(0),
);
(tx.serialize(), Balance { coin: Coin::Monero, amount: Amount(AMOUNT) })
}
}
}
pub fn address(&self) -> ExternalAddress {
use serai_client::networks;
match self {
Wallet::Bitcoin { public_key, .. } => {
use bitcoin_serai::bitcoin::{Network, Address};
ExternalAddress::new(
networks::bitcoin::Address(Address::p2pkh(public_key, Network::Regtest))
.try_into()
.unwrap(),
)
.unwrap()
}
Wallet::Monero { view_pair, .. } => {
use monero_serai::wallet::address::{Network, AddressSpec};
ExternalAddress::new(
networks::monero::Address::new(
view_pair.address(Network::Mainnet, AddressSpec::Standard),
)
.unwrap()
.try_into()
.unwrap(),
)
.unwrap()
}
}
}
}