Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/bitvm2-ga/src/operator/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -966,7 +966,7 @@ pub fn take2_timelocks(network: Network) -> (u32, u32) {
+ if network == Network::Regtest { 6 } else { 0 }, // Regtest extra delay
num_blocks_per_network(network, CONNECTOR_D_TIMELOCK)
+ if network == Network::Testnet { 6 } else { 0 } // Testnet extra delay
+ if network == Network::Testnet4 { 60 } else { 0 } // Testnet4 extra delay
+ if network == Network::Testnet4 { 100 } else { 0 } // Testnet4 extra delay
+ if network == Network::Regtest { 6 } else { 0 }, // Regtest extra delay
)
}
Expand Down
1 change: 1 addition & 0 deletions node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,7 @@ bitvm2-noded \
| `GOAT_ADDRESS` | Conditional | GOAT address (required for Operator/Challenger) | - |
| `ENABLE_RELAYER` | No | Enable relayer mode for Committee nodes | `false` |
| `BTC_CHAIN_URL` | No | Bitcoin Esplora API endpoint | Public Esplora |
| `MARA_SLIPSTREAM_API_URL` | No | MARA slipstream API base URL (used for non-standard tx broadcast) | mainnet: `https://slipstream.mara.com/api`; testnet4: `https://teststream.mara.com/api` |
| `GOAT_PROOF_BUILD_URL` | No | Proof Builder RPC endpoint | - |
| `NODE_NAME` | No | Node display name | `ZKM` |
| `OPERATOR_NODE_SERVICE_FEE` | No | Operator service fee rate | `0.001` |
Expand Down
16 changes: 16 additions & 0 deletions node/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ pub const ENV_GOAT_SEQUENCER_SET_MULTI_SIG_VERIFIER_ADDRESS: &str =
pub const ENV_ENABLE_RELAYER: &str = "ENABLE_RELAYER";
pub const ENV_ENABLE_UPDATE_SPV_CONTRACT: &str = "ENABLE_UPDATE_SPV_CONTRACT";
pub const ENV_BTC_BLOCK_CONFIRMS: &str = "BTC_BLOCK_CONFIRMS";
pub const ENV_MARA_SLIPSTREAM_API_URL: &str = "MARA_SLIPSTREAM_API_URL";
pub const DEFAULT_MARA_SLIPSTREAM_MAINNET_API_URL: &str = "https://slipstream.mara.com/api";
pub const DEFAULT_MARA_SLIPSTREAM_TESTNET4_API_URL: &str = "https://teststream.mara.com/api";

pub const ENV_GOAT_PRIVATE_KEY: &str = "GOAT_PRIVATE_KEY";

Expand Down Expand Up @@ -349,6 +352,19 @@ pub fn get_btc_url_from_env() -> Option<String> {
std::env::var(ENV_BTC_CHAIN_URL).ok()
}

pub fn get_mara_slipstream_api_base_url(network: Network) -> String {
if let Ok(url) = std::env::var(ENV_MARA_SLIPSTREAM_API_URL)
&& !url.trim().is_empty()
{
return url;
}

match network {
Network::Bitcoin => DEFAULT_MARA_SLIPSTREAM_MAINNET_API_URL.to_string(),
_ => DEFAULT_MARA_SLIPSTREAM_TESTNET4_API_URL.to_string(),
}
}

pub fn get_sequencer_set_monitor_start_cosmos_block_from_env() -> Option<u64> {
std::env::var(ENV_SEQUENCER_SET_MONITOR_START_COSMOS_BLOCK)
.ok()
Expand Down
173 changes: 163 additions & 10 deletions node/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,15 +355,6 @@ pub mod todo_funcs {
let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?);
Ok(operator_master_key.preimage_for_graph(graph_id, index))
}
pub async fn broadcast_nonstandard_tx(btc_client: &BTCClient, tx: &Transaction) -> Result<()> {
match broadcast_tx(btc_client, tx).await {
Ok(_) => Ok(()),
Err(e) => {
tracing::warn!("broadcast_nonstandard_tx not implemented yet: {} , Skipped", e);
Ok(())
}
}
}
}

pub mod evm_swap_utils {
Expand Down Expand Up @@ -1667,6 +1658,58 @@ pub async fn get_fee_rate(client: &BTCClient) -> Result<f64> {
}
}

