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
7 changes: 7 additions & 0 deletions libs/types-internal/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ pub enum EventType {
/// The signature of the failed Solana transaction.
signature: Signature,
},
/// A previously submitted Solana transaction has an expired blockhash
/// and a null on-chain status, meaning it will never be executed.
/// The transaction has been marked for resubmission.
ExpiredTransaction {
/// The signature of the expired Solana transaction.
signature: Signature,
},
}

/// The purpose of a submitted Solana transaction.
Expand Down
7 changes: 7 additions & 0 deletions minter/cksol_minter.did
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,13 @@ type EventType = variant {
// The signature of the failed Solana transaction.
signature: Signature;
};
// A previously submitted Solana transaction has an expired blockhash
// and a null on-chain status, meaning it will never be executed.
// The transaction has been marked for resubmission.
ExpiredTransaction : record {
// The signature of the expired Solana transaction.
signature: Signature;
};
};

// A single transaction can deposit to multiple accounts, so the signature alone
Expand Down
3 changes: 3 additions & 0 deletions minter/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ fn get_events(
EventType::FailedTransaction { signature } => event::EventType::FailedTransaction {
signature: signature.into(),
},
EventType::ExpiredTransaction { signature } => event::EventType::ExpiredTransaction {
signature: signature.into(),
},
}
}

Expand Down
12 changes: 11 additions & 1 deletion minter/src/monitor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,18 @@ pub async fn monitor_submitted_transactions<R: CanisterRuntime>(runtime: R) {
return;
}

for signature in expired_signatures {
mutate_state(|state| {
// Skip if the transaction was finalized concurrently.
if state.submitted_transactions().contains_key(&signature) {
process_event(state, EventType::ExpiredTransaction { signature }, &runtime);
}
});
}

