Skip to content

Commit fe3e645

Browse files
authored
Merge pull request zcash#7035 from str4d/fix-wtx-is-from-me-orchard
wallet: Check spent Orchard notes in `CWallet{Tx}::IsFromMe`
2 parents 2352fbc + 74a1a5d commit fe3e645

File tree

6 files changed

+97
-1
lines changed

6 files changed

+97
-1
lines changed

doc/release-notes.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,18 @@ release-notes at release time)
44
Notable changes
55
===============
66

7+
Fixed Orchard bug in transparent balance APIs
8+
---------------------------------------------
9+
10+
Several RPC methods inherited from Bitcoin Core internally rely on
11+
`CWalletTx::IsFromMe` for detecting involvement of the wallet in the input
12+
side of a transaction. For example:
13+
- The `getbalance` RPC method uses it as part of identifying "trusted"
14+
zero-confirmation transactions to include in the balance calculation.
15+
- The `gettransaction` RPC method uses it to decide whether to include a
16+
`fee` field in its response.
17+
18+
When Orchard was integrated into `zcashd`, this method was not updated to
19+
account for it, meaning that unshielding transactions spending Orchard notes
20+
would not be correctly accounted for in transparent-specific RPC methods. A
21+
similar bug involving Sprout and Sapling spends was fixed in v4.5.1.

qa/rpc-tests/wallet_isfromme.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python3
2-
# Copyright (c) 2021-2024 The Zcash developers
2+
# Copyright (c) 2021-2025 The Zcash developers
33
# Distributed under the MIT software license, see the accompanying
44
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
55

@@ -8,6 +8,7 @@
88
BLOSSOM_BRANCH_ID,
99
CANOPY_BRANCH_ID,
1010
HEARTWOOD_BRANCH_ID,
11+
NU5_BRANCH_ID,
1112
OVERWINTER_BRANCH_ID,
1213
SAPLING_BRANCH_ID,
1314
assert_equal,
@@ -32,6 +33,7 @@ def setup_network(self, split=False):
3233
nuparams(BLOSSOM_BRANCH_ID, 1),
3334
nuparams(HEARTWOOD_BRANCH_ID, 1),
3435
nuparams(CANOPY_BRANCH_ID, 1),
36+
nuparams(NU5_BRANCH_ID, 110),
3537
'-allowdeprecated=getnewaddress',
3638
'-allowdeprecated=z_getnewaddress',
3739
]])
@@ -85,5 +87,51 @@ def run_test (self):
8587
# "getbalance '' 0" should count both outputs. The bug failed here.
8688
assert_equal(Decimal(node.getbalance('', 0)), Decimal('12.5') - coinbase_fee - fee)
8789

90+
# Mine the transaction; we get another coinbase output.
91+
node.generate(8)
92+
self.sync_all()
93+
assert_equal(Decimal(node.getbalance('', 0)), Decimal('62.5') - coinbase_fee - fee)
94+
95+
# Send all available funds to an Orchard UA.
96+
acct = node.z_getnewaccount()['account']
97+
orchard_ua = node.z_getaddressforaccount(acct, ['orchard'])['address']
98+
recipients = [{'address': orchard_ua, 'amount': Decimal('6.25') - coinbase_fee - 2*fee}]
99+
# minconf = 1 is requires for `TransactionEffects::ApproveAndBuild`
100+
# to use an Orchard anchor.
101+
opid = node.z_sendmany(taddr, recipients, 1, fee, 'AllowRevealedSenders')
102+
wait_and_assert_operationid_status(node, opid)
103+
self.sync_all()
104+
assert_equal(Decimal(node.getbalance('', 0)), Decimal('62.5') - Decimal('6.25'))
105+
106+
# Mine the transaction; we get another coinbase output.
107+
node.generate(1)
108+
self.sync_all()
109+
assert_equal(Decimal(node.getbalance('', 0)), Decimal('68.75') - Decimal('6.25'))
110+
111+
# Now send the funds back to a new t-address.
112+
taddr = node.getnewaddress()
113+
recipients = [{'address': taddr, 'amount': Decimal('6.25') - coinbase_fee - 3*fee}]
114+
# TODO: this fails for ZIP_317_FEE due to a dust threshold problem:
115+
# "Insufficient funds: have 6.24985, need 0.00000054 more to surpass the dust
116+
# threshold and avoid being forced to over-pay the fee. Alternatively, you could
117+
# specify a fee of 0.0001 to allow overpayment of the conventional fee and have
118+
# this transaction proceed.; note that coinbase outputs will not be selected if
119+
# you specify ANY_TADDR, any transparent recipients are included, or if the
120+
# `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker."
121+
opid = node.z_sendmany(orchard_ua, recipients, 1, fee, 'AllowRevealedRecipients')
122+
wait_and_assert_operationid_status(node, opid)
123+
self.sync_all()
124+
125+
# At this point we have created the conditions for the bug fixed in
126+
# https://github.com/zcash/zcash/pull/5325.
127+
128+
# listunspent should show the coinbase outputs, and optionally the
129+
# newly-received unshielding output.
130+
assert_equal(len(node.listunspent()), 10)
131+
assert_equal(len(node.listunspent(0)), 11)
132+
133+
# "getbalance '' 0" should count both outputs. The Orchard bug failed here.
134+
assert_equal(Decimal(node.getbalance('', 0)), Decimal('68.75') - coinbase_fee - 3*fee)
135+
88136
if __name__ == '__main__':
89137
WalletIsFromMe().main ()

