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 std::list<std::pair<crypto::hash, payment_details>> pds,
uint64_t amount,
const crypto::hash& txid,
const cryptonote::subaddress_index& subaddr_index) {
if (txid == crypto::null_hash) {
const transfer_details& td) {
if (td.m_txid == crypto::null_hash) {
return nullptr;
}
for (const auto& p: pds) {
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 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,
// as there is no locking mechanism to safeguard reading transaction history
// from wallet2.
@ -174,7 +191,7 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
m_wallet.get_payments_out(txs, min_height);
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) {
snapshot.emplace_back(td.m_txid, TxInfo::INCOMING);
TxInfo& recv = snapshot.back();
@ -188,14 +205,18 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
recv.m_amount = td.m_amount;
recv.m_unlock_time = td.m_tx.unlock_time;
// Check if the payment exists and update metadata if found.
const auto* pd = find_matching_payment(pds, td.m_amount, td.m_txid, td.m_subaddr_index);
if (pd) {
// Check if the payment or change exists and update metadata if found.
if (const auto* pd = find_matching_payment(pds, td)) {
recv.m_height = pd->m_block_height;
recv.m_timestamp = pd->m_timestamp;
recv.m_fee = pd->m_fee;
recv.m_coinbase = pd->m_coinbase;
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 {
recv.m_state = TxInfo::OFF_CHAIN;
}
@ -219,18 +240,16 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
spent.m_state = TxInfo::ON_CHAIN;
}
for (const auto& in: tx.m_tx.vin) {
if (in.type() != typeid(cryptonote::txin_to_key)) continue;
const auto& txin = boost::get<cryptonote::txin_to_key>(in);
for (const auto& ring : tx.m_rings) {
snapshot.emplace_back(pair.first, TxInfo::OUTGOING);
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_amount = txin.amount;
spent.m_height = tx.m_block_height;
spent.m_unlock_time = tx.m_unlock_time;
spent.m_timestamp = tx.m_timestamp;
spent.m_fee = fee;
spent.m_change = tx.m_change;
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) {
snapshot.emplace_back(pair.first, TxInfo::INCOMING);
TxInfo& change = snapshot.back();
@ -283,16 +302,14 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
change.m_state = state;
}
for (const auto& in: utx.m_tx.vin) {
if (in.type() != typeid(cryptonote::txin_to_key)) continue;
const auto& txin = boost::get<cryptonote::txin_to_key>(in);
for (const auto& ring : utx.m_rings) {
snapshot.emplace_back(pair.first, TxInfo::OUTGOING);
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_amount = txin.amount;
spent.m_timestamp = utx.m_timestamp;
spent.m_fee = fee;
spent.m_change = utx.m_change;
spent.m_state = state;
}
}

View File

@ -1,5 +1,7 @@
package im.molly.monero
import java.time.Instant
data class Balance(
val pendingAmount: MoneroAmount,
val timeLockedAmounts: List<TimeLocked<MoneroAmount>>,
@ -7,18 +9,26 @@ data class Balance(
val confirmedAmount: MoneroAmount = timeLockedAmounts.sumOf { it.value }
val totalAmount: MoneroAmount = confirmedAmount + pendingAmount
fun unlockedAmountAt(targetTime: BlockchainTime): MoneroAmount =
timeLockedAmounts
.filter { it.isUnlocked(targetTime) }
.sumOf { it.value }
fun unlockedAmountAt(
blockHeight: Int,
currentInstant: Instant = Instant.now(),
): MoneroAmount {
val targetTime = BlockchainTime(blockHeight, currentInstant)
return timeLockedAmounts.filter { it.isUnlocked(targetTime) }.sumOf { it.value }
}
fun lockedAmountsAt(targetTime: BlockchainTime): Map<BlockchainTimeSpan, MoneroAmount> =
timeLockedAmounts
fun lockedAmountsAt(
blockHeight: Int,
currentInstant: Instant = Instant.now(),
): Map<BlockchainTimeSpan, MoneroAmount> {
val targetTime = BlockchainTime(blockHeight, currentInstant)
return timeLockedAmounts
.filter { it.isLocked(targetTime) }
.groupBy({ it.timeUntilUnlock(targetTime) }, { it.value })
.mapValues { (_, amounts) ->
amounts.sum()
}
}
}
fun Iterable<TimeLocked<Enote>>.calculateBalance(): Balance {
@ -26,7 +36,7 @@ fun Iterable<TimeLocked<Enote>>.calculateBalance(): Balance {
val lockedAmounts = mutableListOf<TimeLocked<MoneroAmount>>()
for (timeLocked in filter { it.value.spent }) {
for (timeLocked in filter { !it.value.spent }) {
if (timeLocked.value.age == 0) {
pendingAmount += timeLocked.value.amount
} 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
data class BlockchainTimeSpan(val duration: Duration, val blocks: Int) {
val timeRemaining: Duration
get() = maxOf(Duration.ZERO, duration)
companion object {
val ZERO = BlockchainTimeSpan(duration = Duration.ZERO, blocks = 0)
}
}
class TimeLocked<T>(val value: T, val unlockTime: BlockchainTime) {
fun isLocked(currentTime: BlockchainTime): Boolean = currentTime < unlockTime
class TimeLocked<T>(val value: T, val unlockTime: BlockchainTime?) {
fun isLocked(currentTime: BlockchainTime): Boolean {
return currentTime < (unlockTime ?: return false)
}
fun isUnlocked(currentTime: BlockchainTime) = !isLocked(currentTime)
fun timeUntilUnlock(currentTime: BlockchainTime): BlockchainTimeSpan {
return if (isLocked(currentTime)) {
unlockTime.minus(currentTime)
unlockTime!!.minus(currentTime)
} else {
BlockchainTimeSpan.ZERO
}

View File

@ -3,6 +3,8 @@ package im.molly.monero
@JvmInline
value class HashDigest(private val hashDigest: String) {
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)
}
SubAddress.prefixes[prefix]?.let { network ->
TODO()
return SubAddress(network)
}
IntegratedAddress.prefixes[prefix]?.let { network ->
TODO()
@ -60,7 +60,7 @@ data class SubAddress(
companion object {
val prefixes = mapOf(
42L to MoneroNetwork.Mainnet,
64L to MoneroNetwork.Testnet,
63L to MoneroNetwork.Testnet,
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 (tx.state !is TxState.Failed) {
val defaultUnlockTime = BlockchainTime.Block(
height = tx.blockHeight!! + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE,
referencePoint = blockchainContext,
)
val maxUnlockTime = max(defaultUnlockTime, tx.timeLock ?: BlockchainTime.Genesis)
val lockedEnotesToAdd = tx.received.map { enote -> TimeLocked(enote, maxUnlockTime) }
val maxUnlockTime = tx.blockHeight?.let { height ->
val defaultUnlockTime = BlockchainTime.Block(
height = height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE,
referencePoint = blockchainContext,
)
max(defaultUnlockTime, tx.timeLock ?: BlockchainTime.Genesis)
}
val lockedEnotesToAdd = tx.received.map { enote ->
TimeLocked(enote, maxUnlockTime)
}
validEnotes.addAll(lockedEnotesToAdd)
}
@ -114,14 +117,14 @@ private fun List<TxInfo>.createTransaction(
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 spentKeyImages = outTxs.mapNotNull { it.keyImage }
val sentEnotes = enoteByKeyImage.filterKeys { ki -> ki in spentKeyImages }.values.toSet()
val payments = outTxs.map { it.toPaymentDetail() }
val payments = outTxs.mapNotNull { it.toPaymentDetail() }
return Transaction(
hash = HashDigest(txHash),
@ -166,7 +169,10 @@ private fun TxInfo.toEnote(blockchainHeight: Int): Enote {
)
}
private fun TxInfo.toPaymentDetail() = PaymentDetail(
amount = MoneroAmount(atomicUnits = amount),
recipient = PublicAddress.parse(recipient!!),
)
private fun TxInfo.toPaymentDetail(): PaymentDetail? {
val recipient = PublicAddress.parse(recipient ?: return null)
return PaymentDetail(
amount = MoneroAmount(atomicUnits = amount),
recipient = recipient,
)
}