diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index db89b721..177e720d 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -9,7 +9,7 @@ use bitcoin::hashes::Hash; use bitcoin::{Address as BtcAddress, TxMerkleNode, Txid}; use bitcoin::{Block, Network}; use esplora_client::{AsyncClient, Builder, MerkleProof, Utxo}; -use store::localdb::LocalDB; +use store::{ipfs::IPFS, localdb::LocalDB}; use uuid::Uuid; pub struct BitVM2Client { @@ -17,6 +17,7 @@ pub struct BitVM2Client { pub esplora: AsyncClient, pub btc_network: Network, pub chain_service: Chain, + pub ipfs: IPFS, } impl BitVM2Client { @@ -27,6 +28,7 @@ impl BitVM2Client { btc_network: Network, goat_network: GoatNetwork, goat_config: GoatInitConfig, + ipfs_endpoint: &str, ) -> Self { let local_db = LocalDB::new(&format!("sqlite:{db_path}"), true).await; local_db.migrate().await; @@ -37,6 +39,7 @@ impl BitVM2Client { .expect("Could not build esplora client"), btc_network, chain_service: Chain::new(get_chain_adaptor(goat_network, goat_config, None)), + ipfs: IPFS::new(ipfs_endpoint), } } diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 3559e997..421fcc47 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -31,6 +31,7 @@ mod tests { Network::Testnet, GoatNetwork::Test, global_init_config, + "http://localhost:5001", ) .await; let tx_id = diff --git a/crates/store/src/ipfs.rs b/crates/store/src/ipfs.rs index c18a72ed..c64cca55 100644 --- a/crates/store/src/ipfs.rs +++ b/crates/store/src/ipfs.rs @@ -16,36 +16,36 @@ pub struct IPFS { #[derive(Deserialize, Debug, PartialEq, Hash)] #[serde(rename_all = "PascalCase")] pub struct Link { - hash: String, - mod_time: String, - mode: u32, - name: String, - size: u32, - target: String, + pub hash: String, + pub mod_time: String, + pub mode: u32, + pub name: String, + pub size: u32, + pub target: String, #[serde(rename = "Type")] - type_: u32, + pub type_: u32, } #[derive(Deserialize, Debug, PartialEq, Hash)] #[serde(rename_all = "PascalCase")] pub struct Object { - hash: String, - links: Vec, + pub hash: String, + pub links: Vec, } #[derive(Deserialize, Debug, PartialEq, Hash)] #[serde(rename_all = "PascalCase")] pub struct Objects { - objects: Vec, + pub objects: Vec, } /// If the name is empty, it's the directory name #[derive(Deserialize, Debug, PartialEq, Hash)] #[serde(rename_all = "PascalCase")] pub struct AddedFile { - name: String, - hash: String, - size: String, + pub name: String, + pub hash: String, + pub size: String, } // Collects all files and returns relative + absolute paths @@ -154,6 +154,7 @@ pub mod tests { } // it works, but skip for avoiding creating too much garbage + // use std::io::Write; // let base_dir = tempfile::tempdir().unwrap(); // vec!["1.txt", "2.txt"].iter().for_each(|name| { // let mut file = std::fs::File::create( diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index b3bd0c65..e7617d60 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -1,4 +1,4 @@ -mod ipfs; +pub mod ipfs; pub mod localdb; mod schema; diff --git a/node/Cargo.toml b/node/Cargo.toml index f2679f44..3a5e8402 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -82,3 +82,4 @@ ark-serialize = { workspace = true } [dev-dependencies] tempfile = "3.19.1" +serial_test = "3.2.0" diff --git a/node/src/action.rs b/node/src/action.rs index 60c92d5c..3fdc6fb3 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -9,6 +9,7 @@ use bitvm2_lib::keys::*; use bitvm2_lib::types::{Bitvm2Graph, Bitvm2Parameters, CustomInputs}; use bitvm2_lib::verifier::export_challenge_tx; use bitvm2_lib::{committee::*, operator::*, verifier::*}; +use client::client::BitVM2Client; use goat::transactions::{assert::utils::COMMIT_TX_NUM, pre_signed::PreSignedTransaction}; use libp2p::gossipsub::MessageId; use libp2p::{PeerId, Swarm, gossipsub}; @@ -95,6 +96,7 @@ pub struct GraphFinalize { pub instance_id: Uuid, pub graph_id: Uuid, pub graph: Bitvm2Graph, + pub graph_ipfs_cid: String, } #[derive(Serialize, Deserialize)] @@ -179,6 +181,7 @@ impl GOATMessage { /// * peers: send pub async fn recv_and_dispatch( swarm: &mut Swarm, + client: &BitVM2Client, actor: Actor, peer_id: PeerId, id: MessageId, @@ -204,7 +207,6 @@ pub async fn recv_and_dispatch( println!("Handle message: {:?}", message); let content: GOATMessageContent = message.to_typed()?; // TODO: validate message - let client = client()?; match (content, actor) { // pegin // CreateInstance sent by bootnode @@ -378,6 +380,12 @@ pub async fn recv_and_dispatch( &receive_data.agg_nonces, &mut graph, )?; + let prekickoff_tx = graph.pre_kickoff.tx().clone(); + let node_keypair = + OperatorMasterKey::new(env::get_bitvm_key()?).master_keypair(); + sign_and_broadcast_prekickoff_tx(&client, node_keypair, prekickoff_tx).await?; + let graph_ipfs_cid = + publish_graph_to_ipfs(client, receive_data.graph_id, &graph).await?; store_graph( &client, receive_data.instance_id, @@ -386,23 +394,26 @@ pub async fn recv_and_dispatch( Some(GraphStatus::CommitteePresigned.to_string()), ) .await?; - let prekickoff_tx = graph.pre_kickoff.tx().clone(); - let node_keypair = - OperatorMasterKey::new(env::get_bitvm_key()?).master_keypair(); - sign_and_broadcast_prekickoff_tx(&client, node_keypair, prekickoff_tx).await?; + update_graph_status_or_ipfs_base( + client, + receive_data.graph_id, + None, + Some(graph_ipfs_cid.clone()), + ) + .await?; let message_content = GOATMessageContent::GraphFinalize(GraphFinalize { instance_id: receive_data.instance_id, graph_id: receive_data.graph_id, graph, + graph_ipfs_cid, }); - // TODO: ipfs send_to_peer(swarm, GOATMessage::from_typed(Actor::All, &message_content)?)?; force_stop_current_graph(); } }; } (GOATMessageContent::GraphFinalize(receive_data), _) => { - // TODO: validate graph + // TODO: validate graph & ipfs store_graph( &client, receive_data.instance_id, @@ -411,6 +422,13 @@ pub async fn recv_and_dispatch( Some(GraphStatus::CommitteePresigned.to_string()), ) .await?; + update_graph_status_or_ipfs_base( + client, + receive_data.graph_id, + None, + Some(receive_data.graph_ipfs_cid.clone()), + ) + .await?; } // peg-out @@ -454,7 +472,7 @@ pub async fn recv_and_dispatch( Amount::from_sat(graph.challenge.min_crowdfunding_amount()), receive_data.instance_id, receive_data.graph_id, - &graph, + &graph.kickoff.tx().compute_txid(), ) .await? { diff --git a/node/src/env.rs b/node/src/env.rs index 8777e3b5..2a354687 100644 --- a/node/src/env.rs +++ b/node/src/env.rs @@ -3,7 +3,7 @@ use alloy::eips::BlockNumberOrTag; use alloy::primitives::Address as EvmAddress; use bitcoin::{Network, PublicKey, key::Keypair}; use bitvm2_lib::keys::NodeMasterKey; -use client::chain::goat_adaptor::GoatInitConfig; +use client::chain::{chain_adaptor::GoatNetwork, goat_adaptor::GoatInitConfig}; use reqwest::Url; use std::str::FromStr; @@ -13,8 +13,14 @@ pub const ENV_GOAT_GATEWAY_CONTRACT_CREATION: &str = "GOAT_GATEWAY_CONTRACT_CREA pub const ENV_GOAT_GATEWAY_CONTRACT_TO_BLOCK: &str = "GOAT_GATEWAY_CONTRACT_TO_BLOCK"; pub const ENV_GOAT_PRIVATE_KEY: &str = "GOAT_PRIVATE_KEY"; pub const ENV_GOAT_CHAIN_ID: &str = "GOAT_CHAIN_ID"; +pub const ENV_BITVM_SECRET: &str = "BITVM_SECRET"; +pub const ENV_PEER_KEY: &str = "KEY"; +pub const ENV_PERR_ID: &str = "PEER_ID"; +pub const ENV_ACTOR: &str = "ACTOR"; +pub const ENV_IPFS_ENDPOINT: &str = "IPFS_ENDPOINT"; pub const SCRIPT_CACHE_FILE_NAME: &str = "cache/partial_script.bin"; +pub const IPFS_GRAPH_CACHE_DIR: &str = "cache/graph_cache/"; pub const DUST_AMOUNT: u64 = goat::transactions::base::DUST_AMOUNT; pub const MAX_CUSTOM_INPUTS: usize = 100; @@ -36,15 +42,20 @@ pub const CHALLENGE_RATE: u64 = 0; // 0% pub const RATE_MULTIPLIER: u64 = 10000; const COMMITTEE_MEMBER_NUMBER: usize = 3; -const NETWORK: Network = Network::Testnet; +const BTC_NETWORK: Network = Network::Testnet; +const GOAT_NETWORK: GoatNetwork = GoatNetwork::Test; pub fn get_network() -> Network { - NETWORK + BTC_NETWORK +} + +pub fn get_goat_network() -> GoatNetwork { + GOAT_NETWORK } pub fn get_bitvm_key() -> Result> { // TODO: what if node restart with different BITVM_SECRET ? - let bitvm_secret = std::env::var("BITVM_SECRET").expect("BITVM_SECRET is missing"); + let bitvm_secret = std::env::var(ENV_BITVM_SECRET).expect("{ENV_BITVM_SECRET} is missing"); Ok(Keypair::from_seckey_str_global(&bitvm_secret)?) } @@ -85,3 +96,37 @@ pub fn get_bitvm2_client_config() -> GoatInitConfig { chain_id: chain_id.parse().expect("fail to parse int"), } } + +pub enum IpfsTxName { + AssertCommit0, + AssertCommit1, + AssertCommit2, + AssertCommit3, + AssertFinal, + AssertInit, + Challenge, + Disprove, + Kickoff, + Pegin, + Take1, + Take2, +} + +impl IpfsTxName { + pub fn as_str(&self) -> &'static str { + match self { + IpfsTxName::AssertCommit0 => "assert-commit0.hex", + IpfsTxName::AssertCommit1 => "assert-commit1.hex", + IpfsTxName::AssertCommit2 => "assert-commit2.hex", + IpfsTxName::AssertCommit3 => "assert-commit3.hex", + IpfsTxName::AssertFinal => "assert-final.hex", + IpfsTxName::AssertInit => "assert-init.hex", + IpfsTxName::Challenge => "challenge.hex", + IpfsTxName::Disprove => "disprove.hex", + IpfsTxName::Kickoff => "kickoff.hex", + IpfsTxName::Pegin => "pegin.hex", + IpfsTxName::Take1 => "take1.hex", + IpfsTxName::Take2 => "take2.hex", + } + } +} diff --git a/node/src/main.rs b/node/src/main.rs index 00e53d78..304cd384 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -1,6 +1,8 @@ #![feature(trivial_bounds)] use base64::Engine; use clap::{Parser, Subcommand, command}; +use client::client::BitVM2Client; +use env::ENV_IPFS_ENDPOINT; use libp2p::PeerId; use libp2p::futures::StreamExt; use libp2p::{gossipsub, kad, mdns, multiaddr::Protocol, noise, swarm::SwarmEvent, tcp, yamux}; @@ -25,6 +27,7 @@ mod rpc_service; mod utils; use crate::action::GOATMessage; +use crate::env::{ENV_ACTOR, ENV_PEER_KEY, ENV_PERR_ID}; use crate::middleware::behaviour::AllBehavioursEvent; use anyhow::Result; use middleware::AllBehaviours; @@ -107,19 +110,19 @@ async fn main() -> Result<(), Box> { let local_key = identity::generate_local_key(); let base64_key = base64::engine::general_purpose::STANDARD .encode(&local_key.to_protobuf_encoding()?); - tracing::info!("export KEY={}", base64_key); - tracing::info!("export PEER_ID={}", local_key.public().to_peer_id()); + tracing::info!("export {}={}", ENV_PEER_KEY, base64_key); + tracing::info!("export {}={}", ENV_PERR_ID, local_key.public().to_peer_id()); } } return Ok(()); } // load role let actor = - Actor::from_str(std::env::var("ACTOR").unwrap_or("Challenger".to_string()).as_str()) + Actor::from_str(std::env::var(ENV_ACTOR).unwrap_or("Challenger".to_string()).as_str()) .unwrap(); - let local_key = std::env::var("KEY").expect("KEY is missing"); - let arg_peer_id = std::env::var("PEER_ID").expect("Peer ID is missing"); + let local_key = std::env::var(ENV_PEER_KEY).expect("KEY is missing"); + let arg_peer_id = std::env::var(ENV_PERR_ID).expect("Peer ID is missing"); let _ = tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).try_init(); let mut metric_registry = Registry::default(); @@ -207,8 +210,24 @@ async fn main() -> Result<(), Box> { tracing::debug!("RPC service listening on {}", &opt.rpc_addr); let rpc_addr = opt.rpc_addr.clone(); let db_path = opt.db_path.clone(); - - tokio::spawn(rpc_service::serve(rpc_addr, db_path, Arc::new(Mutex::new(metric_registry)))); + let ipfs_url = std::env::var(ENV_IPFS_ENDPOINT).expect("IPFS_ENDPOINT is missing"); + + let client = BitVM2Client::new( + &db_path, + None, + env::get_network(), + env::get_goat_network(), + env::get_bitvm2_client_config(), + &ipfs_url, + ) + .await; + + tokio::spawn(rpc_service::serve( + rpc_addr, + db_path.clone(), + ipfs_url.clone(), + Arc::new(Mutex::new(metric_registry)), + )); // Read full lines from stdin let mut interval = interval(Duration::from_secs(20)); let mut stdin = io::BufReader::new(io::stdin()).lines(); @@ -243,7 +262,7 @@ async fn main() -> Result<(), Box> { actor: actor.clone(), content: "tick".as_bytes().to_vec(), })?; - match action::recv_and_dispatch(&mut swarm, actor.clone(), peer_id, GOATMessage::default_message_id(), &tick_data).await{ + match action::recv_and_dispatch(&mut swarm, &client, actor.clone(), peer_id, GOATMessage::default_message_id(), &tick_data).await{ Ok(_) => {} Err(e) => { tracing::error!(e) } } @@ -256,7 +275,7 @@ async fn main() -> Result<(), Box> { message_id: id, message, })) => { - match action::recv_and_dispatch(&mut swarm, actor.clone(), peer_id, id, &message.data).await { + match action::recv_and_dispatch(&mut swarm, &client, actor.clone(), peer_id, id, &message.data).await { Ok(_) => {}, Err(e) => { tracing::error!(e) } } diff --git a/node/src/rpc_service/mod.rs b/node/src/rpc_service/mod.rs index fd5560a8..c2b3ca8b 100644 --- a/node/src/rpc_service/mod.rs +++ b/node/src/rpc_service/mod.rs @@ -47,6 +47,7 @@ pub struct AppState { impl AppState { pub async fn create_arc_app_state( db_path: &str, + ipfs_url: &str, registry: Arc>, ) -> anyhow::Result> { let bitvm2_client = BitVM2Client::new( @@ -55,6 +56,7 @@ impl AppState { Network::Testnet, GoatNetwork::Test, get_bitvm2_client_config(), + ipfs_url, ) .await; let metrics_state = MetricsState::new(registry); @@ -88,9 +90,10 @@ async fn root() -> &'static str { pub(crate) async fn serve( addr: String, db_path: String, + ipfs_url: String, registry: Arc>, ) -> anyhow::Result<()> { - let app_state = AppState::create_arc_app_state(&db_path, registry).await?; + let app_state = AppState::create_arc_app_state(&db_path, &ipfs_url, registry).await?; let server = Router::new() .route("/", get(root)) .route("/v1/nodes", post(create_node)) @@ -212,6 +215,10 @@ mod tests { listener.local_addr().unwrap().to_string() } + fn local_ipfs_url() -> String { + "http://localhost:5001".to_string() + } + #[tokio::test(flavor = "multi_thread")] async fn test_nodes_api() -> Result<(), Box> { init_tracing(); @@ -219,6 +226,7 @@ mod tests { tokio::spawn(rpc_service::serve( addr.clone(), temp_file(), + local_ipfs_url(), Arc::new(Mutex::new(Registry::default())), )); sleep(Duration::from_secs(1)).await; @@ -276,6 +284,7 @@ mod tests { tokio::spawn(rpc_service::serve( addr.clone(), temp_file(), + local_ipfs_url(), Arc::new(Mutex::new(Registry::default())), )); sleep(Duration::from_secs(1)).await; diff --git a/node/src/utils.rs b/node/src/utils.rs index b5c07137..b1f0b5dc 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -2,6 +2,7 @@ use crate::action::CreateGraphPrepare; use crate::env::*; use crate::rpc_service::current_time_secs; use ark_serialize::CanonicalDeserialize; +use bitcoin::consensus::encode::serialize_hex; use bitcoin::key::Keypair; use bitcoin::{ Address, Amount, EcdsaSighashType, Network, OutPoint, PublicKey, ScriptBuf, Sequence, @@ -30,7 +31,7 @@ use goat::utils::num_blocks_per_network; use musig2::{PartialSignature, PubNonce}; use statics::*; use std::fs::{self, File}; -use std::io::{BufReader, BufWriter}; +use std::io::{BufReader, BufWriter, Write}; use std::path::Path; use std::str::FromStr; use store::{Graph, GraphStatus}; @@ -90,7 +91,12 @@ pub async fn should_generate_graph( (get_fee_rate(client).await? * 2.0 * CHEKSIG_P2WSH_INPUT_VBYTES as f64).ceil() as u64, ); let total_effective_balance: Amount = - utxos.iter().map(|utxo| utxo.value - utxo_spent_fee).sum(); + utxos + .iter() + .map(|utxo| { + if utxo.value > utxo_spent_fee { utxo.value - utxo_spent_fee } else { Amount::ZERO } + }) + .sum(); Ok(total_effective_balance > get_stake_amount(create_graph_prepare_data.pegin_amount.to_sat())) } @@ -198,14 +204,14 @@ pub fn get_partial_scripts() -> Result, Box> if Path::new(scripts_cache_path).exists() { let file = File::open(scripts_cache_path)?; let reader = BufReader::new(file); - let scripts_bytes: Vec = bincode::deserialize_from(reader).unwrap(); + let scripts_bytes: Vec = bincode::deserialize_from(reader)?; Ok(scripts_bytes.into_iter().map(|x| script! {}.push_script(x)).collect()) } else { let partial_scripts = generate_partial_scripts(&get_vk()?); if let Some(parent) = Path::new(scripts_cache_path).parent() { - fs::create_dir_all(parent).unwrap(); + fs::create_dir_all(parent)?; }; - let file = File::create(scripts_cache_path).unwrap(); + let file = File::create(scripts_cache_path)?; let scripts_bytes: Vec = partial_scripts.iter().map(|scr| scr.clone().compile()).collect(); let writer = BufWriter::new(file); @@ -426,10 +432,9 @@ pub async fn should_challenge( challenge_amount: Amount, _instance_id: Uuid, graph_id: Uuid, - graph: &Bitvm2Graph, + kickoff_txid: &Txid, ) -> Result> { // check if kickoff is confirmed on L1 - let kickoff_txid = graph.kickoff.tx().compute_txid(); if let None = client.esplora.get_tx(&kickoff_txid).await? { return Ok(false); } @@ -521,10 +526,6 @@ pub async fn validate_assert( Ok(verify_proof(&get_vk()?, proof_sigs, &disprove_scripts, &wots_pubkeys)) } -pub fn client() -> Result> { - Err("TODO".into()) -} - /// Retrieves the Groth16 proof, public inputs, and verifying key /// for the given graph. /// @@ -757,18 +758,18 @@ pub async fn store_graph( .update_graph(Graph { graph_id, instance_id, - graph_ipfs_base_url: "".to_string(), //TODO + graph_ipfs_base_url: "".to_string(), pegin_txid: graph.pegin.tx().compute_txid().to_string(), amount: graph.parameters.pegin_amount.to_sat() as i64, status: status.unwrap_or_else(|| GraphStatus::OperatorPresigned.to_string()), kickoff_txid: Some(graph.kickoff.tx().compute_txid().to_string()), - challenge_txid: Some(graph.challenge.tx().compute_txid().to_string()), + challenge_txid: None, take1_txid: Some(graph.take1.tx().compute_txid().to_string()), assert_init_txid: Some(graph.assert_init.tx().compute_txid().to_string()), assert_commit_txids: Some(format!("{:?}", assert_commit_txids)), assert_final_txid: Some(graph.assert_final.tx().compute_txid().to_string()), take2_txid_txid: Some(graph.take2.tx().compute_txid().to_string()), - disprove_txid: Some(graph.disprove.tx().compute_txid().to_string()), + disprove_txid: None, operator: graph.parameters.operator_pubkey.to_string(), raw_data: Some(serde_json::to_string(&graph).expect("to json string")), created_at: current_time_secs(), @@ -811,3 +812,402 @@ pub async fn get_graph( let res: Bitvm2Graph = serde_json::from_str(graph.raw_data.unwrap().as_str())?; Ok(res) } + +pub async fn publish_graph_to_ipfs( + client: &BitVM2Client, + graph_id: Uuid, + graph: &Bitvm2Graph, +) -> Result> { + fn write_tx( + base_dir: &str, + tx_name: IpfsTxName, + tx: &Transaction, + ) -> Result<(), Box> { + // write tx_hex to base_dir/tx_name + let tx_hex = serialize_hex(tx); + let tx_cache_path = format!("{}{}", base_dir, tx_name.as_str()); + let mut file = File::create(&tx_cache_path)?; + file.write_all(tx_hex.as_bytes())?; + Ok(()) + } + + let base_dir = format!("{}{}/", IPFS_GRAPH_CACHE_DIR, graph_id.to_string()); + fs::create_dir_all(base_dir.clone())?; + write_tx(&base_dir, IpfsTxName::AssertCommit0, graph.assert_commit.commit_txns[0].tx())?; + write_tx(&base_dir, IpfsTxName::AssertCommit1, graph.assert_commit.commit_txns[1].tx())?; + write_tx(&base_dir, IpfsTxName::AssertCommit2, graph.assert_commit.commit_txns[2].tx())?; + write_tx(&base_dir, IpfsTxName::AssertCommit3, graph.assert_commit.commit_txns[3].tx())?; + write_tx(&base_dir, IpfsTxName::AssertInit, graph.assert_init.tx())?; + write_tx(&base_dir, IpfsTxName::AssertFinal, graph.assert_final.tx())?; + write_tx(&base_dir, IpfsTxName::Challenge, graph.challenge.tx())?; + write_tx(&base_dir, IpfsTxName::Disprove, graph.disprove.tx())?; + write_tx(&base_dir, IpfsTxName::Kickoff, graph.kickoff.tx())?; + write_tx(&base_dir, IpfsTxName::Pegin, graph.pegin.tx())?; + write_tx(&base_dir, IpfsTxName::Take1, graph.take1.tx())?; + write_tx(&base_dir, IpfsTxName::Take2, graph.take2.tx())?; + let cids = client.ipfs.add(&Path::new(&base_dir)).await?; + let dir_cid = cids + .iter() + .find(|f| f.name.is_empty()) + .map(|f| f.hash.clone()) + .ok_or("cid for graph dir not found")?; + + // try to delete the cache files to free up disk, failed deletions do not affect subsequent executions, so there is no need to return an error + let _ = fs::remove_dir_all(base_dir); + Ok(dir_cid) +} + +#[cfg(test)] +mod tests { + use super::*; + use client::chain::{chain_adaptor::GoatNetwork, goat_adaptor::GoatInitConfig}; + use reqwest::Url; + use serial_test::serial; + use std::fmt; + + async fn test_client() -> BitVM2Client { + let global_init_config = GoatInitConfig { + rpc_url: "https://rpc.testnet3.goat.network".parse::().expect("decode url"), + gateway_address: "0xeD8AeeD334fA446FA03Aa00B28aFf02FA8aC02df" + .parse() + .expect("parse contract address"), + gateway_creation_block: 0, + to_block: None, + private_key: None, + chain_id: 48816_u32, + }; + // let local_db = LocalDB::new(&format!("sqlite:{db_path}"), true).await; + let tmp_db = tempfile::NamedTempFile::new().unwrap(); + BitVM2Client::new( + tmp_db.path().as_os_str().to_str().unwrap(), + None, + Network::Testnet, + GoatNetwork::Test, + global_init_config, + "http://44.229.236.82:5001", + ) + .await + } + + fn mock_input() -> CustomInputs { + let input_amount = Amount::from_sat(10000); + let fee_amount = Amount::from_sat(2000); + let mock_input = Input { + outpoint: OutPoint { + txid: Txid::from_str( + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + ) + .unwrap(), + vout: 0, + }, + amount: Amount::from_btc(10000.0).unwrap(), + }; + let change_address = Address::p2wsh(&ScriptBuf::default(), get_network()); + CustomInputs { inputs: vec![mock_input.clone()], input_amount, fee_amount, change_address } + } + + #[test] + fn test_statics() { + let instance_id = Uuid::new_v4(); + let graph_id = Uuid::new_v4(); + let other_graph_id = Uuid::new_v4(); + + assert!(is_processing_graph() == false); + assert!(try_start_new_graph(instance_id, graph_id)); + assert!(is_processing_graph() == true); + assert!(current_processing_graph() == Some((instance_id, graph_id))); + + finish_current_graph_processing(instance_id, other_graph_id); + assert!(is_processing_graph() == true); + + finish_current_graph_processing(instance_id, graph_id); + assert!(is_processing_graph() == false); + + try_start_new_graph(instance_id, graph_id); + assert!(is_processing_graph() == true); + force_stop_current_graph(); + assert!(is_processing_graph() == false); + } + + #[tokio::test] + #[serial(env)] + async fn test_should_generate_graph() { + let client = test_client().await; + let mock_create_graph_prepare_data = CreateGraphPrepare { + instance_id: Uuid::new_v4(), + network: get_network(), + depositor_evm_address: [0xff; 20], + pegin_amount: Amount::from_sat(100000), + user_inputs: mock_input(), + committee_member_pubkey: PublicKey::from_str( + "028b839569cde368894237913fe4fbd25d75eaf1ed019a39d479e693dac35be19e", + ) + .unwrap(), + committee_members_num: 2, + }; + + // rich operator + unsafe { + std::env::set_var(ENV_ACTOR, "Operator"); + std::env::set_var( + ENV_BITVM_SECRET, + "3076ca1dfc1e383be26d5dd3c0c427340f96139fa8c2520862cf551ec2d670ac", + ); + } + let node_address = node_p2wsh_address(get_network(), &get_node_pubkey().unwrap()); + let utxos = client.esplora.get_address_utxo(node_address.clone()).await.unwrap(); + let balance: Amount = utxos.iter().map(|utxo| utxo.value).sum(); + let flag = should_generate_graph(&client, &mock_create_graph_prepare_data).await.unwrap(); + println!( + "node: {}, balance: {} BTC, should_generate_graph: {}", + node_address, + balance.to_btc(), + flag + ); + + // poor operator + unsafe { + std::env::set_var( + ENV_BITVM_SECRET, + "ee0817eac0c13aa8ee2dd3256304041f09f0499d1089b56495310ae8093583e2", + ); + } + let node_address = node_p2wsh_address(get_network(), &get_node_pubkey().unwrap()); + let utxos = client.esplora.get_address_utxo(node_address.clone()).await.unwrap(); + let balance: Amount = utxos.iter().map(|utxo| utxo.value).sum(); + let flag = should_generate_graph(&client, &mock_create_graph_prepare_data).await.unwrap(); + println!( + "node: {}, balance: {} BTC, should_generate_graph: {}", + node_address, + balance.to_btc(), + flag + ); + } + + #[tokio::test] + #[ignore = "test graph required"] + async fn test_is_withdraw_initialized_on_l2() { + let client = test_client().await; + let unused_instance_id = Uuid::new_v4(); + // TODO: post test graph to L2 + let initialized_graph_id = Uuid::from_slice(&hex::decode("").unwrap()).unwrap(); + let uninitialized_graph_id = Uuid::from_slice(&hex::decode("").unwrap()).unwrap(); + assert_eq!( + true, + is_withdraw_initialized_on_l2(&client, unused_instance_id, initialized_graph_id) + .await + .unwrap() + ); + assert_eq!( + false, + is_withdraw_initialized_on_l2(&client, unused_instance_id, uninitialized_graph_id) + .await + .unwrap() + ); + } + + #[tokio::test] + async fn test_is_take1_timelock_expired() { + let client = test_client().await; + let kickoff_txid = + Txid::from_str("4dd13ca25ef6edb4506394a402db2368d02d9467bc47326d3553310483f2ed04") + .unwrap(); + assert_eq!(true, is_take1_timelock_expired(&client, kickoff_txid).await.unwrap()); + } + + #[tokio::test] + async fn test_is_take2_timelock_expired() { + let client = test_client().await; + let assert_final_txid = + Txid::from_str("a2dedfbf376b8c0c183b4dfac7b0765b129a345c870f9fabbdf8c48072697a27") + .unwrap(); + assert_eq!(true, is_take2_timelock_expired(&client, assert_final_txid).await.unwrap()); + } + + #[tokio::test] + #[serial(env)] + async fn test_select_operator_inputs() { + let client = test_client().await; + let stake_amount = Amount::from_sat(1600000); + struct UtxoDisplay(Option); + + impl fmt::Display for UtxoDisplay { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.0 { + Some(v) => { + let items: Vec = v + .inputs + .iter() + .map(|input| { + format!( + "{}:{}:{}", + input.outpoint.txid.to_string(), + input.outpoint.vout, + input.amount.to_btc() + ) + }) + .collect(); + write!(f, "[ {} ]", items.join(", ")) + } + _ => { + write!(f, "insufficient balance") + } + } + } + } + + // rich operator + unsafe { + std::env::set_var(ENV_ACTOR, "Operator"); + std::env::set_var( + ENV_BITVM_SECRET, + "3076ca1dfc1e383be26d5dd3c0c427340f96139fa8c2520862cf551ec2d670ac", + ); + } + let node_address = node_p2wsh_address(get_network(), &get_node_pubkey().unwrap()); + let inputs = select_operator_inputs(&client, stake_amount).await.unwrap(); + println!( + "node: {}, stake_amount: {} BTC, utxos: {}", + node_address, + stake_amount, + UtxoDisplay(inputs) + ); + + // poor operator + unsafe { + std::env::set_var( + ENV_BITVM_SECRET, + "ee0817eac0c13aa8ee2dd3256304041f09f0499d1089b56495310ae8093583e2", + ); + } + let node_address = node_p2wsh_address(get_network(), &get_node_pubkey().unwrap()); + let inputs = select_operator_inputs(&client, stake_amount).await.unwrap(); + println!( + "node: {}, stake_amount: {} BTC, utxos: {}", + node_address, + stake_amount, + UtxoDisplay(inputs) + ); + } + + #[tokio::test] + #[serial(env)] + async fn test_should_challenge() { + let client = test_client().await; + let challenge_amount = Amount::from_sat(1600000); + let mock_instance_id = Uuid::new_v4(); + let mock_graph_id = Uuid::new_v4(); + let invalid_kickoff_txid = + Txid::from_str("0c598f63bffe9d7468ce6930bf0fe1ba5c6e125c9c9e38674ee380dd2c6d97f6") + .unwrap(); + // TODO: add test case: valid kickoff tx + + // rich challenger + unsafe { + std::env::set_var(ENV_ACTOR, "Challenger"); + std::env::set_var( + ENV_BITVM_SECRET, + "3076ca1dfc1e383be26d5dd3c0c427340f96139fa8c2520862cf551ec2d670ac", + ); + } + let node_address = node_p2wsh_address(get_network(), &get_node_pubkey().unwrap()); + let utxos = client.esplora.get_address_utxo(node_address.clone()).await.unwrap(); + let balance: Amount = utxos.iter().map(|utxo| utxo.value).sum(); + let flag = should_challenge( + &client, + challenge_amount, + mock_instance_id, + mock_graph_id, + &invalid_kickoff_txid, + ) + .await + .unwrap(); + println!( + "kickoff(invalid): {}, node: {}, balance: {} BTC, should_challenge: {}", + invalid_kickoff_txid.to_string(), + node_address, + balance.to_btc(), + flag + ); + + // poor challenger + unsafe { + std::env::set_var( + ENV_BITVM_SECRET, + "ee0817eac0c13aa8ee2dd3256304041f09f0499d1089b56495310ae8093583e2", + ); + } + let node_address = node_p2wsh_address(get_network(), &get_node_pubkey().unwrap()); + let utxos = client.esplora.get_address_utxo(node_address.clone()).await.unwrap(); + let balance: Amount = utxos.iter().map(|utxo| utxo.value).sum(); + let flag = should_challenge( + &client, + challenge_amount, + mock_instance_id, + mock_graph_id, + &invalid_kickoff_txid, + ) + .await + .unwrap(); + println!( + "kickoff(invalid): {}, node: {}, balance: {} BTC, should_challenge: {}", + invalid_kickoff_txid.to_string(), + node_address, + balance.to_btc(), + flag + ); + } + + #[tokio::test] + async fn test_validate_challenge() { + let client = test_client().await; + let kickoff_txid = + Txid::from_str("0c598f63bffe9d7468ce6930bf0fe1ba5c6e125c9c9e38674ee380dd2c6d97f6") + .unwrap(); + let challenge_txid = + Txid::from_str("d2a2beff7dc0f93fc41505b646c6fa174991b0c4e415a96359607c37ba88e376") + .unwrap(); + let mismatch_challenge_txid = + Txid::from_str("c6a033812a1370973f94d956704ed1a68f490141a3c21bce64454d38a2c23794") + .unwrap(); + let nonexistent_challenge_txid = + Txid::from_str("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d") + .unwrap(); + + assert_eq!( + true, + validate_challenge(&client, &kickoff_txid, &challenge_txid).await.unwrap() + ); + assert_eq!( + false, + validate_challenge(&client, &kickoff_txid, &mismatch_challenge_txid).await.unwrap() + ); + assert_eq!( + false, + validate_challenge(&client, &kickoff_txid, &nonexistent_challenge_txid).await.unwrap() + ); + } + + #[tokio::test] + async fn test_validate_disprove() { + let client = test_client().await; + let assert_final_txid = + Txid::from_str("2da6b0f73cd8835d5b76b62b9bd22314ee61212d348f6a4dbad915253f121012") + .unwrap(); + let disprove_txid = + Txid::from_str("5773755d1d0f750830edae5e1afcb37ab106e2dd46e164b09bf6213a0f45b0e1") + .unwrap(); + let mismatch_disprove_txid = + Txid::from_str("c6a033812a1370973f94d956704ed1a68f490141a3c21bce64454d38a2c23794") + .unwrap(); + + assert_eq!( + true, + validate_disprove(&client, &assert_final_txid, &disprove_txid).await.unwrap() + ); + assert_eq!( + false, + validate_disprove(&client, &assert_final_txid, &mismatch_disprove_txid).await.unwrap() + ); + } +}