Skip to content
Open
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
6 changes: 6 additions & 0 deletions crates/client/src/graphs/graph_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,12 @@ impl WithdrawPathsEvent {
WithdrawPathsEvent::WithdrawUnhappyEvent(v) => v.transaction_hash.clone(),
}
}
pub fn graph_id(&self) -> &str {
match self {
WithdrawPathsEvent::WithdrawHappyEvent(v) => &v.graph_id,
WithdrawPathsEvent::WithdrawUnhappyEvent(v) => &v.graph_id,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WithdrawDisprovedEvent {
Expand Down
100 changes: 99 additions & 1 deletion node/src/scheduled_tasks/event_watch/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::utils::{
use alloy::primitives::U256;
use bitvm_lib::actors::Actor;
use client::btc_chain::BTCClient;
use client::goat_chain::GOATClient;
use client::goat_chain::{GOATClient, WithdrawStatus};
use client::graphs::GraphQueryClient;
use client::graphs::graph_query::{
BridgeInEvent, BridgeInRequestEvent, CancelWithdrawEvent, CommitteeResponseEvent,
Expand Down Expand Up @@ -141,6 +141,20 @@ pub async fn fetch_and_handle_gateway_events<'a>(
} else {
tracing::debug!("{summary}");
}
let withdraw_paths_events = retain_withdraw_outcomes_on_gateway(
&goat_client,
withdraw_paths_events,
|event| event.graph_id(),
WithdrawStatus::Complete,
)
.await?;
let withdraw_disproved_events = retain_withdraw_outcomes_on_gateway(
&goat_client,
withdraw_disproved_events,
|event| &event.graph_id,
WithdrawStatus::Disproved,
)
.await?;
handle_user_withdraw_events(storage_processor, init_withdraw_events, cancel_withdraw_events)
.await?;
handle_proceed_withdraw_events(storage_processor, proceed_withdraw_events).await?;
Expand Down Expand Up @@ -340,6 +354,35 @@ async fn graph_belongs_to_instance<'a>(
}
}

/// Keeps only the withdraw outcomes the Gateway contract itself holds.
///
/// A terminal graph status stops `graph_scan` from following the graph on
/// Bitcoin and obsoletes the instance's other graphs, so it must not rest on
/// the subgraph alone: a wrong or compromised subgraph would make the node stop
/// challenging a graph that is still open, letting Take-2 through. An outcome
/// the contract does not hold is dropped; the graph stays followed on Bitcoin,
/// where its real outcome is still observed.
async fn retain_withdraw_outcomes_on_gateway<E>(
goat_client: &GOATClient,
events: Vec<E>,
graph_id: impl Fn(&E) -> &str,
expected: WithdrawStatus,
) -> anyhow::Result<Vec<E>> {
let mut retained = Vec::with_capacity(events.len());
for event in events {
let id = Uuid::from_str(&strip_hex_prefix_owned(graph_id(&event)))?;
let status = goat_client.gateway_get_withdraw_data(&id).await?.status;
if status == expected {
retained.push(event);
} else {
warn!(
"ignore subgraph withdraw outcome for graph {id}: the Gateway holds {status}, not {expected}"
);
}
}
Ok(retained)
}

async fn handle_withdraw_paths_events<'a>(
storage_processor: &mut StorageProcessor<'a>,
withdraw_paths_events: Vec<WithdrawPathsEvent>,
Expand Down Expand Up @@ -731,6 +774,61 @@ mod tests {
}
}

#[tokio::test]
async fn only_withdraw_outcomes_the_gateway_holds_are_kept() {
use client::goat_chain::WithdrawData;
let (goat_client, mock) = GOATClient::new_mock_client();
let withdraw = |status| {
Ok(WithdrawData {
pegin_txid: [0; 32],
operator_address: [0; 20],
status,
instance_id: [0; 16],
lock_amount: U256::ZERO,
btc_block_height_withdraw: U256::ZERO,
})
};
let (settled, open, disproved) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4());
mock.set_withdraw_data(*settled.as_bytes(), withdraw(WithdrawStatus::Complete));
mock.set_withdraw_data(*open.as_bytes(), withdraw(WithdrawStatus::Processing));
mock.set_withdraw_data(*disproved.as_bytes(), withdraw(WithdrawStatus::Disproved));
let ids = [settled, open, disproved].map(|id| format!("0x{}", id.simple()));

let kept = retain_withdraw_outcomes_on_gateway(
&goat_client,
ids.to_vec(),
|id| id,
WithdrawStatus::Complete,
)
.await
.unwrap();
// A subgraph Take outcome for a withdraw still in its challenge
// period, or one the Gateway disproved, is not written.
assert_eq!(kept, [ids[0].clone()]);
let kept = retain_withdraw_outcomes_on_gateway(
&goat_client,
ids.to_vec(),
|id| id,
WithdrawStatus::Disproved,
)
.await
.unwrap();
assert_eq!(kept, [ids[2].clone()]);

// A failed lookup fails the range, so it is fetched again.
let unknown = format!("0x{}", Uuid::new_v4().simple());
assert!(
retain_withdraw_outcomes_on_gateway(
&goat_client,
vec![unknown],
|id| id,
WithdrawStatus::Complete,
)
.await
.is_err()
);
}

#[tokio::test]
async fn a_posted_graph_unknown_locally_is_pulled_by_the_watching_roles() {
let (db, _dir) = db().await;
Expand Down
Loading