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: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[env]
ZKM_IMM_WRAP_VK = "1"
7 changes: 4 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ p3-field = { git = "https://github.com/ProjectZKM/Plonky3" }
#zkm-verifier = { path = "../Ziren/crates/verifier" }

bitvm-lib = { package = "bitvm-gc", path = "crates/bitvm-gc" }
verifiable-circuit-babe = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" }
garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" }
soldering-host = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" }
verifiable-circuit-babe = { git = "https://github.com/KSlashh/bitvm2-gc", branch = "patch-deps" }
garbled-snark-verifier = { git = "https://github.com/KSlashh/bitvm2-gc", branch = "patch-deps" }
soldering-host = { git = "https://github.com/KSlashh/bitvm2-gc", branch = "patch-deps" }

#verifiable-circuit-babe = { path = "../bitvm2-gc/verifiable-circuit-babe"}
#garbled-snark-verifier = { path = "../bitvm2-gc/garbled-snark-verifier"}
Expand Down
163 changes: 121 additions & 42 deletions crates/bitvm-gc/src/timelocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ pub const NODE_BITCOIN_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig {
};
pub const NODE_TESTNET_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig {
connector_z: 100,
connector_a: 16,
prover_connector: 20,
connector_d: 40,
connector_a: 6,
prover_connector: 16,
connector_d: 32,
watchtower_challenge: 20,
operator_ack: 32,
operator_commit: 40,
connector_f: 52,
operator_ack: 28,
operator_commit: 42,
connector_f: 56,
};
pub const NODE_SIGNET_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig {
connector_z: 6,
Expand Down Expand Up @@ -66,20 +66,85 @@ pub fn estimated_block_interval_secs(network: Network) -> i64 {
}
}

const MIN_REACTION_SECS: i64 = 3600;
/// Non-serialized timing policy used to validate the on-chain timelock config.
///
/// A graph commits to the CSV values, while every node applies this local network
/// policy before accepting those values.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ProtocolTimingBudget {
/// Confirmation depth required before downstream Bitcoin evidence is used.
pub evidence_confirmations: u32,
/// Event discovery, P2P propagation, scheduling, and local signing.
pub reaction_blocks: u32,
/// P99 time for a watchtower to retrieve and prepare its proof commitment.
pub watchtower_proof_blocks: u32,
/// Target blocks for a transaction to be included, including fee bumping.
pub inclusion_blocks: u32,
/// Reorg and transient service failure allowance.
pub safety_blocks: u32,
}

impl ProtocolTimingBudget {
const fn action_window(self) -> u32 {
self.reaction_blocks
.saturating_add(self.inclusion_blocks)
.saturating_add(self.safety_blocks)
}

const fn watchtower_proof_window(self) -> u32 {
self.action_window().saturating_add(self.watchtower_proof_blocks)
}

const fn confirmed_action_window(self) -> u32 {
self.evidence_confirmations
.saturating_add(self.inclusion_blocks)
.saturating_add(self.safety_blocks)
}
}

const BITCOIN_TIMING_BUDGET: ProtocolTimingBudget = ProtocolTimingBudget {
evidence_confirmations: 6,
reaction_blocks: 6,
watchtower_proof_blocks: 24,
inclusion_blocks: 6,
safety_blocks: 6,
};

