Skip to content

Commit

Permalink
feat!: add scanned transaction handling for one-sided payments with c…
Browse files Browse the repository at this point in the history
…allbacks (#3794)


Description
---
- Added separate statuses for scanned one-sided transactions to enable unique events and validation.
- Added validation for one-sided payments updating their mined height and confirmations.
- Added callbacks for scanned one-sided transactions (unconfirmed and confirmed)
- Updated the wallet FFI with the callbacks.

Motivation and Context
---
Currently when a one-sided output is found by scanning the blockchain a Faux transaction is created for it with the Imported status, the same as for a manually imported output.  Outputs found by scanning (for one-sided payments) should be handled differently to a manual import and their mined height and number confirmations should be validated.

How Has This Been Tested?
---
- Unit tests
- Cucumber tests
- System level testing
  • Loading branch information
hansieodendaal committed Feb 9, 2022
1 parent e6e845c commit 5453c9e
Show file tree
Hide file tree
Showing 41 changed files with 1,386 additions and 407 deletions.
14 changes: 14 additions & 0 deletions applications/ffi_client/index.js
Expand Up @@ -69,6 +69,18 @@ try {
console.log("txMinedUnconfirmed: ", ptr, confirmations);
}
);
// callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut TariCompletedTransaction),
const txFauxConfirmed = ffi.Callback("void", ["pointer"], function (ptr) {
console.log("txFauxConfirmed: ", ptr);
});
// callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64),
const txFauxUnconfirmed = ffi.Callback(
"void",
["pointer"],
function (ptr, confirmations) {
console.log("txFauxUnconfirmed: ", ptr, confirmations);
}
);
// callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool),
const directSendResult = ffi.Callback("void", [u64, bool], function (i, j) {
console.log("directSendResult: ", i, j);
Expand Down Expand Up @@ -112,6 +124,8 @@ try {
txBroadcast,
txMined,
txMinedUnconfirmed,
txFauxConfirmed,
txFauxUnconfirmed,
directSendResult,
safResult,
txCancelled,
Expand Down
3 changes: 3 additions & 0 deletions applications/ffi_client/lib/index.js
Expand Up @@ -66,6 +66,9 @@ const libWallet = ffi.Library("./libtari_wallet_ffi.dylib", {
fn,
fn,
fn,
fn,
fn,
bool,
errPtr,
],
],
Expand Down
12 changes: 12 additions & 0 deletions applications/ffi_client/recovery.js
Expand Up @@ -80,6 +80,18 @@ try {
console.log("txMinedUnconfirmed: ", ptr, confirmations);
}
);
// callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut TariCompletedTransaction),
const txFauxConfirmed = ffi.Callback("void", ["pointer"], function (ptr) {
console.log("txFauxConfirmed: ", ptr);
});
// callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64),
const txFauxUnconfirmed = ffi.Callback(
"void",
["pointer"],
function (ptr, confirmations) {
console.log("txFauxUnconfirmed: ", ptr, confirmations);
}
);
// callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool),
const directSendResult = ffi.Callback("void", [u64, bool], function (i, j) {
console.log("directSendResult: ", i, j);
Expand Down
4 changes: 4 additions & 0 deletions applications/tari_app_grpc/proto/wallet.proto
Expand Up @@ -192,6 +192,10 @@ enum TransactionStatus {
TRANSACTION_STATUS_NOT_FOUND = 7;
// The transaction was rejected by the mempool
TRANSACTION_STATUS_REJECTED = 8;
// This is faux transaction mainly for one-sided transaction outputs or wallet recovery outputs have been found
TRANSACTION_STATUS_FAUX_UNCONFIRMED = 9;
// All Imported and FauxUnconfirmed transactions will end up with this status when the outputs have been confirmed
TRANSACTION_STATUS_FAUX_CONFIRMED = 10;
}

message GetCompletedTransactionsRequest { }
Expand Down
2 changes: 2 additions & 0 deletions applications/tari_app_grpc/src/conversions/transaction.rs
Expand Up @@ -98,6 +98,8 @@ impl From<TransactionStatus> for grpc::TransactionStatus {
Pending => grpc::TransactionStatus::Pending,
Coinbase => grpc::TransactionStatus::Coinbase,
Rejected => grpc::TransactionStatus::Rejected,
FauxUnconfirmed => grpc::TransactionStatus::FauxUnconfirmed,
FauxConfirmed => grpc::TransactionStatus::FauxConfirmed,
}
}
}
Expand Down
Expand Up @@ -94,13 +94,15 @@ impl WalletEventMonitor {
self.trigger_balance_refresh();
notifier.transaction_received(tx_id);
},
TransactionEvent::TransactionMinedUnconfirmed{tx_id, num_confirmations, is_valid: _} => {
TransactionEvent::TransactionMinedUnconfirmed{tx_id, num_confirmations, is_valid: _} |
TransactionEvent::FauxTransactionUnconfirmed{tx_id, num_confirmations, is_valid: _}=> {
self.trigger_confirmations_refresh(tx_id, num_confirmations).await;
self.trigger_tx_state_refresh(tx_id).await;
self.trigger_balance_refresh();
notifier.transaction_mined_unconfirmed(tx_id, num_confirmations);
},
TransactionEvent::TransactionMined{tx_id, is_valid: _} => {
TransactionEvent::TransactionMined{tx_id, is_valid: _} |
TransactionEvent::FauxTransactionConfirmed{tx_id, is_valid: _}=> {
self.trigger_confirmations_cleanup(tx_id).await;
self.trigger_tx_state_refresh(tx_id).await;
self.trigger_balance_refresh();
Expand All @@ -114,7 +116,8 @@ impl WalletEventMonitor {
TransactionEvent::ReceivedTransaction(tx_id) |
TransactionEvent::ReceivedTransactionReply(tx_id) |
TransactionEvent::TransactionBroadcast(tx_id) |
TransactionEvent::TransactionMinedRequestTimedOut(tx_id) | TransactionEvent::TransactionImported(tx_id) => {
TransactionEvent::TransactionMinedRequestTimedOut(tx_id) |
TransactionEvent::TransactionImported(tx_id) => {
self.trigger_tx_state_refresh(tx_id).await;
self.trigger_balance_refresh();
},
Expand Down
62 changes: 61 additions & 1 deletion base_layer/common_types/src/transaction.rs
Expand Up @@ -17,7 +17,7 @@ pub enum TransactionStatus {
Broadcast,
/// This transaction has been mined and included in a block.
MinedUnconfirmed,
/// This transaction was generated as part of importing a spendable UTXO
/// This transaction was generated as part of importing a spendable unblinded UTXO
Imported,
/// This transaction is still being negotiated by the parties
Pending,
Expand All @@ -27,6 +27,27 @@ pub enum TransactionStatus {
MinedConfirmed,
/// This transaction was Rejected by the mempool
Rejected,
/// This is faux transaction mainly for one-sided transaction outputs or wallet recovery outputs have been found
FauxUnconfirmed,
/// All Imported and FauxUnconfirmed transactions will end up with this status when the outputs have been confirmed
FauxConfirmed,
}

impl TransactionStatus {
pub fn is_faux(&self) -> bool {
match self {
TransactionStatus::Completed => false,
TransactionStatus::Broadcast => false,
TransactionStatus::MinedUnconfirmed => false,
TransactionStatus::Imported => true,
TransactionStatus::Pending => false,
TransactionStatus::Coinbase => false,
TransactionStatus::MinedConfirmed => false,
TransactionStatus::Rejected => false,
TransactionStatus::FauxUnconfirmed => true,
TransactionStatus::FauxConfirmed => true,
}
}
}

#[derive(Debug, Error)]
Expand All @@ -48,6 +69,8 @@ impl TryFrom<i32> for TransactionStatus {
5 => Ok(TransactionStatus::Coinbase),
6 => Ok(TransactionStatus::MinedConfirmed),
7 => Ok(TransactionStatus::Rejected),
8 => Ok(TransactionStatus::FauxUnconfirmed),
9 => Ok(TransactionStatus::FauxConfirmed),
code => Err(TransactionConversionError { code }),
}
}
Expand All @@ -71,6 +94,43 @@ impl Display for TransactionStatus {
TransactionStatus::Pending => write!(f, "Pending"),
TransactionStatus::Coinbase => write!(f, "Coinbase"),
TransactionStatus::Rejected => write!(f, "Rejected"),
TransactionStatus::FauxUnconfirmed => write!(f, "FauxUnconfirmed"),
TransactionStatus::FauxConfirmed => write!(f, "FauxConfirmed"),
}
}
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ImportStatus {
/// This transaction import status is used when importing a spendable UTXO
Imported,
/// This transaction import status is used when a one-sided transaction has been scanned but is unconfirmed
FauxUnconfirmed,
/// This transaction import status is used when a one-sided transaction has been scanned and confirmed
FauxConfirmed,
}

impl TryFrom<ImportStatus> for TransactionStatus {
type Error = TransactionConversionError;

fn try_from(value: ImportStatus) -> Result<Self, Self::Error> {
match value {
ImportStatus::Imported => Ok(TransactionStatus::Imported),
ImportStatus::FauxUnconfirmed => Ok(TransactionStatus::FauxUnconfirmed),
ImportStatus::FauxConfirmed => Ok(TransactionStatus::FauxConfirmed),
}
}
}

impl TryFrom<TransactionStatus> for ImportStatus {
type Error = TransactionConversionError;

fn try_from(value: TransactionStatus) -> Result<Self, Self::Error> {
match value {
TransactionStatus::Imported => Ok(ImportStatus::Imported),
TransactionStatus::FauxUnconfirmed => Ok(ImportStatus::FauxUnconfirmed),
TransactionStatus::FauxConfirmed => Ok(ImportStatus::FauxConfirmed),
_ => Err(TransactionConversionError { code: i32::MAX }),
}
}
}
Expand Down
52 changes: 40 additions & 12 deletions base_layer/wallet/src/output_manager_service/handle.rs
Expand Up @@ -25,7 +25,7 @@ use std::{fmt, fmt::Formatter, sync::Arc};
use aes_gcm::Aes256Gcm;
use tari_common_types::{
transaction::TxId,
types::{HashOutput, PublicKey},
types::{BlockHash, HashOutput, PublicKey},
};
use tari_core::{
covenants::Covenant,
Expand Down Expand Up @@ -106,8 +106,14 @@ pub enum OutputManagerRequest {
num_kernels: usize,
num_outputs: usize,
},
ScanForRecoverableOutputs(Vec<TransactionOutput>),
ScanOutputs(Vec<TransactionOutput>),
ScanForRecoverableOutputs {
outputs: Vec<TransactionOutput>,
tx_id: TxId,
},
ScanOutputs {
outputs: Vec<TransactionOutput>,
tx_id: TxId,
},
AddKnownOneSidedPaymentScript(KnownOneSidedPaymentScript),
CreateOutputWithFeatures {
value: MicroTari,
Expand Down Expand Up @@ -165,8 +171,8 @@ impl fmt::Display for OutputManagerRequest {
"FeeEstimate(amount: {}, fee_per_gram: {}, num_kernels: {}, num_outputs: {})",
amount, fee_per_gram, num_kernels, num_outputs
),
ScanForRecoverableOutputs(_) => write!(f, "ScanForRecoverableOutputs"),
ScanOutputs(_) => write!(f, "ScanOutputs"),
ScanForRecoverableOutputs { .. } => write!(f, "ScanForRecoverableOutputs"),
ScanOutputs { .. } => write!(f, "ScanOutputs"),
AddKnownOneSidedPaymentScript(_) => write!(f, "AddKnownOneSidedPaymentScript"),
CreateOutputWithFeatures { value, features } => {
write!(f, "CreateOutputWithFeatures({}, {})", value, features,)
Expand Down Expand Up @@ -220,12 +226,21 @@ pub enum OutputManagerResponse {
RewoundOutputs(Vec<UnblindedOutput>),
ScanOutputs(Vec<UnblindedOutput>),
AddKnownOneSidedPaymentScript,
CreateOutputWithFeatures { output: Box<UnblindedOutputBuilder> },
CreatePayToSelfWithOutputs { transaction: Box<Transaction>, tx_id: TxId },
CreateOutputWithFeatures {
output: Box<UnblindedOutputBuilder>,
},
CreatePayToSelfWithOutputs {
transaction: Box<Transaction>,
tx_id: TxId,
},
ReinstatedCancelledInboundTx,
CoinbaseAbandonedSet,
ClaimHtlcTransaction((TxId, MicroTari, MicroTari, Transaction)),
OutputStatusesByTxId(Vec<OutputStatus>),
OutputStatusesByTxId {
statuses: Vec<OutputStatus>,
mined_height: Option<u64>,
block_hash: Option<BlockHash>,
},
}

pub type OutputManagerEventSender = broadcast::Sender<Arc<OutputManagerEvent>>;
Expand Down Expand Up @@ -642,10 +657,11 @@ impl OutputManagerHandle {
pub async fn scan_for_recoverable_outputs(
&mut self,
outputs: Vec<TransactionOutput>,
tx_id: TxId,
) -> Result<Vec<UnblindedOutput>, OutputManagerError> {
match self
.handle
.call(OutputManagerRequest::ScanForRecoverableOutputs(outputs))
.call(OutputManagerRequest::ScanForRecoverableOutputs { outputs, tx_id })
.await??
{
OutputManagerResponse::RewoundOutputs(outputs) => Ok(outputs),
Expand All @@ -656,8 +672,13 @@ impl OutputManagerHandle {
pub async fn scan_outputs_for_one_sided_payments(
&mut self,
outputs: Vec<TransactionOutput>,
tx_id: TxId,
) -> Result<Vec<UnblindedOutput>, OutputManagerError> {
match self.handle.call(OutputManagerRequest::ScanOutputs(outputs)).await?? {
match self
.handle
.call(OutputManagerRequest::ScanOutputs { outputs, tx_id })
.await??
{
OutputManagerResponse::ScanOutputs(outputs) => Ok(outputs),
_ => Err(OutputManagerError::UnexpectedApiResponse),
}
Expand Down Expand Up @@ -749,13 +770,20 @@ impl OutputManagerHandle {
}
}

pub async fn get_output_statuses_by_tx_id(&mut self, tx_id: TxId) -> Result<Vec<OutputStatus>, OutputManagerError> {
pub async fn get_output_statuses_by_tx_id(
&mut self,
tx_id: TxId,
) -> Result<(Vec<OutputStatus>, Option<u64>, Option<BlockHash>), OutputManagerError> {
match self
.handle
.call(OutputManagerRequest::GetOutputStatusesByTxId(tx_id))
.await??
{
OutputManagerResponse::OutputStatusesByTxId(s) => Ok(s),
OutputManagerResponse::OutputStatusesByTxId {
statuses,
mined_height,
block_hash,
} => Ok((statuses, mined_height, block_hash)),
_ => Err(OutputManagerError::UnexpectedApiResponse),
}
}
Expand Down
Expand Up @@ -24,7 +24,10 @@ use std::{sync::Arc, time::Instant};

use log::*;
use rand::rngs::OsRng;
use tari_common_types::types::{PrivateKey, PublicKey, RangeProof};
use tari_common_types::{
transaction::TxId,
types::{PrivateKey, PublicKey, RangeProof},
};
use tari_core::transactions::{
transaction::{TransactionOutput, UnblindedOutput},
CryptoFactories,
Expand Down Expand Up @@ -73,6 +76,7 @@ where TBackend: OutputManagerBackend + 'static
pub async fn scan_and_recover_outputs(
&mut self,
outputs: Vec<TransactionOutput>,
tx_id: TxId,
) -> Result<Vec<UnblindedOutput>, OutputManagerError> {
let start = Instant::now();
let outputs_length = outputs.len();
Expand Down Expand Up @@ -133,7 +137,7 @@ where TBackend: OutputManagerBackend + 'static
Some(proof),
)?;
let output_hex = db_output.commitment.to_hex();
if let Err(e) = self.db.add_unspent_output(db_output).await {
if let Err(e) = self.db.add_unspent_output_with_tx_id(tx_id, db_output).await {
match e {
OutputManagerStorageError::DuplicateOutput => {
info!(
Expand Down

0 comments on commit 5453c9e

Please sign in to comment.