lib: make output public key optional in Enote

This commit is contained in:
Oscar Mira 2023-10-19 01:44:22 +02:00
parent 75b33a24f3
commit 7dba0c1b18
7 changed files with 125 additions and 97 deletions

View File

@ -178,7 +178,8 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
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();
recv.m_key = td.get_public_key(); recv.m_public_key = td.get_public_key();
recv.m_public_key_known = true;
recv.m_key_image = td.m_key_image; recv.m_key_image = td.m_key_image;
recv.m_key_image_known = td.m_key_image_known; recv.m_key_image_known = td.m_key_image_known;
recv.m_subaddress_major = td.m_subaddr_index.major; recv.m_subaddress_major = td.m_subaddr_index.major;
@ -247,7 +248,6 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
// Add pending transfers to our own wallet. // Add pending transfers to our own wallet.
snapshot.emplace_back(pair.first, TxInfo::INCOMING); snapshot.emplace_back(pair.first, TxInfo::INCOMING);
TxInfo& recv = snapshot.back(); TxInfo& recv = snapshot.back();
// TODO: recv.m_key
recv.m_recipient = m_wallet.get_subaddress_as_str(*dest_subaddr_idx); recv.m_recipient = m_wallet.get_subaddress_as_str(*dest_subaddr_idx);
recv.m_subaddress_major = (*dest_subaddr_idx).major; recv.m_subaddress_major = (*dest_subaddr_idx).major;
recv.m_subaddress_minor = (*dest_subaddr_idx).minor; recv.m_subaddress_minor = (*dest_subaddr_idx).minor;
@ -273,7 +273,6 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
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();
// TODO: change.m_key
change.m_recipient = m_wallet.get_subaddress_as_str({utx.m_subaddr_account, 0}); change.m_recipient = m_wallet.get_subaddress_as_str({utx.m_subaddr_account, 0});
change.m_subaddress_major = utx.m_subaddr_account; change.m_subaddress_major = utx.m_subaddr_account;
change.m_subaddress_minor = 0; // All changes go to 0-th subaddress change.m_subaddress_minor = 0; // All changes go to 0-th subaddress
@ -306,7 +305,6 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
for (uint64_t amount: upd.m_amounts) { for (uint64_t amount: upd.m_amounts) {
snapshot.emplace_back(upd.m_tx_hash, TxInfo::INCOMING); snapshot.emplace_back(upd.m_tx_hash, TxInfo::INCOMING);
TxInfo& recv = snapshot.back(); TxInfo& recv = snapshot.back();
// TODO: recv.m_key
recv.m_recipient = m_wallet.get_subaddress_as_str(upd.m_subaddr_index); recv.m_recipient = m_wallet.get_subaddress_as_str(upd.m_subaddr_index);
recv.m_subaddress_major = upd.m_subaddr_index.major; recv.m_subaddress_major = upd.m_subaddr_index.major;
recv.m_subaddress_minor = upd.m_subaddr_index.minor; recv.m_subaddress_minor = upd.m_subaddr_index.minor;
@ -569,25 +567,26 @@ Java_im_molly_monero_WalletNative_nativeGetCurrentBlockchainHeight(
} }
ScopedJvmLocalRef<jobject> nativeToJvmTxInfo(JNIEnv* env, ScopedJvmLocalRef<jobject> nativeToJvmTxInfo(JNIEnv* env,
const TxInfo& info) { const TxInfo& tx) {
LOG_FATAL_IF(info.m_height >= CRYPTONOTE_MAX_BLOCK_NUMBER, "Blockchain max height reached"); LOG_FATAL_IF(tx.m_height >= CRYPTONOTE_MAX_BLOCK_NUMBER, "Blockchain max height reached");
// TODO: Check amount overflow // TODO: Check amount overflow
return {env, TxInfoClass.newObject( return {env, TxInfoClass.newObject(
env, TxInfo_ctor, env, TxInfo_ctor,
nativeToJvmString(env, pod_to_hex(info.m_tx_hash)).obj(), nativeToJvmString(env, pod_to_hex(tx.m_tx_hash)).obj(),
nativeToJvmString(env, pod_to_hex(info.m_key)).obj(), tx.m_public_key_known ? nativeToJvmString(env, pod_to_hex(tx.m_public_key)).obj() : nullptr,
info.m_key_image_known ? nativeToJvmString(env, pod_to_hex(info.m_key_image)).obj(): nullptr, tx.m_key_image_known ? nativeToJvmString(env, pod_to_hex(tx.m_key_image)).obj() : nullptr,
info.m_subaddress_major, tx.m_subaddress_major,
info.m_subaddress_minor, tx.m_subaddress_minor,
(!info.m_recipient.empty()) ? nativeToJvmString(env, info.m_recipient).obj() : nullptr, (!tx.m_recipient.empty()) ? nativeToJvmString(env, tx.m_recipient).obj() : nullptr,
info.m_amount, tx.m_amount,
static_cast<jint>(info.m_height), static_cast<jint>(tx.m_height),
info.m_state, tx.m_state,
info.m_unlock_time, tx.m_unlock_time,
info.m_timestamp, tx.m_timestamp,
info.m_fee, tx.m_fee,
info.m_coinbase, tx.m_change,
info.m_type == TxInfo::INCOMING) tx.m_coinbase,
tx.m_type == TxInfo::INCOMING)
}; };
} }

View File

@ -19,7 +19,7 @@ using unconfirmed_transfer_details = tools::wallet2::unconfirmed_transfer_detail
// Basic structure combining transaction details with input or output info. // Basic structure combining transaction details with input or output info.
struct TxInfo { struct TxInfo {
crypto::hash m_tx_hash; crypto::hash m_tx_hash;
crypto::public_key m_key; crypto::public_key m_public_key;
crypto::key_image m_key_image; crypto::key_image m_key_image;
uint32_t m_subaddress_major; uint32_t m_subaddress_major;
uint32_t m_subaddress_minor; uint32_t m_subaddress_minor;
@ -31,6 +31,7 @@ struct TxInfo {
uint64_t m_fee; uint64_t m_fee;
uint64_t m_change; uint64_t m_change;
bool m_coinbase; bool m_coinbase;
bool m_public_key_known;
bool m_key_image_known; bool m_key_image_known;
enum TxType { enum TxType {
@ -47,7 +48,7 @@ struct TxInfo {
TxInfo(crypto::hash tx_hash, TxType type): TxInfo(crypto::hash tx_hash, TxType type):
m_tx_hash(tx_hash), m_tx_hash(tx_hash),
m_key(crypto::public_key{}), m_public_key(crypto::public_key{}),
m_key_image(crypto::key_image{}), m_key_image(crypto::key_image{}),
m_subaddress_major(-1), m_subaddress_major(-1),
m_subaddress_minor(-1), m_subaddress_minor(-1),
@ -59,6 +60,7 @@ struct TxInfo {
m_fee(0), m_fee(0),
m_change(0), m_change(0),
m_coinbase(false), m_coinbase(false),
m_public_key_known(false),
m_key_image_known(false), m_key_image_known(false),
m_type(type), m_type(type),
m_state(OFF_CHAIN) {} m_state(OFF_CHAIN) {}

View File

@ -13,7 +13,7 @@ data class Block(
data class BlockHeader( data class BlockHeader(
val height: Int, val height: Int,
val timestamp: Long, val epochSecond: Long,
) { ) {
companion object { companion object {
const val MAX_HEIGHT = CRYPTONOTE_MAX_BLOCK_NUMBER - 1 const val MAX_HEIGHT = CRYPTONOTE_MAX_BLOCK_NUMBER - 1

View File

@ -25,6 +25,8 @@ open class BlockchainTime(
override fun compareTo(other: BlockchainTime): Int = override fun compareTo(other: BlockchainTime): Int =
this.height.compareTo(other.height) this.height.compareTo(other.height)
override fun toString(): String = "Block #$height | $timestamp"
data object Genesis : BlockchainTime(0, Instant.ofEpochSecond(1397818193)) data object Genesis : BlockchainTime(0, Instant.ofEpochSecond(1397818193))
class Block(height: Int, referencePoint: BlockchainTime = Genesis) : class Block(height: Int, referencePoint: BlockchainTime = Genesis) :
@ -42,17 +44,20 @@ open class BlockchainTime(
} }
companion object { companion object {
val AVERAGE_BLOCK_TIME = Duration.ofSeconds(DIFFICULTY_TARGET_V2) val AVERAGE_BLOCK_TIME: Duration = Duration.ofSeconds(DIFFICULTY_TARGET_V2)
fun estimateTimestamp(targetHeight: Int, referencePoint: BlockchainTime): Instant { fun estimateTimestamp(targetHeight: Int, referencePoint: BlockchainTime): Instant {
require(targetHeight >= 0) { "Block height $targetHeight must not be negative" } require(targetHeight >= 0) {
"Block height $targetHeight must not be negative"
}
return if (targetHeight == 0) { return when (targetHeight) {
Genesis.timestamp 0 -> Genesis.timestamp
} else { else -> {
val heightDiff = targetHeight - referencePoint.height val heightDiff = targetHeight - referencePoint.height
val estTimeDiff = AVERAGE_BLOCK_TIME.multipliedBy(heightDiff.toLong()) val estTimeDiff = AVERAGE_BLOCK_TIME.multipliedBy(heightDiff.toLong())
referencePoint.timestamp.plus(estTimeDiff) referencePoint.timestamp.plus(estTimeDiff)
}
} }
} }
@ -86,6 +91,10 @@ open class BlockchainTime(
operator fun minus(other: BlockchainTime): BlockchainTimeSpan = other.until(this) operator fun minus(other: BlockchainTime): BlockchainTimeSpan = other.until(this)
} }
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) { data class BlockchainTimeSpan(val duration: Duration, val blocks: Int) {
companion object { companion object {
val ZERO = BlockchainTimeSpan(duration = Duration.ZERO, blocks = 0) val ZERO = BlockchainTimeSpan(duration = Duration.ZERO, blocks = 0)

View File

@ -1,15 +1,25 @@
package im.molly.monero package im.molly.monero
data class Enote( class Enote(
val amount: MoneroAmount, val amount: MoneroAmount,
val owner: AccountAddress, val owner: AccountAddress,
val key: PublicKey, val key: PublicKey?,
val keyImage: HashDigest?, val keyImage: HashDigest?,
val age: Int, val age: Int,
val sourceTxId: String?,
) { ) {
var spent: Boolean = false
init { init {
require(age >= 0) { "Enote age $age must not be negative" } require(age >= 0) { "Enote age $age must not be negative" }
} }
var spent: Boolean = false
private set
fun markAsSpent() {
spent = true
}
override fun hashCode() = System.identityHashCode(this)
override fun equals(other: Any?) = this === other
} }

View File

@ -4,18 +4,21 @@ data class Transaction(
val hash: HashDigest, val hash: HashDigest,
// TODO: val version: ProtocolInfo, // TODO: val version: ProtocolInfo,
val state: TxState, val state: TxState,
val timeLock: BlockchainTime, val timeLock: BlockchainTime?,
val sent: Set<Enote>, val sent: Set<Enote>,
val received: Set<Enote>, val received: Set<Enote>,
val payments: List<PaymentDetail>, val payments: List<PaymentDetail>,
val fee: MoneroAmount, val fee: MoneroAmount,
val change: MoneroAmount, val change: MoneroAmount,
) { ) {
val txId: String get() = hash.toString() val txId: String
get() = hash.toString()
val blockHeight: Int?
get() = (state as? TxState.OnChain)?.blockHeader?.height
} }
sealed interface TxState { sealed interface TxState {
data class OnChain( data class OnChain(
val blockHeader: BlockHeader, val blockHeader: BlockHeader,
) : TxState ) : TxState

View File

@ -2,18 +2,20 @@ package im.molly.monero.internal
import android.os.Parcelable import android.os.Parcelable
import im.molly.monero.AccountAddress import im.molly.monero.AccountAddress
import im.molly.monero.MoneroAmount
import im.molly.monero.BlockHeader import im.molly.monero.BlockHeader
import im.molly.monero.BlockchainTime import im.molly.monero.BlockchainTime
import im.molly.monero.CalledByNative import im.molly.monero.CalledByNative
import im.molly.monero.Enote import im.molly.monero.Enote
import im.molly.monero.HashDigest import im.molly.monero.HashDigest
import im.molly.monero.MoneroAmount
import im.molly.monero.PaymentDetail import im.molly.monero.PaymentDetail
import im.molly.monero.PublicAddress import im.molly.monero.PublicAddress
import im.molly.monero.PublicKey import im.molly.monero.PublicKey
import im.molly.monero.TimeLocked import im.molly.monero.TimeLocked
import im.molly.monero.Transaction import im.molly.monero.Transaction
import im.molly.monero.TxState import im.molly.monero.TxState
import im.molly.monero.internal.constants.CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE
import im.molly.monero.max
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**
@ -27,7 +29,7 @@ import kotlinx.parcelize.Parcelize
internal data class TxInfo internal data class TxInfo
@CalledByNative("wallet.cc") constructor( @CalledByNative("wallet.cc") constructor(
val txHash: String, val txHash: String,
val key: String, val publicKey: String?,
val keyImage: String?, val keyImage: String?,
val subAddressMajor: Int, val subAddressMajor: Int,
val subAddressMinor: Int, val subAddressMinor: Int,
@ -61,85 +63,87 @@ internal data class TxInfo
internal fun List<TxInfo>.consolidateTransactions( internal fun List<TxInfo>.consolidateTransactions(
blockchainContext: BlockchainTime, blockchainContext: BlockchainTime,
): Pair<Map<String, Transaction>, Set<TimeLocked<Enote>>> { ): Pair<Map<String, Transaction>, Set<TimeLocked<Enote>>> {
val (enoteByKey, enoteByKeyImage) = extractEnotesFromIncomingTxs(blockchainContext) // Extract enotes from incoming transactions
val allEnotes = filter { it.incoming }.map { it.toEnote(blockchainContext.height) }
val timeLockedEnotes = HashSet<TimeLocked<Enote>>(enoteByKey.size) val enoteByTxId = allEnotes.groupBy { enote -> enote.sourceTxId!! }
// Group transactions by their hash and then map each group to a Transaction val enoteByKeyImage = allEnotes.mapNotNull { enote ->
val groupedByTxId = groupBy { it.txHash } enote.keyImage?.let { keyImage -> keyImage.toString() to enote }
}.toMap()
val validEnotes = HashSet<TimeLocked<Enote>>(allEnotes.size)
// Group transaction info by their hash and then map each group to a Transaction
val groupedByTxId = groupBy { txInfo -> txInfo.txHash }
val txById = groupedByTxId.mapValues { (_, infoList) -> val txById = groupedByTxId.mapValues { (_, infoList) ->
createTransaction(blockchainContext, infoList, enoteByKey, enoteByKeyImage) val tx = infoList.createTransaction(blockchainContext, enoteByTxId, enoteByKeyImage)
.also { tx ->
if (tx.state !is TxState.Failed) {
val lockedEnotesToAdd =
tx.received.map { enote -> TimeLocked(enote, tx.timeLock) }
timeLockedEnotes.addAll(lockedEnotesToAdd)
tx.sent.forEach { enote -> enote.spent = true }
}
}
}
return txById to timeLockedEnotes // 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)
private fun List<TxInfo>.extractEnotesFromIncomingTxs( val lockedEnotesToAdd = tx.received.map { enote -> TimeLocked(enote, maxUnlockTime) }
blockchainContext: BlockchainTime, validEnotes.addAll(lockedEnotesToAdd)
): Pair<Map<String, Enote>, Map<String, Enote>> {
val enoteByKey = mutableMapOf<String, Enote>()
val enoteByKeyImage = mutableMapOf<String, Enote>()
for (txInfo in filter { it.incoming }) {
enoteByKey.computeIfAbsent(txInfo.key) {
val enote = txInfo.toEnote(blockchainContext.height)
txInfo.keyImage?.let { keyImage ->
enoteByKeyImage[keyImage] = enote
}
enote
} }
// Mark the sent enotes as spent
tx.sent.forEach { enote -> enote.markAsSpent() }
tx
} }
return enoteByKey to enoteByKeyImage return txById to validEnotes
} }
private fun createTransaction( private fun List<TxInfo>.createTransaction(
blockchainContext: BlockchainTime, blockchainContext: BlockchainTime,
infoList: List<TxInfo>, enoteByTxId: Map<String, List<Enote>>,
enoteMap: Map<String, Enote>, enoteByKeyImage: Map<String, Enote>,
keyImageMap: Map<String, Enote>,
): Transaction { ): Transaction {
val txHash = infoList.first().txHash val txHash = first().txHash
val unlockTime = infoList.maxOf { it.unlockTime }
val fee = infoList.maxOf { it.fee }
val change = infoList.maxOf { it.change }
val (ins, outs) = infoList.partition { it.incoming } val fee = maxOf { it.fee }
val change = maxOf { it.change }
val receivedEnotes = ins.map { enoteMap.getValue(it.key) } val timeLock = maxOf { it.unlockTime }.let { unlockTime ->
val spentKeyImages = outs.mapNotNull { it.keyImage }.toSet() if (unlockTime == 0L) null else blockchainContext.resolveUnlockTime(unlockTime)
val sentEnotes = keyImageMap.filterKeys { spentKeyImages.contains(it) }.values }
val payments = outs.map { it.toPaymentDetail() }
val receivedEnotes = enoteByTxId.getValue(txHash).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() }
return Transaction( return Transaction(
hash = HashDigest(txHash), hash = HashDigest(txHash),
state = determineTxState(infoList), state = determineTxState(),
timeLock = blockchainContext.resolveUnlockTime(unlockTime), timeLock = timeLock,
sent = sentEnotes.toSet(), sent = sentEnotes,
received = receivedEnotes.toSet(), received = receivedEnotes,
payments = payments, payments = payments,
fee = MoneroAmount(fee), fee = MoneroAmount(atomicUnits = fee),
change = MoneroAmount(change), change = MoneroAmount(atomicUnits = change),
) )
} }
private fun determineTxState(infoList: List<TxInfo>): TxState { private fun List<TxInfo>.determineTxState(): TxState {
val txInfo = infoList.distinctBy { it.state }.single() val uniqueTx = distinctBy { it.state }.single()
return when (txInfo.state) { return when (uniqueTx.state) {
TxInfo.OFF_CHAIN -> TxState.OffChain TxInfo.OFF_CHAIN -> TxState.OffChain
TxInfo.PENDING -> TxState.InMemoryPool TxInfo.PENDING -> TxState.InMemoryPool
TxInfo.FAILED -> TxState.Failed TxInfo.FAILED -> TxState.Failed
TxInfo.ON_CHAIN -> TxState.OnChain(BlockHeader(txInfo.height, txInfo.timestamp)) TxInfo.ON_CHAIN -> TxState.OnChain(BlockHeader(uniqueTx.height, uniqueTx.timestamp))
else -> error("Invalid tx state value: ${txInfo.state}") else -> error("Invalid tx state value: ${uniqueTx.state}")
} }
} }
@ -153,15 +157,16 @@ private fun TxInfo.toEnote(blockchainHeight: Int): Enote {
val calculatedAge = if (height == 0) 0 else blockchainHeight - height + 1 val calculatedAge = if (height == 0) 0 else blockchainHeight - height + 1
return Enote( return Enote(
amount = MoneroAmount(amount), amount = MoneroAmount(atomicUnits = amount),
owner = ownerAddress, owner = ownerAddress,
key = PublicKey(key), key = publicKey?.let { PublicKey(it) },
keyImage = keyImage?.let { HashDigest(it) }, keyImage = keyImage?.let { HashDigest(it) },
age = calculatedAge, age = calculatedAge,
sourceTxId = txHash,
) )
} }
private fun TxInfo.toPaymentDetail() = PaymentDetail( private fun TxInfo.toPaymentDetail() = PaymentDetail(
amount = MoneroAmount(amount), amount = MoneroAmount(atomicUnits = amount),
recipient = PublicAddress.parse(recipient!!), recipient = PublicAddress.parse(recipient!!),
) )