let to_resubmit: Vec<_> = read_state(|state| {
expired_signatures
state
.transactions_to_resubmit()
.iter()
.filter_map(|sig| {
state
Expand Down
29 changes: 18 additions & 11 deletions minter/src/monitor/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,11 +260,15 @@ mod resubmission {

monitor_submitted_transactions(runtime).await;

EventsAssert::from_recorded().expect_contains_event_eq(EventType::ResubmittedTransaction {
old_signature,
new_signature,
new_slot: RESUBMISSION_SLOT,
});
EventsAssert::from_recorded()
.expect_contains_event_eq(EventType::ExpiredTransaction {
signature: old_signature,
})
.expect_contains_event_eq(EventType::ResubmittedTransaction {
old_signature,
new_signature,
new_slot: RESUBMISSION_SLOT,
});

read_state(|s| {
assert_eq!(s.submitted_transactions().len(), 1);
Expand Down Expand Up @@ -316,11 +320,15 @@ mod resubmission {

monitor_submitted_transactions(runtime).await;

EventsAssert::from_recorded().expect_contains_event_eq(EventType::ResubmittedTransaction {
old_signature,
new_signature,
new_slot: RESUBMISSION_SLOT,
});
EventsAssert::from_recorded()
.expect_contains_event_eq(EventType::ExpiredTransaction {
signature: old_signature,
})
.expect_contains_event_eq(EventType::ResubmittedTransaction {
old_signature,
new_signature,
new_slot: RESUBMISSION_SLOT,
});
}

#[tokio::test]
Expand Down Expand Up @@ -355,7 +363,6 @@ mod resubmission {
.add_signature([0xA0 + i as u8; 64]);
}

// Round 2: get_recent_slot_and_blockhash for resubmission (getSlot + getBlock)
runtime = runtime
.add_stub_response(SlotResult::Consistent(Ok(RESUBMISSION_SLOT)))
.add_stub_response(BlockResult::Consistent(Ok(confirmed_block())));
Expand Down
3 changes: 3 additions & 0 deletions minter/src/state/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ fn apply_state_transition(state: &mut State, payload: &EventType, timestamp: u64
EventType::FailedTransaction { signature } => {
state.process_transaction_failed(signature);
}
EventType::ExpiredTransaction { signature } => {
state.process_transaction_expired(signature);
}
}
}

Expand Down
9 changes: 9 additions & 0 deletions minter/src/state/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ pub enum EventType {
#[cbor(n(0), with = "cbor::signature")]
signature: Signature,
},
/// A previously submitted Solana transaction has an expired blockhash
/// and a null on-chain status, meaning it will never be executed.
/// The transaction has been marked for resubmission.
#[n(10)]
ExpiredTransaction {
/// The signature of the expired Solana transaction.
#[cbor(n(0), with = "cbor::signature")]
signature: Signature,
},
}

/// Payload of the `AcceptedWithdrawalRequest` event.
Expand Down
37 changes: 37 additions & 0 deletions minter/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ pub struct State {
failed_withdrawal_requests: BTreeMap<LedgerBurnIndex, SentWithdrawalRequest>,
deposits_to_consolidate: BTreeMap<LedgerMintIndex, (Account, Lamport)>,
submitted_transactions: BTreeMap<Signature, SolanaTransaction>,
transactions_to_resubmit: BTreeSet<Signature>,
succeeded_transactions: BTreeSet<Signature>,
failed_transactions: BTreeMap<Signature, SolanaTransaction>,
consolidation_transactions: BTreeMap<Signature, ConsolidationTransaction>,
Expand Down Expand Up @@ -201,6 +202,29 @@ impl State {
&self.submitted_transactions
}

pub fn transactions_to_resubmit(&self) -> &BTreeSet<Signature> {
&self.transactions_to_resubmit
}

pub fn process_transaction_expired(&mut self, signature: &Signature) {
assert!(
!self.succeeded_transactions.contains(signature),
"BUG: cannot mark already succeeded transaction {signature} for resubmission"
);
assert!(
!self.failed_transactions.contains_key(signature),
"BUG: cannot mark already failed transaction {signature} for resubmission"
);
assert!(
self.submitted_transactions.contains_key(signature),
"BUG: cannot mark non-submitted transaction {signature} for resubmission"
);
assert!(
self.transactions_to_resubmit.insert(*signature),
"BUG: transaction {signature} is already queued for resubmission"
);
}

pub fn succeeded_transactions(&self) -> &BTreeSet<Signature> {
&self.succeeded_transactions
}
Expand Down Expand Up @@ -591,6 +615,10 @@ impl State {
None,
"Attempted to resubmit transaction with signature {new_signature:?} that already exists"
);
assert!(
self.transactions_to_resubmit.remove(old_signature),
"BUG: transaction {old_signature} is not queued for resubmission"
);
if let Some(info) = self.consolidation_transactions.remove(old_signature) {
self.consolidation_transactions.insert(*new_signature, info);
}
Expand Down Expand Up @@ -622,6 +650,10 @@ impl State {
.checked_sub(tx_fee)
.expect("BUG: consolidation amount is less than transaction fee");
}
assert!(
!self.transactions_to_resubmit.contains(signature),
"BUG: transaction {signature} is queued for resubmission but is being marked as succeeded"
);
assert!(
self.succeeded_transactions.insert(*signature),
"Attempted to mark transaction {signature:?} as succeeded twice"
Expand All @@ -644,6 +676,10 @@ impl State {
.unwrap_or_else(|| {
panic!("Attempted to mark unknown transaction {signature:?} as failed")
});
assert!(
!self.transactions_to_resubmit.contains(signature),
"BUG: transaction {signature} is queued for resubmission but is being marked as failed"
);
assert_eq!(
self.failed_transactions.insert(*signature, transaction),
None,
Expand Down Expand Up @@ -711,6 +747,7 @@ impl TryFrom<InitArgs> for State {
failed_withdrawal_requests: BTreeMap::new(),
deposits_to_consolidate: BTreeMap::new(),
submitted_transactions: BTreeMap::new(),
transactions_to_resubmit: BTreeSet::new(),
succeeded_transactions: BTreeSet::new(),
failed_transactions: BTreeMap::new(),
consolidation_transactions: BTreeMap::new(),
Expand Down
9 changes: 6 additions & 3 deletions minter/src/state/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ use crate::{
arb::arb_event,
deposit_id,
events::{
accept_deposit, accept_withdrawal, accept_withdrawal_at, fail_transaction,
mint_deposit, resubmit_transaction, submit_withdrawal, succeed_transaction,
accept_deposit, accept_withdrawal, accept_withdrawal_at, expire_transaction,
fail_transaction, mint_deposit, resubmit_transaction, submit_withdrawal,
succeed_transaction,
},
init_balance, init_state, ledger_canister_id,
runtime::TestCanisterRuntime,
Expand Down Expand Up @@ -64,6 +65,7 @@ mod state_from_init_args {
failed_withdrawal_requests: BTreeMap::new(),
deposits_to_consolidate: BTreeMap::new(),
submitted_transactions: BTreeMap::new(),
transactions_to_resubmit: BTreeSet::new(),
succeeded_transactions: BTreeSet::new(),
failed_transactions: BTreeMap::new(),
consolidation_transactions: BTreeMap::new(),
Expand Down Expand Up @@ -617,7 +619,8 @@ mod oldest_incomplete_withdrawal_created_at {
Some(1_000_000_000)
);

// Resubmit the transaction with a new signature
// Expire then resubmit the transaction with a new signature
expire_transaction(signature(0xAA));
resubmit_transaction(signature(0xAA), signature(0xBB), 42);

// created_at timestamps should be unchanged
Expand Down
11 changes: 11 additions & 0 deletions minter/src/test_fixtures/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,16 @@ pub mod events {
});
}

pub fn expire_transaction(signature: Signature) {
mutate_state(|state| {
process_event(
state,
EventType::ExpiredTransaction { signature },
&runtime(),
)
});
}

pub fn resubmit_transaction(
old_signature: Signature,
new_signature: Signature,
Expand Down Expand Up @@ -558,6 +568,7 @@ pub mod arb {
),
arb_signature().prop_map(|signature| EventType::SucceededTransaction { signature }),
arb_signature().prop_map(|signature| EventType::FailedTransaction { signature }),
arb_signature().prop_map(|signature| EventType::ExpiredTransaction { signature }),
]
}

Expand Down
Loading