lib: fix multiple bugs in balance recovery

This commit is contained in:
Oscar Mira 2023-10-19 17:32:18 +02:00
parent 7dba0c1b18
commit 471f22777c
6 changed files with 86 additions and 46 deletions

View File

@ -139,21 +139,38 @@ cryptonote::account_base& Wallet::require_account() {
const payment_details* find_matching_payment( const payment_details* find_matching_payment(
const std::list<std::pair<crypto::hash, payment_details>> pds, const std::list<std::pair<crypto::hash, payment_details>> pds,
uint64_t amount, const transfer_details& td) {
const crypto::hash& txid, if (td.m_txid == crypto::null_hash) {
const cryptonote::subaddress_index& subaddr_index) {
if (txid == crypto::null_hash) {
return nullptr; return nullptr;
} }
for (const auto& p: pds) { for (const auto& p: pds) {
const auto& pd = p.second; const auto& pd = p.second;
if (pd.m_amount == amount && pd.m_tx_hash == txid && pd.m_subaddr_index == subaddr_index) { if (td.m_amount == pd.m_amount
&& td.m_subaddr_index == pd.m_subaddr_index
&& td.m_txid == pd.m_tx_hash) {
return &pd; return &pd;
} }
} }
return nullptr; return nullptr;
}; };
const confirmed_transfer_details* find_matching_transfer_for_change(
const std::list<std::pair<crypto::hash, confirmed_transfer_details>> txs,
const transfer_details& td) {
if (td.m_txid == crypto::null_hash || td.m_subaddr_index.minor != 0) {
return nullptr;
}
for (const auto& p: txs) {
const auto& tx = p.second;
if (td.m_amount == tx.m_change
&& td.m_subaddr_index.major == tx.m_subaddr_account
&& td.m_txid == p.first) {
return &tx;
}
}
return nullptr;
};
// Only call this function from the callback thread or during initialization, // Only call this function from the callback thread or during initialization,
// as there is no locking mechanism to safeguard reading transaction history // as there is no locking mechanism to safeguard reading transaction history
// from wallet2. // from wallet2.
@ -174,7 +191,7 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
m_wallet.get_payments_out(txs, min_height); m_wallet.get_payments_out(txs, min_height);
m_wallet.get_unconfirmed_payments_out(utxs); m_wallet.get_unconfirmed_payments_out(utxs);
// Iterate through the known owned outputs (incoming transactions). // Iterate through the known owned outputs.
for (const auto& td: tds) { for (const auto& td: tds) {
snapshot.emplace_back(td.m_txid, TxInfo::INCOMING); snapshot.emplace_back(td.m_txid, TxInfo::INCOMING);
TxInfo& recv = snapshot.back(); TxInfo& recv = snapshot.back();
@ -188,14 +205,18 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
recv.m_amount = td.m_amount; recv.m_amount = td.m_amount;
recv.m_unlock_time = td.m_tx.unlock_time; recv.m_unlock_time = td.m_tx.unlock_time;
// Check if the payment exists and update metadata if found. // Check if the payment or change exists and update metadata if found.
const auto* pd = find_matching_payment(pds, td.m_amount, td.m_txid, td.m_subaddr_index); if (const auto* pd = find_matching_payment(pds, td)) {
if (pd) {
recv.m_height = pd->m_block_height; recv.m_height = pd->m_block_height;
recv.m_timestamp = pd->m_timestamp; recv.m_timestamp = pd->m_timestamp;
recv.m_fee = pd->m_fee; recv.m_fee = pd->m_fee;
recv.m_coinbase = pd->m_coinbase; recv.m_coinbase = pd->m_coinbase;
recv.m_state = TxInfo::ON_CHAIN; recv.m_state = TxInfo::ON_CHAIN;
} else if (const auto tx = find_matching_transfer_for_change(txs, td)) {
recv.m_height = tx->m_block_height;
recv.m_timestamp = tx->m_timestamp;
recv.m_fee = tx->m_amount_in - tx->m_amount_out;
recv.m_state = TxInfo::ON_CHAIN;
} else { } else {
recv.m_state = TxInfo::OFF_CHAIN; recv.m_state = TxInfo::OFF_CHAIN;
} }
@ -219,18 +240,16 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
spent.m_state = TxInfo::ON_CHAIN; spent.m_state = TxInfo::ON_CHAIN;
} }
for (const auto& in: tx.m_tx.vin) { for (const auto& ring : tx.m_rings) {
if (in.type() != typeid(cryptonote::txin_to_key)) continue;
const auto& txin = boost::get<cryptonote::txin_to_key>(in);
snapshot.emplace_back(pair.first, TxInfo::OUTGOING); snapshot.emplace_back(pair.first, TxInfo::OUTGOING);
TxInfo& spent = snapshot.back(); TxInfo& spent = snapshot.back();
spent.m_key_image = txin.k_image; spent.m_key_image = ring.first;
spent.m_key_image_known = true; spent.m_key_image_known = true;
spent.m_amount = txin.amount;
spent.m_height = tx.m_block_height; spent.m_height = tx.m_block_height;
spent.m_unlock_time = tx.m_unlock_time; spent.m_unlock_time = tx.m_unlock_time;
spent.m_timestamp = tx.m_timestamp; spent.m_timestamp = tx.m_timestamp;
spent.m_fee = fee; spent.m_fee = fee;
spent.m_change = tx.m_change;
spent.m_state = TxInfo::ON_CHAIN; spent.m_state = TxInfo::ON_CHAIN;
} }
} }
@ -269,7 +288,7 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
} }
} }
// Change is ours too. // Change is ours too, but the output is not yet in transfer_details
if (utx.m_change > 0) { if (utx.m_change > 0) {
snapshot.emplace_back(pair.first, TxInfo::INCOMING); snapshot.emplace_back(pair.first, TxInfo::INCOMING);
TxInfo& change = snapshot.back(); TxInfo& change = snapshot.back();
@ -283,16 +302,14 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
change.m_state = state; change.m_state = state;
} }
for (const auto& in: utx.m_tx.vin) { for (const auto& ring : utx.m_rings) {
if (in.type() != typeid(cryptonote::txin_to_key)) continue;
const auto& txin = boost::get<cryptonote::txin_to_key>(in);
snapshot.emplace_back(pair.first, TxInfo::OUTGOING); snapshot.emplace_back(pair.first, TxInfo::OUTGOING);
TxInfo& spent = snapshot.back(); TxInfo& spent = snapshot.back();
spent.m_key_image = txin.k_image; spent.m_key_image = ring.first;
spent.m_key_image_known = true; spent.m_key_image_known = true;
spent.m_amount = txin.amount;
spent.m_timestamp = utx.m_timestamp; spent.m_timestamp = utx.m_timestamp;
spent.m_fee = fee; spent.m_fee = fee;
spent.m_change = utx.m_change;
spent.m_state = state; spent.m_state = state;
} }
} }

