diff --git a/.cargo/config.offline b/.cargo/config.offline index e386661d5..28c93ea74 100644 --- a/.cargo/config.offline +++ b/.cargo/config.offline @@ -11,7 +11,7 @@ replace-with = "vendored-sources" [source."https://github.com/zcash/orchard.git"] git = "https://github.com/zcash/orchard.git" -rev = "a5f701f3186fb619b40f2dee9939e64e4c9eb7cc" +rev = "f4587f790d7317df85a9ee77ce693a06ed6d8d02" replace-with = "vendored-sources" [source."https://github.com/zcash/incrementalmerkletree.git"] diff --git a/Cargo.lock b/Cargo.lock index 7f2a74e3b..2b20b6cae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1242,7 +1242,7 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "orchard" version = "0.1.0-beta.1" -source = "git+https://github.com/zcash/orchard.git?rev=a5f701f3186fb619b40f2dee9939e64e4c9eb7cc#a5f701f3186fb619b40f2dee9939e64e4c9eb7cc" +source = "git+https://github.com/zcash/orchard.git?rev=f4587f790d7317df85a9ee77ce693a06ed6d8d02#f4587f790d7317df85a9ee77ce693a06ed6d8d02" dependencies = [ "aes", "arrayvec 0.7.2", diff --git a/Cargo.toml b/Cargo.toml index 19ab20586..9227b9a9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,7 @@ codegen-units = 1 [patch.crates-io] hdwallet = { git = "https://github.com/nuttycom/hdwallet", rev = "576683b9f2865f1118c309017ff36e01f84420c9" } incrementalmerkletree = { git = "https://github.com/zcash/incrementalmerkletree.git", rev = "62c33e4480a71170b02b9eb7d4b0160194f414ee" } -orchard = { git = "https://github.com/zcash/orchard.git", rev = "a5f701f3186fb619b40f2dee9939e64e4c9eb7cc" } +orchard = { git = "https://github.com/zcash/orchard.git", rev = "f4587f790d7317df85a9ee77ce693a06ed6d8d02" } zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289" } zcash_encoding = { git = "https://github.com/zcash/librustzcash.git", rev = "67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289" } zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289" } diff --git a/src/rust/include/rust/orchard/wallet.h b/src/rust/include/rust/orchard/wallet.h index 2e3a344f7..caff0c8b2 100644 --- a/src/rust/include/rust/orchard/wallet.h +++ b/src/rust/include/rust/orchard/wallet.h @@ -263,6 +263,56 @@ void orchard_wallet_get_filtered_notes( push_note_callback_t push_cb ); +/** + * A C struct used to transfer Orchard action spend information across the FFI boundary. + * This must have the same in-memory representation as the `FFIActionSpend` type in + * orchard_ffi/wallet.rs. + */ +struct RawOrchardActionSpend { + uint32_t spendActionIdx; + unsigned char outpointTxId[32]; + uint32_t outpointActionIdx; + CAmount noteValue; +}; + +/** + * A C struct used to transfer Orchard action output information across the FFI boundary. + * This must have the same in-memory representation as the `FFIActionOutput` type in + * orchard_ffi/wallet.rs. + */ +struct RawOrchardActionOutput { + uint32_t outputActionIdx; + OrchardRawAddressPtr* addr; + CAmount noteValue; + unsigned char memo[512]; + bool isOutgoing; +}; + +typedef void (*push_spend_t)(void* callbackReceiver, const RawOrchardActionSpend data); + +typedef void (*push_output_t)(void* callbackReceiver, const RawOrchardActionOutput data); + +/** + * Trial-decrypt the specfied Orchard bundle, and + * uses the provided callback to push RawOrchardActionData values corresponding to the + * actions of that bundle on to the provided result vector. Note that the push_cb callback can perform any + * necessary conversion from a RawOrchardActionData value in addition to modifying the + * provided result vector. + * + * The following pointers must be freed by the caller: + * - `RawOrchardActionData::spend` must be freed using `orchard_action_spend_free` + * - `RawOrchardActionData::output` must be freed using `orchard_action_output_free` + * - `RawOrchardActionData::output::addr` must be freed using `orchard_address_free` + */ +bool orchard_wallet_get_txdata( + const OrchardWalletPtr* wallet, + const OrchardBundlePtr* bundle, + const unsigned char*, + size_t raw_ovks_len, + void* callbackReceiver, + push_spend_t push_spend_cb, + push_output_t push_output_cb + ); typedef void (*push_txid_callback_t)(void* resultVector, unsigned char txid[32]); diff --git a/src/rust/src/wallet.rs b/src/rust/src/wallet.rs index 8e8bc9e1f..94276af7a 100644 --- a/src/rust/src/wallet.rs +++ b/src/rust/src/wallet.rs @@ -16,7 +16,7 @@ use zcash_primitives::{ use orchard::{ bundle::Authorized, - keys::{FullViewingKey, IncomingViewingKey, SpendingKey}, + keys::{FullViewingKey, IncomingViewingKey, OutgoingViewingKey, SpendingKey}, note::Nullifier, tree::{MerkleHashOrchard, MerklePath}, Address, Bundle, Note, @@ -359,7 +359,7 @@ impl Wallet { .cloned() .collect::>(); - for (action_idx, ivk, note, recipient, memo) in bundle.decrypt_outputs_for_keys(&keys) { + for (action_idx, ivk, note, recipient, memo) in bundle.decrypt_outputs_with_keys(&keys) { assert!(self.add_decrypted_note( None, txid, @@ -991,6 +991,116 @@ pub extern "C" fn orchard_wallet_get_filtered_notes( } } +/// A type used to pass decrypted spend information across the FFI boundary. +/// This must have the same representation as `struct RawOrchardSpendData` +/// in `rust/include/rust/orchard/wallet.h`. +#[repr(C)] +pub struct FFIActionSpend { + spend_action_idx: u32, + outpoint_txid: [u8; 32], + outpoint_action_idx: u32, + value: i64, +} + +/// A type used to pass decrypted output information across the FFI boundary. +/// This must have the same representation as `struct RawOrchardOutputData` +/// in `rust/include/rust/orchard/wallet.h`. +#[repr(C)] +pub struct FFIActionOutput { + action_idx: u32, + recipient: *mut Address, + value: i64, + memo: [u8; 512], + is_outgoing: bool, +} + +/// A C++-allocated function pointer that can send a FFIActionSpend value +/// to a receiver. +pub type SpendPushCB = unsafe extern "C" fn(obj: Option, data: FFIActionSpend); + +/// A C++-allocated function pointer that can send a FFIActionOutput value +/// to a receiver. +pub type OutputPushCB = + unsafe extern "C" fn(obj: Option, data: FFIActionOutput); + +#[no_mangle] +pub extern "C" fn orchard_wallet_get_txdata( + wallet: *const Wallet, + bundle: *const Bundle, + raw_ovks: *const [u8; 32], + raw_ovks_len: usize, + callback_receiver: Option, + spend_push_cb: Option, + output_push_cb: Option, +) -> bool { + let wallet = unsafe { wallet.as_ref() }.expect("Wallet pointer may not be null."); + let raw_ovks = unsafe { slice::from_raw_parts(raw_ovks, raw_ovks_len) }; + let ovks: Vec = raw_ovks + .iter() + .map(|k| OutgoingViewingKey::from(*k)) + .collect(); + if let Some(bundle) = unsafe { bundle.as_ref() } { + let keys = wallet + .key_store + .viewing_keys + .keys() + .cloned() + .collect::>(); + + let incoming: BTreeMap = bundle + .decrypt_outputs_with_keys(&keys) + .into_iter() + .map(|(idx, _, note, addr, memo)| (idx, (note, addr, memo))) + .collect(); + + let outgoing: BTreeMap = bundle + .recover_outputs_with_ovks(&ovks) + .into_iter() + .map(|(idx, _, note, addr, memo)| (idx, (note, addr, memo))) + .collect(); + + for (idx, action) in bundle.actions().iter().enumerate() { + let nf = action.nullifier(); + if let Some(spend) = wallet.nullifiers.get(nf).and_then(|outpoint| { + wallet + .wallet_received_notes + .get(&outpoint.txid) + .and_then(|txnotes| txnotes.decrypted_notes.get(&outpoint.action_idx)) + .map(|dnote| FFIActionSpend { + spend_action_idx: idx as u32, + outpoint_txid: *outpoint.txid.as_ref(), + outpoint_action_idx: outpoint.action_idx as u32, + value: dnote.note.value().inner() as i64, + }) + }) { + unsafe { (spend_push_cb.unwrap())(callback_receiver, spend) }; + } + if let Some((note, addr, memo)) = incoming.get(&idx) { + let output = FFIActionOutput { + action_idx: idx as u32, + recipient: Box::into_raw(Box::new(*addr)), + value: note.value().inner() as i64, + memo: *memo, + is_outgoing: false, + }; + unsafe { (output_push_cb.unwrap())(callback_receiver, output) }; + } else if let Some((note, addr, memo)) = outgoing.get(&idx) { + let output = FFIActionOutput { + action_idx: idx as u32, + recipient: Box::into_raw(Box::new(*addr)), + value: note.value().inner() as i64, + memo: *memo, + is_outgoing: true, + }; + unsafe { (output_push_cb.unwrap())(callback_receiver, output) }; + } + } + true + } else { + false + } +} + pub type PushTxId = unsafe extern "C" fn(obj: Option, txid: *const [u8; 32]); #[no_mangle] diff --git a/src/wallet/orchard.h b/src/wallet/orchard.h index 3ebc5e5cf..bc3cf3a1a 100644 --- a/src/wallet/orchard.h +++ b/src/wallet/orchard.h @@ -110,6 +110,73 @@ public: } }; +class OrchardActionSpend { +private: + OrchardOutPoint outPoint; + CAmount noteValue; +public: + OrchardActionSpend(OrchardOutPoint outPoint, CAmount noteValue): outPoint(outPoint), noteValue(noteValue) { } + + OrchardOutPoint GetOutPoint() const { + return outPoint; + } + + CAmount GetNoteValue() const { + return noteValue; + } +}; + +class OrchardActionOutput { +private: + libzcash::OrchardRawAddress recipient; + CAmount noteValue; + std::array memo; + bool isOutgoing; +public: + OrchardActionOutput( + libzcash::OrchardRawAddress recipient, CAmount noteValue, std::array memo, bool isOutgoing): + recipient(recipient), noteValue(noteValue), memo(memo), isOutgoing(isOutgoing) { } + + const libzcash::OrchardRawAddress& GetRecipient() { + return recipient; + } + + CAmount GetNoteValue() const { + return noteValue; + } + + const std::array& GetMemo() const { + return memo; + } + + bool IsOutgoing() const { + return isOutgoing; + } +}; + +class OrchardActions { +private: + std::map spends; + std::map outputs; +public: + OrchardActions() {} + + void AddSpend(uint32_t actionIdx, OrchardActionSpend spend) { + spends.insert({actionIdx, spend}); + } + + void AddOutput(uint32_t actionIdx, OrchardActionOutput output) { + outputs.insert({actionIdx, output}); + } + + const std::map& GetSpends() { + return spends; + } + + const std::map& GetOutputs() { + return outputs; + } +}; class OrchardWallet { @@ -355,6 +422,40 @@ public: void GarbageCollect() { orchard_wallet_gc_note_commitment_tree(inner.get()); } + + static void PushSpendAction(void* receiver, RawOrchardActionSpend rawSpend) { + uint256 txid; + std::move(std::begin(rawSpend.outpointTxId), std::end(rawSpend.outpointTxId), txid.begin()); + auto spend = OrchardActionSpend( + OrchardOutPoint(txid, rawSpend.outpointActionIdx), + rawSpend.noteValue); + reinterpret_cast(receiver)->AddSpend(rawSpend.spendActionIdx, spend); + } + + static void PushOutputAction(void* receiver, RawOrchardActionOutput rawOutput) { + std::array memo; + std::move(std::begin(rawOutput.memo), std::end(rawOutput.memo), memo.begin()); + auto output = OrchardActionOutput( + libzcash::OrchardRawAddress(rawOutput.addr), + rawOutput.noteValue, + memo, + rawOutput.isOutgoing); + + reinterpret_cast(receiver)->AddOutput(rawOutput.outputActionIdx, output); + } + + OrchardActions GetTxActions(const CTransaction& tx, const std::vector& ovks) const { + OrchardActions result; + orchard_wallet_get_txdata( + inner.get(), + tx.GetOrchardBundle().inner.get(), + reinterpret_cast(ovks.data()), + ovks.size(), + &result, + PushSpendAction, + PushOutputAction); + return result; + } }; class OrchardWalletNoteCommitmentTreeWriter diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index f23a1b986..66bd97c6a 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -4168,6 +4168,30 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp) auto legacyAcctOVKs = legacyKey.GetOVKsForShielding(); ovks.insert(legacyAcctOVKs.first); ovks.insert(legacyAcctOVKs.second); + + // Generate the OVKs for shielding for all unified key components + for (const auto& [_, ufvkid] : pwalletMain->mapUnifiedAccountKeys) { + auto ufvk = pwalletMain->GetUnifiedFullViewingKey(ufvkid); + if (ufvk.has_value()) { + auto tkey = ufvk.value().GetTransparentKey(); + if (tkey.has_value()) { + auto tovks = tkey.value().GetOVKsForShielding(); + ovks.insert(tovks.first); + ovks.insert(tovks.second); + } + auto skey = ufvk.value().GetSaplingKey(); + if (skey.has_value()) { + auto sovks = skey.value().GetOVKs(); + ovks.insert(sovks.first); + ovks.insert(sovks.second); + } + auto okey = ufvk.value().GetOrchardKey(); + if (okey.has_value()) { + ovks.insert(okey.value().ToExternalOutgoingViewingKey()); + ovks.insert(okey.value().ToInternalOutgoingViewingKey()); + } + } + } } // Sapling spends