pub async fn broadcast_nonstandard_tx(btc_client: &BTCClient, tx: &Transaction) -> Result<()> {
match broadcast_tx(btc_client, tx).await {
Ok(_) => Ok(()),
Err(e) => {
let network = btc_client.network();
let base_url = get_mara_slipstream_api_base_url(network);
let submit_url = format!("{}/transactions", base_url.trim_end_matches('/'));
let tx_hex = hex::encode(serialize(tx));

warn!(
"normal broadcast failed, fallback to MARA slipstream api. network: {network:?}, url: {submit_url}, err: {e}"
);

let response = reqwest::Client::new()
.post(&submit_url)
.json(&serde_json::json!({ "tx_hex": tx_hex }))
.send()
.await
.map_err(|fallback_err| {
anyhow!(
"fallback broadcast request failed. normal error: {e}; fallback error: {fallback_err}"
)
})?;

let status = response.status();
let body = response
.text()
.await
.unwrap_or_else(|_| "<failed to read response body>".to_string());

if !status.is_success() {
bail!(
"fallback broadcast failed. normal error: {e}; fallback status: {status}; fallback body: {body}"
);
}

let status_field = serde_json::from_str::<serde_json::Value>(&body)
.ok()
.and_then(|v| v.get("status").and_then(|s| s.as_str()).map(str::to_string));
if let Some(status_field) = status_field
&& !status_field.eq_ignore_ascii_case("success")
{
bail!(
"fallback broadcast returned non-success status. normal error: {e}; fallback body: {body}"
);
}

Ok(())
}
}
}

/// Broadcasts a raw transaction to the Bitcoin network using the mempool API.
///
/// Requirements:
Expand Down Expand Up @@ -2218,7 +2261,7 @@ pub async fn build_sign_and_broadcast_non_standard_tx(
&node_keypair,
)?;
}
todo_funcs::broadcast_nonstandard_tx(client, &tx).await?;
broadcast_nonstandard_tx(client, &tx).await?;
Ok(tx.compute_txid())
}
None => {
Expand Down Expand Up @@ -4637,6 +4680,116 @@ pub async fn get_largest_watchtower_challenge_block(
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;

async fn build_large_nonstandard_tx_for_debug(
btc_client: &BTCClient,
node_keypair: &Keypair,
payload_size: usize,
) -> Result<Transaction> {
let mut msg = "large_opreturn_msg".to_string().as_bytes().to_vec();
msg.extend(vec![0u8; payload_size - msg.len()]);
let scr = script! {
OP_RETURN
{msg}
}
.compile();
let mut tx = Transaction {
version: bitcoin::transaction::Version(2),
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![],
output: vec![TxOut { value: Amount::ZERO, script_pubkey: scr }],
};

let node_pubkey: PublicKey = node_keypair.public_key().into();
let node_address = node_p2wsh_address(get_network(), &node_pubkey);

let (inputs, _, change_amount) = get_proper_utxo_set(
btc_client,
tx.weight().to_vbytes_ceil(),
node_address.clone(),
Amount::ZERO,
1.0,
)
.await?
.ok_or_else(|| {
anyhow!("insufficient UTXOs on {} for debug nonstandard tx test", node_address)
})?;

for input in &inputs {
tx.input.push(TxIn {
previous_output: input.outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
});
}

if change_amount > Amount::from_sat(DUST_AMOUNT) {
tx.output
.push(TxOut { script_pubkey: node_address.script_pubkey(), value: change_amount });
}

for (i, input) in inputs.iter().enumerate() {
node_sign(&mut tx, i, input.amount, EcdsaSighashType::All, node_keypair)?;
}

Ok(tx)
}

#[tokio::test]
#[ignore]
async fn debug_test_broadcast_nonstandard_tx_and_check_mempool_visibility() -> Result<()> {
if std::env::var(ENV_BITVM_SECRET).is_err() {
bail!("BITVM_SECRET is required and should correspond to a funded testnet key");
}

let network = get_network();
if network != Network::Testnet4 {
bail!("refuse to run this debug test on non-testnet4 network: {network}");
}

let btc_url =
get_btc_url_from_env().unwrap_or_else(|| "https://mempool.space/testnet4".to_string());
let btc_client = BTCClient::new(network, Some(&btc_url));
let node_keypair = get_bitvm_key()?;

// Use an oversized OP_RETURN payload to make tx non-standard for regular mempool relay.
let tx = build_large_nonstandard_tx_for_debug(&btc_client, &node_keypair, 200_000).await?;
let txid = tx.compute_txid();
println!(
"debug nonstandard tx built: txid={}, vbytes={}",
txid,
tx.weight().to_vbytes_ceil()
);

let normal_err = broadcast_tx(&btc_client, &tx).await.err().ok_or_else(|| {
anyhow!("expected normal mempool broadcast to fail for non-standard tx")
})?;
println!("normal broadcast failed as expected: {normal_err:#}");

broadcast_nonstandard_tx(&btc_client, &tx).await?;
println!("broadcast_nonstandard_tx succeeded for non-standard tx");

let mut found_in_mempool = false;
for _ in 0..10 {
if btc_client.get_tx(&txid).await?.is_some() {
found_in_mempool = true;
break;
}
tokio::time::sleep(Duration::from_secs(1)).await;
}

if found_in_mempool {
println!(
"successfully found non-standard tx in mempool after broadcast_nonstandard_tx"
);
} else {
println!("failed to find non-standard tx in mempool after broadcast_nonstandard_tx");
}

Ok(())
}

#[tokio::test]
#[ignore = "test on regtest"]
Expand Down
Loading