View File

@ -1,5 +1,7 @@
package im.molly.monero package im.molly.monero
import java.time.Instant
data class Balance( data class Balance(
val pendingAmount: MoneroAmount, val pendingAmount: MoneroAmount,
val timeLockedAmounts: List<TimeLocked<MoneroAmount>>, val timeLockedAmounts: List<TimeLocked<MoneroAmount>>,
@ -7,18 +9,26 @@ data class Balance(
val confirmedAmount: MoneroAmount = timeLockedAmounts.sumOf { it.value } val confirmedAmount: MoneroAmount = timeLockedAmounts.sumOf { it.value }
val totalAmount: MoneroAmount = confirmedAmount + pendingAmount val totalAmount: MoneroAmount = confirmedAmount + pendingAmount
fun unlockedAmountAt(targetTime: BlockchainTime): MoneroAmount = fun unlockedAmountAt(
timeLockedAmounts blockHeight: Int,
.filter { it.isUnlocked(targetTime) } currentInstant: Instant = Instant.now(),
.sumOf { it.value } ): MoneroAmount {
val targetTime = BlockchainTime(blockHeight, currentInstant)
return timeLockedAmounts.filter { it.isUnlocked(targetTime) }.sumOf { it.value }
}
fun lockedAmountsAt(targetTime: BlockchainTime): Map<BlockchainTimeSpan, MoneroAmount> = fun lockedAmountsAt(
timeLockedAmounts blockHeight: Int,
currentInstant: Instant = Instant.now(),
): Map<BlockchainTimeSpan, MoneroAmount> {
val targetTime = BlockchainTime(blockHeight, currentInstant)
return timeLockedAmounts
.filter { it.isLocked(targetTime) } .filter { it.isLocked(targetTime) }
.groupBy({ it.timeUntilUnlock(targetTime) }, { it.value }) .groupBy({ it.timeUntilUnlock(targetTime) }, { it.value })
.mapValues { (_, amounts) -> .mapValues { (_, amounts) ->
amounts.sum() amounts.sum()
} }
}
} }
fun Iterable<TimeLocked<Enote>>.calculateBalance(): Balance { fun Iterable<TimeLocked<Enote>>.calculateBalance(): Balance {
@ -26,7 +36,7 @@ fun Iterable<TimeLocked<Enote>>.calculateBalance(): Balance {
val lockedAmounts = mutableListOf<TimeLocked<MoneroAmount>>() val lockedAmounts = mutableListOf<TimeLocked<MoneroAmount>>()
for (timeLocked in filter { it.value.spent }) { for (timeLocked in filter { !it.value.spent }) {
if (timeLocked.value.age == 0) { if (timeLocked.value.age == 0) {
pendingAmount += timeLocked.value.amount pendingAmount += timeLocked.value.amount
} else { } else {

View File

@ -96,19 +96,24 @@ fun max(a: BlockchainTime, b: BlockchainTime) = if (a >= b) a else b
fun min(a: BlockchainTime, b: BlockchainTime) = if (a <= b) a else b fun min(a: BlockchainTime, b: BlockchainTime) = if (a <= b) a else b
data class BlockchainTimeSpan(val duration: Duration, val blocks: Int) { data class BlockchainTimeSpan(val duration: Duration, val blocks: Int) {
val timeRemaining: Duration
get() = maxOf(Duration.ZERO, duration)
companion object { companion object {
val ZERO = BlockchainTimeSpan(duration = Duration.ZERO, blocks = 0) val ZERO = BlockchainTimeSpan(duration = Duration.ZERO, blocks = 0)
} }
} }
class TimeLocked<T>(val value: T, val unlockTime: BlockchainTime) { class TimeLocked<T>(val value: T, val unlockTime: BlockchainTime?) {
fun isLocked(currentTime: BlockchainTime): Boolean = currentTime < unlockTime fun isLocked(currentTime: BlockchainTime): Boolean {
return currentTime < (unlockTime ?: return false)
}
fun isUnlocked(currentTime: BlockchainTime) = !isLocked(currentTime) fun isUnlocked(currentTime: BlockchainTime) = !isLocked(currentTime)
fun timeUntilUnlock(currentTime: BlockchainTime): BlockchainTimeSpan { fun timeUntilUnlock(currentTime: BlockchainTime): BlockchainTimeSpan {
return if (isLocked(currentTime)) { return if (isLocked(currentTime)) {
unlockTime.minus(currentTime) unlockTime!!.minus(currentTime)
} else { } else {
BlockchainTimeSpan.ZERO BlockchainTimeSpan.ZERO
} }

View File

@ -3,6 +3,8 @@ package im.molly.monero
@JvmInline @JvmInline
value class HashDigest(private val hashDigest: String) { value class HashDigest(private val hashDigest: String) {
init { init {
require(hashDigest.length == 32) { "Hash length must be 32 bytes" } require(hashDigest.length == 64) { "Hash length must be 64 hex chars" }
} }
override fun toString(): String = hashDigest
} }

View File

@ -25,7 +25,7 @@ interface PublicAddress {
return StandardAddress(network) return StandardAddress(network)
} }
SubAddress.prefixes[prefix]?.let { network -> SubAddress.prefixes[prefix]?.let { network ->
TODO() return SubAddress(network)
} }
IntegratedAddress.prefixes[prefix]?.let { network -> IntegratedAddress.prefixes[prefix]?.let { network ->
TODO() TODO()
@ -60,7 +60,7 @@ data class SubAddress(
companion object { companion object {
val prefixes = mapOf( val prefixes = mapOf(
42L to MoneroNetwork.Mainnet, 42L to MoneroNetwork.Mainnet,
64L to MoneroNetwork.Testnet, 63L to MoneroNetwork.Testnet,
36L to MoneroNetwork.Stagenet, 36L to MoneroNetwork.Stagenet,
) )
} }

View File

@ -81,13 +81,16 @@ internal fun List<TxInfo>.consolidateTransactions(
// If transaction isn't failed, calculate unlock time and save enotes // If transaction isn't failed, calculate unlock time and save enotes
if (tx.state !is TxState.Failed) { if (tx.state !is TxState.Failed) {
val defaultUnlockTime = BlockchainTime.Block( val maxUnlockTime = tx.blockHeight?.let { height ->
height = tx.blockHeight!! + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE, val defaultUnlockTime = BlockchainTime.Block(
referencePoint = blockchainContext, height = height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE,
) referencePoint = blockchainContext,
val maxUnlockTime = max(defaultUnlockTime, tx.timeLock ?: BlockchainTime.Genesis) )
max(defaultUnlockTime, tx.timeLock ?: BlockchainTime.Genesis)
val lockedEnotesToAdd = tx.received.map { enote -> TimeLocked(enote, maxUnlockTime) } }
val lockedEnotesToAdd = tx.received.map { enote ->
TimeLocked(enote, maxUnlockTime)
}
validEnotes.addAll(lockedEnotesToAdd) validEnotes.addAll(lockedEnotesToAdd)
} }
@ -114,14 +117,14 @@ private fun List<TxInfo>.createTransaction(
if (unlockTime == 0L) null else blockchainContext.resolveUnlockTime(unlockTime) if (unlockTime == 0L) null else blockchainContext.resolveUnlockTime(unlockTime)
} }
val receivedEnotes = enoteByTxId.getValue(txHash).toSet() val receivedEnotes = enoteByTxId.getOrDefault(txHash, emptyList()).toSet()
val outTxs = filter { !it.incoming } val outTxs = filter { !it.incoming }
val spentKeyImages = outTxs.mapNotNull { it.keyImage } val spentKeyImages = outTxs.mapNotNull { it.keyImage }
val sentEnotes = enoteByKeyImage.filterKeys { ki -> ki in spentKeyImages }.values.toSet() val sentEnotes = enoteByKeyImage.filterKeys { ki -> ki in spentKeyImages }.values.toSet()
val payments = outTxs.map { it.toPaymentDetail() } val payments = outTxs.mapNotNull { it.toPaymentDetail() }
return Transaction( return Transaction(
hash = HashDigest(txHash), hash = HashDigest(txHash),
@ -166,7 +169,10 @@ private fun TxInfo.toEnote(blockchainHeight: Int): Enote {
) )
} }
private fun TxInfo.toPaymentDetail() = PaymentDetail( private fun TxInfo.toPaymentDetail(): PaymentDetail? {
amount = MoneroAmount(atomicUnits = amount), val recipient = PublicAddress.parse(recipient ?: return null)
recipient = PublicAddress.parse(recipient!!), return PaymentDetail(
) amount = MoneroAmount(atomicUnits = amount),
recipient = recipient,
)
}