src/rust/include/rust/orchard/wallet.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,13 @@ bool orchard_wallet_get_txdata(
335335
push_output_t push_output_cb
336336
);
337337

338+
/**
339+
* Returns true if this nullifier belongs to one of the notes in our wallet.
340+
*/
341+
bool orchard_wallet_is_nullifier_from_me(
342+
const OrchardWalletPtr* wallet,
343+
const unsigned char *nullifier);
344+
338345
typedef void (*push_txid_callback_t)(void* resultVector, unsigned char txid[32]);
339346

340347
/**

src/rust/src/wallet.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,18 @@ pub extern "C" fn orchard_wallet_get_txdata(
11831183
}
11841184
}
11851185

1186+
#[no_mangle]
1187+
pub extern "C" fn orchard_wallet_is_nullifier_from_me(
1188+
wallet: *const Wallet,
1189+
nullifier: *const [c_uchar; 32],
1190+
) -> bool {
1191+
let wallet = unsafe { wallet.as_ref() }.expect("Wallet pointer may not be null.");
1192+
let nullifier =
1193+
Nullifier::from_bytes(unsafe { nullifier.as_ref() }.expect("nullifier may not be null."));
1194+
1195+
wallet.nullifiers.contains_key(&nullifier.unwrap())
1196+
}
1197+
11861198
pub type PushTxId = unsafe extern "C" fn(obj: Option<FFICallbackReceiver>, txid: *const [u8; 32]);
11871199

11881200
#[no_mangle]

src/wallet/orchard.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,10 @@ class OrchardWallet
437437
reinterpret_cast<std::vector<uint256>*>(txidsRet)->push_back(txid_out);
438438
}
439439

440+
bool IsNullifierFromMe(const std::array<uint8_t, 32>& nullifier) const {
441+
return orchard_wallet_is_nullifier_from_me(inner.get(), nullifier.data());
442+
}
443+
440444
std::vector<uint256> GetPotentialSpendsFromNullifier(const uint256& nullifier) const {
441445
std::vector<uint256> result;
442446
orchard_wallet_get_potential_spends_from_nullifier(

src/wallet/wallet.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4082,6 +4082,11 @@ bool CWallet::IsFromMe(const CTransaction& tx) const
40824082
return true;
40834083
}
40844084
}
4085+
for (const auto& action : tx.GetOrchardBundle().GetDetails()->actions()) {
4086+
if (orchardWallet.IsNullifierFromMe(action.nullifier())) {
4087+
return true;
4088+
}
4089+
}
40854090
return false;
40864091
}
40874092

@@ -4981,6 +4986,11 @@ bool CWalletTx::IsFromMe(const isminefilter& filter) const
49814986
return true;
49824987
}
49834988
}
4989+
for (const auto& action : GetOrchardBundle().GetDetails()->actions()) {
4990+
if (pwallet->orchardWallet.IsNullifierFromMe(action.nullifier())) {
4991+
return true;
4992+
}
4993+
}
49844994
return false;
49854995
}
49864996

0 commit comments

Comments
 (0)