fn min_reaction_blocks(network: Network) -> u32 {
const TESTNET_TIMING_BUDGET: ProtocolTimingBudget = ProtocolTimingBudget {
evidence_confirmations: 10,
reaction_blocks: 2,
watchtower_proof_blocks: 12,
inclusion_blocks: 2,
safety_blocks: 2,
};

const SIGNET_TIMING_BUDGET: ProtocolTimingBudget = ProtocolTimingBudget {
evidence_confirmations: 1,
reaction_blocks: 1,
watchtower_proof_blocks: 2,
inclusion_blocks: 1,
safety_blocks: 1,
};

const REGTEST_TIMING_BUDGET: ProtocolTimingBudget = ProtocolTimingBudget {
evidence_confirmations: 0,
reaction_blocks: 1,
watchtower_proof_blocks: 0,
inclusion_blocks: 0,
safety_blocks: 0,
};

pub fn protocol_timing_budget(network: Network) -> ProtocolTimingBudget {
match network {
Network::Bitcoin | Network::Testnet | Network::Testnet4 => {
let interval = estimated_block_interval_secs(network);
((MIN_REACTION_SECS + interval - 1) / interval) as u32
}
Network::Signet | Network::Regtest => 1,
Network::Bitcoin => BITCOIN_TIMING_BUDGET,
Network::Testnet | Network::Testnet4 => TESTNET_TIMING_BUDGET,
Network::Signet => SIGNET_TIMING_BUDGET,
Network::Regtest => REGTEST_TIMING_BUDGET,
}
}

pub fn validate_timelock_config(network: Network, config: &TimelockConfig) -> Result<()> {
let min_blocks = min_reaction_blocks(network);
let budget = protocol_timing_budget(network);
for (name, value) in [
("connector_z", config.connector_z),
("connector_a", config.connector_a),
Expand All @@ -90,10 +155,10 @@ pub fn validate_timelock_config(network: Network, config: &TimelockConfig) -> Re
("operator_commit", config.operator_commit),
("connector_f", config.connector_f),
] {
if value < min_blocks {
if value < budget.reaction_blocks {
bail!(
"timelock_config.{name} must be at least {min_blocks} blocks \
(~{MIN_REACTION_SECS}s reaction window), got {value}"
"timelock_config.{name} must be at least {} reaction blocks, got {value}",
budget.reaction_blocks,
);
}
}
Expand All @@ -105,52 +170,66 @@ pub fn validate_timelock_config(network: Network, config: &TimelockConfig) -> Re
);
}

ensure_reaction_margin(
"prover_connector",
config.prover_connector,
"connector_d",
config.connector_d,
min_blocks,
let action_window = budget.action_window();
ensure_at_least("connector_a", config.connector_a, action_window)?;
ensure_at_least(
"watchtower_challenge",
config.watchtower_challenge,
budget.watchtower_proof_window(),
)?;
ensure_gt("connector_a", config.connector_a, "min_reaction_blocks", min_blocks)?;
ensure_lt(
ensure_gap_at_least(
"watchtower_challenge",
config.watchtower_challenge,
"operator_ack",
config.operator_ack,
action_window,
)?;
ensure_gap_at_least(
"max(watchtower_challenge, operator_ack)",
config.watchtower_challenge.max(config.operator_ack),
"operator_commit",
config.operator_commit,
budget.confirmed_action_window(),
)?;
ensure_at_least("prover_connector", config.prover_connector, action_window)?;
ensure_gap_at_least(
"prover_connector",
config.prover_connector,
"connector_d",
config.connector_d,
action_window,
)?;
ensure_gap_at_least(
"operator_commit",
config.operator_commit,
"connector_f",
config.connector_f,
budget.confirmed_action_window(),
)?;
ensure_lt("operator_ack", config.operator_ack, "operator_commit", config.operator_commit)?;
ensure_lt("operator_commit", config.operator_commit, "connector_f", config.connector_f)?;

Ok(())
}

fn ensure_reaction_margin(
fn ensure_gap_at_least(
left_name: &str,
left: u32,
right_name: &str,
right: u32,
min_margin: u32,
required_blocks: u32,
) -> Result<()> {
if left.saturating_add(min_margin) >= right {
let actual_blocks = right.saturating_sub(left);
if actual_blocks < required_blocks {
bail!(
"timelock_config.{left_name} must be more than {min_margin} blocks less than \
timelock_config.{right_name}"
"timelock_config.{right_name} - timelock_config.{left_name} must be at least \
{required_blocks} blocks, got {actual_blocks}"
);
}
Ok(())
}

fn ensure_lt(left_name: &str, left: u32, right_name: &str, right: u32) -> Result<()> {
if left >= right {
bail!("timelock_config.{left_name} must be < timelock_config.{right_name}");
}
Ok(())
}

fn ensure_gt(left_name: &str, left: u32, right_name: &str, right: u32) -> Result<()> {
if left <= right {
bail!("timelock_config.{left_name} must be > {right_name} ({right})");
fn ensure_at_least(name: &str, value: u32, required_blocks: u32) -> Result<()> {
if value < required_blocks {
bail!("timelock_config.{name} must be at least {required_blocks} blocks, got {value}");
}
Ok(())
}
Expand Down
29 changes: 24 additions & 5 deletions crates/client/src/btc_chain/esplora_bitcoin_adaptor.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
use crate::btc_chain::bitcoin_adaptor::BitcoinAdaptor;
use crate::timeout_config::get_btc_request_timeout_secs;
use anyhow::anyhow;
use bitcoin::block::Header;
use bitcoin::{Address as BtcAddress, Block, Network, Transaction, Txid};
use esplora_client::{AsyncClient, Builder, MerkleProof, Tx, Utxo};
use std::fmt;
use std::future::Future;
use std::time::Duration;
use tracing::warn;

const TEST_URL: &str = "https://mempool.space/testnet/api";
const MAIN_URL: &str = "https://mempool.space/api";

#[derive(Debug)]
pub struct BtcRpcTimeoutError {
pub request_name: &'static str,
pub timeout_secs: u64,
}

impl fmt::Display for BtcRpcTimeoutError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"esplora request timeout: {}, timeout_secs={}",
self.request_name, self.timeout_secs
)
}
}

impl std::error::Error for BtcRpcTimeoutError {}

pub fn get_esplora_url(network: Network) -> &'static str {
match network {
Network::Bitcoin => MAIN_URL,
Expand Down Expand Up @@ -49,10 +67,11 @@ impl EsploraBitcoinAdaptor {
timeout_secs = self.request_timeout.as_secs(),
"esplora request timeout"
);
Err(anyhow!(
"esplora request timeout: {request_name}, timeout_secs={} ",
self.request_timeout.as_secs()
))
Err(BtcRpcTimeoutError {
request_name,
timeout_secs: self.request_timeout.as_secs(),
}
.into())
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/client/src/btc_chain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::str::FromStr;
pub mod bitcoin_adaptor;
pub mod bitcoin_chain;
mod esplora_bitcoin_adaptor;
pub use esplora_bitcoin_adaptor::BtcRpcTimeoutError;
pub mod mempool_v1_type;
mod mock_bitcoin_adaptor;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS message_debug_reason (
message_id TEXT NOT NULL,
reason_code TEXT NOT NULL,
reason_detail TEXT NOT NULL,
first_seen_at BIGINT NOT NULL,
last_seen_at BIGINT NOT NULL,
occurrences BIGINT NOT NULL DEFAULT 1,
PRIMARY KEY (message_id, reason_code, reason_detail)
);
19 changes: 19 additions & 0 deletions crates/store/migrations/20260812120000_create_p2p_inbox_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS p2p_inbox (
message_id TEXT NOT NULL PRIMARY KEY,
business_id TEXT,
actor TEXT NOT NULL,
from_peer TEXT NOT NULL,
msg_type TEXT NOT NULL,
content BLOB NOT NULL,
content_size BIGINT NOT NULL,
state TEXT NOT NULL DEFAULT 'Pending',
attempt_count BIGINT NOT NULL DEFAULT 0,
next_retry_at BIGINT NOT NULL DEFAULT 0,
lease_until BIGINT NOT NULL DEFAULT 0,
last_error TEXT,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_p2p_inbox_ready
ON p2p_inbox (state, next_retry_at, created_at);
15 changes: 15 additions & 0 deletions crates/store/migrations/20260812121000_create_p2p_outbox_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS p2p_outbox (
message_id TEXT NOT NULL PRIMARY KEY,
msg_type TEXT NOT NULL,
content BLOB NOT NULL,
state TEXT NOT NULL DEFAULT 'Pending',
attempt_count BIGINT NOT NULL DEFAULT 0,
next_retry_at BIGINT NOT NULL DEFAULT 0,
lease_until BIGINT NOT NULL DEFAULT 0,
last_error TEXT,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_p2p_outbox_ready
ON p2p_outbox (state, next_retry_at, created_at);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE p2p_inbox
ADD COLUMN lease_token TEXT NOT NULL DEFAULT '';
Loading
Loading