Merge pull request #5641 from nuttycom/feature/wallet_orchard-restore_orchard_spends
Restore Orchard spentness cache in LoadCaches
This commit is contained in:
commit
19bce27321
|
@ -14,14 +14,10 @@ from test_framework.util import (
|
|||
CANOPY_BRANCH_ID,
|
||||
NU5_BRANCH_ID,
|
||||
assert_equal,
|
||||
connect_nodes_bi,
|
||||
get_coinbase_address,
|
||||
nuparams,
|
||||
start_nodes,
|
||||
stop_nodes,
|
||||
sync_blocks,
|
||||
wait_and_assert_operationid_status,
|
||||
wait_bitcoinds,
|
||||
)
|
||||
|
||||
from finalsaplingroot import ORCHARD_TREE_EMPTY_ROOT
|
||||
|
@ -122,19 +118,7 @@ class OrchardReorgTest(BitcoinTestFramework):
|
|||
|
||||
# Reconnect the nodes; node 0 will re-org to node 2's chain.
|
||||
print("Re-joining the network so that node 0 reorgs")
|
||||
# We can't use `self.join_network()` because the coinbase-spending second Orchard
|
||||
# transaction doesn't propagate from node 1's mempool to node 2 on restart. Inline
|
||||
# the block-syncing parts here.
|
||||
assert self.is_network_split
|
||||
stop_nodes(self.nodes)
|
||||
wait_bitcoinds()
|
||||
self.nodes = self.setup_nodes()
|
||||
connect_nodes_bi(self.nodes, 1, 2)
|
||||
sync_blocks(self.nodes[1:3])
|
||||
connect_nodes_bi(self.nodes, 0, 1)
|
||||
connect_nodes_bi(self.nodes, 2, 3)
|
||||
self.is_network_split = False
|
||||
sync_blocks(self.nodes)
|
||||
self.join_network()
|
||||
|
||||
# Verify that node 0's latest Orchard root matches what we expect.
|
||||
orchardroot_postreorg = self.nodes[0].getblock(self.nodes[2].getbestblockhash())['finalorchardroot']
|
||||
|
|
|
@ -52,7 +52,7 @@ class BitcoinTestFramework(object):
|
|||
def setup_nodes(self):
|
||||
return start_nodes(self.num_nodes, self.options.tmpdir)
|
||||
|
||||
def setup_network(self, split = False):
|
||||
def setup_network(self, split = False, do_mempool_sync = True):
|
||||
self.nodes = self.setup_nodes()
|
||||
|
||||
# Connect the nodes as a "chain". This allows us
|
||||
|
@ -64,12 +64,13 @@ class BitcoinTestFramework(object):
|
|||
if not split:
|
||||
connect_nodes_bi(self.nodes, 1, 2)
|
||||
sync_blocks(self.nodes[1:3])
|
||||
sync_mempools(self.nodes[1:3])
|
||||
if do_mempool_sync:
|
||||
sync_mempools(self.nodes[1:3])
|
||||
|
||||
connect_nodes_bi(self.nodes, 0, 1)
|
||||
connect_nodes_bi(self.nodes, 2, 3)
|
||||
self.is_network_split = split
|
||||
self.sync_all()
|
||||
self.sync_all(do_mempool_sync)
|
||||
|
||||
def split_network(self):
|
||||
"""
|
||||
|
@ -80,15 +81,17 @@ class BitcoinTestFramework(object):
|
|||
wait_bitcoinds()
|
||||
self.setup_network(True)
|
||||
|
||||
def sync_all(self):
|
||||
def sync_all(self, do_mempool_sync = True):
|
||||
if self.is_network_split:
|
||||
sync_blocks(self.nodes[:2])
|
||||
sync_blocks(self.nodes[2:])
|
||||
sync_mempools(self.nodes[:2])
|
||||
sync_mempools(self.nodes[2:])
|
||||
if do_mempool_sync:
|
||||
sync_mempools(self.nodes[:2])
|
||||
sync_mempools(self.nodes[2:])
|
||||
else:
|
||||
sync_blocks(self.nodes)
|
||||
sync_mempools(self.nodes)
|
||||
if do_mempool_sync:
|
||||
sync_mempools(self.nodes)
|
||||
|
||||
def join_network(self):
|
||||
"""
|
||||
|
@ -97,7 +100,7 @@ class BitcoinTestFramework(object):
|
|||
assert self.is_network_split
|
||||
stop_nodes(self.nodes)
|
||||
wait_bitcoinds()
|
||||
self.setup_network(False)
|
||||
self.setup_network(False, False)
|
||||
|
||||
def main(self):
|
||||
|
||||
|
|
|
@ -7,14 +7,10 @@ from test_framework.test_framework import BitcoinTestFramework
|
|||
from test_framework.util import (
|
||||
NU5_BRANCH_ID,
|
||||
assert_equal,
|
||||
connect_nodes_bi,
|
||||
get_coinbase_address,
|
||||
nuparams,
|
||||
start_nodes,
|
||||
stop_nodes,
|
||||
sync_blocks,
|
||||
wait_and_assert_operationid_status,
|
||||
wait_bitcoinds,
|
||||
)
|
||||
|
||||
from decimal import Decimal
|
||||
|
@ -130,17 +126,7 @@ class WalletOrchardTest(BitcoinTestFramework):
|
|||
|
||||
# Reconnect the nodes; nodes 2 and 3 will re-org to node 0's chain.
|
||||
print("Re-joining the network so that nodes 2 and 3 reorg")
|
||||
# We can't use `self.join_network()` because the the nodes's mempools fail to synchronize on restart
|
||||
assert self.is_network_split
|
||||
stop_nodes(self.nodes)
|
||||
wait_bitcoinds()
|
||||
self.nodes = self.setup_nodes()
|
||||
connect_nodes_bi(self.nodes, 1, 2)
|
||||
sync_blocks(self.nodes[1:3])
|
||||
connect_nodes_bi(self.nodes, 0, 1)
|
||||
connect_nodes_bi(self.nodes, 2, 3)
|
||||
self.is_network_split = False
|
||||
sync_blocks(self.nodes)
|
||||
self.join_network()
|
||||
|
||||
# split 0/1's chain should have won, so their wallet balance should be consistent
|
||||
assert_equal(
|
||||
|
@ -151,25 +137,43 @@ class WalletOrchardTest(BitcoinTestFramework):
|
|||
# un-mined and returned to the mempool
|
||||
assert_equal(set([rollback_tx]), set(self.nodes[2].getrawmempool()))
|
||||
|
||||
# our sole Sapling note is spent, so our confirmed balance is currently 0
|
||||
# acct2's sole Sapling note is spent by a transaction in the mempool, so our
|
||||
# confirmed balance is currently 0
|
||||
assert_equal(
|
||||
{'pools': {}, 'minimum_confirmations': 1},
|
||||
self.nodes[2].z_getbalanceforaccount(acct2))
|
||||
|
||||
# our incoming change (unconfirmed, still in the mempool) is 9 zec
|
||||
# acct2's incoming change (unconfirmed, still in the mempool) is 9 zec
|
||||
assert_equal(
|
||||
{'pools': {'sapling': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 0},
|
||||
self.nodes[2].z_getbalanceforaccount(acct2, 0))
|
||||
|
||||
# the transaction was un-mined, so we have no confirmed balance
|
||||
# The transaction was un-mined, so acct3 should have no confirmed balance
|
||||
assert_equal(
|
||||
{'pools': {}, 'minimum_confirmations': 1},
|
||||
self.nodes[3].z_getbalanceforaccount(acct3))
|
||||
|
||||
# our unconfirmed balance is 1 zec
|
||||
# acct3's unconfirmed balance is 1 zec
|
||||
assert_equal(
|
||||
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 0},
|
||||
self.nodes[3].z_getbalanceforaccount(acct3, 0))
|
||||
|
||||
# Manually resend the transaction in node 2's mempool
|
||||
self.nodes[2].resendwallettransactions()
|
||||
|
||||
# Sync the network
|
||||
self.sync_all()
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
|
||||
# The un-mined transaction should now have been re-mined
|
||||
assert_equal(
|
||||
{'pools': {'sapling': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 1},
|
||||
self.nodes[2].z_getbalanceforaccount(acct2))
|
||||
|
||||
assert_equal(
|
||||
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 1},
|
||||
self.nodes[3].z_getbalanceforaccount(acct3))
|
||||
|
||||
if __name__ == '__main__':
|
||||
WalletOrchardTest().main()
|
||||
|
|
|
@ -92,24 +92,34 @@ static_assert(
|
|||
"RawOrchardActionIVK struct should have exactly a 128-bit in-memory representation.");
|
||||
static_assert(alignof(RawOrchardActionIVK) == 8, "RawOrchardActionIVK struct alignment is not 64 bits.");
|
||||
|
||||
typedef void (*push_action_ivk_callback_t)(void* resultCollection, const RawOrchardActionIVK actionIvk);
|
||||
typedef void (*push_action_ivk_callback_t)(void* rec, const RawOrchardActionIVK actionIvk);
|
||||
|
||||
typedef void (*push_spend_action_idx_callback_t)(void* rec, uint32_t actionIdx);
|
||||
|
||||
/**
|
||||
* Searches the provided bundle for notes that are visible to the specified wallet's
|
||||
* incoming viewing keys, and adds those notes to the wallet. For each note decryptable
|
||||
* by one of the wallet's keys, this method will insert a `RawOrchardActionIVK` value into
|
||||
* the provided `resultCollection` using the `push_cb` callback. Note that this callback
|
||||
* can perform transformations on the provided RawOrchardActionIVK in this process.
|
||||
* the provided `callbackReceiver` referent using the `push_cb` callback. Note that
|
||||
* this callback can perform transformations on the provided RawOrchardActionIVK in this
|
||||
* process. For each action spending one of the wallet's notes, this method will pass
|
||||
* a `uint32_t` action index corresponding to that action to the `callbackReceiver` referent;
|
||||
* using the specified callback; usually, this will push the value into a result vector owned
|
||||
* by the caller.
|
||||
*
|
||||
* The provided bundle must be a component of the transaction from which `txid` was
|
||||
* derived.
|
||||
*
|
||||
* Returns `true` if the bundle is involved with the wallet; i.e. if it contains
|
||||
* notes spendable by the wallet, or spends any of the wallet's notes.
|
||||
*/
|
||||
void orchard_wallet_add_notes_from_bundle(
|
||||
bool orchard_wallet_add_notes_from_bundle(
|
||||
OrchardWalletPtr* wallet,
|
||||
const unsigned char txid[32],
|
||||
const OrchardBundlePtr* bundle,
|
||||
void* resultCollection,
|
||||
push_action_ivk_callback_t push_cb
|
||||
void* callbackReceiver,
|
||||
push_action_ivk_callback_t push_cb,
|
||||
push_spend_action_idx_callback_t spend_cb
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -122,13 +132,15 @@ void orchard_wallet_add_notes_from_bundle(
|
|||
* The value the `blockHeight` pointer points to be set to the height at which the
|
||||
* transaction was mined, or `nullptr` if the transaction is not in the main chain.
|
||||
*/
|
||||
bool orchard_wallet_restore_notes(
|
||||
bool orchard_wallet_load_bundle(
|
||||
OrchardWalletPtr* wallet,
|
||||
const uint32_t* blockHeight,
|
||||
const unsigned char txid[32],
|
||||
const OrchardBundlePtr* bundle,
|
||||
const RawOrchardActionIVK* actionIvks,
|
||||
size_t actionIvksLen
|
||||
size_t actionIvksLen,
|
||||
const uint32_t* actionsSpendingWalletNotes,
|
||||
size_t actionsSpendingWalletNotesLen
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -157,10 +169,10 @@ void orchard_wallet_commitment_tree_root(
|
|||
unsigned char* root_ret);
|
||||
|
||||
/**
|
||||
* Returns whether the specified transaction contains any Orchard notes that belong to
|
||||
* Returns whether the specified transaction involves any Orchard notes that belong to
|
||||
* this wallet.
|
||||
*/
|
||||
bool orchard_wallet_tx_contains_my_notes(
|
||||
bool orchard_wallet_tx_involves_my_notes(
|
||||
const OrchardWalletPtr* wallet,
|
||||
const unsigned char txid[32]);
|
||||
|
||||
|
|
|
@ -168,7 +168,7 @@ pub enum RewindError {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BundleDecryptionError {
|
||||
pub enum BundleLoadError {
|
||||
/// The action at the specified index failed to decrypt with
|
||||
/// the provided IVK.
|
||||
ActionDecryptionFailed(usize),
|
||||
|
@ -176,6 +176,26 @@ pub enum BundleDecryptionError {
|
|||
/// to the incoming viewing key that successfullly decrypted a
|
||||
/// note.
|
||||
FvkNotFound(IncomingViewingKey),
|
||||
/// An action index identified as potentially spending one of our
|
||||
/// notes is not a valid action index for the bundle.
|
||||
InvalidActionIndex(usize),
|
||||
}
|
||||
|
||||
/// A struct used to return metadata about how a bundle was determined
|
||||
/// to be involved with the wallet.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BundleWalletInvolvement {
|
||||
receive_action_metadata: BTreeMap<usize, IncomingViewingKey>,
|
||||
spend_action_metadata: Vec<usize>,
|
||||
}
|
||||
|
||||
impl BundleWalletInvolvement {
|
||||
pub fn new() -> Self {
|
||||
BundleWalletInvolvement {
|
||||
receive_action_metadata: BTreeMap::new(),
|
||||
spend_action_metadata: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
|
@ -314,30 +334,17 @@ impl Wallet {
|
|||
|
||||
/// Add note data for those notes that are decryptable with one of this wallet's
|
||||
/// incoming viewing keys to the wallet, and return a map from each decrypted
|
||||
/// action's index to the incoming vieing key that successfully decrypted that
|
||||
/// action's index to the incoming viewing key that successfully decrypted that
|
||||
/// action.
|
||||
pub fn add_notes_from_bundle(
|
||||
&mut self,
|
||||
txid: &TxId,
|
||||
bundle: &Bundle<Authorized, Amount>,
|
||||
) -> BTreeMap<usize, IncomingViewingKey> {
|
||||
// Check for spends of our notes by matching against the nullifiers
|
||||
// we're tracking, and when we detect one, associate the current
|
||||
// txid and action as spending the note.
|
||||
for (action_idx, action) in bundle.actions().iter().enumerate() {
|
||||
let nf = action.nullifier();
|
||||
// If a nullifier corresponds to one of our notes, add its inpoint as a
|
||||
// potential spend (the transaction may not end up being mined).
|
||||
if self.nullifiers.contains_key(nf) {
|
||||
self.potential_spends
|
||||
.entry(*nf)
|
||||
.or_insert_with(BTreeSet::new)
|
||||
.insert(InPoint {
|
||||
txid: *txid,
|
||||
action_idx,
|
||||
});
|
||||
}
|
||||
}
|
||||
) -> BundleWalletInvolvement {
|
||||
let mut involvement = BundleWalletInvolvement::new();
|
||||
// If we recognize any of our notes as being consumed as inputs to actions
|
||||
// in this bundle, record them as potential spends.
|
||||
involvement.spend_action_metadata = self.add_potential_spends(txid, bundle);
|
||||
|
||||
let keys = self
|
||||
.key_store
|
||||
|
@ -345,7 +352,6 @@ impl Wallet {
|
|||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let mut result = BTreeMap::new();
|
||||
|
||||
for (action_idx, ivk, note, recipient, memo) in bundle.decrypt_outputs_for_keys(&keys) {
|
||||
assert!(self.add_decrypted_note(
|
||||
|
@ -357,22 +363,49 @@ impl Wallet {
|
|||
recipient,
|
||||
memo
|
||||
));
|
||||
result.insert(action_idx, ivk);
|
||||
involvement.receive_action_metadata.insert(action_idx, ivk);
|
||||
}
|
||||
|
||||
result
|
||||
involvement
|
||||
}
|
||||
|
||||
/// Add note data to the wallet by attempting to
|
||||
/// incoming viewing keys to the wallet, and return a map from incoming viewing
|
||||
/// key to the vector of action indices that that key decrypts.
|
||||
pub fn add_notes_from_bundle_with_hints(
|
||||
/// Restore note and potential spend data from a bundle using the provided
|
||||
/// metadata.
|
||||
///
|
||||
/// - `tx_height`: if the transaction containing the bundle has been mined,
|
||||
/// this should contain the block height it was mined at
|
||||
/// - `txid`: The ID for the transaction from which the provided bundle was
|
||||
/// extracted.
|
||||
/// - `bundle`: the bundle to decrypt notes from
|
||||
/// - `hints`: a map from action index to the incoming viewing key that decrypts
|
||||
/// that action. If the IVK does not decrypt the action, or if it is not
|
||||
/// associated with a FVK in this wallet, `load_bundle` will return an error.
|
||||
/// - `potential_spend_idxs`: a list of action indices that were previously
|
||||
/// detected as spending our notes. If an index is out of range, `load_bundle`
|
||||
/// will return an error.
|
||||
pub fn load_bundle(
|
||||
&mut self,
|
||||
tx_height: Option<BlockHeight>,
|
||||
txid: &TxId,
|
||||
bundle: &Bundle<Authorized, Amount>,
|
||||
hints: BTreeMap<usize, &IncomingViewingKey>,
|
||||
) -> Result<(), BundleDecryptionError> {
|
||||
potential_spend_idxs: &[u32],
|
||||
) -> Result<(), BundleLoadError> {
|
||||
for action_idx in potential_spend_idxs {
|
||||
let action_idx: usize = (*action_idx).try_into().unwrap();
|
||||
if action_idx < bundle.actions().len() {
|
||||
self.add_potential_spend(
|
||||
bundle.actions()[action_idx].nullifier(),
|
||||
InPoint {
|
||||
txid: *txid,
|
||||
action_idx,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return Err(BundleLoadError::InvalidActionIndex(action_idx));
|
||||
}
|
||||
}
|
||||
|
||||
for (action_idx, ivk) in hints.into_iter() {
|
||||
if let Some((note, recipient, memo)) = bundle.decrypt_output_with_key(action_idx, ivk) {
|
||||
if !self.add_decrypted_note(
|
||||
|
@ -384,15 +417,16 @@ impl Wallet {
|
|||
recipient,
|
||||
memo,
|
||||
) {
|
||||
return Err(BundleDecryptionError::FvkNotFound(ivk.clone()));
|
||||
return Err(BundleLoadError::FvkNotFound(ivk.clone()));
|
||||
}
|
||||
} else {
|
||||
return Err(BundleDecryptionError::ActionDecryptionFailed(action_idx));
|
||||
return Err(BundleLoadError::ActionDecryptionFailed(action_idx));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Common functionality for add_notes_from_bundle and load_bundle
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn add_decrypted_note(
|
||||
&mut self,
|
||||
|
@ -444,6 +478,43 @@ impl Wallet {
|
|||
}
|
||||
}
|
||||
|
||||
/// For each Orchard action in the provided bundle, if the wallet
|
||||
/// is tracking a note corresponding to the action's revealed nullifer,
|
||||
/// mark that note as potentially spent.
|
||||
pub fn add_potential_spends(
|
||||
&mut self,
|
||||
txid: &TxId,
|
||||
bundle: &Bundle<Authorized, Amount>,
|
||||
) -> Vec<usize> {
|
||||
// Check for spends of our notes by matching against the nullifiers
|
||||
// we're tracking, and when we detect one, associate the current
|
||||
// txid and action as spending the note.
|
||||
let mut spend_action_idxs = vec![];
|
||||
for (action_idx, action) in bundle.actions().iter().enumerate() {
|
||||
let nf = action.nullifier();
|
||||
// If a nullifier corresponds to one of our notes, add its inpoint as a
|
||||
// potential spend (the transaction may not end up being mined).
|
||||
if self.nullifiers.contains_key(nf) {
|
||||
self.add_potential_spend(
|
||||
nf,
|
||||
InPoint {
|
||||
txid: *txid,
|
||||
action_idx,
|
||||
},
|
||||
);
|
||||
spend_action_idxs.push(action_idx);
|
||||
}
|
||||
}
|
||||
spend_action_idxs
|
||||
}
|
||||
|
||||
fn add_potential_spend(&mut self, nf: &Nullifier, inpoint: InPoint) {
|
||||
self.potential_spends
|
||||
.entry(*nf)
|
||||
.or_insert_with(BTreeSet::new)
|
||||
.insert(inpoint);
|
||||
}
|
||||
|
||||
/// Add note commitments for the Orchard components of a transaction to the note
|
||||
/// commitment tree, and mark the tree at the notes decryptable by this wallet so that
|
||||
/// in the future we can produce authentication paths to those notes.
|
||||
|
@ -525,8 +596,8 @@ impl Wallet {
|
|||
|
||||
/// Returns whether the transaction contains any notes either sent to or spent by this
|
||||
/// wallet.
|
||||
pub fn tx_contains_my_notes(&self, txid: &TxId) -> bool {
|
||||
self.wallet_received_notes.get(txid).is_some()
|
||||
pub fn tx_involves_my_notes(&self, txid: &TxId) -> bool {
|
||||
self.wallet_received_notes.contains_key(txid)
|
||||
|| self.nullifiers.values().any(|v| v.txid == *txid)
|
||||
}
|
||||
|
||||
|
@ -680,41 +751,56 @@ pub struct FFIActionIvk {
|
|||
ivk_ptr: *mut IncomingViewingKey,
|
||||
}
|
||||
|
||||
/// A C++-allocated function pointer that can push a FFIActionIVK value
|
||||
/// onto a C++ vector.
|
||||
/// A C++-allocated function pointer that can pass a FFIActionIVK value
|
||||
/// to a C++ callback receiver.
|
||||
pub type ActionIvkPushCb =
|
||||
unsafe extern "C" fn(obj: Option<FFICallbackReceiver>, value: FFIActionIvk);
|
||||
|
||||
/// A C++-allocated function pointer that can pass a FFIActionIVK value
|
||||
/// to a C++ callback receiver.
|
||||
pub type SpendIndexPushCb = unsafe extern "C" fn(obj: Option<FFICallbackReceiver>, value: u32);
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn orchard_wallet_add_notes_from_bundle(
|
||||
wallet: *mut Wallet,
|
||||
txid: *const [c_uchar; 32],
|
||||
bundle: *const Bundle<Authorized, Amount>,
|
||||
result: Option<FFICallbackReceiver>,
|
||||
push_cb: Option<ActionIvkPushCb>,
|
||||
) {
|
||||
cb_receiver: Option<FFICallbackReceiver>,
|
||||
action_ivk_push_cb: Option<ActionIvkPushCb>,
|
||||
spend_idx_push_cb: Option<SpendIndexPushCb>,
|
||||
) -> bool {
|
||||
let wallet = unsafe { wallet.as_mut() }.expect("Wallet pointer may not be null");
|
||||
let txid = TxId::from_bytes(*unsafe { txid.as_ref() }.expect("txid may not be null."));
|
||||
if let Some(bundle) = unsafe { bundle.as_ref() } {
|
||||
let added = wallet.add_notes_from_bundle(&txid, bundle);
|
||||
for (action_idx, ivk) in added.into_iter() {
|
||||
let involved =
|
||||
!(added.receive_action_metadata.is_empty() && added.spend_action_metadata.is_empty());
|
||||
for (action_idx, ivk) in added.receive_action_metadata.into_iter() {
|
||||
let action_ivk = FFIActionIvk {
|
||||
action_idx: action_idx.try_into().unwrap(),
|
||||
ivk_ptr: Box::into_raw(Box::new(ivk.clone())),
|
||||
};
|
||||
unsafe { (push_cb.unwrap())(result, action_ivk) };
|
||||
unsafe { (action_ivk_push_cb.unwrap())(cb_receiver, action_ivk) };
|
||||
}
|
||||
for action_idx in added.spend_action_metadata {
|
||||
unsafe { (spend_idx_push_cb.unwrap())(cb_receiver, action_idx.try_into().unwrap()) };
|
||||
}
|
||||
involved
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn orchard_wallet_restore_notes(
|
||||
pub extern "C" fn orchard_wallet_load_bundle(
|
||||
wallet: *mut Wallet,
|
||||
block_height: *const u32,
|
||||
txid: *const [c_uchar; 32],
|
||||
bundle: *const Bundle<Authorized, Amount>,
|
||||
hints: *const FFIActionIvk,
|
||||
hints_len: usize,
|
||||
potential_spend_idxs: *const u32,
|
||||
potential_spend_idxs_len: usize,
|
||||
) -> bool {
|
||||
let wallet = unsafe { wallet.as_mut() }.expect("Wallet pointer may not be null");
|
||||
let block_height = unsafe { block_height.as_ref() }.map(|h| BlockHeight::from(*h));
|
||||
|
@ -722,6 +808,8 @@ pub extern "C" fn orchard_wallet_restore_notes(
|
|||
let bundle = unsafe { bundle.as_ref() }.expect("bundle pointer may not be null.");
|
||||
|
||||
let hints_data = unsafe { slice::from_raw_parts(hints, hints_len) };
|
||||
let potential_spend_idxs =
|
||||
unsafe { slice::from_raw_parts(potential_spend_idxs, potential_spend_idxs_len) };
|
||||
|
||||
let mut hints = BTreeMap::new();
|
||||
for action_ivk in hints_data {
|
||||
|
@ -731,7 +819,7 @@ pub extern "C" fn orchard_wallet_restore_notes(
|
|||
);
|
||||
}
|
||||
|
||||
match wallet.add_notes_from_bundle_with_hints(block_height, &txid, bundle, hints) {
|
||||
match wallet.load_bundle(block_height, &txid, bundle, hints, potential_spend_idxs) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
error!("Failed to restore decrypted notes to wallet: {:?}", e);
|
||||
|
@ -837,14 +925,14 @@ pub extern "C" fn orchard_wallet_get_ivk_for_address(
|
|||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn orchard_wallet_tx_contains_my_notes(
|
||||
pub extern "C" fn orchard_wallet_tx_involves_my_notes(
|
||||
wallet: *const Wallet,
|
||||
txid: *const [c_uchar; 32],
|
||||
) -> bool {
|
||||
let wallet = unsafe { wallet.as_ref() }.expect("Wallet pointer may not be null.");
|
||||
let txid = TxId::from_bytes(*unsafe { txid.as_ref() }.expect("txid may not be null."));
|
||||
|
||||
wallet.tx_contains_my_notes(&txid)
|
||||
wallet.tx_involves_my_notes(&txid)
|
||||
}
|
||||
|
||||
/// A type used to pass note metadata across the FFI boundary.
|
||||
|
|
|
@ -44,7 +44,7 @@ CTransaction FakeOrchardTx(const OrchardSpendingKey& sk, libzcash::diversifier_i
|
|||
return maybeTx.GetTxOrThrow();
|
||||
}
|
||||
|
||||
TEST(OrchardWalletTests, TxContainsMyNotes) {
|
||||
TEST(OrchardWalletTests, TxInvolvesMyNotes) {
|
||||
auto consensusParams = RegtestActivateNU5();
|
||||
OrchardWallet wallet;
|
||||
|
||||
|
@ -58,21 +58,21 @@ TEST(OrchardWalletTests, TxContainsMyNotes) {
|
|||
wallet.AddNotesIfInvolvingMe(tx);
|
||||
|
||||
// Check that we detect the transaction as ours
|
||||
EXPECT_TRUE(wallet.TxContainsMyNotes(tx.GetHash()));
|
||||
EXPECT_TRUE(wallet.TxInvolvesMyNotes(tx.GetHash()));
|
||||
|
||||
// Create a transaction sending to a different diversified address
|
||||
auto tx1 = FakeOrchardTx(sk, libzcash::diversifier_index_t(0xffffffffffffffff));
|
||||
wallet.AddNotesIfInvolvingMe(tx1);
|
||||
|
||||
// Check that we also detect this transaction as ours
|
||||
EXPECT_TRUE(wallet.TxContainsMyNotes(tx1.GetHash()));
|
||||
EXPECT_TRUE(wallet.TxInvolvesMyNotes(tx1.GetHash()));
|
||||
|
||||
// Now generate a new key, and send a transaction to it without adding
|
||||
// the key to the wallet; it should not be detected as ours.
|
||||
auto skNotOurs = RandomOrchardSpendingKey();
|
||||
auto tx2 = FakeOrchardTx(skNotOurs, libzcash::diversifier_index_t(0));
|
||||
wallet.AddNotesIfInvolvingMe(tx2);
|
||||
EXPECT_FALSE(wallet.TxContainsMyNotes(tx2.GetHash()));
|
||||
EXPECT_FALSE(wallet.TxInvolvesMyNotes(tx2.GetHash()));
|
||||
|
||||
RegtestDeactivateNU5();
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
#include "rust/orchard/wallet.h"
|
||||
#include "zcash/address/orchard.hpp"
|
||||
|
||||
class OrchardWallet;
|
||||
|
||||
class OrchardNoteMetadata
|
||||
{
|
||||
private:
|
||||
|
@ -51,6 +53,58 @@ public:
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A container and serialization wrapper for storing information derived from
|
||||
* a transaction that is relevant to restoring Orchard wallet caches.
|
||||
*/
|
||||
class OrchardWalletTxMeta
|
||||
{
|
||||
private:
|
||||
// A map from action index to the IVK belonging to our wallet that decrypts
|
||||
// that action
|
||||
std::map<uint32_t, libzcash::OrchardIncomingViewingKey> mapOrchardActionData;
|
||||
// A vector of the action indices that spend notes belonging to our wallet
|
||||
std::vector<uint32_t> vActionsSpendingMyNotes;
|
||||
|
||||
friend class OrchardWallet;
|
||||
public:
|
||||
OrchardWalletTxMeta() {}
|
||||
|
||||
ADD_SERIALIZE_METHODS;
|
||||
|
||||
template <typename Stream, typename Operation>
|
||||
inline void SerializationOp(Stream& s, Operation ser_action) {
|
||||
int nVersion = s.GetVersion();
|
||||
if (!(s.GetType() & SER_GETHASH)) {
|
||||
READWRITE(nVersion);
|
||||
}
|
||||
READWRITE(mapOrchardActionData);
|
||||
READWRITE(vActionsSpendingMyNotes);
|
||||
}
|
||||
|
||||
const std::map<uint32_t, libzcash::OrchardIncomingViewingKey>& GetMyActionIVKs() const {
|
||||
return mapOrchardActionData;
|
||||
}
|
||||
|
||||
const std::vector<uint32_t>& GetActionsSpendingMyNotes() const {
|
||||
return vActionsSpendingMyNotes;
|
||||
}
|
||||
|
||||
bool empty() const {
|
||||
return (mapOrchardActionData.empty() && vActionsSpendingMyNotes.empty());
|
||||
}
|
||||
|
||||
friend bool operator==(const OrchardWalletTxMeta& a, const OrchardWalletTxMeta& b) {
|
||||
return (a.mapOrchardActionData == b.mapOrchardActionData &&
|
||||
a.vActionsSpendingMyNotes == b.vActionsSpendingMyNotes);
|
||||
}
|
||||
|
||||
friend bool operator!=(const OrchardWalletTxMeta& a, const OrchardWalletTxMeta& b) {
|
||||
return !(a == b);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class OrchardWallet
|
||||
{
|
||||
private:
|
||||
|
@ -111,28 +165,36 @@ public:
|
|||
return orchard_wallet_rewind(inner.get(), (uint32_t) nBlockHeight, &blocksRewoundRet);
|
||||
}
|
||||
|
||||
static void PushOrchardActionIVK(void* orchardActionsIVKRet, RawOrchardActionIVK actionIVK) {
|
||||
reinterpret_cast<std::map<uint32_t, libzcash::OrchardIncomingViewingKey>*>(orchardActionsIVKRet)->insert_or_assign(
|
||||
static void PushOrchardActionIVK(void* rec, RawOrchardActionIVK actionIVK) {
|
||||
reinterpret_cast<OrchardWalletTxMeta*>(rec)->mapOrchardActionData.insert_or_assign(
|
||||
actionIVK.actionIdx, libzcash::OrchardIncomingViewingKey(actionIVK.ivk)
|
||||
);
|
||||
}
|
||||
|
||||
static void PushSpendActionIdx(void* rec, uint32_t actionIdx) {
|
||||
reinterpret_cast<OrchardWalletTxMeta*>(rec)->vActionsSpendingMyNotes.push_back(actionIdx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add notes that are decryptable with IVKs for which the wallet
|
||||
* contains the full viewing key to the wallet, and return the
|
||||
* mapping from each decrypted Orchard action index to the IVK
|
||||
* that was used to decrypt that action's note.
|
||||
* metadata describing the wallet's involvement with this action,
|
||||
* or std::nullopt if the transaction does not involve the wallet.
|
||||
*/
|
||||
std::map<uint32_t, libzcash::OrchardIncomingViewingKey> AddNotesIfInvolvingMe(const CTransaction& tx) {
|
||||
std::map<uint32_t, libzcash::OrchardIncomingViewingKey> result;
|
||||
orchard_wallet_add_notes_from_bundle(
|
||||
std::optional<OrchardWalletTxMeta> AddNotesIfInvolvingMe(const CTransaction& tx) {
|
||||
OrchardWalletTxMeta txMeta;
|
||||
if (orchard_wallet_add_notes_from_bundle(
|
||||
inner.get(),
|
||||
tx.GetHash().begin(),
|
||||
tx.GetOrchardBundle().inner.get(),
|
||||
&result,
|
||||
PushOrchardActionIVK
|
||||
);
|
||||
return result;
|
||||
&txMeta,
|
||||
PushOrchardActionIVK,
|
||||
PushSpendActionIdx
|
||||
)) {
|
||||
return txMeta;
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -140,24 +202,25 @@ public:
|
|||
* Orchard bundle with provided incoming viewing keys, and adds those
|
||||
* notes to the wallet.
|
||||
*/
|
||||
bool RestoreDecryptedNotes(
|
||||
bool LoadWalletTx(
|
||||
const std::optional<int> nBlockHeight,
|
||||
const CTransaction& tx,
|
||||
std::map<uint32_t, libzcash::OrchardIncomingViewingKey> hints
|
||||
const OrchardWalletTxMeta& txMeta
|
||||
) {
|
||||
std::vector<RawOrchardActionIVK> rawHints;
|
||||
for (const auto& [action_idx, ivk] : hints) {
|
||||
for (const auto& [action_idx, ivk] : txMeta.mapOrchardActionData) {
|
||||
rawHints.push_back({ action_idx, ivk.inner.get() });
|
||||
}
|
||||
uint32_t blockHeight = nBlockHeight.has_value() ? (uint32_t) nBlockHeight.value() : 0;
|
||||
return orchard_wallet_restore_notes(
|
||||
return orchard_wallet_load_bundle(
|
||||
inner.get(),
|
||||
nBlockHeight.has_value() ? &blockHeight : nullptr,
|
||||
tx.GetHash().begin(),
|
||||
tx.GetOrchardBundle().inner.get(),
|
||||
rawHints.data(),
|
||||
rawHints.size()
|
||||
);
|
||||
rawHints.size(),
|
||||
txMeta.vActionsSpendingMyNotes.data(),
|
||||
txMeta.vActionsSpendingMyNotes.size());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -190,8 +253,8 @@ public:
|
|||
return value;
|
||||
}
|
||||
|
||||
bool TxContainsMyNotes(const uint256& txid) {
|
||||
return orchard_wallet_tx_contains_my_notes(
|
||||
bool TxInvolvesMyNotes(const uint256& txid) {
|
||||
return orchard_wallet_tx_involves_my_notes(
|
||||
inner.get(),
|
||||
txid.begin());
|
||||
}
|
||||
|
|
|
@ -849,6 +849,7 @@ bool CWallet::LoadUnifiedAddressMetadata(const ZcashdUnifiedAddressMetadata &add
|
|||
bool CWallet::LoadCaches()
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
AssertLockHeld(cs_main);
|
||||
|
||||
auto seed = GetMnemonicSeed();
|
||||
|
||||
|
@ -946,13 +947,13 @@ bool CWallet::LoadCaches()
|
|||
|
||||
// Restore decrypted Orchard notes.
|
||||
for (const auto& [_, walletTx] : mapWallet) {
|
||||
if (!walletTx.mapOrchardActionData.empty()) {
|
||||
if (!walletTx.orchardTxMeta.empty()) {
|
||||
const CBlockIndex* pTxIndex;
|
||||
std::optional<int> blockHeight;
|
||||
if (walletTx.GetDepthInMainChain(pTxIndex) > 0) {
|
||||
blockHeight = pTxIndex->nHeight;
|
||||
}
|
||||
if (!orchardWallet.RestoreDecryptedNotes(blockHeight, walletTx, walletTx.mapOrchardActionData)) {
|
||||
if (!orchardWallet.LoadWalletTx(blockHeight, walletTx, walletTx.orchardTxMeta)) {
|
||||
LogPrintf("%s: Error: Failed to decrypt previously decrypted notes for txid %s.\n",
|
||||
__func__, walletTx.GetHash().GetHex());
|
||||
return false;
|
||||
|
@ -2177,6 +2178,9 @@ void CWallet::AddToSpends(const uint256& wtxid)
|
|||
for (const SpendDescription &spend : thisTx.vShieldedSpend) {
|
||||
AddToSaplingSpends(spend.nullifier, wtxid);
|
||||
}
|
||||
|
||||
// for Orchard, the effects of this operation are performed by
|
||||
// AddNotesIfInvolvingMe and LoadUnifiedCaches
|
||||
}
|
||||
|
||||
void CWallet::ClearNoteWitnessCache()
|
||||
|
@ -2425,7 +2429,7 @@ void CWallet::DecrementNoteWitnesses(const Consensus::Params& consensus, const C
|
|||
// pindex->nHeight is the height of the block being removed, so we rewind
|
||||
// to the previous block height
|
||||
uint32_t blocksRewound{0};
|
||||
if (!orchardWallet.Rewind(pindex->nHeight - 1, blocksRewound)) {
|
||||
if (!orchardWallet.Rewind(pindex->nHeight - 1, blocksRewound) || blocksRewound != 1) {
|
||||
LogPrintf(
|
||||
"DecrementNoteWitnesses: Orchard wallet rewind unsuccessful at height %d; rewound %d",
|
||||
pindex->nHeight - 1, blocksRewound);
|
||||
|
@ -2895,9 +2899,9 @@ bool CWallet::UpdatedNoteData(const CWalletTx& wtxIn, CWalletTx& wtx)
|
|||
wtx.mapSaplingNoteData = tmp;
|
||||
}
|
||||
|
||||
bool unchangedOrchardFlag = (wtxIn.mapOrchardActionData.empty() || wtxIn.mapOrchardActionData == wtx.mapOrchardActionData);
|
||||
bool unchangedOrchardFlag = (wtxIn.orchardTxMeta.empty() || wtxIn.orchardTxMeta == wtx.orchardTxMeta);
|
||||
if (!unchangedOrchardFlag) {
|
||||
wtx.mapOrchardActionData = wtxIn.mapOrchardActionData;
|
||||
wtx.orchardTxMeta = wtxIn.orchardTxMeta;
|
||||
}
|
||||
|
||||
return !unchangedSproutFlag || !unchangedSaplingFlag || !unchangedOrchardFlag;
|
||||
|
@ -2945,12 +2949,15 @@ bool CWallet::AddToWalletIfInvolvingMe(
|
|||
}
|
||||
|
||||
// Orchard
|
||||
mapOrchardActionData_t orchardActionData;
|
||||
std::optional<OrchardWalletTxMeta> orchardTxMeta;
|
||||
if (consensus.NetworkUpgradeActive(nHeight, Consensus::UPGRADE_NU5)) {
|
||||
orchardActionData = orchardWallet.AddNotesIfInvolvingMe(tx);
|
||||
orchardTxMeta = orchardWallet.AddNotesIfInvolvingMe(tx);
|
||||
}
|
||||
|
||||
if (fExisted || IsMine(tx) || IsFromMe(tx) || sproutNoteData.size() > 0 || saplingNoteData.size() > 0 || orchardWallet.TxContainsMyNotes(tx.GetHash()))
|
||||
if (fExisted || IsMine(tx) || IsFromMe(tx) ||
|
||||
sproutNoteData.size() > 0 ||
|
||||
saplingNoteData.size() > 0 ||
|
||||
orchardTxMeta.has_value())
|
||||
{
|
||||
CWalletTx wtx(this, tx);
|
||||
|
||||
|
@ -2962,8 +2969,8 @@ bool CWallet::AddToWalletIfInvolvingMe(
|
|||
wtx.SetSaplingNoteData(saplingNoteData);
|
||||
}
|
||||
|
||||
if (orchardActionData.size() > 0) {
|
||||
wtx.SetOrchardActionData(orchardActionData);
|
||||
if (orchardTxMeta.has_value()) {
|
||||
wtx.SetOrchardTxMeta(orchardTxMeta.value());
|
||||
}
|
||||
|
||||
// Get merkle branch if transaction was found in a block
|
||||
|
@ -3563,16 +3570,20 @@ void CWalletTx::SetSaplingNoteData(const mapSaplingNoteData_t& noteData)
|
|||
}
|
||||
}
|
||||
|
||||
void CWalletTx::SetOrchardActionData(const mapOrchardActionData_t& actionData)
|
||||
void CWalletTx::SetOrchardTxMeta(OrchardWalletTxMeta txMeta)
|
||||
{
|
||||
mapOrchardActionData.clear();
|
||||
for (const auto& [action_idx, ivk] : actionData) {
|
||||
if (action_idx < GetOrchardBundle().GetNumActions()) {
|
||||
mapOrchardActionData.insert_or_assign(action_idx, ivk);
|
||||
} else {
|
||||
throw std::logic_error("CWalletTx::SetOrchardActionData(): Invalid action index");
|
||||
auto numActions = GetOrchardBundle().GetNumActions();
|
||||
for (const auto& [action_idx, ivk] : txMeta.GetMyActionIVKs()) {
|
||||
if (action_idx >= numActions) {
|
||||
throw std::logic_error("CWalletTx::SetOrchardTxMeta(): Invalid action index");
|
||||
}
|
||||
}
|
||||
for (uint32_t action_idx : txMeta.GetActionsSpendingMyNotes()) {
|
||||
if (action_idx >= numActions) {
|
||||
throw std::logic_error("CWalletTx::SetOrchardTxMeta(): Invalid action index");
|
||||
}
|
||||
}
|
||||
orchardTxMeta = txMeta;
|
||||
}
|
||||
|
||||
std::pair<SproutNotePlaintext, SproutPaymentAddress> CWalletTx::DecryptSproutNote(
|
||||
|
@ -4092,9 +4103,11 @@ int CWallet::ScanForWalletTransactions(
|
|||
CWalletDB walletdb(strWalletFile, "r+", false);
|
||||
for (auto hash : myTxHashes) {
|
||||
CWalletTx wtx = mapWallet[hash];
|
||||
if (!wtx.mapSaplingNoteData.empty() || !wtx.mapOrchardActionData.empty()) {
|
||||
if (!wtx.mapSaplingNoteData.empty() || !wtx.orchardTxMeta.empty()) {
|
||||
if (!walletdb.WriteTx(wtx)) {
|
||||
LogPrintf("Rescanning... WriteToDisk failed to update Sapling note data for: %s\n", hash.ToString());
|
||||
LogPrintf(
|
||||
"Rescanning... WriteToDisk failed to update Sapling/Orchard note data for tx: %s\n",
|
||||
hash.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -335,7 +335,6 @@ public:
|
|||
|
||||
typedef std::map<JSOutPoint, SproutNoteData> mapSproutNoteData_t;
|
||||
typedef std::map<SaplingOutPoint, SaplingNoteData> mapSaplingNoteData_t;
|
||||
typedef std::map<uint32_t, libzcash::OrchardIncomingViewingKey> mapOrchardActionData_t;
|
||||
|
||||
/** Sprout note, its location in a transaction, and number of confirmations. */
|
||||
struct SproutNoteEntry
|
||||
|
@ -464,11 +463,8 @@ public:
|
|||
mapValue_t mapValue;
|
||||
mapSproutNoteData_t mapSproutNoteData;
|
||||
mapSaplingNoteData_t mapSaplingNoteData;
|
||||
mapOrchardActionData_t mapOrchardActionData;
|
||||
// ORCHARD note data is not stored with the CMerkleTx directly, but is
|
||||
// accessible via pwallet->orchardWallet. Here we just store the indices
|
||||
// of the actions that belong to this wallet.
|
||||
std::vector<size_t> vOrchardActionIndices;
|
||||
OrchardWalletTxMeta orchardTxMeta;
|
||||
|
||||
std::vector<std::pair<std::string, std::string> > vOrderForm;
|
||||
unsigned int fTimeReceivedIsTxTime;
|
||||
unsigned int nTimeReceived; //!< time received by this node
|
||||
|
@ -582,7 +578,7 @@ public:
|
|||
}
|
||||
|
||||
if (fOverwintered && nVersion >= ZIP225_TX_VERSION) {
|
||||
READWRITE(mapOrchardActionData);
|
||||
READWRITE(orchardTxMeta);
|
||||
}
|
||||
|
||||
if (ser_action.ForRead())
|
||||
|
@ -619,7 +615,7 @@ public:
|
|||
|
||||
void SetSproutNoteData(const mapSproutNoteData_t& noteData);
|
||||
void SetSaplingNoteData(const mapSaplingNoteData_t& noteData);
|
||||
void SetOrchardActionData(const mapOrchardActionData_t& actionData);
|
||||
void SetOrchardTxMeta(OrchardWalletTxMeta actionData);
|
||||
|
||||
std::pair<libzcash::SproutNotePlaintext, libzcash::SproutPaymentAddress> DecryptSproutNote(
|
||||
JSOutPoint jsop) const;
|
||||
|
|
Loading…
Reference in New Issue