Daniel Karzel
3 years ago
8 changed files with 616 additions and 234 deletions
@ -0,0 +1,29 @@ |
|||
use bdk::bitcoin::util::psbt::{Global, PartiallySignedTransaction}; |
|||
use bdk::bitcoin::{Transaction, Txid}; |
|||
use std::collections::BTreeMap; |
|||
|
|||
pub fn dummy_partially_signed_transaction() -> PartiallySignedTransaction { |
|||
// very simple dummy psbt that does not contain anything
|
|||
// pulled in from github.com-1ecc6299db9ec823/bitcoin-0.27.1/src/util/psbt/mod.rs:238
|
|||
|
|||
PartiallySignedTransaction { |
|||
global: Global { |
|||
unsigned_tx: Transaction { |
|||
version: 2, |
|||
lock_time: 0, |
|||
input: vec![], |
|||
output: vec![], |
|||
}, |
|||
xpub: Default::default(), |
|||
version: 0, |
|||
proprietary: BTreeMap::new(), |
|||
unknown: BTreeMap::new(), |
|||
}, |
|||
inputs: vec![], |
|||
outputs: vec![], |
|||
} |
|||
} |
|||
|
|||
pub fn dummy_tx_id() -> Txid { |
|||
dummy_partially_signed_transaction().extract_tx().txid() |
|||
} |
@ -0,0 +1,143 @@ |
|||
use anyhow::Result; |
|||
use bdk::bitcoin; |
|||
use bdk::bitcoin::util::bip32::ExtendedPrivKey; |
|||
use bdk::bitcoin::{Amount, Network}; |
|||
use cfd_protocol::secp256k1_zkp::rand::{CryptoRng, RngCore}; |
|||
use cfd_protocol::secp256k1_zkp::{schnorrsig, SecretKey}; |
|||
use cfd_protocol::Announcement; |
|||
use std::str::FromStr; |
|||
|
|||
pub fn dummy_wallet( |
|||
rng: &mut (impl RngCore + CryptoRng), |
|||
utxo_amount: Amount, |
|||
num_utxos: u8, |
|||
) -> Result<bdk::Wallet<(), bdk::database::MemoryDatabase>> { |
|||
use bdk::{populate_test_db, testutils}; |
|||
|
|||
let mut seed = [0u8; 32]; |
|||
rng.fill_bytes(&mut seed); |
|||
|
|||
let key = ExtendedPrivKey::new_master(Network::Regtest, &seed)?; |
|||
let descriptors = testutils!(@descriptors (&format!("wpkh({}/*)", key))); |
|||
|
|||
let mut database = bdk::database::MemoryDatabase::new(); |
|||
|
|||
for index in 0..num_utxos { |
|||
populate_test_db!( |
|||
&mut database, |
|||
testutils! { |
|||
@tx ( (@external descriptors, index as u32) => utxo_amount.as_sat() ) (@confirmations 1) |
|||
}, |
|||
Some(100) |
|||
); |
|||
} |
|||
|
|||
let wallet = bdk::Wallet::new_offline(&descriptors.0, None, Network::Regtest, database)?; |
|||
|
|||
Ok(wallet) |
|||
} |
|||
|
|||
#[allow(dead_code)] |
|||
pub struct OliviaData { |
|||
id: String, |
|||
pk: schnorrsig::PublicKey, |
|||
nonce_pks: Vec<schnorrsig::PublicKey>, |
|||
price: u64, |
|||
attestations: Vec<SecretKey>, |
|||
} |
|||
|
|||
impl OliviaData { |
|||
// Note: protocol tests have example 1 as well, but so far we are not asserting on that level of
|
|||
// granularity; example 1 can be pulled in once needed
|
|||
pub fn example_0() -> Self { |
|||
Self::example( |
|||
Self::EVENT_ID_0, |
|||
Self::PRICE_0, |
|||
&Self::NONCE_PKS_0, |
|||
&Self::ATTESTATIONS_0, |
|||
) |
|||
} |
|||
|
|||
/// Generate an example of all the data from `olivia` needed to test the
|
|||
/// CFD protocol end-to-end.
|
|||
fn example(id: &str, price: u64, nonce_pks: &[&str], attestations: &[&str]) -> Self { |
|||
let oracle_pk = schnorrsig::PublicKey::from_str(Self::OLIVIA_PK).unwrap(); |
|||
|
|||
let id = id.to_string(); |
|||
|
|||
let nonce_pks = nonce_pks |
|||
.iter() |
|||
.map(|pk| schnorrsig::PublicKey::from_str(pk).unwrap()) |
|||
.collect(); |
|||
|
|||
let attestations = attestations |
|||
.iter() |
|||
.map(|pk| SecretKey::from_str(pk).unwrap()) |
|||
.collect(); |
|||
|
|||
Self { |
|||
id, |
|||
pk: oracle_pk, |
|||
nonce_pks, |
|||
attestations, |
|||
price, |
|||
} |
|||
} |
|||
|
|||
pub fn announcement(&self) -> Announcement { |
|||
Announcement { |
|||
id: self.id.clone(), |
|||
nonce_pks: self.nonce_pks.clone(), |
|||
} |
|||
} |
|||
|
|||
const OLIVIA_PK: &'static str = |
|||
"ddd4636845a90185991826be5a494cde9f4a6947b1727217afedc6292fa4caf7"; |
|||
|
|||
const EVENT_ID_0: &'static str = "/x/BitMEX/BXBT/2021-10-05T02:00:00.price?n=20"; |
|||
const NONCE_PKS_0: [&'static str; 20] = [ |
|||
"d02d163cf9623f567c4e3faf851a9266ac1ede13da4ca4141f3a7717fba9a739", |
|||
"bc310f26aa5addbc382f653d8530aaead7c25e3546abc24639f490e36d4bdb88", |
|||
"2661375f570dcc32300d442e85b6d72dfa3232dccda45e8fb4a2d1e758d1d374", |
|||
"fcc68fbf071d391b14c0867cb4defb5a8abc12418dff3dfc2f84fd4025cb2716", |
|||
"cf5c2b7fe3851c64a7ff9635a9bfc50cdd301401d002f2da049f4c6a20e8457b", |
|||
"14f1005d8c2832a2c4666dd732dd9bb3af9c8f70ebcdaec96869b1ca0c8e0de6", |
|||
"299ee1c9c20fab8b067adf452a7d7661b5e7f5dd6bc707562805002e7cb8443e", |
|||
"bcb4e5a594346de298993a7a31762f598b5224b977e23182369e9ed3e5127f78", |
|||
"25e09a16ee5d469069abfb62cd5e1f20af50cf15241f571e64fa28b127304574", |
|||
"3ed5a1422f43299caf281123aba88bd4bc61ec863f4afb79c7ce7663ad44db5d", |
|||
"a7e0f61212735c192c4bf16e0a3e925e65f9f3feb6f1e5e8d6f5c18cf2dbb5a8", |
|||
"a36a631015d9036d0c321fea7cf12f589aa196e7279b4a290de5112c2940e540", |
|||
"b5bdd931f81970139e7301ac654b378077c3ed993ca7893ed93fee5fc6f7a782", |
|||
"00090816e256b41e042dce38bde99ab3cf9482f9b066836988d3ed54833638e8", |
|||
"3530408e93c251f5f488d3b1c608157177c459d6fab1966abebf765bcc9338d2", |
|||
"603269ce88d112ff7fcfcaab82f228be97deca37f8190084d509c71b51a30432", |
|||
"f0587414fcc6c56aef11d4a1d287ad6b55b237c5b8a5d5d93eb9ca06f6466ccf", |
|||
"763009afb0ffd99c7b835488cb3b0302f3b78f59bbfd5292bedab8ef9da8c1b7", |
|||
"3867af9048309a05004a164bdea09899f23ff1d83b6491b2b53a1b7b92e0eb2e", |
|||
"688118e6b59e27944c277513db2711a520f4283c7c53a11f58d9f6a46d82c964", |
|||
]; |
|||
const PRICE_0: u64 = 49262; |
|||
const ATTESTATIONS_0: [&'static str; 20] = [ |
|||
"5bc7663195971daaa1e3e6a81b4bca65882791644bc446fc060cbc118a3ace0f", |
|||
"721d0cb56a0778a1ca7907f81a0787f34385b13f854c845c4c5539f7f6267958", |
|||
"044aeef0d525c8ff48758c80939e95807bc640990cc03f53ab6fc0b262045221", |
|||
"79f5175423ec6ee69c8d0e55251db85f3015c2edfa5a03095443fbbf35eb2282", |
|||
"233b9ec549e9cc7c702109d29636db85a3ec63a66f3b53444bcc7586d36ca439", |
|||
"2961a00320b7c9a70220060019a6ca88e18c205fadd2f873c174e5ccbbed527e", |
|||
"bdb76e8f81c39ade4205ead9b68118757fc49ec22769605f26ef904b235283d6", |
|||
"6e75dafedf4ed685513ec1f5c93508de4fad2be05b46001ac00c03474f4690e1", |
|||
"cfcfc27eb9273b343b3042f0386e77efe329066be079788bb00ab47d72f26780", |
|||
"2d931ffd2963e74566365674583abc427bdb6ae571c4887d81f1920f0850665d", |
|||
"33b6f1112fa046cbc04be44c615e70519702662c1f72d8d49b3c4613614a8a46", |
|||
"19e569b15410fa9a758c1a6c211eae8c1547efbe0ac6a7709902be93415f2f09", |
|||
"d859dd5c9a58e1836d1eea3ebe7f48198a681d29e5a5cd6922532d2e94a53a1d", |
|||
"3387eb2ad5e64cd102167766bb72b447f4a2e5129d161e422f9d41cd7d1cc281", |
|||
"db35a9778a1e3abc8d8ab2f4a79346ae2154c9e0b4932d859d1f3e244f67ae76", |
|||
"c3be969e8b889cfb2ece71123e6be5538a2d3a1229637b18bccc179073c38059", |
|||
"6f73263f430e10b82d0fd06c4ddd3b8a6b58c3e756745bd0d9e71a399e517921", |
|||
"0818c9c245d7d2162cd393c562a121f80405a27d22ae465e95030c31ebb4bd24", |
|||
"b7c03f0bd6d63bd78ad4ea0f3452ff9717ba65ca42038e6e90a1aa558b7942dc", |
|||
"90c4d8ec9f408ccb62a62daa993c20f2f86799e1fdea520c6d060418e55fd216", |
|||
]; |
|||
} |
@ -0,0 +1,3 @@ |
|||
pub mod monitor; |
|||
pub mod oracle; |
|||
pub mod wallet; |
@ -0,0 +1,29 @@ |
|||
use daemon::{monitor, oracle}; |
|||
use xtra_productivity::xtra_productivity; |
|||
|
|||
/// Test Stub simulating the Monitor actor
|
|||
pub struct Monitor; |
|||
impl xtra::Actor for Monitor {} |
|||
|
|||
#[xtra_productivity(message_impl = false)] |
|||
impl Monitor { |
|||
async fn handle(&mut self, _msg: monitor::Sync) {} |
|||
|
|||
async fn handle(&mut self, _msg: monitor::StartMonitoring) { |
|||
todo!("stub this if needed") |
|||
} |
|||
|
|||
async fn handle(&mut self, _msg: monitor::CollaborativeSettlement) { |
|||
todo!("stub this if needed") |
|||
} |
|||
|
|||
async fn handle(&mut self, _msg: oracle::Attestation) { |
|||
todo!("stub this if needed") |
|||
} |
|||
} |
|||
|
|||
impl Default for Monitor { |
|||
fn default() -> Self { |
|||
Monitor |
|||
} |
|||
} |
@ -0,0 +1,45 @@ |
|||
use daemon::model::BitMexPriceEventId; |
|||
use daemon::oracle; |
|||
use time::OffsetDateTime; |
|||
use xtra_productivity::xtra_productivity; |
|||
|
|||
pub struct Oracle { |
|||
announcement: Option<oracle::Announcement>, |
|||
} |
|||
|
|||
impl Oracle { |
|||
pub fn with_dummy_announcement( |
|||
mut self, |
|||
dummy_announcement: cfd_protocol::Announcement, |
|||
) -> Self { |
|||
self.announcement = Some(oracle::Announcement { |
|||
id: BitMexPriceEventId::new(OffsetDateTime::UNIX_EPOCH, 0), |
|||
expected_outcome_time: OffsetDateTime::now_utc(), |
|||
nonce_pks: dummy_announcement.nonce_pks, |
|||
}); |
|||
|
|||
self |
|||
} |
|||
} |
|||
|
|||
impl xtra::Actor for Oracle {} |
|||
|
|||
#[xtra_productivity(message_impl = false)] |
|||
impl Oracle { |
|||
async fn handle_get_announcement( |
|||
&mut self, |
|||
_msg: oracle::GetAnnouncement, |
|||
) -> Option<oracle::Announcement> { |
|||
self.announcement.clone() |
|||
} |
|||
|
|||
async fn handle(&mut self, _msg: oracle::MonitorAttestation) {} |
|||
|
|||
async fn handle(&mut self, _msg: oracle::Sync) {} |
|||
} |
|||
|
|||
impl Default for Oracle { |
|||
fn default() -> Self { |
|||
Oracle { announcement: None } |
|||
} |
|||
} |
@ -0,0 +1,93 @@ |
|||
use crate::harness::cfd_protocol::dummy_wallet; |
|||
use anyhow::{anyhow, Result}; |
|||
use bdk::bitcoin::util::psbt::PartiallySignedTransaction; |
|||
use bdk::bitcoin::{ecdsa, Amount, Txid}; |
|||
use cfd_protocol::secp256k1_zkp::Secp256k1; |
|||
use cfd_protocol::{PartyParams, WalletExt}; |
|||
use daemon::model::{Timestamp, WalletInfo}; |
|||
use daemon::wallet; |
|||
use rand::thread_rng; |
|||
use xtra_productivity::xtra_productivity; |
|||
|
|||
pub struct Wallet { |
|||
party_params: bool, |
|||
wallet_info: Option<WalletInfo>, |
|||
psbt: Option<PartiallySignedTransaction>, |
|||
txid: Option<Txid>, |
|||
} |
|||
|
|||
impl Wallet { |
|||
pub fn with_party_params(mut self) -> Self { |
|||
self.party_params = true; |
|||
self |
|||
} |
|||
|
|||
pub fn with_wallet_info(mut self) -> Self { |
|||
let s = Secp256k1::new(); |
|||
let public_key = ecdsa::PublicKey::new(s.generate_keypair(&mut thread_rng()).1); |
|||
let address = bdk::bitcoin::Address::p2pkh(&public_key, bdk::bitcoin::Network::Testnet); |
|||
|
|||
self.wallet_info = Some(WalletInfo { |
|||
balance: bdk::bitcoin::Amount::ONE_BTC, |
|||
address, |
|||
last_updated_at: Timestamp::now().unwrap(), |
|||
}); |
|||
|
|||
self |
|||
} |
|||
|
|||
pub fn with_psbt(mut self, psbt: PartiallySignedTransaction) -> Self { |
|||
self.psbt = Some(psbt); |
|||
self |
|||
} |
|||
|
|||
pub fn with_txid(mut self, txid: Txid) -> Self { |
|||
self.txid = Some(txid); |
|||
self |
|||
} |
|||
} |
|||
|
|||
impl xtra::Actor for Wallet {} |
|||
|
|||
#[xtra_productivity(message_impl = false)] |
|||
impl Wallet { |
|||
async fn handle(&mut self, msg: wallet::BuildPartyParams) -> Result<PartyParams> { |
|||
if self.party_params { |
|||
let mut rng = thread_rng(); |
|||
let wallet = dummy_wallet(&mut rng, Amount::from_btc(0.4).unwrap(), 5).unwrap(); |
|||
|
|||
let party_params = wallet |
|||
.build_party_params(msg.amount, msg.identity_pk) |
|||
.unwrap(); |
|||
|
|||
return Ok(party_params); |
|||
} |
|||
|
|||
panic!("Stub not configured to return party params") |
|||
} |
|||
async fn handle(&mut self, _msg: wallet::Sync) -> Result<WalletInfo> { |
|||
self.wallet_info |
|||
.clone() |
|||
.ok_or_else(|| anyhow!("Stub not configured to return WalletInfo")) |
|||
} |
|||
async fn handle(&mut self, _msg: wallet::Sign) -> Result<PartiallySignedTransaction> { |
|||
self.psbt |
|||
.clone() |
|||
.ok_or_else(|| anyhow!("Stub not configured to return PartiallySignedTransaction")) |
|||
} |
|||
async fn handle(&mut self, _msg: wallet::TryBroadcastTransaction) -> Result<Txid> { |
|||
self.txid |
|||
.ok_or_else(|| anyhow!("Stub not configured to return Txid")) |
|||
} |
|||
} |
|||
|
|||
impl Default for Wallet { |
|||
fn default() -> Self { |
|||
Wallet { |
|||
party_params: false, |
|||
wallet_info: None, |
|||
psbt: None, |
|||
txid: None, |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,192 @@ |
|||
use crate::harness::mocks::monitor::Monitor; |
|||
use crate::harness::mocks::oracle::Oracle; |
|||
use crate::harness::mocks::wallet::Wallet; |
|||
use crate::schnorrsig; |
|||
use daemon::maker_cfd::CfdAction; |
|||
use daemon::model::cfd::{Cfd, Order}; |
|||
use daemon::model::Usd; |
|||
use daemon::{connection, db, maker_cfd, maker_inc_connections, taker_cfd}; |
|||
use sqlx::SqlitePool; |
|||
use std::net::SocketAddr; |
|||
use std::str::FromStr; |
|||
use std::task::Poll; |
|||
use tokio::sync::watch; |
|||
use xtra::spawn::TokioGlobalSpawnExt; |
|||
use xtra::Actor; |
|||
|
|||
pub mod bdk; |
|||
pub mod cfd_protocol; |
|||
pub mod mocks; |
|||
|
|||
// TODO: Remove all these parameters once we have the mock framework (because we have
|
|||
// Arcs then and can just default inside)
|
|||
pub async fn start_both( |
|||
taker_oracle: Oracle, |
|||
taker_monitor: Monitor, |
|||
taker_wallet: Wallet, |
|||
maker_oracle: Oracle, |
|||
maker_monitor: Monitor, |
|||
maker_wallet: Wallet, |
|||
) -> (Maker, Taker) { |
|||
let oracle_pk: schnorrsig::PublicKey = schnorrsig::PublicKey::from_str( |
|||
"ddd4636845a90185991826be5a494cde9f4a6947b1727217afedc6292fa4caf7", |
|||
) |
|||
.unwrap(); |
|||
|
|||
let maker = Maker::start(oracle_pk, maker_oracle, maker_monitor, maker_wallet).await; |
|||
let taker = Taker::start( |
|||
oracle_pk, |
|||
maker.listen_addr, |
|||
taker_oracle, |
|||
taker_monitor, |
|||
taker_wallet, |
|||
) |
|||
.await; |
|||
(maker, taker) |
|||
} |
|||
|
|||
/// Maker Test Setup
|
|||
#[derive(Clone)] |
|||
pub struct Maker { |
|||
pub cfd_actor_addr: |
|||
xtra::Address<maker_cfd::Actor<Oracle, Monitor, maker_inc_connections::Actor, Wallet>>, |
|||
pub order_feed: watch::Receiver<Option<Order>>, |
|||
pub cfd_feed: watch::Receiver<Vec<Cfd>>, |
|||
#[allow(dead_code)] // we need to keep the xtra::Address for refcounting
|
|||
pub inc_conn_actor_addr: xtra::Address<maker_inc_connections::Actor>, |
|||
pub listen_addr: SocketAddr, |
|||
} |
|||
|
|||
impl Maker { |
|||
pub async fn start( |
|||
oracle_pk: schnorrsig::PublicKey, |
|||
oracle: Oracle, |
|||
monitor: Monitor, |
|||
wallet: Wallet, |
|||
) -> Self { |
|||
let db = in_memory_db().await; |
|||
|
|||
let wallet_addr = wallet.create(None).spawn_global(); |
|||
|
|||
let settlement_time_interval_hours = time::Duration::hours(24); |
|||
|
|||
let maker = daemon::MakerActorSystem::new( |
|||
db, |
|||
wallet_addr, |
|||
oracle_pk, |
|||
|_, _| oracle, |
|||
|_, _| async { Ok(monitor) }, |
|||
|channel0, channel1| maker_inc_connections::Actor::new(channel0, channel1), |
|||
settlement_time_interval_hours, |
|||
) |
|||
.await |
|||
.unwrap(); |
|||
|
|||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); |
|||
|
|||
let address = listener.local_addr().unwrap(); |
|||
|
|||
let listener_stream = futures::stream::poll_fn(move |ctx| { |
|||
let message = match futures::ready!(listener.poll_accept(ctx)) { |
|||
Ok((stream, address)) => { |
|||
dbg!("new connection"); |
|||
maker_inc_connections::ListenerMessage::NewConnection { stream, address } |
|||
} |
|||
Err(e) => maker_inc_connections::ListenerMessage::Error { source: e }, |
|||
}; |
|||
|
|||
Poll::Ready(Some(message)) |
|||
}); |
|||
|
|||
tokio::spawn(maker.inc_conn_addr.clone().attach_stream(listener_stream)); |
|||
|
|||
Self { |
|||
cfd_actor_addr: maker.cfd_actor_addr, |
|||
order_feed: maker.order_feed_receiver, |
|||
cfd_feed: maker.cfd_feed_receiver, |
|||
inc_conn_actor_addr: maker.inc_conn_addr, |
|||
listen_addr: address, |
|||
} |
|||
} |
|||
|
|||
pub fn publish_order(&mut self, new_order_params: maker_cfd::NewOrder) { |
|||
self.cfd_actor_addr.do_send(new_order_params).unwrap(); |
|||
} |
|||
|
|||
pub fn reject_take_request(&self, order: Order) { |
|||
self.cfd_actor_addr |
|||
.do_send(CfdAction::RejectOrder { order_id: order.id }) |
|||
.unwrap(); |
|||
} |
|||
|
|||
pub fn accept_take_request(&self, order: Order) { |
|||
self.cfd_actor_addr |
|||
.do_send(CfdAction::AcceptOrder { order_id: order.id }) |
|||
.unwrap(); |
|||
} |
|||
} |
|||
|
|||
/// Taker Test Setup
|
|||
#[derive(Clone)] |
|||
pub struct Taker { |
|||
pub order_feed: watch::Receiver<Option<Order>>, |
|||
pub cfd_feed: watch::Receiver<Vec<Cfd>>, |
|||
pub cfd_actor_addr: xtra::Address<taker_cfd::Actor<Oracle, Monitor, Wallet>>, |
|||
} |
|||
|
|||
impl Taker { |
|||
pub async fn start( |
|||
oracle_pk: schnorrsig::PublicKey, |
|||
maker_address: SocketAddr, |
|||
oracle: Oracle, |
|||
monitor: Monitor, |
|||
wallet: Wallet, |
|||
) -> Self { |
|||
let connection::Actor { |
|||
send_to_maker, |
|||
read_from_maker, |
|||
} = connection::Actor::new(maker_address).await; |
|||
|
|||
let db = in_memory_db().await; |
|||
|
|||
let wallet_addr = wallet.create(None).spawn_global(); |
|||
|
|||
let taker = daemon::TakerActorSystem::new( |
|||
db, |
|||
wallet_addr, |
|||
oracle_pk, |
|||
send_to_maker, |
|||
read_from_maker, |
|||
|_, _| oracle, |
|||
|_, _| async { Ok(monitor) }, |
|||
) |
|||
.await |
|||
.unwrap(); |
|||
|
|||
Self { |
|||
order_feed: taker.order_feed_receiver, |
|||
cfd_feed: taker.cfd_feed_receiver, |
|||
cfd_actor_addr: taker.cfd_actor_addr, |
|||
} |
|||
} |
|||
|
|||
pub fn take_order(&self, order: Order, quantity: Usd) { |
|||
self.cfd_actor_addr |
|||
.do_send(taker_cfd::TakeOffer { |
|||
order_id: order.id, |
|||
quantity, |
|||
}) |
|||
.unwrap(); |
|||
} |
|||
} |
|||
|
|||
async fn in_memory_db() -> SqlitePool { |
|||
// Note: Every :memory: database is distinct from every other. So, opening two database
|
|||
// connections each with the filename ":memory:" will create two independent in-memory
|
|||
// databases. see: https://www.sqlite.org/inmemorydb.html
|
|||
let pool = SqlitePool::connect(":memory:").await.unwrap(); |
|||
|
|||
db::run_migrations(&pool).await.unwrap(); |
|||
|
|||
pool |
|||
} |
Loading…
Reference in new issue