@ -70,158 +70,146 @@ async fn run_until_internal(
) -> Result < AliceState > {
info ! ( "Current state: {}" , state ) ;
if is_target_state ( & state ) {
Ok ( state )
} else {
match state {
AliceState ::Started { state3 } = > {
timeout (
env_config . bob_time_to_act ,
bitcoin_wallet
. watch_until_status ( & state3 . tx_lock , | status | status . has_been_seen ( ) ) ,
)
. await
. context ( "Failed to find lock Bitcoin tx" ) ? ? ;
bitcoin_wallet
. watch_until_status ( & state3 . tx_lock , | status | {
status . is_confirmed_with ( env_config . bitcoin_finality_confirmations )
} )
. await ? ;
let state = AliceState ::BtcLocked { state3 } ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
AliceState ::BtcLocked { state3 } = > {
// Record the current monero wallet block height so we don't have to scan from
// block 0 for scenarios where we create a refund wallet.
let monero_wallet_restore_blockheight = monero_wallet . block_height ( ) . await ? ;
return Ok ( state ) ;
}
let transfer_proof = monero_wallet
. transfer ( state3 . lock_xmr_transfer_request ( ) )
. await ? ;
match state {
AliceState ::Started { state3 } = > {
timeout (
env_config . bob_time_to_act ,
bitcoin_wallet . watch_until_status ( & state3 . tx_lock , | status | status . has_been_seen ( ) ) ,
)
. await
. context ( "Failed to find lock Bitcoin tx" ) ? ? ;
bitcoin_wallet
. watch_until_status ( & state3 . tx_lock , | status | {
status . is_confirmed_with ( env_config . bitcoin_finality_confirmations )
} )
. await ? ;
let state = AliceState ::BtcLocked { state3 } ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
AliceState ::BtcLocked { state3 } = > {
// Record the current monero wallet block height so we don't have to scan from
// block 0 for scenarios where we create a refund wallet.
let monero_wallet_restore_blockheight = monero_wallet . block_height ( ) . await ? ;
// TODO(Franck): Wait for Monero to be confirmed once
// Waiting for XMR confirmations should not be done in here, but in a separate
// state! We have to record that Alice has already sent the transaction.
// Otherwise Alice might publish the lock tx twice!
let transfer_proof = monero_wallet
. transfer ( state3 . lock_xmr_transfer_request ( ) )
. await ? ;
event_loop_handle
. send_transfer_proof ( transfer_proof )
. await ? ;
// TODO(Franck): Wait for Monero to be confirmed once
// Waiting for XMR confirmations should not be done in here, but in a separate
// state! We have to record that Alice has already sent the transaction.
// Otherwise Alice might publish the lock tx twice!
let state = AliceState ::XmrLocked {
state3 ,
monero_wallet_restore_blockheight ,
} ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
AliceState ::XmrLocked {
event_loop_handle
. send_transfer_proof ( transfer_proof )
. await ? ;
let state = AliceState ::XmrLocked {
state3 ,
monero_wallet_restore_blockheight ,
} = > {
let state = match state3 . expired_timelocks ( bitcoin_wallet . as_ref ( ) ) . await ? {
ExpiredTimelocks ::None = > {
select ! {
_ = state3 . wait_for_cancel_timelock_to_expire ( bitcoin_wallet . as_ref ( ) ) = > {
AliceState ::CancelTimelockExpired {
state3 ,
monero_wallet_restore_blockheight ,
}
} ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
AliceState ::XmrLocked {
state3 ,
monero_wallet_restore_blockheight ,
} = > {
let state = match state3 . expired_timelocks ( bitcoin_wallet . as_ref ( ) ) . await ? {
ExpiredTimelocks ::None = > {
select ! {
_ = state3 . wait_for_cancel_timelock_to_expire ( bitcoin_wallet . as_ref ( ) ) = > {
AliceState ::CancelTimelockExpired {
state3 ,
monero_wallet_restore_blockheight ,
}
enc_sig = event_loop_handle . recv_encrypted_signature ( ) = > {
tracing ::info ! ( "Received encrypted signature" ) ;
}
enc_sig = event_loop_handle . recv_encrypted_signature ( ) = > {
tracing ::info ! ( "Received encrypted signature" ) ;
AliceState ::EncSigLearned {
state3 ,
encrypted_signature : Box ::new ( enc_sig ? ) ,
monero_wallet_restore_blockheight ,
}
AliceState ::EncSigLearned {
state3 ,
encrypted_signature : Box ::new ( enc_sig ? ) ,
monero_wallet_restore_blockheight ,
}
}
}
_ = > AliceState ::CancelTimelockExpired {
state3 ,
monero_wallet_restore_blockheight ,
} ,
} ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet . clone ( ) ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
AliceState ::EncSigLearned {
state3 ,
encrypted_signature ,
monero_wallet_restore_blockheight ,
} = > {
let state = match state3 . expired_timelocks ( bitcoin_wallet . as_ref ( ) ) . await ? {
ExpiredTimelocks ::None = > {
match TxRedeem ::new ( & state3 . tx_lock , & state3 . redeem_address ) . complete (
* encrypted_signature ,
state3 . a . clone ( ) ,
state3 . s_a . to_secpfun_scalar ( ) ,
state3 . B ,
) {
Ok ( tx ) = > match bitcoin_wallet . broadcast ( tx , "redeem" ) . await {
Ok ( ( _ , finality ) ) = > match finality . await {
Ok ( _ ) = > AliceState ::BtcRedeemed ,
Err ( e ) = > {
bail ! ( "Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed." , e )
}
} ,
}
_ = > AliceState ::CancelTimelockExpired {
state3 ,
monero_wallet_restore_blockheight ,
} ,
} ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet . clone ( ) ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
AliceState ::EncSigLearned {
state3 ,
encrypted_signature ,
monero_wallet_restore_blockheight ,
} = > {
let state = match state3 . expired_timelocks ( bitcoin_wallet . as_ref ( ) ) . await ? {
ExpiredTimelocks ::None = > {
match TxRedeem ::new ( & state3 . tx_lock , & state3 . redeem_address ) . complete (
* encrypted_signature ,
state3 . a . clone ( ) ,
state3 . s_a . to_secpfun_scalar ( ) ,
state3 . B ,
) {
Ok ( tx ) = > match bitcoin_wallet . broadcast ( tx , "redeem" ) . await {
Ok ( ( _ , finality ) ) = > match finality . await {
Ok ( _ ) = > AliceState ::BtcRedeemed ,
Err ( e ) = > {
error ! ( "Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried." , e ) ;
state3
. wait_for_cancel_timelock_to_expire ( bitcoin_wallet . as_ref ( ) )
. await ? ;
AliceState ::CancelTimelockExpired {
state3 ,
monero_wallet_restore_blockheight ,
}
bail ! ( "Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed." , e )
}
} ,
Err ( e ) = > {
error ! ( " Construct ing the redeem transaction failed with {}, attempting to wait for cancellation now.", e ) ;
error ! ( "Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried." , e ) ;
state3
. wait_for_cancel_timelock_to_expire ( bitcoin_wallet . as_ref ( ) )
. await ? ;
@ -231,236 +219,241 @@ async fn run_until_internal(
monero_wallet_restore_blockheight ,
}
}
} ,
Err ( e ) = > {
error ! ( "Constructing the redeem transaction failed with {}, attempting to wait for cancellation now." , e ) ;
state3
. wait_for_cancel_timelock_to_expire ( bitcoin_wallet . as_ref ( ) )
. await ? ;
AliceState ::CancelTimelockExpired {
state3 ,
monero_wallet_restore_blockheight ,
}
}
}
_ = > AliceState ::CancelTimelockExpired {
state3 ,
monero_wallet_restore_blockheight ,
} ,
} ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
AliceState ::CancelTimelockExpired {
state3 ,
monero_wallet_restore_blockheight ,
} = > {
let tx_cancel = state3 . tx_cancel ( ) ;
// If Bob hasn't yet broadcasted the tx cancel, we do it
if bitcoin_wallet
. get_raw_transaction ( tx_cancel . txid ( ) )
. await
. is_err ( )
{
let transaction = tx_cancel
. complete_as_alice (
state3 . a . clone ( ) ,
state3 . B ,
state3 . tx_cancel_sig_bob . clone ( ) ,
)
. context ( "Failed to complete Bitcoin cancel transaction" ) ? ;
if let Err ( e ) = bitcoin_wallet . broadcast ( transaction , "cancel" ) . await {
tracing ::debug ! (
"Assuming transaction is already broadcasted because: {:#}" ,
e
)
}
// TODO(Franck): Wait until transaction is mined and
// returned mined block height
}
let state = AliceState ::BtcCancelled {
_ = > AliceState ::CancelTimelockExpired {
state3 ,
monero_wallet_restore_blockheight ,
} ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
AliceState ::BtcCancelled {
state3 ,
monero_wallet_restore_blockheight ,
} = > {
let tx_refund = state3 . tx_refund ( ) ;
let tx_cancel = state3 . tx_cancel ( ) ;
let seen_refund_tx =
bitcoin_wallet . watch_until_status ( & tx_refund , | status | status . has_been_seen ( ) ) ;
let punish_timelock_expired = bitcoin_wallet
. watch_until_status ( & tx_cancel , | status | {
status . is_confirmed_with ( state3 . punish_timelock )
} ) ;
let state = tokio ::select ! {
seen_refund = seen_refund_tx = > {
seen_refund . context ( "Failed to monitor refund transaction" ) ? ;
let published_refund_tx = bitcoin_wallet . get_raw_transaction ( tx_refund . txid ( ) ) . await ? ;
let spend_key = tx_refund . extract_monero_private_key (
published_refund_tx ,
state3 . s_a ,
state3 . a . clone ( ) ,
state3 . S_b_bitcoin ,
) ? ;
AliceState ::BtcRefunded {
spend_key ,
state3 ,
monero_wallet_restore_blockheight ,
}
}
_ = punish_timelock_expired = > {
AliceState ::BtcPunishable {
state3 ,
monero_wallet_restore_blockheight ,
}
}
} ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet . clone ( ) ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
} ,
} ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
AliceState ::CancelTimelockExpired {
state3 ,
monero_wallet_restore_blockheight ,
} = > {
let tx_cancel = state3 . tx_cancel ( ) ;
// If Bob hasn't yet broadcasted the tx cancel, we do it
if bitcoin_wallet
. get_raw_transaction ( tx_cancel . txid ( ) )
. await
. is_err ( )
{
let transaction = tx_cancel
. complete_as_alice ( state3 . a . clone ( ) , state3 . B , state3 . tx_cancel_sig_bob . clone ( ) )
. context ( "Failed to complete Bitcoin cancel transaction" ) ? ;
if let Err ( e ) = bitcoin_wallet . broadcast ( transaction , "cancel" ) . await {
tracing ::debug ! (
"Assuming transaction is already broadcasted because: {:#}" ,
e
)
}
// TODO(Franck): Wait until transaction is mined and
// returned mined block height
}
AliceState ::BtcRefunded {
spend_key ,
state3 ,
monero_wallet_restore_blockheight ,
} = > {
let view_key = state3 . v ;
monero_wallet
. create_from ( spend_key , view_key , monero_wallet_restore_blockheight )
. await ? ;
let state = AliceState ::XmrRefunded ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
Ok ( state )
}
AliceState ::BtcPunishable {
let state = AliceState ::BtcCancelled {
state3 ,
monero_wallet_restore_blockheight ,
} = > {
let signed_tx_punish = state3 . tx_punish ( ) . complete (
state3 . tx_punish_sig_bob . clone ( ) ,
state3 . a . clone ( ) ,
state3 . B ,
) ? ;
let punish_tx_finalised = async {
let ( txid , finality ) =
bitcoin_wallet . broadcast ( signed_tx_punish , "punish" ) . await ? ;
finality . await ? ;
Result ::< _ , anyhow ::Error > ::Ok ( txid )
} ;
let tx_refund = state3 . tx_refund ( ) ;
let refund_tx_seen =
bitcoin_wallet . watch_until_status ( & tx_refund , | status | status . has_been_seen ( ) ) ;
pin_mut ! ( punish_tx_finalised ) ;
pin_mut ! ( refund_tx_seen ) ;
match select ( refund_tx_seen , punish_tx_finalised ) . await {
Either ::Left ( ( Ok ( ( ) ) , _ ) ) = > {
let published_refund_tx =
bitcoin_wallet . get_raw_transaction ( tx_refund . txid ( ) ) . await ? ;
let spend_key = tx_refund . extract_monero_private_key (
published_refund_tx ,
state3 . s_a ,
state3 . a . clone ( ) ,
state3 . S_b_bitcoin ,
) ? ;
let state = AliceState ::BtcRefunded {
spend_key ,
state3 ,
monero_wallet_restore_blockheight ,
} ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet . clone ( ) ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
Either ::Left ( ( Err ( e ) , _ ) ) = > {
bail ! ( e . context ( "Failed to monitor refund transaction" ) )
} ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
AliceState ::BtcCancelled {
state3 ,
monero_wallet_restore_blockheight ,
} = > {
let tx_refund = state3 . tx_refund ( ) ;
let tx_cancel = state3 . tx_cancel ( ) ;
let seen_refund_tx =
bitcoin_wallet . watch_until_status ( & tx_refund , | status | status . has_been_seen ( ) ) ;
let punish_timelock_expired = bitcoin_wallet . watch_until_status ( & tx_cancel , | status | {
status . is_confirmed_with ( state3 . punish_timelock )
} ) ;
let state = tokio ::select ! {
seen_refund = seen_refund_tx = > {
seen_refund . context ( "Failed to monitor refund transaction" ) ? ;
let published_refund_tx = bitcoin_wallet . get_raw_transaction ( tx_refund . txid ( ) ) . await ? ;
let spend_key = tx_refund . extract_monero_private_key (
published_refund_tx ,
state3 . s_a ,
state3 . a . clone ( ) ,
state3 . S_b_bitcoin ,
) ? ;
AliceState ::BtcRefunded {
spend_key ,
state3 ,
monero_wallet_restore_blockheight ,
}
Either ::Right ( _ ) = > {
let state = AliceState ::BtcPunished ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet . clone ( ) ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
_ = punish_timelock_expired = > {
AliceState ::BtcPunishable {
state3 ,
monero_wallet_restore_blockheight ,
}
}
} ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet . clone ( ) ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
AliceState ::BtcRefunded {
spend_key ,
state3 ,
monero_wallet_restore_blockheight ,
} = > {
let view_key = state3 . v ;
monero_wallet
. create_from ( spend_key , view_key , monero_wallet_restore_blockheight )
. await ? ;
let state = AliceState ::XmrRefunded ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
Ok ( state )
}
AliceState ::BtcPunishable {
state3 ,
monero_wallet_restore_blockheight ,
} = > {
let signed_tx_punish = state3 . tx_punish ( ) . complete (
state3 . tx_punish_sig_bob . clone ( ) ,
state3 . a . clone ( ) ,
state3 . B ,
) ? ;
let punish_tx_finalised = async {
let ( txid , finality ) = bitcoin_wallet . broadcast ( signed_tx_punish , "punish" ) . await ? ;
finality . await ? ;
Result ::< _ , anyhow ::Error > ::Ok ( txid )
} ;
let tx_refund = state3 . tx_refund ( ) ;
let refund_tx_seen =
bitcoin_wallet . watch_until_status ( & tx_refund , | status | status . has_been_seen ( ) ) ;
pin_mut ! ( punish_tx_finalised ) ;
pin_mut ! ( refund_tx_seen ) ;
match select ( refund_tx_seen , punish_tx_finalised ) . await {
Either ::Left ( ( Ok ( ( ) ) , _ ) ) = > {
let published_refund_tx =
bitcoin_wallet . get_raw_transaction ( tx_refund . txid ( ) ) . await ? ;
let spend_key = tx_refund . extract_monero_private_key (
published_refund_tx ,
state3 . s_a ,
state3 . a . clone ( ) ,
state3 . S_b_bitcoin ,
) ? ;
let state = AliceState ::BtcRefunded {
spend_key ,
state3 ,
monero_wallet_restore_blockheight ,
} ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet . clone ( ) ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
Either ::Left ( ( Err ( e ) , _ ) ) = > {
bail ! ( e . context ( "Failed to monitor refund transaction" ) )
}
Either ::Right ( _ ) = > {
let state = AliceState ::BtcPunished ;
let db_state = ( & state ) . into ( ) ;
db . insert_latest_state ( swap_id , database ::Swap ::Alice ( db_state ) )
. await ? ;
run_until_internal (
state ,
is_target_state ,
event_loop_handle ,
bitcoin_wallet . clone ( ) ,
monero_wallet ,
env_config ,
swap_id ,
db ,
)
. await
}
}
AliceState ::XmrRefunded = > Ok ( AliceState ::XmrRefunded ) ,
AliceState ::BtcRedeemed = > Ok ( AliceState ::BtcRedeemed ) ,
AliceState ::BtcPunished = > Ok ( AliceState ::BtcPunished ) ,
AliceState ::SafelyAborted = > Ok ( AliceState ::SafelyAborted ) ,
}
AliceState ::XmrRefunded = > Ok ( AliceState ::XmrRefunded ) ,
AliceState ::BtcRedeemed = > Ok ( AliceState ::BtcRedeemed ) ,
AliceState ::BtcPunished = > Ok ( AliceState ::BtcPunished ) ,
AliceState ::SafelyAborted = > Ok ( AliceState ::SafelyAborted ) ,
}
}