diff --git a/src/Makefile.am b/src/Makefile.am index 388c3f1ff..2a4723414 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -334,6 +334,7 @@ libbitcoin_wallet_a_SOURCES = \ wallet/asyncrpcoperation_shieldcoinbase.cpp \ wallet/crypter.cpp \ wallet/db.cpp \ + wallet/orchard.cpp \ wallet/paymentdisclosure.cpp \ wallet/paymentdisclosuredb.cpp \ wallet/rpcdisclosure.cpp \ diff --git a/src/miner.cpp b/src/miner.cpp index ed89d77dd..500ab7636 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -233,7 +233,7 @@ public: } if (orchardBundle.has_value()) { - auto authorizedBundle = orchardBundle.value().ProveAndSign(dataToBeSigned); + auto authorizedBundle = orchardBundle.value().ProveAndSign({}, dataToBeSigned); if (authorizedBundle.has_value()) { mtx.orchardBundle = authorizedBundle.value(); } else { diff --git a/src/rust/include/rust/builder.h b/src/rust/include/rust/builder.h index 677ec08e3..cc1074c24 100644 --- a/src/rust/include/rust/builder.h +++ b/src/rust/include/rust/builder.h @@ -13,6 +13,11 @@ extern "C" { #endif +/// A type-safe pointer to a Rust-allocated struct containing the information +/// needed to spend an Orchard note. +struct OrchardSpendInfoPtr; +typedef struct OrchardSpendInfoPtr OrchardSpendInfoPtr; + /// Pointer to Rust-allocated Orchard bundle builder. struct OrchardBuilderPtr; typedef struct OrchardBuilderPtr OrchardBuilderPtr; @@ -22,6 +27,10 @@ typedef struct OrchardBuilderPtr OrchardBuilderPtr; struct OrchardUnauthorizedBundlePtr; typedef struct OrchardUnauthorizedBundlePtr OrchardUnauthorizedBundlePtr; +/// Frees the memory associated with an Orchard spend info struct that was +/// allocated by Rust. +void orchard_spend_info_free(OrchardSpendInfoPtr* ptr); + /// Construct a new Orchard transaction builder. /// /// If `anchor` is `null`, the root of the empty Orchard commitment tree is used. @@ -33,6 +42,16 @@ OrchardBuilderPtr* orchard_builder_new( /// Frees an Orchard builder returned from `orchard_builder_new`. void orchard_builder_free(OrchardBuilderPtr* ptr); +/// Adds a note to be spent in this bundle. +/// +/// Returns `false` if the Merkle path in `spend_info` does not have the +/// required anchor. +/// +/// `spend_info` is always freed by this method. +bool orchard_builder_add_spend( + OrchardBuilderPtr* ptr, + OrchardSpendInfoPtr* spend_info); + /// Adds an address which will receive funds in this bundle. /// /// `ovk` is a pointer to the outgoing viewing key to make this recipient recoverable by, @@ -63,6 +82,8 @@ void orchard_unauthorized_bundle_free(OrchardUnauthorizedBundlePtr* bundle); /// `bundle` is always freed by this method. OrchardBundlePtr* orchard_unauthorized_bundle_prove_and_sign( OrchardUnauthorizedBundlePtr* bundle, + const OrchardSpendingKeyPtr** keys, + size_t keys_len, const unsigned char* sighash); /// Calculates a ZIP 244 shielded signature digest for the given under-construction diff --git a/src/rust/include/rust/orchard/keys.h b/src/rust/include/rust/orchard/keys.h index 06a543219..164bc76d3 100644 --- a/src/rust/include/rust/orchard/keys.h +++ b/src/rust/include/rust/orchard/keys.h @@ -197,6 +197,20 @@ OrchardIncomingViewingKeyPtr* orchard_full_viewing_key_to_incoming_viewing_key( OrchardIncomingViewingKeyPtr* orchard_full_viewing_key_to_internal_incoming_viewing_key( const OrchardFullViewingKeyPtr* key); +/** + * Returns the external outgoing viewing key for the specified full viewing key. + */ +OrchardIncomingViewingKeyPtr* orchard_full_viewing_key_to_external_outgoing_viewing_key( + const OrchardFullViewingKeyPtr* fvk, + uint8_t *ovk_ret); + +/** + * Returns the internal outgoing viewing key for the specified full viewing key. + */ +OrchardIncomingViewingKeyPtr* orchard_full_viewing_key_to_internal_outgoing_viewing_key( + const OrchardFullViewingKeyPtr* fvk, + uint8_t *ovk_ret); + /** * Implements equality testing between full viewing keys. */ diff --git a/src/rust/include/rust/orchard/wallet.h b/src/rust/include/rust/orchard/wallet.h index 0215703c4..43ce5092a 100644 --- a/src/rust/include/rust/orchard/wallet.h +++ b/src/rust/include/rust/orchard/wallet.h @@ -6,6 +6,7 @@ #define ZCASH_RUST_INCLUDE_RUST_ORCHARD_WALLET_H #include "rust/orchard/keys.h" +#include "rust/builder.h" #ifdef __cplusplus extern "C" { @@ -251,6 +252,20 @@ void orchard_wallet_get_potential_spends( push_txid_callback_t push_cb ); +/** + * Fetches the information needed to spend the wallet note at the given outpoint, + * relative to the current root known to the wallet of the Orchard commitment + * tree. + * + * Returns `null` if the outpoint is not known to the wallet, or the Orchard + * bundle containing the note has not been passed to + * `orchard_wallet_append_bundle_commitments`. + */ +OrchardSpendInfoPtr* orchard_wallet_get_spend_info( + const OrchardWalletPtr* wallet, + const unsigned char txid[32], + uint32_t action_idx); + #ifdef __cplusplus } #endif diff --git a/src/rust/src/builder_ffi.rs b/src/rust/src/builder_ffi.rs index 6ff5e8af7..1be373705 100644 --- a/src/rust/src/builder_ffi.rs +++ b/src/rust/src/builder_ffi.rs @@ -1,14 +1,17 @@ use std::convert::TryInto; use std::ptr; +use std::slice; use incrementalmerkletree::Hashable; +use libc::size_t; +use orchard::keys::SpendingKey; use orchard::{ builder::{Builder, InProgress, Unauthorized, Unproven}, bundle::{Authorized, Flags}, - keys::OutgoingViewingKey, - tree::MerkleHashOrchard, + keys::{FullViewingKey, OutgoingViewingKey}, + tree::{MerkleHashOrchard, MerklePath}, value::NoteValue, - Bundle, + Bundle, Note, }; use rand_core::OsRng; use tracing::error; @@ -22,6 +25,29 @@ use zcash_primitives::transaction::{ use crate::{transaction_ffi::PrecomputedTxParts, ORCHARD_PK}; +pub struct OrchardSpendInfo { + fvk: FullViewingKey, + note: Note, + merkle_path: MerklePath, +} + +impl OrchardSpendInfo { + pub fn from_parts(fvk: FullViewingKey, note: Note, merkle_path: MerklePath) -> Self { + OrchardSpendInfo { + fvk, + note, + merkle_path, + } + } +} + +#[no_mangle] +pub extern "C" fn orchard_spend_info_free(spend_info: *mut OrchardSpendInfo) { + if !spend_info.is_null() { + drop(unsafe { Box::from_raw(spend_info) }); + } +} + #[no_mangle] pub extern "C" fn orchard_builder_new( spends_enabled: bool, @@ -37,6 +63,27 @@ pub extern "C" fn orchard_builder_new( ))) } +#[no_mangle] +pub extern "C" fn orchard_builder_add_spend( + builder: *mut Builder, + orchard_spend_info: *mut OrchardSpendInfo, +) -> bool { + let builder = unsafe { builder.as_mut() }.expect("Builder may not be null."); + let orchard_spend_info = unsafe { Box::from_raw(orchard_spend_info) }; + + match builder.add_spend( + orchard_spend_info.fvk, + orchard_spend_info.note, + orchard_spend_info.merkle_path, + ) { + Ok(()) => true, + Err(e) => { + error!("Failed to add Orchard spend: {}", e); + false + } + } +} + #[no_mangle] pub extern "C" fn orchard_builder_add_recipient( builder: *mut Builder, @@ -100,15 +147,27 @@ pub extern "C" fn orchard_unauthorized_bundle_free( #[no_mangle] pub extern "C" fn orchard_unauthorized_bundle_prove_and_sign( bundle: *mut Bundle, Amount>, + keys: *const *const SpendingKey, + keys_len: size_t, sighash: *const [u8; 32], ) -> *mut Bundle { let bundle = unsafe { Box::from_raw(bundle) }; + let keys = unsafe { slice::from_raw_parts(keys, keys_len) }; let sighash = unsafe { sighash.as_ref() }.expect("sighash pointer may not be null."); let pk = unsafe { ORCHARD_PK.as_ref() }.unwrap(); + let signing_keys = keys + .iter() + .map(|sk| { + unsafe { sk.as_ref() } + .expect("SpendingKey pointers must not be null") + .into() + }) + .collect::>(); + let res = bundle .create_proof(pk) - .and_then(|b| b.apply_signatures(OsRng, *sighash, &[])); + .and_then(|b| b.apply_signatures(OsRng, *sighash, &signing_keys)); match res { Ok(signed) => Box::into_raw(Box::new(signed)), diff --git a/src/rust/src/orchard_keys_ffi.rs b/src/rust/src/orchard_keys_ffi.rs index f5675ad8c..079de6439 100644 --- a/src/rust/src/orchard_keys_ffi.rs +++ b/src/rust/src/orchard_keys_ffi.rs @@ -2,8 +2,10 @@ use std::io::{Read, Write}; use std::slice; use tracing::error; -use orchard::keys::{DiversifierIndex, FullViewingKey, IncomingViewingKey, SpendingKey}; -use orchard::Address; +use orchard::{ + keys::{DiversifierIndex, FullViewingKey, IncomingViewingKey, OutgoingViewingKey, SpendingKey}, + Address, +}; use crate::{ streams_ffi::{CppStreamReader, CppStreamWriter, ReadCb, StreamObj, WriteCb}, @@ -282,6 +284,31 @@ pub extern "C" fn orchard_full_viewing_key_to_internal_incoming_viewing_key( .unwrap_or(std::ptr::null_mut()) } +#[no_mangle] +pub extern "C" fn orchard_full_viewing_key_to_external_outgoing_viewing_key( + fvk: *const FullViewingKey, + ovk_ret: *mut [u8; 32], +) { + let fvk = unsafe { fvk.as_ref() }.expect("fvk must not be null"); + let ovk_ret = unsafe { ovk_ret.as_mut() }.expect("ovk_ret must not be null"); + + let ovk = OutgoingViewingKey::from(fvk); + *ovk_ret = *ovk.as_ref(); +} + +#[no_mangle] +pub extern "C" fn orchard_full_viewing_key_to_internal_outgoing_viewing_key( + fvk: *const FullViewingKey, + ovk_ret: *mut [u8; 32], +) { + let fvk = unsafe { fvk.as_ref() }.expect("fvk must not be null"); + let ovk_ret = unsafe { ovk_ret.as_mut() }.expect("ovk_ret must not be null"); + + let internal_fvk = fvk.derive_internal(); + let ovk = OutgoingViewingKey::from(&internal_fvk); + *ovk_ret = *ovk.as_ref(); +} + #[no_mangle] pub extern "C" fn orchard_full_viewing_key_eq( k0: *const FullViewingKey, diff --git a/src/rust/src/wallet.rs b/src/rust/src/wallet.rs index d9f886557..3f634a97b 100644 --- a/src/rust/src/wallet.rs +++ b/src/rust/src/wallet.rs @@ -1,8 +1,10 @@ -use incrementalmerkletree::{bridgetree::BridgeTree, Frontier, Tree}; +use std::convert::TryInto; +use std::ptr; +use std::slice; + +use incrementalmerkletree::{bridgetree::BridgeTree, Frontier, Position, Tree}; use libc::c_uchar; use std::collections::{BTreeMap, BTreeSet}; -use std::convert::TryInto; -use std::slice; use tracing::error; use zcash_primitives::{ @@ -14,11 +16,11 @@ use orchard::{ bundle::Authorized, keys::{FullViewingKey, IncomingViewingKey, SpendingKey}, note::Nullifier, - tree::MerkleHashOrchard, + tree::{MerkleHashOrchard, MerklePath}, Address, Bundle, Note, }; -use crate::zcashd_orchard::OrderedAddress; +use crate::{builder_ffi::OrchardSpendInfo, zcashd_orchard::OrderedAddress}; use super::incremental_merkle_tree_ffi::MERKLE_DEPTH; @@ -50,6 +52,8 @@ pub struct InPoint { pub struct DecryptedNote { note: Note, memo: [u8; 512], + /// The position of the note's commitment within the global Merkle tree. + position: Option, } /// A data structure tracking the note data that was decrypted from a single transaction, @@ -285,10 +289,13 @@ impl Wallet { // observed the note is set to `None` as the transaction will no longer have been // observed as having been mined. for (txid, n) in self.wallet_received_notes.iter_mut() { - // Erase block height information for any received notes - // that have been un-mined by the rewind. + // Erase block height and commitment tree information for any received + // notes that have been un-mined by the rewind. if !to_retain.contains(txid) { n.tx_height = None; + for dnote in n.decrypted_notes.values_mut() { + dnote.position = None; + } } } self.last_observed = Some(last_observed); @@ -411,7 +418,11 @@ impl Wallet { self.nullifiers.insert(nf, outpoint); // add the decrypted note data to the wallet - let note_data = DecryptedNote { note, memo }; + let note_data = DecryptedNote { + note, + memo, + position: None, + }; self.wallet_received_notes .entry(*txid) .or_insert_with(|| TxNotes { @@ -476,7 +487,7 @@ impl Wallet { tx_notes.tx_height = Some(block_height); } - let my_notes_for_tx = self.wallet_received_notes.get(txid); + let mut my_notes_for_tx = self.wallet_received_notes.get_mut(txid); for (action_idx, action) in bundle.actions().iter().enumerate() { // append the note commitment for each action to the witness tree if !self @@ -487,8 +498,13 @@ impl Wallet { } // for notes that are ours, witness the current state of the tree - if my_notes_for_tx.map_or(false, |n| n.decrypted_notes.contains_key(&action_idx)) { - self.witness_tree.witness(); + if let Some(dnote) = my_notes_for_tx + .as_mut() + .and_then(|n| n.decrypted_notes.get_mut(&action_idx)) + { + let (pos, cmx) = self.witness_tree.witness().expect("tree is not empty"); + assert_eq!(cmx, MerkleHashOrchard::from_cmx(action.cmx())); + dnote.position = Some(pos); } // For nullifiers that are ours that we detect as spent by this action, @@ -554,6 +570,42 @@ impl Wallet { pub fn note_commitment_tree_root(&self) -> MerkleHashOrchard { self.witness_tree.root() } + + /// Fetches the information necessary to spend the note at the given `OutPoint`, + /// relative to the current root known to the wallet of the Orchard commitment tree. + /// + /// Returns `None` if the `OutPoint` is not known to the wallet, or the Orchard bundle + /// containing the note has not been passed to `Wallet::append_bundle_commitments`. + pub fn get_spend_info(&self, outpoint: OutPoint) -> Option { + // TODO: Take `confirmations` parameter and obtain the Merkle path to the root at + // that checkpoint, not the latest root. + self.wallet_received_notes + .get(&outpoint.txid) + .and_then(|tx_notes| tx_notes.decrypted_notes.get(&outpoint.action_idx)) + .and_then(|dnote| { + self.key_store + .ivk_for_address(&dnote.note.recipient()) + .and_then(|ivk| self.key_store.viewing_keys.get(ivk)) + .zip(dnote.position) + .map(|(fvk, position)| { + let path = self + .witness_tree + .authentication_path( + position, + &MerkleHashOrchard::from_cmx(&dnote.note.commitment().into()), + ) + .expect("wallet always has paths to positioned notes"); + OrchardSpendInfo::from_parts( + fvk.clone(), + dnote.note, + MerklePath::from_parts( + u64::from(position).try_into().unwrap(), + path.try_into().unwrap(), + ), + ) + }) + }) + } } // @@ -844,3 +896,26 @@ pub extern "C" fn orchard_wallet_get_potential_spends( } } } + +#[no_mangle] +pub extern "C" fn orchard_wallet_get_spend_info( + wallet: *const Wallet, + txid: *const [c_uchar; 32], + action_idx: usize, +) -> *mut OrchardSpendInfo { + 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.")); + + let outpoint = OutPoint { txid, action_idx }; + + if let Some(ret) = wallet.get_spend_info(outpoint) { + Box::into_raw(Box::new(ret)) + } else { + tracing::error!( + "Requested note in action {} of transaction {} wasn't in the wallet", + outpoint.action_idx, + outpoint.txid + ); + ptr::null_mut() + } +} diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp index 2b26012ed..3fbb3f411 100644 --- a/src/test/coins_tests.cpp +++ b/src/test/coins_tests.cpp @@ -290,7 +290,7 @@ public: uint256 orchardAnchor; uint256 dataToBeSigned; auto builder = orchard::Builder(true, true, orchardAnchor); - mutableTx.orchardBundle = builder.Build().value().ProveAndSign(dataToBeSigned).value(); + mutableTx.orchardBundle = builder.Build().value().ProveAndSign({}, dataToBeSigned).value(); orchardNullifier = mutableTx.orchardBundle.GetNullifiers()[0]; tx = CTransaction(mutableTx); @@ -322,7 +322,7 @@ template<> void AppendRandomLeaf(OrchardMerkleFrontier &tree) { uint256 orchardAnchor; uint256 dataToBeSigned; auto builder = orchard::Builder(true, true, orchardAnchor); - auto bundle = builder.Build().value().ProveAndSign(dataToBeSigned).value(); + auto bundle = builder.Build().value().ProveAndSign({}, dataToBeSigned).value(); tree.AppendBundle(bundle); } diff --git a/src/transaction_builder.cpp b/src/transaction_builder.cpp index ba3dce4f0..ef1e0cbc2 100644 --- a/src/transaction_builder.cpp +++ b/src/transaction_builder.cpp @@ -41,6 +41,23 @@ Builder::Builder( inner.reset(orchard_builder_new(spendsEnabled, outputsEnabled, anchor.IsNull() ? nullptr : anchor.begin())); } +bool Builder::AddSpend(orchard::SpendInfo spendInfo) +{ + if (!inner) { + throw std::logic_error("orchard::Builder has already been used"); + } + + if (orchard_builder_add_spend( + inner.get(), + spendInfo.inner.release())) + { + hasActions = true; + return true; + } else { + return false; + } +} + void Builder::AddOutput( const std::optional& ovk, const libzcash::OrchardRawAddress& to, @@ -75,14 +92,20 @@ std::optional Builder::Build() { } std::optional UnauthorizedBundle::ProveAndSign( + const std::vector& keys, uint256 sighash) { if (!inner) { throw std::logic_error("orchard::UnauthorizedBundle has already been used"); } + std::vector pKeys; + for (const auto& key : keys) { + pKeys.push_back(key.inner.get()); + } + auto authorizedBundle = orchard_unauthorized_bundle_prove_and_sign( - inner.release(), sighash.begin()); + inner.release(), pKeys.data(), pKeys.size(), sighash.begin()); if (authorizedBundle == nullptr) { return std::nullopt; } else { @@ -264,6 +287,34 @@ void TransactionBuilder::SetExpiryHeight(uint32_t nExpiryHeight) mtx.nExpiryHeight = nExpiryHeight; } +bool TransactionBuilder::AddOrchardSpend( + libzcash::OrchardSpendingKey sk, + orchard::SpendInfo spendInfo) +{ + if (!orchardBuilder.has_value()) { + // Try to give a useful error. + if (!(jsInputs.empty() && jsOutputs.empty())) { + throw std::runtime_error("TransactionBuilder cannot spend Orchard notes in a Sprout transaction"); + } else if (mtx.nVersion < ZIP225_MIN_TX_VERSION) { + throw std::runtime_error("TransactionBuilder cannot spend Orchard notes before NU5 activation"); + } else { + throw std::runtime_error("TransactionBuilder cannot spend Orchard notes without Orchard anchor"); + } + } + + auto fromAddr = spendInfo.FromAddress(); + auto value = spendInfo.Value(); + auto res = orchardBuilder.value().AddSpend(std::move(spendInfo)); + if (res) { + orchardSpendingKeys.push_back(sk); + if (!firstOrchardSpendAddr.has_value()) { + firstOrchardSpendAddr = fromAddr; + } + valueBalanceOrchard += value; + } + return res; +} + void TransactionBuilder::AddOrchardOutput( const std::optional& ovk, const libzcash::OrchardRawAddress& to, @@ -456,6 +507,9 @@ TransactionBuilderResult TransactionBuilder::Build() } else if (tChangeAddr) { // tChangeAddr has already been validated. AddTransparentOutput(tChangeAddr.value(), change); + } else if (firstOrchardSpendAddr.has_value()) { + auto ovk = orchardSpendingKeys[0].ToFullViewingKey().ToInternalOutgoingViewingKey(); + AddOrchardOutput(ovk, firstOrchardSpendAddr.value(), change, std::nullopt); } else if (!spends.empty()) { auto fvk = spends[0].expsk.full_viewing_key(); auto note = spends[0].note; @@ -586,7 +640,8 @@ TransactionBuilderResult TransactionBuilder::Build() } if (orchardBundle.has_value()) { - auto authorizedBundle = orchardBundle.value().ProveAndSign(dataToBeSigned); + auto authorizedBundle = orchardBundle.value().ProveAndSign( + orchardSpendingKeys, dataToBeSigned); if (authorizedBundle.has_value()) { mtx.orchardBundle = authorizedBundle.value(); } else { diff --git a/src/transaction_builder.h b/src/transaction_builder.h index 2fe0a1d1c..46667ec0a 100644 --- a/src/transaction_builder.h +++ b/src/transaction_builder.h @@ -25,6 +25,7 @@ #define NO_MEMO {{0xF6}} +class OrchardWallet; namespace orchard { class UnauthorizedBundle; } uint256 ProduceZip244SignatureHash( @@ -33,6 +34,45 @@ uint256 ProduceZip244SignatureHash( namespace orchard { +/// The information necessary to spend an Orchard note. +class SpendInfo +{ +private: + /// Memory is allocated by Rust. + std::unique_ptr inner; + libzcash::OrchardRawAddress from; + uint64_t noteValue; + + // SpendInfo() : inner(nullptr, orchard_spend_info_free) {} + SpendInfo( + OrchardSpendInfoPtr* spendInfo, + libzcash::OrchardRawAddress fromIn, + uint64_t noteValueIn + ) : inner(spendInfo, orchard_spend_info_free), from(fromIn), noteValue(noteValueIn) {} + + friend class Builder; + friend class ::OrchardWallet; + +public: + // SpendInfo should never be copied + SpendInfo(const SpendInfo&) = delete; + SpendInfo& operator=(const SpendInfo&) = delete; + SpendInfo(SpendInfo&& spendInfo) : + inner(std::move(spendInfo.inner)), from(std::move(spendInfo.from)), noteValue(std::move(spendInfo.noteValue)) {} + SpendInfo& operator=(SpendInfo&& spendInfo) + { + if (this != &spendInfo) { + inner = std::move(spendInfo.inner); + from = std::move(spendInfo.from); + noteValue = std::move(spendInfo.noteValue); + } + return *this; + } + + inline libzcash::OrchardRawAddress FromAddress() const { return from; }; + inline uint64_t Value() const { return noteValue; }; +}; + /// A builder that constructs an `UnauthorizedBundle` from a set of notes to be spent, /// and recipients to receive funds. class Builder { @@ -60,6 +100,12 @@ public: return *this; } + /// Adds a note to be spent in this bundle. + /// + /// Returns `false` if the given Merkle path does not have the required anchor + /// for the given note. + bool AddSpend(orchard::SpendInfo spendInfo); + /// Adds an address which will receive funds in this bundle. void AddOutput( const std::optional& ovk, @@ -124,7 +170,8 @@ public: /// this bundle must be discarded and a new bundle built. Subsequent usage of this /// object in any way will cause an exception. This emulates Rust's compile-time /// move semantics at runtime. - std::optional ProveAndSign(uint256 sighash); + std::optional ProveAndSign( + const std::vector& keys, uint256 sighash); }; } // namespace orchard @@ -223,6 +270,8 @@ private: std::optional orchardBuilder; CAmount valueBalanceOrchard = 0; + std::vector orchardSpendingKeys; + std::optional firstOrchardSpendAddr; std::vector spends; std::vector outputs; std::vector jsInputs; @@ -293,6 +342,10 @@ public: void SetFee(CAmount fee); + bool AddOrchardSpend( + libzcash::OrchardSpendingKey sk, + orchard::SpendInfo spendInfo); + void AddOrchardOutput( const std::optional& ovk, const libzcash::OrchardRawAddress& to, diff --git a/src/wallet/gtest/test_orchard_wallet.cpp b/src/wallet/gtest/test_orchard_wallet.cpp index 2d99aade6..76f8dcce3 100644 --- a/src/wallet/gtest/test_orchard_wallet.cpp +++ b/src/wallet/gtest/test_orchard_wallet.cpp @@ -1,6 +1,7 @@ #include #include +#include "consensus/validation.h" #include "random.h" #include "transaction_builder.h" #include "utiltest.h" @@ -75,3 +76,82 @@ TEST(OrchardWalletTests, TxContainsMyNotes) { RegtestDeactivateNU5(); } + +// This test is here instead of test_transaction_builder.cpp because it depends +// on OrchardWallet, which only exists if the wallet is compiled in. +TEST(TransactionBuilder, OrchardToOrchard) { + auto consensusParams = RegtestActivateNU5(); + OrchardWallet wallet; + + CBasicKeyStore keystore; + CKey tsk = AddTestCKeyToKeyStore(keystore); + auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID()); + + auto sk = RandomOrchardSpendingKey(); + wallet.AddSpendingKey(sk); + + // Create a transaction sending to the default address for that + // spending key and add it to the wallet. + libzcash::diversifier_index_t j(0); + auto txRecv = FakeOrchardTx(sk, j); + wallet.AddNotesIfInvolvingMe(txRecv); + + // Generate a recipient. + auto recipient = RandomOrchardSpendingKey() + .ToFullViewingKey() + .ToIncomingViewingKey() + .Address(j); + + // Select the one note in the wallet for spending. + std::vector notes; + wallet.GetFilteredNotes( + notes, sk.ToFullViewingKey().ToIncomingViewingKey(), true, true); + ASSERT_EQ(notes.size(), 1); + + // If we attempt to get spend info now, it will fail because the note hasn't + // been witnessed in the Orchard commitment tree. + EXPECT_THROW(wallet.GetSpendInfo(notes), std::logic_error); + + // Append the bundle to the wallet's commitment tree. + CBlock fakeBlock; + fakeBlock.vtx.resize(2); + fakeBlock.vtx[1] = txRecv; + ASSERT_TRUE(wallet.AppendNoteCommitments(2, fakeBlock)); + + // Now we can get spend info for the note. + auto spendInfo = wallet.GetSpendInfo(notes); + EXPECT_EQ(spendInfo[0].Value(), 40000); + + // Get the root of the commitment tree. + OrchardMerkleFrontier tree; + tree.AppendBundle(txRecv.GetOrchardBundle()); + auto orchardAnchor = tree.root(); + + // Create an Orchard-only transaction + // 0.0004 z-ZEC in, 0.00025 z-ZEC out, default fee, 0.00005 z-ZEC change + auto builder = TransactionBuilder(consensusParams, 2, orchardAnchor); + EXPECT_TRUE(builder.AddOrchardSpend(sk, std::move(spendInfo[0]))); + builder.AddOrchardOutput(std::nullopt, recipient, 25000, std::nullopt); + auto maybeTx = builder.Build(); + EXPECT_TRUE(maybeTx.IsTx()); + if (maybeTx.IsError()) { + std::cerr << "Failed to build transaction: " << maybeTx.GetError() << std::endl; + GTEST_FAIL(); + } + auto tx = maybeTx.GetTxOrThrow(); + + EXPECT_EQ(tx.vin.size(), 0); + EXPECT_EQ(tx.vout.size(), 0); + EXPECT_EQ(tx.vJoinSplit.size(), 0); + EXPECT_EQ(tx.vShieldedSpend.size(), 0); + EXPECT_EQ(tx.vShieldedOutput.size(), 0); + EXPECT_TRUE(tx.GetOrchardBundle().IsPresent()); + EXPECT_EQ(tx.GetOrchardBundle().GetValueBalance(), 10000); + + CValidationState state; + EXPECT_TRUE(ContextualCheckTransaction(tx, state, Params(), 3, true)); + EXPECT_EQ(state.GetRejectReason(), ""); + + // Revert to default + RegtestDeactivateNU5(); +} diff --git a/src/wallet/orchard.cpp b/src/wallet/orchard.cpp new file mode 100644 index 000000000..7066e3cf7 --- /dev/null +++ b/src/wallet/orchard.cpp @@ -0,0 +1,28 @@ +// Copyright (c) 2022 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +#include "wallet/orchard.h" + +std::vector OrchardWallet::GetSpendInfo( + const std::vector& noteMetadata) const +{ + std::vector result; + for (const auto& note : noteMetadata) { + auto pSpendInfo = orchard_wallet_get_spend_info( + inner.get(), + note.GetOutPoint().hash.begin(), + note.GetOutPoint().n); + if (pSpendInfo == nullptr) { + throw std::logic_error("Called OrchardWallet::GetSpendInfo with unknown outpoint"); + } else { + auto spendInfo = orchard::SpendInfo( + pSpendInfo, + note.GetAddress(), + note.GetNoteValue()); + + result.push_back(std::move(spendInfo)); + } + } + return result; +} diff --git a/src/wallet/orchard.h b/src/wallet/orchard.h index 4bfc765cb..407c10f6e 100644 --- a/src/wallet/orchard.h +++ b/src/wallet/orchard.h @@ -8,6 +8,8 @@ #include #include "primitives/transaction.h" +#include "transaction_builder.h" + #include "rust/orchard/keys.h" #include "rust/orchard/wallet.h" #include "zcash/address/orchard.hpp" @@ -32,6 +34,10 @@ public: return op; } + const libzcash::OrchardRawAddress& GetAddress() const { + return address; + } + void SetConfirmations(int c) { confirmations = c; } @@ -50,6 +56,8 @@ class OrchardWallet private: std::unique_ptr inner; + friend class ::orchard::UnauthorizedBundle; + public: OrchardWallet() : inner(orchard_wallet_new(), orchard_wallet_free) {} OrchardWallet(OrchardWallet&& wallet_data) : inner(std::move(wallet_data.inner)) {} @@ -262,6 +270,9 @@ public: ); return result; } + + std::vector GetSpendInfo( + const std::vector& noteMetadata) const; }; #endif // ZCASH_ORCHARD_WALLET_H diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 139f2c7e1..48aa25cfe 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3237,6 +3237,13 @@ void CWallet::GetSaplingNoteWitnesses(const std::vector& notes, } } +std::vector CWallet::GetOrchardSpendInfo( + const std::vector& orchardNoteMetadata) const +{ + AssertLockHeld(cs_wallet); + return orchardWallet.GetSpendInfo(orchardNoteMetadata); +} + isminetype CWallet::IsMine(const CTxIn &txin) const { { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 19c73bfd1..eb5a5ff49 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -15,6 +15,7 @@ #include "primitives/block.h" #include "primitives/transaction.h" #include "tinyformat.h" +#include "transaction_builder.h" #include "ui_interface.h" #include "util.h" #include "utilstrencodings.h" @@ -1689,6 +1690,8 @@ public: const std::vector& notes, std::vector>& witnesses, uint256 &final_anchor); + std::vector GetOrchardSpendInfo( + const std::vector& orchardNoteMetadata) const; isminetype IsMine(const CTxIn& txin) const; CAmount GetDebit(const CTxIn& txin, const isminefilter& filter) const; diff --git a/src/zcash/address/orchard.cpp b/src/zcash/address/orchard.cpp index 26e36b3fa..66fe6c847 100644 --- a/src/zcash/address/orchard.cpp +++ b/src/zcash/address/orchard.cpp @@ -31,6 +31,18 @@ OrchardIncomingViewingKey OrchardFullViewingKey::ToInternalIncomingViewingKey() return OrchardIncomingViewingKey(orchard_full_viewing_key_to_internal_incoming_viewing_key(inner.get())); } +uint256 OrchardFullViewingKey::ToExternalOutgoingViewingKey() const { + uint256 ovk; + orchard_full_viewing_key_to_external_outgoing_viewing_key(inner.get(), ovk.begin()); + return ovk; +} + +uint256 OrchardFullViewingKey::ToInternalOutgoingViewingKey() const { + uint256 ovk; + orchard_full_viewing_key_to_internal_outgoing_viewing_key(inner.get(), ovk.begin()); + return ovk; +} + OrchardSpendingKey OrchardSpendingKey::ForAccount( const HDSeed& seed, uint32_t bip44CoinType, diff --git a/src/zcash/address/orchard.hpp b/src/zcash/address/orchard.hpp index c6517e13a..d698ced0b 100644 --- a/src/zcash/address/orchard.hpp +++ b/src/zcash/address/orchard.hpp @@ -12,7 +12,10 @@ #include class OrchardWallet; -namespace orchard { class Builder; } +namespace orchard { + class Builder; + class UnauthorizedBundle; +} namespace libzcash { @@ -199,6 +202,10 @@ public: OrchardIncomingViewingKey ToInternalIncomingViewingKey() const; + uint256 ToExternalOutgoingViewingKey() const; + + uint256 ToInternalOutgoingViewingKey() const; + OrchardFullViewingKey& operator=(OrchardFullViewingKey&& key) { if (this != &key) { @@ -256,6 +263,7 @@ private: OrchardSpendingKey(OrchardSpendingKeyPtr* ptr) : inner(ptr, orchard_spending_key_free) {} + friend class orchard::UnauthorizedBundle; friend class ::OrchardWallet; public: OrchardSpendingKey(OrchardSpendingKey&& key) : inner(std::move(key.inner)) {}