Merge pull request #5658 from str4d/5570-tx-builder-orchard-spends

Add Orchard sender support to the transaction builder
This commit is contained in:
str4d 2022-03-12 04:33:06 +00:00 committed by GitHub
commit 0f59c5b926
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 540 additions and 24 deletions

View File

@ -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 \

View File

@ -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 {

View File

@ -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, whether or not it succeeds.
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

View File

@ -197,6 +197,24 @@ 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.
*
* `ovk_ret` must be 32 bytes.
*/
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.
*
* `ovk_ret` must be 32 bytes.
*/
OrchardIncomingViewingKeyPtr* orchard_full_viewing_key_to_internal_outgoing_viewing_key(
const OrchardFullViewingKeyPtr* fvk,
uint8_t *ovk_ret);
/**
* Implements equality testing between full viewing keys.
*/

View File

@ -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" {
@ -188,6 +189,17 @@ bool orchard_wallet_add_raw_address(
const OrchardRawAddressPtr* addr,
const OrchardIncomingViewingKeyPtr* ivk);
/**
* Returns a pointer to the Orchard spending 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_spending_key_free`.
*/
OrchardSpendingKeyPtr* orchard_wallet_get_spending_key_for_address(
const OrchardWalletPtr* wallet,
const OrchardRawAddressPtr* addr);
/**
* 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.
@ -251,6 +263,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

View File

@ -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<InProgress<Unproven, Unauthorized>, Amount>,
keys: *const *const SpendingKey,
keys_len: size_t,
sighash: *const [u8; 32],
) -> *mut Bundle<Authorized, Amount> {
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::<Vec<_>>();
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)),

View File

@ -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,

View File

@ -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<Position>,
}
/// 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<OrchardSpendInfo> {
// 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(),
),
)
})
})
}
}
//
@ -753,6 +805,22 @@ pub extern "C" fn orchard_wallet_add_raw_address(
wallet.key_store.add_raw_address(*addr, ivk.clone())
}
#[no_mangle]
pub extern "C" fn orchard_wallet_get_spending_key_for_address(
wallet: *const Wallet,
addr: *const Address,
) -> *mut SpendingKey {
let wallet = unsafe { wallet.as_ref() }.expect("Wallet pointer may not be null.");
let addr = unsafe { addr.as_ref() }.expect("Address may not be null.");
wallet
.key_store
.ivk_for_address(addr)
.and_then(|ivk| wallet.key_store.spending_key_for_ivk(ivk))
.map(|sk| Box::into_raw(Box::new(*sk)))
.unwrap_or(std::ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn orchard_wallet_get_ivk_for_address(
wallet: *const Wallet,
@ -844,3 +912,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()
}
}

View File

@ -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);
}

View File

@ -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<uint256>& ovk,
const libzcash::OrchardRawAddress& to,
@ -75,14 +92,20 @@ std::optional<UnauthorizedBundle> Builder::Build() {
}
std::optional<OrchardBundle> UnauthorizedBundle::ProveAndSign(
const std::vector<libzcash::OrchardSpendingKey>& keys,
uint256 sighash)
{
if (!inner) {
throw std::logic_error("orchard::UnauthorizedBundle has already been used");
}
std::vector<const OrchardSpendingKeyPtr*> 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<uint256>& 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 {

View File

@ -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<OrchardSpendInfoPtr, decltype(&orchard_spend_info_free)> 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<uint256>& 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<OrchardBundle> ProveAndSign(uint256 sighash);
std::optional<OrchardBundle> ProveAndSign(
const std::vector<libzcash::OrchardSpendingKey>& keys, uint256 sighash);
};
} // namespace orchard
@ -223,6 +270,8 @@ private:
std::optional<orchard::Builder> orchardBuilder;
CAmount valueBalanceOrchard = 0;
std::vector<libzcash::OrchardSpendingKey> orchardSpendingKeys;
std::optional<libzcash::OrchardRawAddress> firstOrchardSpendAddr;
std::vector<SpendDescriptionInfo> spends;
std::vector<OutputDescriptionInfo> outputs;
std::vector<libzcash::JSInput> jsInputs;
@ -293,6 +342,10 @@ public:
void SetFee(CAmount fee);
bool AddOrchardSpend(
libzcash::OrchardSpendingKey sk,
orchard::SpendInfo spendInfo);
void AddOrchardOutput(
const std::optional<uint256>& ovk,
const libzcash::OrchardRawAddress& to,

View File

@ -1,6 +1,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#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<OrchardNoteMetadata> 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].second.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].second)));
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();
}

41
src/wallet/orchard.cpp Normal file
View File

@ -0,0 +1,41 @@
// 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::optional<libzcash::OrchardSpendingKey> OrchardWallet::GetSpendingKeyForAddress(
const libzcash::OrchardRawAddress& addr) const
{
auto skPtr = orchard_wallet_get_spending_key_for_address(inner.get(), addr.inner.get());
if (skPtr == nullptr) return std::nullopt;
return libzcash::OrchardSpendingKey(skPtr);
}
std::vector<std::pair<libzcash::OrchardSpendingKey, orchard::SpendInfo>> OrchardWallet::GetSpendInfo(
const std::vector<OrchardNoteMetadata>& noteMetadata) const
{
std::vector<std::pair<libzcash::OrchardSpendingKey, orchard::SpendInfo>> 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());
auto sk = GetSpendingKeyForAddress(note.GetAddress());
if (sk.has_value()) {
result.push_back(std::pair(std::move(sk.value()), std::move(spendInfo)));
} else {
throw std::logic_error("Unknown spending key for given outpoint");
}
}
}
return result;
}

View File

@ -8,6 +8,8 @@
#include <array>
#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<OrchardWalletPtr, decltype(&orchard_wallet_free)> inner;
friend class ::orchard::UnauthorizedBundle;
public:
OrchardWallet() : inner(orchard_wallet_new(), orchard_wallet_free) {}
OrchardWallet(OrchardWallet&& wallet_data) : inner(std::move(wallet_data.inner)) {}
@ -196,6 +204,9 @@ public:
orchard_wallet_add_full_viewing_key(inner.get(), fvk.inner.get());
}
std::optional<libzcash::OrchardSpendingKey> GetSpendingKeyForAddress(
const libzcash::OrchardRawAddress& addr) const;
std::optional<libzcash::OrchardIncomingViewingKey> GetIncomingViewingKeyForAddress(
const libzcash::OrchardRawAddress& addr) const {
auto ivkPtr = orchard_wallet_get_ivk_for_address(inner.get(), addr.inner.get());
@ -262,6 +273,9 @@ public:
);
return result;
}
std::vector<std::pair<libzcash::OrchardSpendingKey, orchard::SpendInfo>> GetSpendInfo(
const std::vector<OrchardNoteMetadata>& noteMetadata) const;
};
#endif // ZCASH_ORCHARD_WALLET_H

View File

@ -3237,6 +3237,13 @@ void CWallet::GetSaplingNoteWitnesses(const std::vector<SaplingOutPoint>& notes,
}
}
std::vector<std::pair<libzcash::OrchardSpendingKey, orchard::SpendInfo>> CWallet::GetOrchardSpendInfo(
const std::vector<OrchardNoteMetadata>& orchardNoteMetadata) const
{
AssertLockHeld(cs_wallet);
return orchardWallet.GetSpendInfo(orchardNoteMetadata);
}
isminetype CWallet::IsMine(const CTxIn &txin) const
{
{

View File

@ -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<SaplingOutPoint>& notes,
std::vector<std::optional<SaplingWitness>>& witnesses,
uint256 &final_anchor);
std::vector<std::pair<libzcash::OrchardSpendingKey, orchard::SpendInfo>> GetOrchardSpendInfo(
const std::vector<OrchardNoteMetadata>& orchardNoteMetadata) const;
isminetype IsMine(const CTxIn& txin) const;
CAmount GetDebit(const CTxIn& txin, const isminefilter& filter) const;

View File

@ -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,

View File

@ -12,7 +12,10 @@
#include <optional>
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)) {}