Merge pull request #5637 from nuttycom/feature/wallet_orchard-persist_action_ivks

Implement Orchard note persistence & restore.
This commit is contained in:
Kris Nuttycombe 2022-03-09 22:13:26 -07:00 committed by GitHub
commit b21ca34d44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 424 additions and 126 deletions

View File

@ -7,10 +7,14 @@ 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,
wait_and_assert_operationid_status
stop_nodes,
sync_blocks,
wait_and_assert_operationid_status,
wait_bitcoinds,
)
from decimal import Decimal
@ -82,8 +86,7 @@ class WalletOrchardTest(BitcoinTestFramework):
self.nodes[1].z_getbalanceforaccount(acct1))
# Split the network
# TODO: Enable after wallet persistence.
# self.split_network()
self.split_network()
# Send another tx to ua1
recipients = [{"address": ua1, "amount": Decimal('10')}]
@ -107,7 +110,7 @@ class WalletOrchardTest(BitcoinTestFramework):
recipients = [{"address": ua3, "amount": Decimal('1')}]
myopid = self.nodes[2].z_sendmany(ua2, recipients, 1, 0, True)
wait_and_assert_operationid_status(self.nodes[2], myopid)
rollback_tx = wait_and_assert_operationid_status(self.nodes[2], myopid)
self.sync_all()
self.nodes[2].generate(1)
@ -121,25 +124,52 @@ class WalletOrchardTest(BitcoinTestFramework):
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 1},
self.nodes[3].z_getbalanceforaccount(acct3))
# TODO: enable after wallet persistence
# # Rejoin the network.
# self.join_network()
# Check that the mempools are empty
for i in range(self.num_nodes):
assert_equal(set([]), set(self.nodes[i].getrawmempool()))
# 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)
# split 0/1's chain should have won, so their wallet balance should be consistent
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('2000000000')}}, 'minimum_confirmations': 1},
self.nodes[1].z_getbalanceforaccount(acct1))
# TODO: enable after wallet persistence
# # split 2/3's chain should have been rolled back, so their txn should have been
# # un-mined
# assert_equal(
# {'pools': {'sapling': {'valueZat': Decimal('1000000000')}}, 'minimum_confirmations': 1},
# self.nodes[2].z_getbalanceforaccount(acct2))
# split 2/3's chain should have been rolled back, so their txn should have been
# un-mined and returned to the mempool
assert_equal(set([rollback_tx]), set(self.nodes[2].getrawmempool()))
# assert_equal(
# {'pools': {}, 'minimum_confirmations': 1},
# self.nodes[3].z_getbalanceforaccount(acct3))
# our sole Sapling note is spent, 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
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
assert_equal(
{'pools': {}, 'minimum_confirmations': 1},
self.nodes[3].z_getbalanceforaccount(acct3))
# our unconfirmed balance is 1 zec
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 0},
self.nodes[3].z_getbalanceforaccount(acct3, 0))
if __name__ == '__main__':
WalletOrchardTest().main()

View File

