Add OrchardWallet::GetTxActions

This method detects and returns metadata for any notes belonging to the
wallet that are spent in the transaction. It also trial-decrypts the
Orchard outputs of a transaction using the wallet's incoming viewing
keys along a set of OVKs provided by the caller, and returns metadata
about the notes that were successfully decrypted.
This commit is contained in:
Kris Nuttycombe 2022-03-12 17:10:11 -07:00
parent 9993d948ef
commit 315392ce3b
7 changed files with 290 additions and 5 deletions

View File

@ -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"]

2
Cargo.lock generated
View File

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

View File

@ -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" }

View File

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

View File

@ -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::<Vec<_>>();
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<FFICallbackReceiver>, 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<FFICallbackReceiver>, data: FFIActionOutput);
#[no_mangle]
pub extern "C" fn orchard_wallet_get_txdata(
wallet: *const Wallet,
bundle: *const Bundle<Authorized, Amount>,
raw_ovks: *const [u8; 32],
raw_ovks_len: usize,
callback_receiver: Option<FFICallbackReceiver>,
spend_push_cb: Option<SpendPushCB>,
output_push_cb: Option<OutputPushCB>,
) -> 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<OutgoingViewingKey> = 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::<Vec<_>>();
let incoming: BTreeMap<usize, (Note, Address, [u8; 512])> = bundle
.decrypt_outputs_with_keys(&keys)
.into_iter()
.map(|(idx, _, note, addr, memo)| (idx, (note, addr, memo)))
.collect();
let outgoing: BTreeMap<usize, (Note, Address, [u8; 512])> = 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<FFICallbackReceiver>, txid: *const [u8; 32]);
#[no_mangle]

View File

@ -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<unsigned char, 512> memo;
bool isOutgoing;
public:
OrchardActionOutput(
libzcash::OrchardRawAddress recipient, CAmount noteValue, std::array<unsigned char, 512> 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<unsigned char, 512>& GetMemo() const {
return memo;
}
bool IsOutgoing() const {
return isOutgoing;
}
};
class OrchardActions {
private:
std::map<uint32_t, OrchardActionSpend> spends;
std::map<uint32_t, OrchardActionOutput> 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<uint32_t, OrchardActionSpend>& GetSpends() {
return spends;
}
const std::map<uint32_t, OrchardActionOutput>& 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<OrchardActions*>(receiver)->AddSpend(rawSpend.spendActionIdx, spend);
}
static void PushOutputAction(void* receiver, RawOrchardActionOutput rawOutput) {
std::array<unsigned char, 512> 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<OrchardActions*>(receiver)->AddOutput(rawOutput.outputActionIdx, output);
}
OrchardActions GetTxActions(const CTransaction& tx, const std::vector<uint256>& ovks) const {
OrchardActions result;
orchard_wallet_get_txdata(
inner.get(),
tx.GetOrchardBundle().inner.get(),
reinterpret_cast<const unsigned char*>(ovks.data()),
ovks.size(),
&result,
PushSpendAction,
PushOutputAction);
return result;
}
};
class OrchardWalletNoteCommitmentTreeWriter

View File

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