use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use fedimint_core::TransactionId;
use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
use fedimint_core::time::duration_since_epoch;
use fedimint_core::transaction::{Transaction, TransactionSubmissionOutcome};
use fedimint_core::util::backoff_util::custom_backoff;
use fedimint_core::util::retry;
use fedimint_logging::LOG_CLIENT_NET_API;
use tokio::sync::watch;
use tracing::{debug, warn};
use crate::sm::{Context, DynContext, State, StateTransition};
use crate::{
DynGlobalClientContext, DynState, TxAcceptedEvent, TxRejectedEvent, TxSubmissionStalledEvent,
};
pub const TRANSACTION_SUBMISSION_MODULE_INSTANCE: ModuleInstanceId = 0xffff;
const SUBMISSION_STALL_WARN_AFTER: Duration = Duration::from_mins(30);
const SUBMISSION_STALL_WARN_INTERVAL: Duration = Duration::from_mins(30);
#[derive(Debug, Clone)]
pub struct TxSubmissionContext;
impl Context for TxSubmissionContext {
const KIND: Option<ModuleKind> = None;
}
impl IntoDynInstance for TxSubmissionContext {
type DynType = DynContext;
fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
DynContext::from_typed(instance_id, self)
}
}
#[cfg_attr(doc, aquamarine::aquamarine)]
#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
pub struct TxSubmissionStatesSM {
pub operation_id: OperationId,
pub state: TxSubmissionStates,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
pub enum TxSubmissionStates {
Created(Transaction),
Accepted(TransactionId),
Rejected(TransactionId, String),
NonRetryableError(String),
}
impl State for TxSubmissionStatesSM {
type ModuleContext = TxSubmissionContext;
fn transitions(
&self,
_context: &Self::ModuleContext,
global_context: &DynGlobalClientContext,
) -> Vec<StateTransition<Self>> {
let operation_id = self.operation_id;
let (tx_submitted_sender, tx_submitted_receiver) = watch::channel(false);
match self.state.clone() {
TxSubmissionStates::Created(transaction) => {
let txid = transaction.tx_hash();
vec![
StateTransition::new(
TxSubmissionStates::trigger_created_rejected(
transaction.clone(),
global_context.clone(),
tx_submitted_sender,
operation_id,
),
{
let global_context = global_context.clone();
move |sm_dbtx, error, _| {
let global_context = global_context.clone();
Box::pin(async move {
global_context
.log_event(
sm_dbtx,
TxRejectedEvent {
txid,
operation_id,
error: error.clone(),
},
)
.await;
TxSubmissionStatesSM {
state: TxSubmissionStates::Rejected(txid, error),
operation_id,
}
})
}
},
),
StateTransition::new(
TxSubmissionStates::trigger_created_accepted(
txid,
global_context.clone(),
tx_submitted_receiver,
),
{
let global_context = global_context.clone();
move |sm_dbtx, (), _| {
let global_context = global_context.clone();
Box::pin(async move {
global_context
.log_event(sm_dbtx, TxAcceptedEvent { txid, operation_id })
.await;
TxSubmissionStatesSM {
state: TxSubmissionStates::Accepted(txid),
operation_id,
}
})
}
},
),
]
}
TxSubmissionStates::Accepted(..)
| TxSubmissionStates::Rejected(..)
| TxSubmissionStates::NonRetryableError(..) => {
vec![]
}
}
}
fn operation_id(&self) -> OperationId {
self.operation_id
}
fn fmt_visualization(&self, f: &mut dyn std::fmt::Write, indent: &str) -> std::fmt::Result {
match &self.state {
TxSubmissionStates::Created(tx) => {
let txid = tx.tx_hash();
write!(
f,
"{indent}TxSubmissionStatesSM\n{indent} state: Created txid={} inputs={} outputs={}",
txid.fmt_short(),
tx.inputs.len(),
tx.outputs.len(),
)
}
TxSubmissionStates::Accepted(txid) => {
write!(
f,
"{indent}TxSubmissionStatesSM\n{indent} state: Accepted txid={}",
txid.fmt_short(),
)
}
TxSubmissionStates::Rejected(txid, err) => {
write!(
f,
"{indent}TxSubmissionStatesSM\n{indent} state: Rejected txid={} error={err}",
txid.fmt_short(),
)
}
TxSubmissionStates::NonRetryableError(err) => {
write!(
f,
"{indent}TxSubmissionStatesSM\n{indent} state: NonRetryableError error={err}",
)
}
}
}
}
impl TxSubmissionStates {
async fn trigger_created_rejected(
transaction: Transaction,
context: DynGlobalClientContext,
tx_submitted: watch::Sender<bool>,
operation_id: OperationId,
) -> String {
let txid = transaction.tx_hash();
debug!(target: LOG_CLIENT_NET_API, %txid, "Submitting transaction");
let started_s = duration_since_epoch().as_secs();
let attempts = AtomicU64::new(0);
let next_alert_deadline_s =
AtomicU64::new(started_s + SUBMISSION_STALL_WARN_AFTER.as_secs());
retry(
"tx-submit-sm",
custom_backoff(Duration::from_secs(2), Duration::from_mins(10), None),
|| async {
let attempt = attempts.fetch_add(1, Ordering::Relaxed).saturating_add(1);
if let TransactionSubmissionOutcome(Err(transaction_error)) = context
.api()
.submit_transaction(transaction.clone())
.await
.try_into_inner(context.decoders())?
{
Ok(transaction_error.to_string())
} else {
debug!(
target: LOG_CLIENT_NET_API,
%txid,
"Transaction submission accepted by peer, awaiting consensus",
);
tx_submitted.send_replace(true);
let now_s = duration_since_epoch().as_secs();
if next_alert_deadline_s.load(Ordering::Relaxed) <= now_s {
next_alert_deadline_s.store(
now_s + SUBMISSION_STALL_WARN_INTERVAL.as_secs(),
Ordering::Relaxed,
);
let elapsed_s = now_s.saturating_sub(started_s);
warn!(
target: LOG_CLIENT_NET_API,
%txid,
operation_id = %operation_id.fmt_short(),
%attempt,
%elapsed_s,
"Transaction neither accepted nor rejected; still re-submitting",
);
context
.log_event_no_dbtx(TxSubmissionStalledEvent {
txid,
operation_id,
attempt,
elapsed_s,
})
.await;
}
Err(SubmitAttemptError::StillValid)
}
},
)
.await
.expect("Number of retries is has no limit")
}
async fn trigger_created_accepted(
txid: TransactionId,
context: DynGlobalClientContext,
mut tx_submitted: watch::Receiver<bool>,
) {
let _ = tx_submitted.wait_for(|submitted| *submitted).await;
context.api().await_transaction(txid).await;
debug!(target: LOG_CLIENT_NET_API, %txid, "Transaction accepted in consensus");
}
}
#[derive(Debug, thiserror::Error)]
enum SubmitAttemptError {
#[error(transparent)]
Decode(#[from] DecodeError),
#[error("Transaction is still valid")]
StillValid,
}
impl IntoDynInstance for TxSubmissionStatesSM {
type DynType = DynState;
fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
DynState::from_typed(instance_id, self)
}
}
pub fn tx_submission_sm_decoder() -> Decoder {
let mut decoder_builder = Decoder::builder_system();
decoder_builder.with_decodable_type::<TxSubmissionStatesSM>();
decoder_builder.build()
}