@ -50,15 +50,13 @@ bool orchard_wallet_checkpoint(
);
/**
* Returns whether or not the wallet has any checkpointed state to
* which it can rewind.
* Returns whether or not the wallet has any checkpointed state to which it can rewind.
*/
bool orchard_wallet_is_checkpointed(const OrchardWalletPtr* wallet);
/**
* Rewinds to the most recent checkpoint, and marks as unspent any notes
* previously identified as having been spent by transactions in the
* latest block.
* Rewinds to the most recent checkpoint, and marks as unspent any notes previously
* identified as having been spent by transactions in the latest block.
*
* The `blockHeight` argument provides the height to which the witness tree should be
* rewound, such that after the rewind this height corresponds to the latest block
@ -76,16 +74,60 @@ bool orchard_wallet_rewind(
);
/**
* Searches the provided bundle for notes that are visible to the specified
* wallet's incoming viewing keys, and adds those notes to the wallet.
* A C struct used to transfer action_idx/IVK pairs back from Rust across the FFI
* boundary. This must have the same in-memory representation as the `FFIActionIVK` type
* in orchard_ffi/wallet.rs.
*
* The provided bundle must be a component of the transaction from which
* `txid` was derived.
* Values of the `ivk` pointer must be freed manually; the best way to do this is to
* wrap this pointer in an `OrchardIncomingViewingKey` which handles deallocation
* in the object destructor.
*/
struct RawOrchardActionIVK {
uint64_t actionIdx;
OrchardIncomingViewingKeyPtr* ivk;
};
static_assert(
sizeof(RawOrchardActionIVK) == 16,
"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);
/**
* 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 bundle must be a component of the transaction from which `txid` was
* derived.
*/
void orchard_wallet_add_notes_from_bundle(
OrchardWalletPtr* wallet,
const unsigned char txid[32],
const OrchardBundlePtr* bundle
const OrchardBundlePtr* bundle,
void* resultCollection,
push_action_ivk_callback_t push_cb
);
/**
* Decrypts a selection of notes from the bundle with specified incoming viewing
* keys, and adds those notes to the wallet.
*
* The provided bundle must be a component of the transaction from which
* `txid` was derived.
*
* 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(
OrchardWalletPtr* wallet,
const uint32_t* blockHeight,
const unsigned char txid[32],
const OrchardBundlePtr* bundle,
const RawOrchardActionIVK* actionIvks,
size_t actionIvksLen
);
/**
@ -114,34 +156,32 @@ void orchard_wallet_commitment_tree_root(
unsigned char* root_ret);
/**
* Returns whether the specified transaction contains any Orchard notes that belong
* to this wallet.
* Returns whether the specified transaction contains any Orchard notes that belong to
* this wallet.
*/
bool orchard_wallet_tx_contains_my_notes(
const OrchardWalletPtr* wallet,
const unsigned char txid[32]);
/**
* Add the specified spending key to the wallet's key store.
* This will also compute and add the associated full and
* incoming viewing keys.
* Add the specified spending key to the wallet's key store. This will also compute and
* add the associated full and incoming viewing keys.
*/
void orchard_wallet_add_spending_key(
OrchardWalletPtr* wallet,
const OrchardSpendingKeyPtr* sk);
/**
* Add the specified full viewing key to the wallet's key store.
* This will also compute and add the associated incoming viewing key.
* Add the specified full viewing key to the wallet's key store. This will also compute
* and add the associated incoming viewing key.
*/
void orchard_wallet_add_full_viewing_key(
OrchardWalletPtr* wallet,
const OrchardFullViewingKeyPtr* fvk);
/**
* Add the specified raw address to the wallet's key store,
* associated with the incoming viewing key from which that
* address was derived.
* Add the specified raw address to the wallet's key store, associated with the incoming
* viewing key from which that address was derived.
*/
bool orchard_wallet_add_raw_address(
OrchardWalletPtr* wallet,
@ -149,9 +189,8 @@ bool orchard_wallet_add_raw_address(
const OrchardIncomingViewingKeyPtr* ivk);
/**
* Returns a pointer to the Orchard incoming viewing key
* corresponding to the specified raw address, if it is
* known to the wallet, or `nullptr` otherwise.
* Returns a pointer to the Orchard incoming viewing key corresponding to the specified
* raw address, if it is known to the wallet, or `nullptr` otherwise.
*
* Memory is allocated by Rust and must be manually freed using
* `orchard_incoming_viewing_key_free`.
@ -161,9 +200,9 @@ OrchardIncomingViewingKeyPtr* orchard_wallet_get_ivk_for_address(
const OrchardRawAddressPtr* addr);
/**
* A C struct used to transfer note metadata information across the Rust FFI
* boundary. This must have the same in-memory representation as the
* `NoteMetadata` type in orchard_ffi/wallet.rs.
* A C struct used to transfer note metadata information across the Rust FFI boundary.
* This must have the same in-memory representation as the `FFINoteMetadata` type in
* orchard_ffi/wallet.rs.
*/
struct RawOrchardNoteMetadata {
unsigned char txid[32];
@ -173,20 +212,19 @@ struct RawOrchardNoteMetadata {
unsigned char memo[512];
};
typedef void (*push_callback_t)(void* resultVector, const RawOrchardNoteMetadata noteMeta);
typedef void (*push_note_callback_t)(void* resultVector, const RawOrchardNoteMetadata noteMeta);
/**
* Finds notes that belong to the wallet that were sent to addresses derived
* from the specified incoming viewing key, subject to the specified flags, and
* uses the provided callback to push RawOrchardNoteMetadata values
* corresponding to those notes on to the provided result vector. Note that
* the push_cb callback can perform any necessary conversion from a
* RawOrchardNoteMetadata value in addition to modifying the provided
* Finds notes that belong to the wallet that were sent to addresses derived from the
* specified incoming viewing key, subject to the specified flags, and uses the provided
* callback to push RawOrchardNoteMetadata values corresponding to those notes on to the
* provided result vector. Note that the push_cb callback can perform any necessary
* conversion from a RawOrchardNoteMetadata value in addition to modifying the provided
* result vector.
*
* If `ivk` is null, all notes belonging to the wallet will be returned.
* The `RawOrchardNoteMetadata::addr` pointers for values provided to the
* callback must be manually freed by the caller.
* If `ivk` is null, all notes belonging to the wallet will be returned. The
* `RawOrchardNoteMetadata::addr` pointers for values provided to the callback must be
* manually freed by the caller.
*/
void orchard_wallet_get_filtered_notes(
const OrchardWalletPtr* wallet,
@ -194,17 +232,16 @@ void orchard_wallet_get_filtered_notes(
bool ignoreMined,
bool requireSpendingKey,
void* resultVector,
push_callback_t push_cb
push_note_callback_t push_cb
);
typedef void (*push_txid_callback_t)(void* resultVector, unsigned char txid[32]);
/**
* Returns a vector of transaction IDs for transactions that have been observed
* as spending the given outpoint (transaction ID and action index) by using
* the `push_cb` callback to push transaction IDs onto the provided result
* vector.
* Returns a vector of transaction IDs for transactions that have been observed as
* spending the given outpoint (transaction ID and action index) by using the `push_cb`
* callback to push transaction IDs onto the provided result vector.
*/
void orchard_wallet_get_potential_spends(
const OrchardWalletPtr* wallet,
@ -214,7 +251,6 @@ void orchard_wallet_get_potential_spends(
push_txid_callback_t push_cb
);
#ifdef __cplusplus
}
#endif

View File

@ -2,6 +2,7 @@ use incrementalmerkletree::{bridgetree::BridgeTree, Frontier, Tree};
use libc::c_uchar;
use std::collections::{BTreeMap, BTreeSet};
use std::convert::TryInto;
use std::slice;
use tracing::error;
use zcash_primitives::{
@ -63,18 +64,6 @@ pub struct TxNotes {
decrypted_notes: BTreeMap<usize, DecryptedNote>,
}
/// A type used to pass note metadata across the FFI boundary.
/// This must have the same representation as `struct RawOrchardNoteMetadata`
/// in `rust/include/rust/orchard/wallet.h`.
#[repr(C)]
pub struct NoteMetadata {
txid: [u8; 32],
action_idx: u32,
recipient: *mut Address,
note_value: i64,
memo: [u8; 512],
}
struct KeyStore {
payment_addresses: BTreeMap<OrderedAddress, IncomingViewingKey>,
viewing_keys: BTreeMap<IncomingViewingKey, FullViewingKey>,
@ -174,6 +163,17 @@ pub enum RewindError {
InsufficientCheckpoints(usize),
}
#[derive(Debug, Clone)]
pub enum BundleDecryptionError {
/// The action at the specified index failed to decrypt with
/// the provided IVK.
ActionDecryptionFailed(usize),
/// The wallet did not contain the full viewing key corresponding
/// to the incoming viewing key that successfullly decrypted a
/// note.
FvkNotFound(IncomingViewingKey),
}
impl Wallet {
pub fn empty() -> Self {
Wallet {
@ -306,18 +306,14 @@ 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 the indices of the actions that we
/// were able to decrypt.
/// 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.
pub fn add_notes_from_bundle(
&mut self,
txid: &TxId,
bundle: &Bundle<Authorized, Amount>,
) -> Vec<usize> {
let mut tx_notes = TxNotes {
tx_height: None,
decrypted_notes: BTreeMap::new(),
};
) -> 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.
@ -342,8 +338,68 @@ impl Wallet {
.keys()
.cloned()
.collect::<Vec<_>>();
let mut result = 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(
None,
txid,
action_idx,
ivk.clone(),
note,
recipient,
memo
));
result.insert(action_idx, ivk);
}
result
}
/// 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(
&mut self,
tx_height: Option<BlockHeight>,
txid: &TxId,
bundle: &Bundle<Authorized, Amount>,
hints: BTreeMap<usize, &IncomingViewingKey>,
) -> Result<(), BundleDecryptionError> {
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(
tx_height,
txid,
action_idx,
ivk.clone(),
note,
recipient,
memo,
) {
return Err(BundleDecryptionError::FvkNotFound(ivk.clone()));
}
} else {
return Err(BundleDecryptionError::ActionDecryptionFailed(action_idx));
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn add_decrypted_note(
&mut self,
tx_height: Option<BlockHeight>,
txid: &TxId,
action_idx: usize,
ivk: IncomingViewingKey,
note: Note,
recipient: Address,
memo: [u8; 512],
) -> bool {
// Generate the nullifier for the received note and add it to the nullifiers map so
// that we can detect when the note is later spent.
if let Some(fvk) = self.key_store.viewing_keys.get(&ivk) {
let outpoint = OutPoint {
txid: *txid,
action_idx,
@ -351,26 +407,30 @@ impl Wallet {
// Generate the nullifier for the received note and add it to the nullifiers map so
// that we can detect when the note is later spent.
if let Some(fvk) = self.key_store.viewing_keys.get(&ivk) {
let nf = note.nullifier(fvk);
self.nullifiers.insert(nf, outpoint);
}
let nf = note.nullifier(fvk);
self.nullifiers.insert(nf, outpoint);
// add the decrypted note data to the wallet
let note_data = DecryptedNote { note, memo };
tx_notes.decrypted_notes.insert(action_idx, note_data);
self.wallet_received_notes
.entry(*txid)
.or_insert_with(|| TxNotes {
tx_height,
decrypted_notes: BTreeMap::new(),
})
.decrypted_notes
.insert(action_idx, note_data);
self.nullifiers.insert(nf, outpoint);
// add the association between the address and the IVK used
// to decrypt the note
self.key_store.add_raw_address(recipient, ivk);
result.push(action_idx);
}
self.key_store.add_raw_address(recipient, ivk.clone());
if !tx_notes.decrypted_notes.is_empty() {
self.wallet_received_notes.insert(*txid, tx_notes);
true
} else {
false
}
result
}
/// Add note commitments for the Orchard components of a transaction to the note
@ -496,6 +556,14 @@ impl Wallet {
}
}
//
// FFI
//
/// A type alias for a pointer to a C++ value that is the target of
/// a mutating action by a callback across the FFI
pub type FFICallbackReceiver = std::ptr::NonNull<libc::c_void>;
#[no_mangle]
pub extern "C" fn orchard_wallet_new() -> *mut Wallet {
let empty_wallet = Wallet::empty();
@ -551,16 +619,72 @@ pub extern "C" fn orchard_wallet_rewind(
}
}
/// A type used to pass action decryption metadata across the FFI boundary.
/// This must have the same representation as `struct RawOrchardActionIVK`
/// in `rust/include/rust/orchard/wallet.h`. action_idx is
#[repr(C)]
pub struct FFIActionIvk {
action_idx: u64,
ivk_ptr: *mut IncomingViewingKey,
}
/// A C++-allocated function pointer that can push a FFIActionIVK value
/// onto a C++ vector.
pub type ActionIvkPushCb =
unsafe extern "C" fn(obj: Option<FFICallbackReceiver>, value: FFIActionIvk);
#[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>,
) {
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() } {
wallet.add_notes_from_bundle(&txid, bundle);
let added = wallet.add_notes_from_bundle(&txid, bundle);
for (action_idx, ivk) in added.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) };
}
}
}
#[no_mangle]
pub extern "C" fn orchard_wallet_restore_notes(
wallet: *mut Wallet,
block_height: *const u32,
txid: *const [c_uchar; 32],
bundle: *const Bundle<Authorized, Amount>,
hints: *const FFIActionIvk,
hints_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));
let txid = TxId::from_bytes(*unsafe { txid.as_ref() }.expect("txid may not be null."));
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 mut hints = BTreeMap::new();
for action_ivk in hints_data {
hints.insert(
action_ivk.action_idx.try_into().unwrap(),
unsafe { action_ivk.ivk_ptr.as_ref() }.expect("ivk pointer may not be null"),
);
}
match wallet.add_notes_from_bundle_with_hints(block_height, &txid, bundle, hints) {
Ok(_) => true,
Err(e) => {
error!("Failed to restore decrypted notes to wallet: {:?}", e);
false
}
}
}
@ -655,8 +779,21 @@ pub extern "C" fn orchard_wallet_tx_contains_my_notes(
wallet.tx_contains_my_notes(&txid)
}
pub type VecObj = std::ptr::NonNull<libc::c_void>;
pub type PushCb = unsafe extern "C" fn(obj: Option<VecObj>, meta: NoteMetadata);
/// A type used to pass note metadata across the FFI boundary.
/// This must have the same representation as `struct RawOrchardNoteMetadata`
/// in `rust/include/rust/orchard/wallet.h`.
#[repr(C)]
pub struct FFINoteMetadata {
txid: [u8; 32],
action_idx: u32,
recipient: *mut Address,
note_value: i64,
memo: [u8; 512],
}
/// A C++-allocated function pointer that can push a FFINoteMetadata value
/// onto a C++ vector.
pub type NotePushCb = unsafe extern "C" fn(obj: Option<FFICallbackReceiver>, meta: FFINoteMetadata);
#[no_mangle]
pub extern "C" fn orchard_wallet_get_filtered_notes(
@ -664,14 +801,14 @@ pub extern "C" fn orchard_wallet_get_filtered_notes(
ivk: *const IncomingViewingKey,
ignore_mined: bool,
require_spending_key: bool,
result: Option<VecObj>,
push_cb: Option<PushCb>,
result: Option<FFICallbackReceiver>,
push_cb: Option<NotePushCb>,
) {
let wallet = unsafe { wallet.as_ref() }.expect("Wallet pointer may not be null.");
let ivk = unsafe { ivk.as_ref() };
for (outpoint, dnote) in wallet.get_filtered_notes(ivk, ignore_mined, require_spending_key) {
let metadata = NoteMetadata {
let metadata = FFINoteMetadata {
txid: *outpoint.txid.as_ref(),
action_idx: outpoint.action_idx as u32,
recipient: Box::into_raw(Box::new(dnote.note.recipient())),
@ -682,14 +819,14 @@ pub extern "C" fn orchard_wallet_get_filtered_notes(
}
}
pub type PushTxId = unsafe extern "C" fn(obj: Option<VecObj>, txid: *const [u8; 32]);
pub type PushTxId = unsafe extern "C" fn(obj: Option<FFICallbackReceiver>, txid: *const [u8; 32]);
#[no_mangle]
pub extern "C" fn orchard_wallet_get_potential_spends(
wallet: *const Wallet,
txid: *const [c_uchar; 32],
action_idx: u32,
result: Option<VecObj>,
result: Option<FFICallbackReceiver>,
push_cb: Option<PushTxId>,
) {
let wallet = unsafe { wallet.as_ref() }.expect("Wallet pointer may not be null.");

View File

@ -103,15 +103,53 @@ 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(
actionIVK.actionIdx, libzcash::OrchardIncomingViewingKey(actionIVK.ivk)
);
}
/**
* Add notes that are decryptable with IVKs for which the wallet
* contains the full viewing key to 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.
*/
void AddNotesIfInvolvingMe(const CTransaction& tx) {
std::map<uint32_t, libzcash::OrchardIncomingViewingKey> AddNotesIfInvolvingMe(const CTransaction& tx) {
std::map<uint32_t, libzcash::OrchardIncomingViewingKey> result;
orchard_wallet_add_notes_from_bundle(
inner.get(),
tx.GetHash().begin(),
tx.GetOrchardBundle().inner.get());
tx.GetOrchardBundle().inner.get(),
&result,
PushOrchardActionIVK
);
return result;
}
/**
* Decrypts a selection of notes from the specified transaction's
* Orchard bundle with provided incoming viewing keys, and adds those
* notes to the wallet.
*/
bool RestoreDecryptedNotes(
const std::optional<int> nBlockHeight,
const CTransaction& tx,
std::map<uint32_t, libzcash::OrchardIncomingViewingKey> hints
) {
std::vector<RawOrchardActionIVK> rawHints;
for (const auto& [action_idx, ivk] : hints) {
rawHints.push_back({ action_idx, ivk.inner.get() });
}
uint32_t blockHeight = nBlockHeight.has_value() ? (uint32_t) nBlockHeight.value() : 0;
return orchard_wallet_restore_notes(
inner.get(),
nBlockHeight.has_value() ? &blockHeight : nullptr,
tx.GetHash().begin(),
tx.GetOrchardBundle().inner.get(),
rawHints.data(),
rawHints.size()
);
}
/**
@ -121,6 +159,7 @@ public:
* Returns `false` if the caller attempts to insert a block out-of-order.
*/
bool AppendNoteCommitments(const int nBlockHeight, const CBlock& block) {
assert(nBlockHeight >= 0);
for (int txidx = 0; txidx < block.vtx.size(); txidx++) {
const CTransaction& tx = block.vtx[txidx];
if (!orchard_wallet_append_bundle_commitments(
@ -186,7 +225,6 @@ public:
libzcash::OrchardRawAddress(rawNoteMeta.addr),
rawNoteMeta.noteValue,
memo);
// TODO: noteMeta.confirmations is only available from the C++ wallet
reinterpret_cast<std::vector<OrchardNoteMetadata>*>(orchardNotesRet)->push_back(noteMeta);
}

View File

@ -846,12 +846,13 @@ bool CWallet::LoadUnifiedAddressMetadata(const ZcashdUnifiedAddressMetadata &add
addrmeta.GetReceiverTypes());
}
bool CWallet::LoadUnifiedCaches()
bool CWallet::LoadCaches()
{
AssertLockHeld(cs_wallet);
auto seed = GetMnemonicSeed();
// Restore unified key metadata
for (auto account = mapUnifiedAccountKeys.begin(); account != mapUnifiedAccountKeys.end(); ++account) {
auto ufvkId = account->second;
auto ufvk = GetUnifiedFullViewingKey(ufvkId);
@ -943,6 +944,22 @@ bool CWallet::LoadUnifiedCaches()
}
}
// Restore decrypted Orchard notes.
for (const auto& [_, walletTx] : mapWallet) {
if (!walletTx.mapOrchardActionData.empty()) {
const CBlockIndex* pTxIndex;
std::optional<int> blockHeight;
if (walletTx.GetDepthInMainChain(pTxIndex) > 0) {
blockHeight = pTxIndex->nHeight;
}
if (!orchardWallet.RestoreDecryptedNotes(blockHeight, walletTx, walletTx.mapOrchardActionData)) {
LogPrintf("%s: Error: Failed to decrypt previously decrypted notes for txid %s.\n",
__func__, walletTx.GetHash().GetHex());
return false;
}
}
}
return true;
}
@ -2408,7 +2425,11 @@ 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};
orchardWallet.Rewind(pindex->nHeight - 1, blocksRewound);
if (!orchardWallet.Rewind(pindex->nHeight - 1, blocksRewound)) {
LogPrintf(
"DecrementNoteWitnesses: Orchard wallet rewind unsuccessful at height %d; rewound %d",
pindex->nHeight - 1, blocksRewound);
}
// TODO ORCHARD: Enable the following assertions.
//assert(orchardWallet.Rewind(pindex->nHeight - 1, blocksRewound));
//assert(blocksRewound == 1);
@ -2874,7 +2895,12 @@ bool CWallet::UpdatedNoteData(const CWalletTx& wtxIn, CWalletTx& wtx)
wtx.mapSaplingNoteData = tmp;
}
return !unchangedSproutFlag || !unchangedSaplingFlag;
bool unchangedOrchardFlag = (wtxIn.mapOrchardActionData.empty() || wtxIn.mapOrchardActionData == wtx.mapOrchardActionData);
if (!unchangedOrchardFlag) {
wtx.mapOrchardActionData = wtxIn.mapOrchardActionData;
}
return !unchangedSproutFlag || !unchangedSaplingFlag || !unchangedOrchardFlag;
}
/**
@ -2919,8 +2945,9 @@ bool CWallet::AddToWalletIfInvolvingMe(
}
// Orchard
mapOrchardActionData_t orchardActionData;
if (consensus.NetworkUpgradeActive(nHeight, Consensus::UPGRADE_NU5)) {
orchardWallet.AddNotesIfInvolvingMe(tx);
orchardActionData = orchardWallet.AddNotesIfInvolvingMe(tx);
}
if (fExisted || IsMine(tx) || IsFromMe(tx) || sproutNoteData.size() > 0 || saplingNoteData.size() > 0 || orchardWallet.TxContainsMyNotes(tx.GetHash()))
@ -2935,6 +2962,10 @@ bool CWallet::AddToWalletIfInvolvingMe(
wtx.SetSaplingNoteData(saplingNoteData);
}
if (orchardActionData.size() > 0) {
wtx.SetOrchardActionData(orchardActionData);
}
// Get merkle branch if transaction was found in a block
if (pblock)
wtx.SetMerkleBranch(*pblock);
@ -3497,7 +3528,7 @@ bool CWallet::LoadCryptedLegacyHDSeed(const uint256& seedFp, const std::vector<u
return CCryptoKeyStore::SetCryptedLegacyHDSeed(seedFp, seed);
}
void CWalletTx::SetSproutNoteData(mapSproutNoteData_t &noteData)
void CWalletTx::SetSproutNoteData(const mapSproutNoteData_t& noteData)
{
mapSproutNoteData.clear();
for (const std::pair<JSOutPoint, SproutNoteData> nd : noteData) {
@ -3513,7 +3544,7 @@ void CWalletTx::SetSproutNoteData(mapSproutNoteData_t &noteData)
}
}
void CWalletTx::SetSaplingNoteData(mapSaplingNoteData_t &noteData)
void CWalletTx::SetSaplingNoteData(const mapSaplingNoteData_t& noteData)
{
mapSaplingNoteData.clear();
for (const std::pair<SaplingOutPoint, SaplingNoteData> nd : noteData) {
@ -3525,6 +3556,18 @@ void CWalletTx::SetSaplingNoteData(mapSaplingNoteData_t &noteData)
}
}
void CWalletTx::SetOrchardActionData(const mapOrchardActionData_t& actionData)
{
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");
}
}
}
std::pair<SproutNotePlaintext, SproutPaymentAddress> CWalletTx::DecryptSproutNote(
JSOutPoint jsop) const
{
@ -3983,7 +4026,10 @@ int CWallet::ScanForWalletTransactions(
// can't rewind far enough. This is an unrecoverable failure; it means that we
// can't get back to a valid wallet state without resetting the wallet all
// the way back to NU5 activation.
throw std::runtime_error("CWallet::ScanForWalletTransactions(): Orchard wallet is out of sync. Please restart your node with -rescan.");
LogPrintf("Orchard wallet is out of sync: %d blockd rewound.", blocksRewound);
// TODO ORCHARD: enable after wallet persistence
// throw std::runtime_error("CWallet::ScanForWalletTransactions(): Orchard wallet is out of sync. Please restart your node with -rescan.");
}
}
} else if (isInitScan && pindex->nHeight < nu5_height) {
@ -4034,12 +4080,12 @@ int CWallet::ScanForWalletTransactions(
}
}
// After rescanning, persist Sapling note data that might have changed, e.g. nullifiers.
// Do not flush the wallet here for performance reasons.
// After rescanning, persist Sapling & Orchard note data that might have changed,
// e.g. nullifiers. Do not flush the wallet here for performance reasons.
CWalletDB walletdb(strWalletFile, "r+", false);
for (auto hash : myTxHashes) {
CWalletTx wtx = mapWallet[hash];
if (!wtx.mapSaplingNoteData.empty()) {
if (!wtx.mapSaplingNoteData.empty() || !wtx.mapOrchardActionData.empty()) {
if (!walletdb.WriteTx(wtx)) {
LogPrintf("Rescanning... WriteToDisk failed to update Sapling note data for: %s\n", hash.ToString());
}

View File

@ -334,6 +334,7 @@ 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
@ -462,6 +463,7 @@ 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.
@ -579,8 +581,7 @@ public:
}
if (fOverwintered && nVersion >= ZIP225_TX_VERSION) {
// ORCHARD TODO: serialize/deserialize orchard bits using a pointer
// to the Orchard wallet & the txid as referents
READWRITE(mapOrchardActionData);
}
if (ser_action.ForRead())
@ -615,8 +616,9 @@ public:
MarkDirty();
}
void SetSproutNoteData(mapSproutNoteData_t &noteData);
void SetSaplingNoteData(mapSaplingNoteData_t &noteData);
void SetSproutNoteData(const mapSproutNoteData_t& noteData);
void SetSaplingNoteData(const mapSaplingNoteData_t& noteData);
void SetOrchardActionData(const mapOrchardActionData_t& actionData);
std::pair<libzcash::SproutNotePlaintext, libzcash::SproutPaymentAddress> DecryptSproutNote(
JSOutPoint jsop) const;
@ -1553,7 +1555,7 @@ public:
//!
//! Returns true if and only if there were no detected inconsistencies or
//! failures in reconstructing the cache.
bool LoadUnifiedCaches();
bool LoadCaches();
std::optional<libzcash::UFVKId> FindUnifiedFullViewingKey(const libzcash::UnifiedAddress& addr) const;
std::optional<libzcash::AccountId> GetUnifiedAccountId(const libzcash::UFVKId& ufvkId) const;

View File

@ -964,7 +964,7 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
pcursor->close();
// Load unified address/account/key caches based on what was loaded
if (!pwallet->LoadUnifiedCaches()) {
if (!pwallet->LoadCaches()) {
// We can be more permissive of certain kinds of failures during
// loading; for now we'll interpret failure to reconstruct the
// caches to be "as bad" as losing keys.
@ -1291,11 +1291,11 @@ bool CWalletDB::Recover(CDBEnv& dbenv, const std::string& filename, bool fOnlyKe
ptxn->commit(0);
pdbCopy->close(0);
// Try to load the unified caches, uncovering inconsistencies in wallet
// Try to load the wallet's caches, uncovering inconsistencies in wallet
// records like missing viewing key records despite existing account
// records.
if (!dummyWallet.LoadUnifiedCaches()) {
LogPrintf("WARNING: unified caches could not be reconstructed; salvaged wallet file may have omissions");
if (!dummyWallet.LoadCaches()) {
LogPrintf("WARNING: wallet caches could not be reconstructed; salvaged wallet file may have omissions");
}
return fSuccess;

View File

@ -7,10 +7,12 @@
namespace libzcash {
OrchardRawAddress OrchardIncomingViewingKey::Address(const diversifier_index_t& j) const {
assert(inner.get() != nullptr);
return OrchardRawAddress(orchard_incoming_viewing_key_to_address(inner.get(), j.begin()));
}
std::optional<diversifier_index_t> OrchardIncomingViewingKey::DecryptDiversifier(const OrchardRawAddress& addr) const {
assert(inner.get() != nullptr);
diversifier_index_t j_ret;
if (orchard_incoming_viewing_key_decrypt_diversifier(inner.get(), addr.inner.get(), j_ret.begin())) {
return j_ret;
@ -20,10 +22,12 @@ std::optional<diversifier_index_t> OrchardIncomingViewingKey::DecryptDiversifier
}
OrchardIncomingViewingKey OrchardFullViewingKey::ToIncomingViewingKey() const {
assert(inner.get() != nullptr);
return OrchardIncomingViewingKey(orchard_full_viewing_key_to_incoming_viewing_key(inner.get()));
}
OrchardIncomingViewingKey OrchardFullViewingKey::ToInternalIncomingViewingKey() const {
assert(inner.get() != nullptr);
return OrchardIncomingViewingKey(orchard_full_viewing_key_to_internal_incoming_viewing_key(inner.get()));
}

View File

@ -97,17 +97,17 @@ class OrchardIncomingViewingKey
private:
std::unique_ptr<OrchardIncomingViewingKeyPtr, decltype(&orchard_incoming_viewing_key_free)> inner;
OrchardIncomingViewingKey() :
inner(nullptr, orchard_incoming_viewing_key_free) {}
OrchardIncomingViewingKey(OrchardIncomingViewingKeyPtr* key) :
inner(key, orchard_incoming_viewing_key_free) {}
friend class OrchardFullViewingKey;
friend class OrchardSpendingKey;
friend class ::OrchardWallet;
public:
// DO NOT USE - this is exposed for serialization purposes only.
OrchardIncomingViewingKey() :
inner(nullptr, orchard_incoming_viewing_key_free) {}
OrchardIncomingViewingKey(OrchardIncomingViewingKey&& key) : inner(std::move(key.inner)) {}
OrchardIncomingViewingKey(const OrchardIncomingViewingKey& key) :
@ -139,15 +139,20 @@ public:
friend bool operator==(const OrchardIncomingViewingKey& a, const OrchardIncomingViewingKey& b)
{
assert(a.inner.get() != nullptr);
assert(b.inner.get() != nullptr);
return orchard_incoming_viewing_key_eq(a.inner.get(), b.inner.get());
}
friend bool operator<(const OrchardIncomingViewingKey& c1, const OrchardIncomingViewingKey& c2) {
assert(c1.inner.get() != nullptr);
assert(c2.inner.get() != nullptr);
return orchard_incoming_viewing_key_lt(c1.inner.get(), c2.inner.get());
}
template<typename Stream>
void Serialize(Stream& s) const {
assert(inner.get() != nullptr);
RustStream rs(s);
if (!orchard_incoming_viewing_key_serialize(inner.get(), &rs, RustStream<Stream>::write_callback)) {
throw std::ios_base::failure("Failed to serialize Orchard incoming viewing key");