Merge pull request #5710 from zcash/feature/wallet_orchard

Add Orchard support to the zcashd wallet.
This commit is contained in:
Charlie O'Keefe 2022-03-19 10:25:57 -06:00 committed by GitHub
commit 9974be75fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 8058 additions and 992 deletions

View File

@ -6,7 +6,22 @@ replace-with = "vendored-sources"
[source."https://github.com/zcash/librustzcash.git"]
git = "https://github.com/zcash/librustzcash.git"
rev = "5622b060b1f57de7afc3d0b4e425b9b4b22482a0"
rev = "67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289"
replace-with = "vendored-sources"
[source."https://github.com/zcash/orchard.git"]
git = "https://github.com/zcash/orchard.git"
rev = "f4587f790d7317df85a9ee77ce693a06ed6d8d02"
replace-with = "vendored-sources"
[source."https://github.com/zcash/incrementalmerkletree.git"]
git = "https://github.com/zcash/incrementalmerkletree.git"
rev = "62c33e4480a71170b02b9eb7d4b0160194f414ee"
replace-with = "vendored-sources"
[source."https://github.com/nuttycom/hdwallet.git"]
git = "https://github.com/nuttycom/hdwallet.git"
rev = "576683b9f2865f1118c309017ff36e01f84420c9"
replace-with = "vendored-sources"
[source.vendored-sources]

23
Cargo.lock generated
View File

@ -566,7 +566,7 @@ checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]]
name = "equihash"
version = "0.1.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a"
source = "git+https://github.com/zcash/librustzcash.git?rev=67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289#67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289"
dependencies = [
"blake2b_simd 1.0.0",
"byteorder",
@ -575,7 +575,7 @@ dependencies = [
[[package]]
name = "f4jumble"
version = "0.0.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a"
source = "git+https://github.com/zcash/librustzcash.git?rev=67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289#67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289"
dependencies = [
"blake2b_simd 1.0.0",
]
@ -839,8 +839,7 @@ dependencies = [
[[package]]
name = "incrementalmerkletree"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186fd3ab92aeac865d4b80b410de9a7b341d31ba8281373caed0b6d17b2b5e96"
source = "git+https://github.com/zcash/incrementalmerkletree.git?rev=62c33e4480a71170b02b9eb7d4b0160194f414ee#62c33e4480a71170b02b9eb7d4b0160194f414ee"
dependencies = [
"serde",
]
@ -961,6 +960,7 @@ dependencies = [
"tracing-core",
"tracing-subscriber",
"zcash_address",
"zcash_encoding",
"zcash_history",
"zcash_note_encryption",
"zcash_primitives",
@ -1242,8 +1242,7 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "orchard"
version = "0.1.0-beta.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31f03b6d0aee6d993cac35388b818e04f076ded0ab284979e4d7cd5a8b3c2be"
source = "git+https://github.com/zcash/orchard.git?rev=f4587f790d7317df85a9ee77ce693a06ed6d8d02#f4587f790d7317df85a9ee77ce693a06ed6d8d02"
dependencies = [
"aes",
"arrayvec 0.7.2",
@ -2204,7 +2203,7 @@ dependencies = [
[[package]]
name = "zcash_address"
version = "0.0.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a"
source = "git+https://github.com/zcash/librustzcash.git?rev=67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289#67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289"
dependencies = [
"bech32",
"bs58",
@ -2215,7 +2214,7 @@ dependencies = [
[[package]]
name = "zcash_encoding"
version = "0.0.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a"
source = "git+https://github.com/zcash/librustzcash.git?rev=67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289#67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289"
dependencies = [
"byteorder",
"nonempty",
@ -2224,7 +2223,7 @@ dependencies = [
[[package]]
name = "zcash_history"
version = "0.2.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a"
source = "git+https://github.com/zcash/librustzcash.git?rev=67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289#67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289"
dependencies = [
"bigint",
"blake2b_simd 1.0.0",
@ -2234,7 +2233,7 @@ dependencies = [
[[package]]
name = "zcash_note_encryption"
version = "0.1.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a"
source = "git+https://github.com/zcash/librustzcash.git?rev=67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289#67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289"
dependencies = [
"chacha20",
"chacha20poly1305",
@ -2245,7 +2244,7 @@ dependencies = [
[[package]]
name = "zcash_primitives"
version = "0.5.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a"
source = "git+https://github.com/zcash/librustzcash.git?rev=67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289#67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289"
dependencies = [
"aes",
"bip0039",
@ -2281,7 +2280,7 @@ dependencies = [
[[package]]
name = "zcash_proofs"
version = "0.5.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a"
source = "git+https://github.com/zcash/librustzcash.git?rev=67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289#67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289"
dependencies = [
"bellman",
"blake2b_simd 1.0.0",

View File

@ -49,6 +49,7 @@ tracing = "0.1"
tracing-core = "0.1"
tracing-appender = "0.2"
zcash_address = "0.0"
zcash_encoding = "0.0"
zcash_history = "0.2"
zcash_note_encryption = "0.1"
zcash_primitives = { version = "0.5", features = ["transparent-inputs"] }
@ -86,8 +87,11 @@ codegen-units = 1
[patch.crates-io]
hdwallet = { git = "https://github.com/nuttycom/hdwallet", rev = "576683b9f2865f1118c309017ff36e01f84420c9" }
zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "4f4a25252f0fc6b84c44316d810107bc7ea7f32a" }
zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "4f4a25252f0fc6b84c44316d810107bc7ea7f32a" }
zcash_note_encryption = { git = "https://github.com/zcash/librustzcash.git", rev = "4f4a25252f0fc6b84c44316d810107bc7ea7f32a" }
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "4f4a25252f0fc6b84c44316d810107bc7ea7f32a" }
zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "4f4a25252f0fc6b84c44316d810107bc7ea7f32a" }
incrementalmerkletree = { git = "https://github.com/zcash/incrementalmerkletree.git", rev = "62c33e4480a71170b02b9eb7d4b0160194f414ee" }
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" }
zcash_note_encryption = { git = "https://github.com/zcash/librustzcash.git", rev = "67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289" }
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289" }
zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289" }

View File

@ -81,11 +81,15 @@ Wallet
pool boundaries, they must be explicitly enabled via a parameter to
the 'z_sendmany' call.
- A new boolean parameter, `allowRevealedAmounts`, has been added to the
list of arguments accepted by 'z_sendmany'. This parameter defaults to
`false` and is only required when the transaction being constructed
would reveal transaction amounts as a consequence of ZEC value crossing
shielded pool boundaries via the turnstile.
- A new string parameter, `privacyPolicy`, has been added to the list of
arguments accepted by `z_sendmany`. This parameter enables the caller to
control what kind of information they permit `zcashd` to reveal when creating
the transaction. If the transaction can only be created by revealing more
information than the given strategy permits, `z_sendmany` will return an
error. The parameter defaults to `LegacyCompat`, which applies the most
restrictive strategy `FullPrivacy` when a Unified Address is present as the
sender or a recipient, and otherwise preserves existing behaviour (which
corresponds to the `AllowFullyTransparent` policy).
- Since Sprout outputs are no longer created (with the exception of change)
'z_sendmany' no longer generates payment disclosures (which were only

View File

@ -60,6 +60,7 @@ BASE_SCRIPTS= [
'wallet_persistence.py',
'wallet_listnotes.py',
# vv Tests less than 60s vv
'orchard_reorg.py',
'fundrawtransaction.py',
'reorg_limit.py',
'mempool_limit.py',
@ -72,6 +73,7 @@ BASE_SCRIPTS= [
'wallet_changeindicator.py',
'wallet_import_export.py',
'wallet_isfromme.py',
'wallet_orchard.py',
'wallet_nullifiers.py',
'wallet_sapling.py',
'wallet_sendmany_any_taddr.py',

View File

@ -6,10 +6,12 @@
from decimal import Decimal
from test_framework.authproxy import JSONRPCException
from test_framework.test_framework import BitcoinTestFramework
from test_framework.mininode import nuparams
from test_framework.mininode import COIN, nuparams
from test_framework.util import (
BLOSSOM_BRANCH_ID,
HEARTWOOD_BRANCH_ID,
CANOPY_BRANCH_ID,
NU5_BRANCH_ID,
assert_equal,
assert_raises,
bitcoind_processes,
@ -28,8 +30,12 @@ class ShieldCoinbaseTest (BitcoinTestFramework):
def start_node_with(self, index, extra_args=[]):
args = [
'-experimentalfeatures',
'-orchardwallet',
nuparams(BLOSSOM_BRANCH_ID, 1),
nuparams(HEARTWOOD_BRANCH_ID, 10),
nuparams(CANOPY_BRANCH_ID, 20),
nuparams(NU5_BRANCH_ID, 20),
"-nurejectoldversions=false",
]
return start_node(index, self.options.tmpdir, args + extra_args)
@ -117,5 +123,50 @@ class ShieldCoinbaseTest (BitcoinTestFramework):
assert_equal(self.nodes[0].z_getbalance(node0_taddr), 2)
assert_equal(self.nodes[1].z_getbalance(node1_zaddr), 1)
# Generate a Unified Address for node 1
self.nodes[1].z_getnewaccount()
node1_addr0 = self.nodes[1].z_getaddressforaccount(0)
assert_equal(node1_addr0['account'], 0)
assert_equal(set(node1_addr0['pools']), set(['transparent', 'sapling', 'orchard']))
node1_ua = node1_addr0['unifiedaddress']
# Set node 1's miner address to the UA
self.nodes[1].stop()
bitcoind_processes[1].wait()
self.nodes[1] = self.start_node_with(1, [
"-mineraddress=%s" % node1_ua,
])
connect_nodes(self.nodes[1], 0)
# The UA starts with zero balance.
assert_equal(self.nodes[1].z_getbalanceforaccount(0)['pools'], {})
# Node 1 can mine blocks because the miner selects the Sapling receiver
# of its UA.
print("Mining block with node 1")
self.nodes[1].generate(1)
self.sync_all()
# The UA balance should show that Sapling funds were received.
assert_equal(self.nodes[1].z_getbalanceforaccount(0)['pools'], {
'sapling': {'valueZat': 5 * COIN },
})
# Activate NU5
print("Activating NU5")
self.nodes[0].generate(7)
self.sync_all()
# Now any block mined by node 1 should use the Orchard receiver of its UA.
print("Mining block with node 1")
self.nodes[1].generate(1)
self.sync_all()
assert_equal(self.nodes[1].z_getbalanceforaccount(0)['pools'], {
'sapling': {'valueZat': 5 * COIN },
# 6.25 ZEC because the FR always ends when Canopy activates, and
# regtest has no defined funding streams.
'orchard': {'valueZat': 6.25 * COIN },
})
if __name__ == '__main__':
ShieldCoinbaseTest().main()

130
qa/rpc-tests/orchard_reorg.py Executable file
View File

@ -0,0 +1,130 @@
#!/usr/bin/env python3
# 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 .
#
# Test the effect of reorgs on the Orchard commitment tree.
#
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
BLOSSOM_BRANCH_ID,
HEARTWOOD_BRANCH_ID,
CANOPY_BRANCH_ID,
NU5_BRANCH_ID,
assert_equal,
get_coinbase_address,
nuparams,
start_nodes,
wait_and_assert_operationid_status,
)
from finalsaplingroot import ORCHARD_TREE_EMPTY_ROOT
from decimal import Decimal
class OrchardReorgTest(BitcoinTestFramework):
def __init__(self):
super().__init__()
self.num_nodes = 4
self.setup_clean_chain = True
def setup_nodes(self):
return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
nuparams(BLOSSOM_BRANCH_ID, 1),
nuparams(HEARTWOOD_BRANCH_ID, 5),
nuparams(CANOPY_BRANCH_ID, 5),
nuparams(NU5_BRANCH_ID, 10),
'-nurejectoldversions=false',
'-experimentalfeatures',
'-orchardwallet',
# '-debug',
]] * self.num_nodes)
def run_test(self):
# Activate NU5 so we can test Orchard.
self.nodes[0].generate(10)
self.sync_all()
# Generate a UA with only an Orchard receiver.
account = self.nodes[0].z_getnewaccount()['account']
addr = self.nodes[0].z_getaddressforaccount(account, ['orchard'])
assert_equal(addr['account'], account)
assert_equal(set(addr['pools']), set(['orchard']))
ua = addr['unifiedaddress']
# Before mining any Orchard notes, finalorchardroot should be the empty Orchard root.
assert_equal(
ORCHARD_TREE_EMPTY_ROOT,
self.nodes[0].getblock(self.nodes[0].getbestblockhash())['finalorchardroot'],
)
# finalorchardroot should not change if we mine additional blocks without Orchard notes.
self.nodes[0].generate(100)
self.sync_all()
assert_equal(
ORCHARD_TREE_EMPTY_ROOT,
self.nodes[0].getblock(self.nodes[0].getbestblockhash())['finalorchardroot'],
)
# Create an Orchard note.
recipients = [{'address': ua, 'amount': Decimal('12.5')}]
opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], opid)
# After mining a block, finalorchardroot should have changed.
self.sync_all()
self.nodes[0].generate(1)
self.sync_all()
orchardroot_oneleaf = self.nodes[0].getblock(self.nodes[0].getbestblockhash())['finalorchardroot']
print("Root of Orchard commitment tree with one leaf:", orchardroot_oneleaf)
assert(orchardroot_oneleaf != ORCHARD_TREE_EMPTY_ROOT)
# finalorchardroot should not change if we mine additional blocks without Orchard notes.
self.nodes[0].generate(4)
self.sync_all()
assert_equal(
orchardroot_oneleaf,
self.nodes[0].getblock(self.nodes[0].getbestblockhash())['finalorchardroot'],
)
# Split the network so we can test the effect of a reorg.
print("Splitting the network")
self.split_network()
# Create another Orchard note on node 0.
recipients = [{'address': ua, 'amount': Decimal('12.5')}]
opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], opid)
# Mine two blocks on node 0.
print("Mining 2 blocks on node 0")
self.nodes[0].generate(2)
self.sync_all()
orchardroot_twoleaf = self.nodes[0].getblock(self.nodes[0].getbestblockhash())['finalorchardroot']
print("Root of Orchard commitment tree with two leaves:", orchardroot_twoleaf)
assert(orchardroot_twoleaf != ORCHARD_TREE_EMPTY_ROOT)
assert(orchardroot_twoleaf != orchardroot_oneleaf)
# Generate 10 blocks on node 2.
print("Mining alternate chain on node 2")
self.nodes[2].generate(10)
self.sync_all()
assert_equal(
orchardroot_oneleaf,
self.nodes[2].getblock(self.nodes[2].getbestblockhash())['finalorchardroot'],
)
# Reconnect the nodes; node 0 will re-org to node 2's chain.
print("Re-joining the network so that node 0 reorgs")
self.join_network()
# Verify that node 0's latest Orchard root matches what we expect.
orchardroot_postreorg = self.nodes[0].getblock(self.nodes[2].getbestblockhash())['finalorchardroot']
print("Root of Orchard commitment tree after reorg:", orchardroot_postreorg)
assert_equal(orchardroot_postreorg, orchardroot_oneleaf)
if __name__ == '__main__':
OrchardReorgTest().main()

View File

@ -52,7 +52,7 @@ class BitcoinTestFramework(object):
def setup_nodes(self):
return start_nodes(self.num_nodes, self.options.tmpdir)
def setup_network(self, split = False):
def setup_network(self, split = False, do_mempool_sync = True):
self.nodes = self.setup_nodes()
# Connect the nodes as a "chain". This allows us
@ -64,12 +64,13 @@ class BitcoinTestFramework(object):
if not split:
connect_nodes_bi(self.nodes, 1, 2)
sync_blocks(self.nodes[1:3])
sync_mempools(self.nodes[1:3])
if do_mempool_sync:
sync_mempools(self.nodes[1:3])
connect_nodes_bi(self.nodes, 0, 1)
connect_nodes_bi(self.nodes, 2, 3)
self.is_network_split = split
self.sync_all()
self.sync_all(do_mempool_sync)
def split_network(self):
"""
@ -80,15 +81,17 @@ class BitcoinTestFramework(object):
wait_bitcoinds()
self.setup_network(True)
def sync_all(self):
def sync_all(self, do_mempool_sync = True):
if self.is_network_split:
sync_blocks(self.nodes[:2])
sync_blocks(self.nodes[2:])
sync_mempools(self.nodes[:2])
sync_mempools(self.nodes[2:])
if do_mempool_sync:
sync_mempools(self.nodes[:2])
sync_mempools(self.nodes[2:])
else:
sync_blocks(self.nodes)
sync_mempools(self.nodes)
if do_mempool_sync:
sync_mempools(self.nodes)
def join_network(self):
"""
@ -97,7 +100,7 @@ class BitcoinTestFramework(object):
assert self.is_network_split
stop_nodes(self.nodes)
wait_bitcoinds()
self.setup_network(False)
self.setup_network(False, False)
def main(self):

View File

@ -7,9 +7,11 @@ from test_framework.authproxy import JSONRPCException
from test_framework.mininode import COIN
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
NU5_BRANCH_ID,
assert_equal,
assert_raises_message,
get_coinbase_address,
nuparams,
start_nodes,
wait_and_assert_operationid_status,
)
@ -22,6 +24,7 @@ class WalletAccountsTest(BitcoinTestFramework):
return start_nodes(self.num_nodes, self.options.tmpdir, [[
'-experimentalfeatures',
'-orchardwallet',
nuparams(NU5_BRANCH_ID, 210),
]] * self.num_nodes)
def check_receiver_types(self, ua, expected):
@ -32,14 +35,19 @@ class WalletAccountsTest(BitcoinTestFramework):
# Remember that empty pools are omitted from the output.
def _check_balance_for_rpc(self, rpcmethod, node, account, expected, minconf):
rpc = getattr(self.nodes[node], rpcmethod)
actual = rpc(account) if minconf is None else rpc(account, minconf)
actual = rpc(account, minconf)
assert_equal(set(expected), set(actual['pools']))
total_balance = 0
for pool in expected:
assert_equal(expected[pool] * COIN, actual['pools'][pool]['valueZat'])
assert_equal(actual['minimum_confirmations'], 1 if minconf is None else minconf)
total_balance += expected[pool]
assert_equal(actual['minimum_confirmations'], minconf)
return total_balance
def check_balance(self, node, account, address, expected, minconf=None):
self._check_balance_for_rpc('z_getbalanceforaccount', node, account, expected, minconf)
def check_balance(self, node, account, address, expected, minconf=1):
acct_balance = self._check_balance_for_rpc('z_getbalanceforaccount', node, account, expected, minconf)
z_getbalance = self.nodes[node].z_getbalance(address, minconf)
assert_equal(acct_balance, z_getbalance)
fvk = self.nodes[node].z_exportviewingkey(address)
self._check_balance_for_rpc('z_getbalanceforviewingkey', node, fvk, expected, minconf)
@ -55,7 +63,7 @@ class WalletAccountsTest(BitcoinTestFramework):
# Generate the first address for account 0.
addr0 = self.nodes[0].z_getaddressforaccount(0)
assert_equal(addr0['account'], 0)
assert_equal(set(addr0['pools']), set(['transparent', 'sapling']))
assert_equal(set(addr0['pools']), set(['transparent', 'sapling', 'orchard']))
ua0 = addr0['unifiedaddress']
# We pick mnemonic phrases to ensure that we can always generate the default
@ -70,24 +78,47 @@ class WalletAccountsTest(BitcoinTestFramework):
'no address at diversifier index 0',
self.nodes[0].z_getaddressforaccount, 0, [], 0)
# The second address for account 0 is different to the first address.
addr0_2 = self.nodes[0].z_getaddressforaccount(0)
assert_equal(addr0_2['account'], 0)
assert_equal(set(addr0_2['pools']), set(['transparent', 'sapling', 'orchard']))
ua0_2 = addr0_2['unifiedaddress']
assert(ua0 != ua0_2)
# We can generate a fully-shielded address.
addr0_3 = self.nodes[0].z_getaddressforaccount(0, ['sapling', 'orchard'])
assert_equal(addr0_3['account'], 0)
assert_equal(set(addr0_3['pools']), set(['sapling', 'orchard']))
ua0_3 = addr0_3['unifiedaddress']
# We can generate an address without a Sapling receiver.
addr0_4 = self.nodes[0].z_getaddressforaccount(0, ['transparent', 'orchard'])
assert_equal(addr0_4['account'], 0)
assert_equal(set(addr0_4['pools']), set(['transparent', 'orchard']))
ua0_4 = addr0_4['unifiedaddress']
# The first address for account 1 is different to account 0.
addr1 = self.nodes[0].z_getaddressforaccount(1)
assert_equal(addr1['account'], 1)
assert_equal(set(addr1['pools']), set(['transparent', 'sapling']))
assert_equal(set(addr1['pools']), set(['transparent', 'sapling', 'orchard']))
ua1 = addr1['unifiedaddress']
assert(ua0 != ua1)
# The UA contains the expected receiver kinds.
self.check_receiver_types(ua0, ['transparent', 'sapling'])
self.check_receiver_types(ua1, ['transparent', 'sapling'])
self.check_receiver_types(ua0, ['transparent', 'sapling', 'orchard'])
self.check_receiver_types(ua0_2, ['transparent', 'sapling', 'orchard'])
self.check_receiver_types(ua0_3, [ 'sapling', 'orchard'])
self.check_receiver_types(ua0_4, ['transparent', 'orchard'])
self.check_receiver_types(ua1, ['transparent', 'sapling', 'orchard'])
# The balances of the accounts are all zero.
self.check_balance(0, 0, ua0, {})
self.check_balance(0, 1, ua1, {})
# Manually send funds to one of the receivers in the UA.
# Send coinbase funds to the UA.
print('Sending coinbase funds to account')
recipients = [{'address': ua0, 'amount': Decimal('10')}]
opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0)
opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
txid = wait_and_assert_operationid_status(self.nodes[0], opid)
# The wallet should detect the new note as belonging to the UA.
@ -108,7 +139,8 @@ class WalletAccountsTest(BitcoinTestFramework):
# The default minconf should now detect the balance.
self.check_balance(0, 0, ua0, {'sapling': 10})
# Manually send funds from the UA receiver.
# Send Sapling funds from the UA.
print('Sending account funds to Sapling address')
node1sapling = self.nodes[1].z_getnewaddress('sapling')
recipients = [{'address': node1sapling, 'amount': Decimal('1')}]
opid = self.nodes[0].z_sendmany(ua0, recipients, 1, 0)
@ -128,6 +160,64 @@ class WalletAccountsTest(BitcoinTestFramework):
self.check_balance(0, 0, ua0, {})
self.check_balance(0, 0, ua0, {'sapling': 9}, 0)
# Activate NU5
print('Activating NU5')
self.nodes[2].generate(9)
self.sync_all()
assert_equal(self.nodes[0].getblockchaininfo()['blocks'], 210)
# Send more coinbase funds to the UA.
print('Sending coinbase funds to account')
recipients = [{'address': ua0, 'amount': Decimal('10')}]
opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
txid = wait_and_assert_operationid_status(self.nodes[0], opid)
# The wallet should detect the new note as belonging to the UA.
tx_details = self.nodes[0].z_viewtransaction(txid)
assert_equal(len(tx_details['outputs']), 1)
assert_equal(tx_details['outputs'][0]['type'], 'orchard')
assert_equal(tx_details['outputs'][0]['address'], ua0)
# The new balance should not be visible with the default minconf, but should be
# visible with minconf=0.
self.sync_all()
self.check_balance(0, 0, ua0, {'sapling': 9})
self.check_balance(0, 0, ua0, {'sapling': 9, 'orchard': 10}, 0)
# The total balance with the default minconf should be just the Sapling balance
assert_equal('9.00', self.nodes[0].z_gettotalbalance()['private'])
assert_equal('19.00', self.nodes[0].z_gettotalbalance(0)['private'])
self.nodes[2].generate(1)
self.sync_all()
# Send Orchard funds from the UA.
print('Sending account funds to Orchard-only UA')
node1account = self.nodes[1].z_getnewaccount()['account']
node1orchard = self.nodes[1].z_getaddressforaccount(node1account, ['orchard'])['unifiedaddress']
recipients = [{'address': node1orchard, 'amount': Decimal('1')}]
opid = self.nodes[0].z_sendmany(ua0, recipients, 1, 0)
txid = wait_and_assert_operationid_status(self.nodes[0], opid)
# The wallet should detect the spent note as belonging to the UA.
# TODO ORCHARD: Uncomment this once z_viewtransaction shows Orchard details (#5186).
# tx_details = self.nodes[0].z_viewtransaction(txid)
# assert_equal(len(tx_details['spends']), 1)
# assert_equal(tx_details['spends'][0]['type'], 'orchard')
# assert_equal(tx_details['spends'][0]['address'], ua0)
# assert_equal(len(tx_details['outputs']), 1)
# assert_equal(tx_details['outputs'][0]['type'], 'orchard')
# assert_equal(tx_details['outputs'][0]['address'], ua0)
# The balances of the account should reflect whether zero-conf transactions are
# being considered. The Sapling balance should remain at 9, while the Orchard
# balance will show either 0 (because the spent 10-ZEC note is never shown, as
# that transaction has been created and broadcast, and _might_ get mined up until
# the transaction expires), or 9 (if we include the unmined transaction).
self.sync_all()
self.check_balance(0, 0, ua0, {'sapling': 9})
self.check_balance(0, 0, ua0, {'sapling': 9, 'orchard': 9}, 0)
if __name__ == '__main__':
WalletAccountsTest().main()

View File

@ -68,7 +68,7 @@ class WalletListNotes(BitcoinTestFramework):
change_amount_2 = receive_amount_1 - receive_amount_2 - DEFAULT_FEE
assert_equal('sapling', self.nodes[0].z_validateaddress(saplingzaddr)['type'])
recipients = [{"address": saplingzaddr, "amount":receive_amount_2}]
myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients, 1, DEFAULT_FEE, True)
myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients, 1, DEFAULT_FEE)
txid_2 = wait_and_assert_operationid_status(self.nodes[0], myopid)
self.sync_all()
@ -107,7 +107,7 @@ class WalletListNotes(BitcoinTestFramework):
receive_amount_3 = Decimal('2.0')
change_amount_3 = change_amount_2 - receive_amount_3 - DEFAULT_FEE
recipients = [{"address": saplingzaddr2, "amount":receive_amount_3}]
myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients, 1, DEFAULT_FEE, True)
myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients, 1, DEFAULT_FEE)
txid_3 = wait_and_assert_operationid_status(self.nodes[0], myopid)
self.sync_all()
unspent_tx = self.nodes[0].z_listunspent(0)

View File

@ -12,8 +12,10 @@ from test_framework.util import (
assert_raises_message,
connect_nodes_bi,
get_coinbase_address,
nuparams,
DEFAULT_FEE,
DEFAULT_FEE_ZATS
DEFAULT_FEE_ZATS,
NU5_BRANCH_ID,
)
from test_framework.util import wait_and_assert_operationid_status, start_nodes
from decimal import Decimal
@ -33,7 +35,12 @@ class ListReceivedTest (BitcoinTestFramework):
def setup_network(self):
self.nodes = start_nodes(
self.num_nodes, self.options.tmpdir,
extra_args=[['-experimentalfeatures', '-orchardwallet']] * self.num_nodes)
extra_args=[[
'-experimentalfeatures',
'-orchardwallet',
nuparams(NU5_BRANCH_ID, 225),
]] * self.num_nodes
)
connect_nodes_bi(self.nodes, 0, 1)
connect_nodes_bi(self.nodes, 1, 2)
connect_nodes_bi(self.nodes, 0, 2)
@ -381,9 +388,10 @@ class ListReceivedTest (BitcoinTestFramework):
r = node.z_getaddressforaccount(account)
unified_addr = r['unifiedaddress']
receivers = node.z_listunifiedreceivers(unified_addr)
assert_equal(len(receivers), 2)
assert_equal(len(receivers), 3)
assert 'transparent' in receivers
assert 'sapling' in receivers
assert 'orchard' in receivers
assert_raises_message(
JSONRPCException,
"The provided address is a bare receiver from a Unified Address in this wallet.",
@ -434,9 +442,117 @@ class ListReceivedTest (BitcoinTestFramework):
assert_equal(r[1]['blockindex'], -1) # not yet mined
assert 'blocktime' in r[1]
def test_received_orchard(self, height):
self.generate_and_sync(height+1)
taddr = self.nodes[1].getnewaddress()
acct1 = self.nodes[1].z_getnewaccount()['account']
acct2 = self.nodes[1].z_getnewaccount()['account']
addrResO = self.nodes[1].z_getaddressforaccount(acct1, ['orchard'])
assert_equal(addrResO['pools'], ['orchard'])
uao = addrResO['unifiedaddress']
addrResSO = self.nodes[1].z_getaddressforaccount(acct2, ['sapling', 'orchard'])
assert_equal(addrResSO['pools'], ['sapling', 'orchard'])
uaso = addrResSO['unifiedaddress']
self.nodes[0].sendtoaddress(taddr, 4.0)
self.generate_and_sync(height+2)
acct_node0 = self.nodes[0].z_getnewaccount()['account']
ua_node0 = self.nodes[0].z_getaddressforaccount(acct_node0, ['sapling', 'orchard'])['unifiedaddress']
opid = self.nodes[1].z_sendmany(taddr, [
{'address': uao, 'amount': 1, 'memo': my_memo},
{'address': uaso, 'amount': 2},
], 1, 0, 'AllowRevealedSenders')
txid0 = wait_and_assert_operationid_status(self.nodes[1], opid)
self.sync_all()
# Decrypted transaction details should be correct, even though
# the transaction is still just in the mempool
pt = self.nodes[1].z_viewtransaction(txid0)
assert_equal(pt['txid'], txid0)
assert_equal(len(pt['spends']), 0)
assert_equal(len(pt['outputs']), 2)
# Outputs are not returned in a defined order but the amounts are deterministic
outputs = sorted(pt['outputs'], key=lambda x: x['valueZat'])
assert_equal(outputs[0]['type'], 'orchard')
assert_equal(outputs[0]['address'], uao)
assert_equal(outputs[0]['value'], Decimal('1'))
assert_equal(outputs[0]['valueZat'], 100000000)
assert_equal(outputs[0]['action'], 0)
assert_equal(outputs[0]['outgoing'], False)
assert_equal(outputs[0]['memo'], my_memo)
assert_equal(outputs[0]['memoStr'], my_memo_str)
assert_equal(outputs[1]['type'], 'orchard')
assert_equal(outputs[1]['address'], uaso)
assert_equal(outputs[1]['value'], Decimal('2'))
assert_equal(outputs[1]['valueZat'], 200000000)
assert_equal(outputs[1]['action'], 1)
assert_equal(outputs[1]['outgoing'], False)
assert_equal(outputs[1]['memo'], no_memo)
assert 'memoStr' not in outputs[1]
self.generate_and_sync(height+3)
opid = self.nodes[1].z_sendmany(uao, [
{'address': uaso, 'amount': Decimal('0.3')},
{'address': ua_node0, 'amount': Decimal('0.2')}
])
txid1 = wait_and_assert_operationid_status(self.nodes[1], opid)
self.sync_all()
pt = self.nodes[1].z_viewtransaction(txid1)
assert_equal(pt['txid'], txid1)
assert_equal(len(pt['spends']), 1) # one spend we can see
assert_equal(len(pt['outputs']), 3) # one output + one change output we can see
spends = pt['spends']
assert_equal(spends[0]['type'], 'orchard')
assert_equal(spends[0]['action'], 0)
assert_equal(spends[0]['txidPrev'], txid0)
assert_equal(spends[0]['actionPrev'], 0)
assert_equal(spends[0]['address'], uao)
assert_equal(spends[0]['value'], Decimal('1.0'))
assert_equal(spends[0]['valueZat'], 100000000)
outputs = sorted(pt['outputs'], key=lambda x: x['valueZat'])
assert_equal(outputs[0]['type'], 'orchard')
assert_equal(outputs[0]['address'], ua_node0)
assert_equal(outputs[0]['value'], Decimal('0.2'))
assert_equal(outputs[0]['valueZat'], 20000000)
assert_equal(outputs[0]['outgoing'], True)
assert_equal(outputs[0]['walletInternal'], False)
assert_equal(outputs[0]['memo'], no_memo)
assert_equal(outputs[1]['type'], 'orchard')
assert_equal(outputs[1]['address'], uaso)
assert_equal(outputs[1]['value'], Decimal('0.3'))
assert_equal(outputs[1]['valueZat'], 30000000)
assert_equal(outputs[1]['outgoing'], False)
assert_equal(outputs[1]['walletInternal'], False)
assert_equal(outputs[1]['memo'], no_memo)
# Verify that we observe the change output
print("###", uao, "###")
assert_equal(outputs[2]['type'], 'orchard')
assert_equal(outputs[2]['value'], Decimal('0.49999'))
assert_equal(outputs[2]['valueZat'], 49999000)
assert_equal(outputs[2]['outgoing'], False)
assert_equal(outputs[2]['walletInternal'], True)
assert_equal(outputs[2]['memo'], no_memo)
# The change address should have been erased
assert_true('address' not in outputs[2])
def run_test(self):
self.test_received_sprout(200)
self.test_received_sapling(214)
self.test_received_orchard(230)
if __name__ == '__main__':

181
qa/rpc-tests/wallet_orchard.py Executable file
View File

@ -0,0 +1,181 @@
#!/usr/bin/env python3
# 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 .
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
NU5_BRANCH_ID,
assert_equal,
get_coinbase_address,
nuparams,
start_nodes,
wait_and_assert_operationid_status,
)
from decimal import Decimal
# Test wallet behaviour with the Orchard protocol
class WalletOrchardTest(BitcoinTestFramework):
def __init__(self):
super().__init__()
self.num_nodes = 4
def setup_nodes(self):
return start_nodes(self.num_nodes, self.options.tmpdir, [[
'-experimentalfeatures',
'-orchardwallet',
nuparams(NU5_BRANCH_ID, 210),
]] * self.num_nodes)
def run_test(self):
# Sanity-check the test harness
assert_equal(self.nodes[0].getblockcount(), 200)
# Get a new orchard-only unified address
acct1 = self.nodes[1].z_getnewaccount()['account']
addrRes1 = self.nodes[1].z_getaddressforaccount(acct1, ['orchard'])
assert_equal(acct1, addrRes1['account'])
assert_equal(addrRes1['pools'], ['orchard'])
ua1 = addrRes1['unifiedaddress']
# Verify that we have only an Orchard component
receiver_types = self.nodes[0].z_listunifiedreceivers(ua1)
assert_equal(set(['orchard']), set(receiver_types))
# Verify balance
assert_equal({'pools': {}, 'minimum_confirmations': 1}, self.nodes[1].z_getbalanceforaccount(acct1))
# Send some sapling funds to node 2 for later spending after we split the network
acct2 = self.nodes[2].z_getnewaccount()['account']
addrRes2 = self.nodes[2].z_getaddressforaccount(acct2, ['sapling', 'orchard'])
assert_equal(acct2, addrRes2['account'])
ua2 = addrRes2['unifiedaddress']
saplingAddr2 = self.nodes[2].z_listunifiedreceivers(ua2)['sapling']
recipients = [{"address": saplingAddr2, "amount": Decimal('10')}]
myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], myopid)
# Mine the tx & activate NU5
self.sync_all()
self.nodes[0].generate(10)
self.sync_all()
# Check the value sent to saplingAddr2 was received in node 2's account
assert_equal(
{'pools': {'sapling': {'valueZat': Decimal('1000000000')}}, 'minimum_confirmations': 1},
self.nodes[2].z_getbalanceforaccount(acct2))
# Node 0 shields some funds
# t-coinbase -> Orchard
recipients = [{"address": ua1, "amount": Decimal('10')}]
myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], myopid)
self.sync_all()
self.nodes[0].generate(1)
self.sync_all()
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('1000000000')}}, 'minimum_confirmations': 1},
self.nodes[1].z_getbalanceforaccount(acct1))
# Split the network
self.split_network()
# Send another tx to ua1
recipients = [{"address": ua1, "amount": Decimal('10')}]
myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], myopid)
# Mine the tx & generate a majority chain on the 0/1 side of the split
self.sync_all()
self.nodes[0].generate(10)
self.sync_all()
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('2000000000')}}, 'minimum_confirmations': 1},
self.nodes[1].z_getbalanceforaccount(acct1))
# On the other side of the split, send some funds to node 3
acct3 = self.nodes[3].z_getnewaccount()['account']
addrRes3 = self.nodes[3].z_getaddressforaccount(acct3, ['sapling', 'orchard'])
assert_equal(acct3, addrRes3['account'])
ua3 = addrRes3['unifiedaddress']
recipients = [{"address": ua3, "amount": Decimal('1')}]
myopid = self.nodes[2].z_sendmany(ua2, recipients, 1, 0)
rollback_tx = wait_and_assert_operationid_status(self.nodes[2], myopid)
self.sync_all()
self.nodes[2].generate(1)
self.sync_all()
# The remaining change from ua2's Sapling note has been sent to the
# account's internal Orchard change address.
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 1},
self.nodes[2].z_getbalanceforaccount(acct2))
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 1},
self.nodes[3].z_getbalanceforaccount(acct3))
# Check that the mempools are empty
for i in range(self.num_nodes):
assert_equal(set([]), set(self.nodes[i].getrawmempool()))
# Reconnect the nodes; nodes 2 and 3 will re-org to node 0's chain.
print("Re-joining the network so that nodes 2 and 3 reorg")
self.join_network()
# split 0/1's chain should have won, so their wallet balance should be consistent
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('2000000000')}}, 'minimum_confirmations': 1},
self.nodes[1].z_getbalanceforaccount(acct1))
# split 2/3's chain should have been rolled back, so their txn should have been
# un-mined and returned to the mempool
assert_equal(set([rollback_tx]), set(self.nodes[2].getrawmempool()))
# acct2's sole Orchard note is spent by a transaction in the mempool, so our
# confirmed balance is currently 0
assert_equal(
{'pools': {}, 'minimum_confirmations': 1},
self.nodes[2].z_getbalanceforaccount(acct2))
# acct2's incoming change (unconfirmed, still in the mempool) is 9 zec
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 0},
self.nodes[2].z_getbalanceforaccount(acct2, 0))
# The transaction was un-mined, so acct3 should have no confirmed balance
assert_equal(
{'pools': {}, 'minimum_confirmations': 1},
self.nodes[3].z_getbalanceforaccount(acct3))
# acct3's unconfirmed balance is 1 zec
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 0},
self.nodes[3].z_getbalanceforaccount(acct3, 0))
# Manually resend the transaction in node 2's mempool
self.nodes[2].resendwallettransactions()
# Sync the network
self.sync_all()
self.nodes[0].generate(1)
self.sync_all()
# The un-mined transaction should now have been re-mined
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 1},
self.nodes[2].z_getbalanceforaccount(acct2))
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 1},
self.nodes[3].z_getbalanceforaccount(acct3))
if __name__ == '__main__':
WalletOrchardTest().main()

View File

@ -23,7 +23,8 @@ class WalletShieldCoinbaseUANU5(WalletShieldCoinbaseTest):
assert('transparent' not in balances['pools'])
assert('sprout' not in balances['pools'])
# assert('sapling' not in balances['pools'])
assert_equal(balances['pools']['sapling']['valueZat'], expected * COIN)
sapling_balance = balances['pools']['sapling']['valueZat']
assert_equal(sapling_balance, expected * COIN)
# assert_equal(balances['pools']['orchard']['valueZat'], expected * COIN)
# While we're at it, check that z_listunspent only shows outputs with
@ -35,6 +36,8 @@ class WalletShieldCoinbaseUANU5(WalletShieldCoinbaseTest):
[{'type': x['type'], 'address': x['address']} for x in unspent],
)
total_balance = node.z_getbalance(self.addr) * COIN
assert_equal(total_balance, sapling_balance)
if __name__ == '__main__':
print("Test shielding to a unified address with NU5 activated")

View File

@ -21,7 +21,8 @@ class WalletShieldCoinbaseUASapling(WalletShieldCoinbaseTest):
balances = node.z_getbalanceforaccount(self.account)
assert('transparent' not in balances['pools'])
assert('sprout' not in balances['pools'])
assert_equal(balances['pools']['sapling']['valueZat'], expected * COIN)
sapling_balance = balances['pools']['sapling']['valueZat']
assert_equal(sapling_balance, expected * COIN)
assert('orchard' not in balances['pools'])
# While we're at it, check that z_listunspent only shows outputs with
@ -33,6 +34,8 @@ class WalletShieldCoinbaseUASapling(WalletShieldCoinbaseTest):
[{'type': x['type'], 'address': x['address']} for x in unspent],
)
total_balance = node.z_getbalance(self.addr) * COIN
assert_equal(total_balance, sapling_balance)
if __name__ == '__main__':
print("Test shielding to a unified address with sapling activated (but not NU5)")

View File

@ -4,8 +4,15 @@
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_greater_than, connect_nodes_bi, \
DEFAULT_FEE, start_nodes, wait_and_assert_operationid_status
from test_framework.util import (
assert_equal,
assert_greater_than,
assert_raises_message,
connect_nodes_bi,
DEFAULT_FEE,
start_nodes,
wait_and_assert_operationid_status,
)
from test_framework.authproxy import JSONRPCException
from test_framework.mininode import COIN
from decimal import Decimal
@ -27,14 +34,19 @@ class WalletZSendmanyTest(BitcoinTestFramework):
# Remember that empty pools are omitted from the output.
def _check_balance_for_rpc(self, rpcmethod, node, account, expected, minconf):
rpc = getattr(self.nodes[node], rpcmethod)
actual = rpc(account) if minconf is None else rpc(account, minconf)
actual = rpc(account, minconf)
assert_equal(set(expected), set(actual['pools']))
total_balance = 0
for pool in expected:
assert_equal(expected[pool] * COIN, actual['pools'][pool]['valueZat'])
assert_equal(actual['minimum_confirmations'], 1 if minconf is None else minconf)
total_balance += expected[pool]
assert_equal(actual['minimum_confirmations'], minconf)
return total_balance
def check_balance(self, node, account, address, expected, minconf=None):
self._check_balance_for_rpc('z_getbalanceforaccount', node, account, expected, minconf)
def check_balance(self, node, account, address, expected, minconf=1):
acct_balance = self._check_balance_for_rpc('z_getbalanceforaccount', node, account, expected, minconf)
z_getbalance = self.nodes[node].z_getbalance(address, minconf)
assert_equal(acct_balance, z_getbalance)
fvk = self.nodes[node].z_exportviewingkey(address)
self._check_balance_for_rpc('z_getbalanceforviewingkey', node, fvk, expected, minconf)
@ -159,9 +171,36 @@ class WalletZSendmanyTest(BitcoinTestFramework):
# Change went to a fresh address, so use `ANY_TADDR` which
# should hold the rest of our transparent funds.
source = 'ANY_TADDR'
recipients = []
recipients.append({"address":n0ua0, "amount":10})
opid = self.nodes[2].z_sendmany('ANY_TADDR', recipients, 1, 0)
# If we attempt to spend with the default privacy policy, z_sendmany
# fails because it needs to spend transparent coins in a transaction
# involving a Unified Address.
revealed_senders_msg = 'This transaction requires selecting transparent coins, which is not enabled by default because it will publicly reveal transaction senders and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedSenders` or weaker if you wish to allow this transaction to proceed anyway.'
opid = self.nodes[2].z_sendmany(source, recipients, 1, 0)
wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', revealed_senders_msg)
# We can't create a transaction with an unknown privacy policy.
assert_raises_message(
JSONRPCException,
'Unknown privacy policy name \'ZcashIsAwesome\'',
self.nodes[2].z_sendmany,
source, recipients, 1, 0, 'ZcashIsAwesome')
# If we set any policy that does not include AllowRevealedSenders,
# z_sendmany also fails.
for policy in [
'FullPrivacy',
'AllowRevealedAmounts',
'AllowRevealedRecipients',
]:
opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, policy)
wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', revealed_senders_msg)
# By setting the correct policy, we can create the transaction.
opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[2], opid)
self.nodes[2].generate(1)
@ -176,7 +215,32 @@ class WalletZSendmanyTest(BitcoinTestFramework):
# Send some funds to a specific legacy taddr that we can spend from
recipients = []
recipients.append({"address":mytaddr, "amount":5})
opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, 0)
# If we attempt to spend with the default privacy policy, z_sendmany
# returns an error because it needs to create a transparent recipient in
# a transaction involving a Unified Address.
assert_raises_message(
JSONRPCException,
'AllowRevealedRecipients',
self.nodes[0].z_sendmany,
n0ua0, recipients, 1, 0)
# If we set any policy that does not include AllowRevealedRecipients,
# z_sendmany also returns an error.
for policy in [
'FullPrivacy',
'AllowRevealedAmounts',
'AllowRevealedSenders',
'AllowLinkingAccountAddresses',
]:
assert_raises_message(
JSONRPCException,
'AllowRevealedRecipients',
self.nodes[0].z_sendmany,
n0ua0, recipients, 1, 0, policy)
# By setting the correct policy, we can create the transaction.
opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, 0, 'AllowRevealedRecipients')
wait_and_assert_operationid_status(self.nodes[0], opid)
self.nodes[0].generate(1)
@ -199,10 +263,12 @@ class WalletZSendmanyTest(BitcoinTestFramework):
self.check_balance(0, 0, n0ua0, {'sapling': 2})
assert_equal(Decimal(self.nodes[2].z_getbalance(myzaddr)), zbalance)
# Send funds back from the legacy taddr to the UA
# Send funds back from the legacy taddr to the UA. This requires
# AllowRevealedSenders, but we can also use any weaker policy that
# includes it.
recipients = []
recipients.append({"address":n0ua0, "amount":4})
opid = self.nodes[2].z_sendmany(mytaddr, recipients, 1, 0)
opid = self.nodes[2].z_sendmany(mytaddr, recipients, 1, 0, 'AllowFullyTransparent')
wait_and_assert_operationid_status(self.nodes[2], opid)
self.nodes[2].generate(1)
@ -225,5 +291,99 @@ class WalletZSendmanyTest(BitcoinTestFramework):
self.check_balance(0, 0, n0ua0, {'sapling': 8})
assert_equal(Decimal(self.nodes[2].z_getbalance(myzaddr)), zbalance)
#
# Test that z_sendmany avoids UA linkability unless we allow it.
#
# Generate a new account with two new addresses.
n1account = self.nodes[1].z_getnewaccount()['account']
n1ua0 = self.nodes[1].z_getaddressforaccount(n1account)['unifiedaddress']
n1ua1 = self.nodes[1].z_getaddressforaccount(n1account)['unifiedaddress']
# Send funds to the transparent receivers of both addresses.
for ua in [n1ua0, n1ua1]:
taddr = self.nodes[1].z_listunifiedreceivers(ua)['transparent']
self.nodes[0].sendtoaddress(taddr, 2)
self.sync_all()
self.nodes[2].generate(1)
self.sync_all()
# The account should see all funds.
assert_equal(
self.nodes[1].z_getbalanceforaccount(n1account)['pools'],
{'transparent': {'valueZat': 4 * COIN}},
)
# The addresses should see only the transparent funds sent to them.
assert_equal(self.nodes[1].z_getbalance(n1ua0), 2)
assert_equal(self.nodes[1].z_getbalance(n1ua1), 2)
# If we try to send 3 ZEC from n1ua0, it will fail with too-few funds.
recipients = [{"address":n0ua0, "amount":3}]
linked_addrs_msg = 'Insufficient funds: have 2.00, need 3.00 (This transaction may require selecting transparent coins that were sent to multiple Unified Addresses, which is not enabled by default because it would create a public link between the transparent receivers of these addresses. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowLinkingAccountAddresses` or weaker if you wish to allow this transaction to proceed anyway.)'
opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0)
wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', linked_addrs_msg)
# If we try it again with any policy that is too strong, it also fails.
for policy in [
'FullPrivacy',
'AllowRevealedAmounts',
'AllowRevealedRecipients',
'AllowRevealedSenders',
'AllowFullyTransparent',
]:
opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, policy)
wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', linked_addrs_msg)
# Once we provide a sufficiently-weak policy, the transaction succeeds.
opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, 'AllowLinkingAccountAddresses')
wait_and_assert_operationid_status(self.nodes[1], opid)
self.sync_all()
self.nodes[2].generate(1)
self.sync_all()
# The account should see the remaining funds, and they should have been
# sent to the Sapling change address (because NU5 is not active).
assert_equal(
self.nodes[1].z_getbalanceforaccount(n1account)['pools'],
{'sapling': {'valueZat': 1 * COIN}},
)
# The addresses should both show the same balance, as they both show the
# Sapling balance.
assert_equal(self.nodes[1].z_getbalance(n1ua0), 1)
assert_equal(self.nodes[1].z_getbalance(n1ua1), 1)
#
# Test NoPrivacy policy
#
# Send some legacy transparent funds to n1ua0, creating Sapling outputs.
recipients = [{"address":n1ua0, "amount":10}]
# This requires the AllowRevealedSenders policy...
opid = self.nodes[2].z_sendmany('ANY_TADDR', recipients, 1, 0)
wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', revealed_senders_msg)
# ... which we can always override with the NoPrivacy policy.
opid = self.nodes[2].z_sendmany('ANY_TADDR', recipients, 1, 0, 'NoPrivacy')
wait_and_assert_operationid_status(self.nodes[2], opid)
self.sync_all()
self.nodes[2].generate(1)
self.sync_all()
# Send some funds from node 1's account to a transparent address.
recipients = [{"address":mytaddr, "amount":5}]
# This requires the AllowRevealedRecipients policy...
assert_raises_message(
JSONRPCException,
'AllowRevealedRecipients',
self.nodes[1].z_sendmany,
n1ua0, recipients, 1, 0)
# ... which we can always override with the NoPrivacy policy.
opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, 'NoPrivacy')
wait_and_assert_operationid_status(self.nodes[1], opid)
if __name__ == '__main__':
WalletZSendmanyTest().main()

View File

@ -346,6 +346,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

@ -59,6 +59,8 @@ zcash_gtest_SOURCES += \
gtest/test_zip32.cpp
if ENABLE_WALLET
zcash_gtest_SOURCES += \
wallet/gtest/test_note_selection.cpp \
wallet/gtest/test_orchard_wallet.cpp \
wallet/gtest/test_paymentdisclosure.cpp \
wallet/gtest/test_wallet.cpp
endif

View File

@ -12,6 +12,15 @@
#include "util/match.h"
namespace Consensus {
std::optional<int> Params::GetActivationHeight(Consensus::UpgradeIndex idx) const {
auto nActivationHeight = vUpgrades[idx].nActivationHeight;
if (nActivationHeight == Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT) {
return std::nullopt;
} else {
return nActivationHeight;
}
}
bool Params::NetworkUpgradeActive(int nHeight, Consensus::UpgradeIndex idx) const {
return NetworkUpgradeState(nHeight, *this, idx) == UPGRADE_ACTIVE;
}

View File

@ -225,6 +225,11 @@ static const unsigned int PRE_BLOSSOM_REGTEST_HALVING_INTERVAL = 144;
* Parameters that influence chain consensus.
*/
struct Params {
/**
* Returns the activation height for the specified network upgrade, if any.
*/
std::optional<int> GetActivationHeight(Consensus::UpgradeIndex idx) const;
/**
* Returns true if the given network upgrade is active as of the given block
* height. Caller must check that the height is >= 0 (and handle unknown

View File

@ -11,6 +11,8 @@
#include "transaction_builder.h"
#include "utiltest.h"
#include <optional>
TEST(RecursiveDynamicUsageTests, TestTransactionTransparent)
{
auto consensusParams = RegtestActivateSapling();
@ -20,7 +22,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionTransparent)
auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID());
CTxDestination taddr = tsk.GetPubKey().GetID();
auto builder = TransactionBuilder(consensusParams, 1, &keystore);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore);
builder.AddTransparentInput(COutPoint(), scriptPubKey, 50000);
builder.AddTransparentOutput(taddr, 40000);
@ -55,7 +57,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionSaplingToSapling)
auto sk = libzcash::SaplingSpendingKey::random();
auto testNote = GetTestSaplingNote(sk.default_address(), 50000);
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(sk.expanded_spending_key(), testNote.note, testNote.tree.root(), testNote.tree.witness());
builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 5000, {});
@ -78,7 +80,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionTransparentToSapling)
auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID());
auto sk = libzcash::SaplingSpendingKey::random();
auto builder = TransactionBuilder(consensusParams, 1, &keystore);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore);
builder.AddTransparentInput(COutPoint(), scriptPubKey, 50000);
builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 40000, {});
@ -102,7 +104,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionSaplingToTransparent)
auto sk = libzcash::SaplingSpendingKey::random();
auto testNote = GetTestSaplingNote(sk.default_address(), 50000);
auto builder = TransactionBuilder(consensusParams, 1, &keystore);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore);
builder.AddSaplingSpend(sk.expanded_spending_key(), testNote.note, testNote.tree.root(), testNote.tree.witness());
builder.AddTransparentOutput(taddr, 40000);

View File

@ -69,34 +69,8 @@ TEST(Keys, EncodeAndDecodeSapling)
#define MAKE_STRING(x) std::string((x), (x) + sizeof(x))
namespace libzcash {
class ReceiverToString {
public:
ReceiverToString() {}
std::string operator()(const SaplingPaymentAddress &zaddr) const {
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << zaddr;
return tfm::format("Sapling(%s)", HexStr(ss.begin(), ss.end()));
}
std::string operator()(const CScriptID &p2sh) const {
return tfm::format("P2SH(%s)", p2sh.GetHex());
}
std::string operator()(const CKeyID &p2pkh) const {
return tfm::format("P2PKH(%s)", p2pkh.GetHex());
}
std::string operator()(const UnknownReceiver &unknown) const {
return tfm::format(
"Unknown(%x, %s)",
unknown.typecode,
HexStr(unknown.data.begin(), unknown.data.end()));
}
};
void PrintTo(const Receiver& receiver, std::ostream* os) {
*os << std::visit(ReceiverToString(), receiver);
*os << DebugPrintReceiver(receiver);
}
void PrintTo(const UnifiedAddress& ua, std::ostream* os) {
*os << "UnifiedAddress(" << testing::PrintToString(ua.GetReceiversAsParsed()) << ")";
@ -126,8 +100,11 @@ TEST(Keys, EncodeAndDecodeUnifiedAddresses)
// These were added to the UA in preference order by the Python test vectors.
if (!test[3].isNull()) {
auto data = ParseHex(test[3].get_str());
libzcash::UnknownReceiver r(0x03, data);
ua.AddReceiver(r);
CDataStream ss(
data,
SER_NETWORK,
PROTOCOL_VERSION);
ua.AddReceiver(libzcash::OrchardRawAddress::Read(ss));
}
if (!test[2].isNull()) {
auto data = ParseHex(test[2].get_str());
@ -186,6 +163,16 @@ TEST(Keys, DeriveUnifiedFullViewingKeys)
if (test.size() == 1) continue; // comment
try {
// [
// t_key_bytes,
// sapling_fvk_bytes,
// orchard_fvk_bytes,
// unknown_fvk_typecode,
// unknown_fvk_bytes,
// unified_fvk,
// root_seed,
// account,
// ],
auto seed_hex = test[6].get_str();
auto seed_data = ParseHex(seed_hex);
RawHDSeed raw_seed(seed_data.begin(), seed_data.end());
@ -235,6 +222,24 @@ TEST(Keys, DeriveUnifiedFullViewingKeys)
auto key = libzcash::SaplingDiversifiableFullViewingKey::Read(ss);
EXPECT_EQ(key, saplingKey);
}
if (!test[2].isNull()) {
auto expectedHex = test[2].get_str();
// Ensure that the serialized Orchard fvk matches the test data.
auto orchardKey = ufvk.GetOrchardKey().value();
CDataStream ssEncode(SER_NETWORK, PROTOCOL_VERSION);
ssEncode << orchardKey;
EXPECT_EQ(ssEncode.size(), 96);
auto skeyHex = HexStr(ssEncode.begin(), ssEncode.end());
EXPECT_EQ(expectedHex, skeyHex);
// Ensure that parsing the test data derives the correct dfvk
auto data = ParseHex(expectedHex);
ASSERT_EQ(data.size(), 96);
CDataStream ss(data, SER_NETWORK, PROTOCOL_VERSION);
auto key = libzcash::OrchardFullViewingKey::Read(ss);
EXPECT_EQ(key, orchardKey);
}
// Enable the following after Orchard keys are supported.
//{
// auto fvk_data = ParseHex(test[5].get_str());
@ -284,14 +289,13 @@ TEST(Keys, EncodeAndDecodeUnifiedFullViewingKeys)
auto key = libzcash::SaplingDiversifiableFullViewingKey::Read(ss);
ASSERT_TRUE(builder.AddSaplingKey(key));
}
// Orchard keys and unknown items are not yet supported; instead,
// we just test that we're able to parse the unified key string
// and that the constituent items match the elements; if no Sapling
// key is present then UFVK construction would fail because it might
// presume the UFVK to be transparent-only.
if (test[1].isNull())
continue;
if (!test[2].isNull()) {
auto data = ParseHex(test[2].get_str());
ASSERT_EQ(data.size(), 96);
CDataStream ss(data, SER_NETWORK, PROTOCOL_VERSION);
auto key = libzcash::OrchardFullViewingKey::Read(ss);
ASSERT_TRUE(builder.AddOrchardKey(key));
}
auto built = builder.build();
ASSERT_TRUE(built.has_value());
@ -304,5 +308,6 @@ TEST(Keys, EncodeAndDecodeUnifiedFullViewingKeys)
EXPECT_EQ(decoded.value().GetTransparentKey(), built.value().GetTransparentKey());
EXPECT_EQ(decoded.value().GetSaplingKey(), built.value().GetSaplingKey());
EXPECT_EQ(decoded.value().GetOrchardKey(), built.value().GetOrchardKey());
}
}

View File

@ -557,20 +557,54 @@ TEST(KeystoreTests, StoreAndRetrieveUFVK) {
EXPECT_EQ(keyStore.GetUnifiedFullViewingKey(ufvkid).value(), zufvk);
auto addrPair = std::get<std::pair<UnifiedAddress, diversifier_index_t>>(zufvk.FindAddress(diversifier_index_t(0), {ReceiverType::Sapling}));
EXPECT_TRUE(addrPair.first.GetSaplingReceiver().has_value());
auto saplingReceiver = addrPair.first.GetSaplingReceiver().value();
auto ufvkmeta = keyStore.GetUFVKMetadataForReceiver(saplingReceiver);
EXPECT_FALSE(ufvkmeta.has_value());
// We detect this even though we haven't added the Sapling address, because
// we trial-decrypt diversifiers (which also means we learn the index).
auto ufvkmetaUnadded = keyStore.GetUFVKMetadataForReceiver(saplingReceiver);
EXPECT_TRUE(ufvkmetaUnadded.has_value());
EXPECT_EQ(ufvkmetaUnadded.value().GetUFVKId(), ufvkid);
EXPECT_EQ(ufvkmetaUnadded.value().GetDiversifierIndex().value(), addrPair.second);
// Adding the Sapling addr -> ivk map entry causes us to find the same UFVK,
// but as we're no longer trial-decrypting we don't learn the index.
auto saplingIvk = zufvk.GetSaplingKey().value().ToIncomingViewingKey();
keyStore.AddSaplingPaymentAddress(saplingIvk, saplingReceiver);
ufvkmeta = keyStore.GetUFVKMetadataForReceiver(saplingReceiver);
auto ufvkmeta = keyStore.GetUFVKMetadataForReceiver(saplingReceiver);
EXPECT_TRUE(ufvkmeta.has_value());
EXPECT_EQ(ufvkmeta.value().first, ufvkid);
EXPECT_FALSE(ufvkmeta.value().second.has_value());
EXPECT_EQ(ufvkmeta.value().GetUFVKId(), ufvkid);
EXPECT_FALSE(ufvkmeta.value().GetDiversifierIndex().has_value());
}
TEST(KeystoreTests, StoreAndRetrieveUFVKByOrchard) {
SelectParams(CBaseChainParams::TESTNET);
CBasicKeyStore keyStore;
auto seed = MnemonicSeed::Random(SLIP44_TESTNET_TYPE);
auto usk = ZcashdUnifiedSpendingKey::ForAccount(seed, SLIP44_TESTNET_TYPE, 0);
EXPECT_TRUE(usk.has_value());
auto ufvk = usk.value().ToFullViewingKey();
auto zufvk = ZcashdUnifiedFullViewingKey::FromUnifiedFullViewingKey(Params(), ufvk);
auto ufvkid = zufvk.GetKeyID();
EXPECT_FALSE(keyStore.GetUnifiedFullViewingKey(ufvkid).has_value());
EXPECT_TRUE(keyStore.AddUnifiedFullViewingKey(zufvk));
EXPECT_EQ(keyStore.GetUnifiedFullViewingKey(ufvkid).value(), zufvk);
auto addrPair = std::get<std::pair<UnifiedAddress, diversifier_index_t>>(zufvk.FindAddress(diversifier_index_t(0), {ReceiverType::Orchard}));
EXPECT_TRUE(addrPair.first.GetOrchardReceiver().has_value());
auto orchardReceiver = addrPair.first.GetOrchardReceiver().value();
// We don't store Orchard addresses in CBasicKeyStore (the addr -> ivk
// mapping is stored in the Rust wallet), but we still detect this because
// we trial-decrypt diversifiers (which also means we learn the index).
auto ufvkmetaUnadded = keyStore.GetUFVKMetadataForReceiver(orchardReceiver);
EXPECT_TRUE(ufvkmetaUnadded.has_value());
EXPECT_EQ(ufvkmetaUnadded.value().GetUFVKId(), ufvkid);
EXPECT_EQ(ufvkmetaUnadded.value().GetDiversifierIndex().value(), addrPair.second);
}
TEST(KeystoreTests, AddTransparentReceiverForUnifiedAddress) {
@ -593,7 +627,7 @@ TEST(KeystoreTests, AddTransparentReceiverForUnifiedAddress) {
ufvkmeta = keyStore.GetUFVKMetadataForReceiver(addrPair.first.GetP2PKHReceiver().value());
EXPECT_TRUE(ufvkmeta.has_value());
EXPECT_EQ(ufvkmeta.value().first, ufvkid);
EXPECT_EQ(ufvkmeta.value().GetUFVKId(), ufvkid);
}

View File

@ -121,7 +121,7 @@ TEST(MempoolLimitTests, WeightedTxInfoFromTx)
// Default fee
{
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(sk.expanded_spending_key(), testNote.note, testNote.tree.root(), testNote.tree.witness());
builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 25000, {});
@ -132,7 +132,7 @@ TEST(MempoolLimitTests, WeightedTxInfoFromTx)
// Lower than standard fee
{
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(sk.expanded_spending_key(), testNote.note, testNote.tree.root(), testNote.tree.witness());
builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 25000, {});
static_assert(DEFAULT_FEE == 1000);
@ -145,7 +145,7 @@ TEST(MempoolLimitTests, WeightedTxInfoFromTx)
// Larger Tx
{
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(sk.expanded_spending_key(), testNote.note, testNote.tree.root(), testNote.tree.witness());
builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 5000, {});
builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 5000, {});

View File

@ -7,71 +7,16 @@
#include "pubkey.h"
#include "rpc/protocol.h"
#include "transaction_builder.h"
#include "gtest/test_transaction_builder.h"
#include "utiltest.h"
#include "zcash/Address.hpp"
#include "zcash/address/mnemonic.h"
#include <optional>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
// Fake an empty view
class TransactionBuilderCoinsViewDB : public CCoinsView {
public:
std::map<uint256, SproutMerkleTree> sproutTrees;
TransactionBuilderCoinsViewDB() {}
bool GetSproutAnchorAt(const uint256 &rt, SproutMerkleTree &tree) const {
auto it = sproutTrees.find(rt);
if (it != sproutTrees.end()) {
tree = it->second;
return true;
} else {
return false;
}
}
bool GetSaplingAnchorAt(const uint256 &rt, SaplingMerkleTree &tree) const {
return false;
}
bool GetNullifier(const uint256 &nf, ShieldedType type) const {
return false;
}
bool GetCoins(const uint256 &txid, CCoins &coins) const {
return false;
}
bool HaveCoins(const uint256 &txid) const {
return false;
}
uint256 GetBestBlock() const {
uint256 a;
return a;
}
uint256 GetBestAnchor(ShieldedType type) const {
uint256 a;
return a;
}
bool BatchWrite(CCoinsMap &mapCoins,
const uint256 &hashBlock,
const uint256 &hashSproutAnchor,
const uint256 &hashSaplingAnchor,
CAnchorsSproutMap &mapSproutAnchors,
CAnchorsSaplingMap &mapSaplingAnchors,
CNullifiersMap &mapSproutNullifiers,
CNullifiersMap saplingNullifiersMap) {
return false;
}
bool GetStats(CCoinsStats &stats) const {
return false;
}
};
TEST(TransactionBuilder, TransparentToSapling)
{
LoadProofParameters();
@ -94,7 +39,7 @@ TEST(TransactionBuilder, TransparentToSapling)
// Create a shielding transaction from transparent to Sapling
// 0.0005 t-ZEC in, 0.0004 z-ZEC out, default fee
auto builder = TransactionBuilder(consensusParams, 1, &keystore);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore);
builder.AddTransparentInput(COutPoint(uint256S("1234"), 0), scriptPubKey, 50000);
builder.AddSaplingOutput(fvk_from.ovk, pk, 40000, {});
auto tx = builder.Build().GetTxOrThrow();
@ -128,7 +73,7 @@ TEST(TransactionBuilder, SaplingToSapling) {
// Create a Sapling-only transaction
// 0.0004 z-ZEC in, 0.00025 z-ZEC out, default fee, 0.00005 z-ZEC change
auto builder = TransactionBuilder(consensusParams, 2);
auto builder = TransactionBuilder(consensusParams, 2, std::nullopt);
builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness());
// Check that trying to add a different anchor fails
@ -171,7 +116,7 @@ TEST(TransactionBuilder, SaplingToSprout) {
// - 0.0004 Sapling-ZEC in - 0.00025 Sprout-ZEC out
// - 0.00005 Sapling-ZEC change
// - default t-ZEC fee
auto builder = TransactionBuilder(consensusParams, 2, nullptr);
auto builder = TransactionBuilder(consensusParams, 2, std::nullopt, nullptr);
builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness());
builder.AddSproutOutput(sproutAddr, 25000);
auto tx = builder.Build().GetTxOrThrow();
@ -225,7 +170,7 @@ TEST(TransactionBuilder, SproutToSproutAndSapling) {
// - 0.00005 Sprout-ZEC change
// - 0.00005 Sapling-ZEC out
// - 0.00005 t-ZEC fee
auto builder = TransactionBuilder(consensusParams, 2, nullptr, &view);
auto builder = TransactionBuilder(consensusParams, 2, std::nullopt, nullptr, &view);
builder.SetFee(5000);
builder.AddSproutInput(sproutSk, sproutNote, sproutWitness);
builder.AddSproutOutput(sproutAddr, 6000);
@ -256,12 +201,60 @@ TEST(TransactionBuilder, SproutToSproutAndSapling) {
RegtestDeactivateSapling();
}
TEST(TransactionBuilder, TransparentToOrchard)
{
auto consensusParams = RegtestActivateNU5();
CBasicKeyStore keystore;
CKey tsk = AddTestCKeyToKeyStore(keystore);
auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID());
auto coinType = Params().BIP44CoinType();
auto seed = MnemonicSeed::Random(coinType);
auto sk = libzcash::OrchardSpendingKey::ForAccount(seed, coinType, 0);
auto fvk = sk.ToFullViewingKey();
auto ivk = fvk.ToIncomingViewingKey();
libzcash::diversifier_index_t j(0);
auto recipient = ivk.Address(j);
TransactionBuilderCoinsViewDB fakeDB;
auto orchardAnchor = fakeDB.GetBestAnchor(ShieldedType::ORCHARD);
// Create a shielding transaction from transparent to Orchard
// 0.0005 t-ZEC in, 0.0004 z-ZEC out, default fee
auto builder = TransactionBuilder(consensusParams, 1, orchardAnchor, &keystore);
builder.AddTransparentInput(COutPoint(uint256S("1234"), 0), scriptPubKey, 50000);
builder.AddOrchardOutput(std::nullopt, recipient, 40000, 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(), 1);
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(), -40000);
CValidationState state;
EXPECT_TRUE(ContextualCheckTransaction(tx, state, Params(), 2, true));
EXPECT_EQ(state.GetRejectReason(), "");
// Revert to default
RegtestDeactivateNU5();
}
TEST(TransactionBuilder, ThrowsOnTransparentInputWithoutKeyStore)
{
SelectParams(CBaseChainParams::REGTEST);
const Consensus::Params& consensusParams = Params().GetConsensus();
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
ASSERT_THROW(builder.AddTransparentInput(COutPoint(), CScript(), 1), std::runtime_error);
}
@ -272,7 +265,7 @@ TEST(TransactionBuilder, RejectsInvalidTransparentOutput)
// Default CTxDestination type is an invalid address
CTxDestination taddr;
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
ASSERT_THROW(builder.AddTransparentOutput(taddr, 50), UniValue);
}
@ -299,13 +292,13 @@ TEST(TransactionBuilder, FailsWithNegativeChange)
// Fail if there is only a Sapling output
// 0.0005 z-ZEC out, default fee
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingOutput(fvk.ovk, pa, 50000, {});
EXPECT_EQ("Change cannot be negative", builder.Build().GetError());
// Fail if there is only a transparent output
// 0.0005 t-ZEC out, default fee
builder = TransactionBuilder(consensusParams, 1, &keystore);
builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore);
builder.AddTransparentOutput(taddr, 50000);
EXPECT_EQ("Change cannot be negative", builder.Build().GetError());
@ -348,14 +341,14 @@ TEST(TransactionBuilder, ChangeOutput)
// No change address and no Sapling spends
{
auto builder = TransactionBuilder(consensusParams, 1, &keystore);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore);
builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000);
EXPECT_EQ("Could not determine change address", builder.Build().GetError());
}
// Change to the same address as the first Sapling spend
{
auto builder = TransactionBuilder(consensusParams, 1, &keystore);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore);
builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000);
builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness());
auto tx = builder.Build().GetTxOrThrow();
@ -370,7 +363,7 @@ TEST(TransactionBuilder, ChangeOutput)
// Change to a Sapling address
{
auto builder = TransactionBuilder(consensusParams, 1, &keystore);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore);
builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000);
builder.SendChangeTo(zChangeAddr, fvkOut.ovk);
auto tx = builder.Build().GetTxOrThrow();
@ -385,7 +378,7 @@ TEST(TransactionBuilder, ChangeOutput)
// Change to a transparent address
{
auto builder = TransactionBuilder(consensusParams, 1, &keystore);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore);
builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000);
builder.SendChangeTo(tkeyid, {});
auto tx = builder.Build().GetTxOrThrow();
@ -419,7 +412,7 @@ TEST(TransactionBuilder, SetFee)
// Default fee
{
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness());
builder.AddSaplingOutput(fvk.ovk, pa, 25000, {});
auto tx = builder.Build().GetTxOrThrow();
@ -434,7 +427,7 @@ TEST(TransactionBuilder, SetFee)
// Configured fee
{
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness());
builder.AddSaplingOutput(fvk.ovk, pa, 25000, {});
builder.SetFee(20000);
@ -463,7 +456,7 @@ TEST(TransactionBuilder, CheckSaplingTxVersion)
auto pk = sk.default_address();
// Cannot add Sapling outputs to a non-Sapling transaction
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
try {
builder.AddSaplingOutput(uint256(), pk, 12345, {});
} catch (std::runtime_error const & err) {

View File

@ -0,0 +1,73 @@
// 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 .
#ifndef ZCASH_GTEST_TEST_TRANSACTION_BUILDER_H
#define ZCASH_GTEST_TEST_TRANSACTION_BUILDER_H
#include "coins.h"
// Fake an empty view
class TransactionBuilderCoinsViewDB : public CCoinsView {
public:
std::map<uint256, SproutMerkleTree> sproutTrees;
TransactionBuilderCoinsViewDB() {}
bool GetSproutAnchorAt(const uint256 &rt, SproutMerkleTree &tree) const {
auto it = sproutTrees.find(rt);
if (it != sproutTrees.end()) {
tree = it->second;
return true;
} else {
return false;
}
}
bool GetSaplingAnchorAt(const uint256 &rt, SaplingMerkleTree &tree) const {
return false;
}
bool GetOrchardAnchorAt(const uint256 &rt, OrchardMerkleFrontier &tree) const {
return false;
}
bool GetNullifier(const uint256 &nf, ShieldedType type) const {
return false;
}
bool GetCoins(const uint256 &txid, CCoins &coins) const {
return false;
}
bool HaveCoins(const uint256 &txid) const {
return false;
}
uint256 GetBestBlock() const {
uint256 a;
return a;
}
uint256 GetBestAnchor(ShieldedType type) const {
uint256 a;
return a;
}
bool BatchWrite(CCoinsMap &mapCoins,
const uint256 &hashBlock,
const uint256 &hashSproutAnchor,
const uint256 &hashSaplingAnchor,
CAnchorsSproutMap &mapSproutAnchors,
CAnchorsSaplingMap &mapSaplingAnchors,
CNullifiersMap &mapSproutNullifiers,
CNullifiersMap saplingNullifiersMap) {
return false;
}
bool GetStats(CCoinsStats &stats) const {
return false;
}
};
#endif

View File

@ -172,7 +172,7 @@ TEST(Validation, ContextualCheckInputsDetectsOldBranchId) {
// Create a transparent transaction that spends the coin, targeting
// a height during the Overwinter epoch.
auto builder = TransactionBuilder(consensusParams, 15, &keystore);
auto builder = TransactionBuilder(consensusParams, 15, std::nullopt, &keystore);
builder.AddTransparentInput(utxo, scriptPubKey, coinValue);
builder.AddTransparentOutput(destination, 40000);
auto tx = builder.Build().GetTxOrThrow();

View File

@ -1105,7 +1105,7 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
#ifdef ENABLE_MINING
if (mapArgs.count("-mineraddress")) {
auto addr = keyIO.DecodePaymentAddress(mapArgs["-mineraddress"]);
if (!(addr.has_value() && std::visit(ExtractMinerAddress(), addr.value()).has_value())) {
if (!(addr.has_value() && std::visit(ExtractMinerAddress(chainparams.GetConsensus(), 0), addr.value()).has_value())) {
return InitError(strprintf(
_("Invalid address for -mineraddress=<addr>: '%s' (must be a Sapling or transparent P2PKH address)"),
mapArgs["-mineraddress"]));
@ -1683,7 +1683,7 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
if (!zaddr.has_value()) {
return InitError(_("-mineraddress is not a valid " PACKAGE_NAME " address."));
}
auto ztxoSelector = pwalletMain->ZTXOSelectorForAddress(zaddr.value(), true);
auto ztxoSelector = pwalletMain->ZTXOSelectorForAddress(zaddr.value(), true, false);
minerAddressInLocalWallet = ztxoSelector.has_value();
}
if (GetBoolArg("-minetolocalwallet", true) && !minerAddressInLocalWallet) {

View File

@ -56,6 +56,7 @@ class DataLenForReceiver {
public:
DataLenForReceiver() {}
size_t operator()(const libzcash::OrchardRawAddress &zaddr) const { return 43; }
size_t operator()(const libzcash::SaplingPaymentAddress &zaddr) const { return 43; }
size_t operator()(const CScriptID &p2sh) const { return 20; }
size_t operator()(const CKeyID &p2pkh) const { return 20; }
@ -76,6 +77,13 @@ class CopyDataForReceiver {
public:
CopyDataForReceiver(unsigned char* data, size_t length) : data(data), length(length) {}
void operator()(const libzcash::OrchardRawAddress &zaddr) const {
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << zaddr;
assert(length == ss.size());
memcpy(data, ss.data(), ss.size());
}
void operator()(const libzcash::SaplingPaymentAddress &zaddr) const {
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << zaddr;

View File

@ -252,6 +252,10 @@ bool CBasicKeyStore::GetSproutViewingKey(
return false;
}
//
// Sapling Keys
//
bool CBasicKeyStore::GetSaplingFullViewingKey(
const libzcash::SaplingIncomingViewingKey &ivk,
libzcash::SaplingExtendedFullViewingKey &extfvkOut) const
@ -308,14 +312,26 @@ bool CBasicKeyStore::AddUnifiedFullViewingKey(
{
LOCK(cs_KeyStore);
auto ufvkId = ufvk.GetKeyID();
// Add the Orchard component of the UFVK to the wallet.
auto orchardKey = ufvk.GetOrchardKey();
if (orchardKey.has_value()) {
auto ivk = orchardKey.value().ToIncomingViewingKey();
mapOrchardKeyUnified.insert(std::make_pair(ivk, ufvkId));
auto ivkInternal = orchardKey.value().ToInternalIncomingViewingKey();
mapOrchardKeyUnified.insert(std::make_pair(ivkInternal, ufvkId));
}
// Add the Sapling component of the UFVK to the wallet.
auto saplingKey = ufvk.GetSaplingKey();
if (saplingKey.has_value()) {
auto ivk = saplingKey.value().ToIncomingViewingKey();
mapSaplingKeyUnified.insert(std::make_pair(ivk, ufvk.GetKeyID()));
mapSaplingKeyUnified.insert(std::make_pair(ivk, ufvkId));
auto changeIvk = saplingKey.value().GetChangeIVK();
mapSaplingKeyUnified.insert(std::make_pair(changeIvk, ufvk.GetKeyID()));
mapSaplingKeyUnified.insert(std::make_pair(changeIvk, ufvkId));
}
// We can't reasonably add the transparent component here, because
@ -325,7 +341,7 @@ bool CBasicKeyStore::AddUnifiedFullViewingKey(
// transparent part of the address must be added to the keystore.
// Add the UFVK by key identifier.
mapUnifiedFullViewingKeys.insert({ufvk.GetKeyID(), ufvk});
mapUnifiedFullViewingKeys.insert({ufvkId, ufvk});
return true;
}
@ -367,12 +383,56 @@ std::optional<libzcash::ZcashdUnifiedFullViewingKey> CBasicKeyStore::GetUnifiedF
}
}
std::optional<std::pair<libzcash::UFVKId, std::optional<libzcash::diversifier_index_t>>>
std::optional<AddressUFVKMetadata>
CBasicKeyStore::GetUFVKMetadataForReceiver(const libzcash::Receiver& receiver) const
{
return std::visit(FindUFVKId(*this), receiver);
}
std::optional<AddressUFVKMetadata>
CBasicKeyStore::GetUFVKMetadataForAddress(const libzcash::UnifiedAddress& addr) const
{
std::optional<libzcash::UFVKId> ufvkId;
std::optional<libzcash::diversifier_index_t> j;
bool jConflict = false;
for (const auto& receiver : addr) {
// skip unknown receivers
if (libzcash::HasKnownReceiverType(receiver)) {
auto rmeta = GetUFVKMetadataForReceiver(receiver);
// We should never generate unified addresses with internal receivers
assert(!(rmeta.has_value() && rmeta.value().IsInternalAddress()));
if (ufvkId.has_value() && rmeta.has_value()) {
// If the unified address contains receivers that are associated with
// different UFVKs, we cannot return a singular value.
if (rmeta.value().GetUFVKId() != ufvkId.value()) {
return std::nullopt;
}
if (rmeta.value().GetDiversifierIndex().has_value()) {
if (j.has_value()) {
if (rmeta.value().GetDiversifierIndex().value() != j.value()) {
jConflict = true;
j = std::nullopt;
}
} else if (!jConflict) {
j = rmeta.value().GetDiversifierIndex().value();
}
}
} else if (rmeta.has_value()) {
ufvkId = rmeta.value().GetUFVKId();
j = rmeta.value().GetDiversifierIndex();
}
}
}
if (ufvkId.has_value()) {
return AddressUFVKMetadata(ufvkId.value(), j, false);
} else {
return std::nullopt;
}
}
std::optional<libzcash::UFVKId> CBasicKeyStore::GetUFVKIdForViewingKey(const libzcash::ViewingKey& vk) const
{
std::optional<libzcash::UFVKId> result;
@ -386,6 +446,15 @@ std::optional<libzcash::UFVKId> CBasicKeyStore::GetUFVKIdForViewingKey(const lib
}
},
[&](const libzcash::UnifiedFullViewingKey& ufvk) {
const auto orchardFvk = ufvk.GetOrchardKey();
if (orchardFvk.has_value()) {
const auto orchardIvk = orchardFvk.value().ToIncomingViewingKey();
const auto ufvkId = mapOrchardKeyUnified.find(orchardIvk);
if (ufvkId != mapOrchardKeyUnified.end()) {
result = ufvkId->second;
return;
}
}
const auto saplingDfvk = ufvk.GetSaplingKey();
if (saplingDfvk.has_value()) {
const auto saplingIvk = saplingDfvk.value().ToIncomingViewingKey();
@ -399,39 +468,86 @@ std::optional<libzcash::UFVKId> CBasicKeyStore::GetUFVKIdForViewingKey(const lib
return result;
}
std::optional<std::pair<libzcash::UFVKId, std::optional<libzcash::diversifier_index_t>>>
FindUFVKId::operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const {
const auto saplingIvk = keystore.mapSaplingIncomingViewingKeys.find(saplingAddr);
if (saplingIvk != keystore.mapSaplingIncomingViewingKeys.end()) {
const auto ufvkId = keystore.mapSaplingKeyUnified.find(saplingIvk->second);
if (ufvkId != keystore.mapSaplingKeyUnified.end()) {
return std::make_pair(ufvkId->second, std::nullopt);
} else {
return std::nullopt;
//
// FindUFVKId :: (KeyStore, Receiver) -> std::optional<AddressUFVKMetadata>
//
std::optional<AddressUFVKMetadata> FindUFVKId::operator()(const libzcash::OrchardRawAddress& orchardAddr) const {
for (const auto& [k, v] : keystore.mapUnifiedFullViewingKeys) {
auto fvk = v.GetOrchardKey();
if (fvk.has_value()) {
auto d_idx = fvk.value().ToIncomingViewingKey().DecryptDiversifier(orchardAddr);
if (d_idx.has_value()) {
return AddressUFVKMetadata(k, d_idx, false);
}
auto internal_d_idx = fvk.value().ToInternalIncomingViewingKey().DecryptDiversifier(orchardAddr);
if (internal_d_idx.has_value()) {
return AddressUFVKMetadata(k, internal_d_idx, true);
}
}
} else {
return std::nullopt;
}
}
std::optional<std::pair<libzcash::UFVKId, std::optional<libzcash::diversifier_index_t>>>
FindUFVKId::operator()(const CScriptID& scriptId) const {
const auto metadata = keystore.mapP2SHUnified.find(scriptId);
if (metadata != keystore.mapP2SHUnified.end()) {
return metadata->second;
} else {
return std::nullopt;
}
}
std::optional<std::pair<libzcash::UFVKId, std::optional<libzcash::diversifier_index_t>>>
FindUFVKId::operator()(const CKeyID& keyId) const {
const auto metadata = keystore.mapP2PKHUnified.find(keyId);
if (metadata != keystore.mapP2PKHUnified.end()) {
return metadata->second;
} else {
return std::nullopt;
}
}
std::optional<std::pair<libzcash::UFVKId, std::optional<libzcash::diversifier_index_t>>>
FindUFVKId::operator()(const libzcash::UnknownReceiver& receiver) const {
return std::nullopt;
}
std::optional<AddressUFVKMetadata> FindUFVKId::operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const {
const auto saplingIvk = keystore.mapSaplingIncomingViewingKeys.find(saplingAddr);
if (saplingIvk != keystore.mapSaplingIncomingViewingKeys.end()) {
// We have either generated this as a receiver via `z_getaddressforaccount` or a
// legacy Sapling address via `z_getnewaddress`, or we have previously detected
// this via trial-decryption of a note.
const auto ufvkId = keystore.mapSaplingKeyUnified.find(saplingIvk->second);
if (ufvkId != keystore.mapSaplingKeyUnified.end()) {
return AddressUFVKMetadata(ufvkId->second, std::nullopt, false);
} else {
// If we have the addr -> ivk map entry but not the ivk -> UFVK map entry,
// then this is definitely a legacy Sapling address.
return std::nullopt;
}
}
// We haven't generated this receiver via `z_getaddressforaccount` (or this is a
// recovery from a backed-up mnemonic which doesn't store receiver types selected by
// users). Trial-decrypt the diversifier of the Sapling address with every UFVK in the
// wallet, to check directly if it belongs to any of them.
for (const auto& [k, v] : keystore.mapUnifiedFullViewingKeys) {
auto dfvk = v.GetSaplingKey();
if (dfvk.has_value()) {
auto d_idx = dfvk.value().DecryptDiversifier(saplingAddr.d);
auto derived_addr = dfvk.value().Address(d_idx);
if (derived_addr.has_value() && derived_addr.value() == saplingAddr) {
return AddressUFVKMetadata(k, d_idx, false);
}
auto internal_d_idx = dfvk.value().DecryptInternalDiversifier(saplingAddr.d);
auto derived_internal_addr = dfvk.value().InternalAddress(internal_d_idx);
if (derived_internal_addr.has_value() && derived_internal_addr.value() == saplingAddr) {
return AddressUFVKMetadata(k, internal_d_idx, true);
}
}
}
// We definitely don't know of any UFVK linked to this Sapling address.
return std::nullopt;
}
std::optional<AddressUFVKMetadata> FindUFVKId::operator()(const CScriptID& scriptId) const {
const auto metadata = keystore.mapP2SHUnified.find(scriptId);
if (metadata != keystore.mapP2SHUnified.end()) {
// At present we never generate transparent internal addresses, so this
// must be an external address
return AddressUFVKMetadata(metadata->second.first, metadata->second.second, false);
} else {
return std::nullopt;
}
}
std::optional<AddressUFVKMetadata> FindUFVKId::operator()(const CKeyID& keyId) const {
const auto metadata = keystore.mapP2PKHUnified.find(keyId);
if (metadata != keystore.mapP2PKHUnified.end()) {
// At present we never generate transparent internal addresses, so this
// must be an external address
return AddressUFVKMetadata(metadata->second.first, metadata->second.second, false);
} else {
return std::nullopt;
}
}
std::optional<AddressUFVKMetadata> FindUFVKId::operator()(const libzcash::UnknownReceiver& receiver) const {
return std::nullopt;
}

View File

@ -18,6 +18,20 @@
#include <boost/signals2/signal.hpp>
class AddressUFVKMetadata {
private:
libzcash::UFVKId ufvkId;
std::optional<libzcash::diversifier_index_t> j;
bool internalAddress;
public:
AddressUFVKMetadata(libzcash::UFVKId ufvkId, std::optional<libzcash::diversifier_index_t> j, bool internalAddress)
: ufvkId(ufvkId), j(j), internalAddress(internalAddress) {}
libzcash::UFVKId GetUFVKId() const { return ufvkId; }
std::optional<libzcash::diversifier_index_t> GetDiversifierIndex() const { return j; }
bool IsInternalAddress() const { return internalAddress; }
};
/** A virtual base class for key stores */
class CKeyStore
{
@ -125,18 +139,21 @@ public:
const libzcash::UnifiedAddress& ua) = 0;
virtual std::optional<libzcash::ZcashdUnifiedFullViewingKey> GetUnifiedFullViewingKey(
const libzcash::UFVKId& keyId
) const = 0;
const libzcash::UFVKId& keyId) const = 0;
virtual std::optional<std::pair<libzcash::UFVKId, std::optional<libzcash::diversifier_index_t>>>
GetUFVKMetadataForReceiver(
const libzcash::Receiver& receiver
) const = 0;
virtual std::optional<AddressUFVKMetadata> GetUFVKMetadataForReceiver(
const libzcash::Receiver& receiver) const = 0;
virtual std::optional<libzcash::UFVKId>
GetUFVKIdForViewingKey(
const libzcash::ViewingKey& vk
) const = 0;
/**
* If all the receivers of the specified address correspond to a single
* UFVK, return that key's metadata. If all the receivers correspond to
* the same diversifier index, that diversifier index is also returned.
*/
virtual std::optional<AddressUFVKMetadata> GetUFVKMetadataForAddress(
const libzcash::UnifiedAddress& addr) const = 0;
virtual std::optional<libzcash::UFVKId> GetUFVKIdForViewingKey(
const libzcash::ViewingKey& vk) const = 0;
};
typedef std::map<CKeyID, CKey> KeyMap;
@ -185,6 +202,7 @@ protected:
std::map<CKeyID, std::pair<libzcash::UFVKId, libzcash::diversifier_index_t>> mapP2PKHUnified;
std::map<CScriptID, std::pair<libzcash::UFVKId, libzcash::diversifier_index_t>> mapP2SHUnified;
std::map<libzcash::SaplingIncomingViewingKey, libzcash::UFVKId> mapSaplingKeyUnified;
std::map<libzcash::OrchardIncomingViewingKey, libzcash::UFVKId> mapOrchardKeyUnified;
std::map<libzcash::UFVKId, libzcash::ZcashdUnifiedFullViewingKey> mapUnifiedFullViewingKeys;
friend class FindUFVKId;
@ -373,15 +391,34 @@ public:
virtual std::optional<libzcash::ZcashdUnifiedFullViewingKey> GetUnifiedFullViewingKey(
const libzcash::UFVKId& keyId) const;
virtual std::optional<std::pair<libzcash::UFVKId, std::optional<libzcash::diversifier_index_t>>>
GetUFVKMetadataForReceiver(
const libzcash::Receiver& receiver
) const;
virtual std::optional<AddressUFVKMetadata> GetUFVKMetadataForReceiver(
const libzcash::Receiver& receiver) const;
virtual std::optional<libzcash::UFVKId>
GetUFVKIdForViewingKey(
const libzcash::ViewingKey& vk
) const;
std::optional<libzcash::ZcashdUnifiedFullViewingKey> GetUFVKForReceiver(
const libzcash::Receiver& receiver) const {
auto ufvkMeta = GetUFVKMetadataForReceiver(receiver);
if (ufvkMeta.has_value()) {
return GetUnifiedFullViewingKey(ufvkMeta.value().GetUFVKId());
} else {
return std::nullopt;
}
}
virtual std::optional<AddressUFVKMetadata> GetUFVKMetadataForAddress(
const libzcash::UnifiedAddress& addr) const;
std::optional<libzcash::ZcashdUnifiedFullViewingKey> GetUFVKForAddress(
const libzcash::UnifiedAddress& addr) const {
auto ufvkMeta = GetUFVKMetadataForAddress(addr);
if (ufvkMeta.has_value()) {
return GetUnifiedFullViewingKey(ufvkMeta.value().GetUFVKId());
} else {
return std::nullopt;
}
}
virtual std::optional<libzcash::UFVKId> GetUFVKIdForViewingKey(
const libzcash::ViewingKey& vk) const;
};
typedef std::vector<unsigned char, secure_allocator<unsigned char> > CKeyingMaterial;
@ -398,14 +435,11 @@ private:
public:
FindUFVKId(const CBasicKeyStore& keystore): keystore(keystore) {}
std::optional<std::pair<libzcash::UFVKId, std::optional<libzcash::diversifier_index_t>>>
operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const;
std::optional<std::pair<libzcash::UFVKId, std::optional<libzcash::diversifier_index_t>>>
operator()(const CScriptID& scriptId) const;
std::optional<std::pair<libzcash::UFVKId, std::optional<libzcash::diversifier_index_t>>>
operator()(const CKeyID& keyId) const;
std::optional<std::pair<libzcash::UFVKId, std::optional<libzcash::diversifier_index_t>>>
operator()(const libzcash::UnknownReceiver& receiver) const;
std::optional<AddressUFVKMetadata> operator()(const libzcash::OrchardRawAddress& orchardAddr) const;
std::optional<AddressUFVKMetadata> operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const;
std::optional<AddressUFVKMetadata> operator()(const CScriptID& scriptId) const;
std::optional<AddressUFVKMetadata> operator()(const CKeyID& keyId) const;
std::optional<AddressUFVKMetadata> operator()(const libzcash::UnknownReceiver& receiver) const;
};
#endif // BITCOIN_KEYSTORE_H

View File

@ -214,31 +214,85 @@ public:
return miner_reward + nFees;
}
void ComputeBindingSig(void* ctx) const {
void ComputeBindingSig(void* saplingCtx, std::optional<orchard::UnauthorizedBundle> orchardBundle) const {
// Empty output script.
uint256 dataToBeSigned;
CScript scriptCode;
try {
dataToBeSigned = SignatureHash(
scriptCode, mtx, NOT_AN_INPUT, SIGHASH_ALL, 0,
CurrentEpochBranchId(nHeight, chainparams.GetConsensus()));
if (orchardBundle.has_value()) {
// Orchard is only usable with v5+ transactions.
dataToBeSigned = ProduceZip244SignatureHash(mtx, orchardBundle.value());
} else {
CScript scriptCode;
dataToBeSigned = SignatureHash(
scriptCode, mtx, NOT_AN_INPUT, SIGHASH_ALL, 0,
CurrentEpochBranchId(nHeight, chainparams.GetConsensus()));
}
} catch (std::logic_error ex) {
librustzcash_sapling_proving_ctx_free(ctx);
librustzcash_sapling_proving_ctx_free(saplingCtx);
throw ex;
}
if (orchardBundle.has_value()) {
auto authorizedBundle = orchardBundle.value().ProveAndSign({}, dataToBeSigned);
if (authorizedBundle.has_value()) {
mtx.orchardBundle = authorizedBundle.value();
} else {
librustzcash_sapling_proving_ctx_free(saplingCtx);
throw new std::runtime_error("Failed to create Orchard proof or signatures");
}
}
bool success = librustzcash_sapling_binding_sig(
ctx,
saplingCtx,
mtx.valueBalanceSapling,
dataToBeSigned.begin(),
mtx.bindingSig.data());
if (!success) {
librustzcash_sapling_proving_ctx_free(ctx);
librustzcash_sapling_proving_ctx_free(saplingCtx);
throw new std::runtime_error("An error occurred computing the binding signature.");
}
}
// Create Orchard output
void operator()(const libzcash::OrchardRawAddress &to) const {
auto ctx = librustzcash_sapling_proving_ctx_init();
// `enableSpends` must be set to `false` for coinbase transactions. This
// means the Orchard anchor is unconstrained, so we set it to the empty
// tree root via a null (all zeroes) uint256.
uint256 orchardAnchor;
auto builder = orchard::Builder(false, true, orchardAnchor);
// Shielded coinbase outputs must be recoverable with an all-zeroes ovk.
uint256 ovk;
auto miner_reward = SetFoundersRewardAndGetMinerValue(ctx);
builder.AddOutput(ovk, to, miner_reward, std::nullopt);
// orchard::Builder pads to two Actions, but does so using a "no OVK" policy for
// dummy outputs, which violates coinbase rules requiring all shielded outputs to
// be recoverable. We manually add a dummy output to sidestep this issue.
// TODO: If/when we have funding streams going to Orchard recipients, this dummy
// output can be removed.
RawHDSeed rawSeed(32, 0);
GetRandBytes(rawSeed.data(), 32);
auto dummyTo = libzcash::OrchardSpendingKey::ForAccount(HDSeed(rawSeed), Params().BIP44CoinType(), 0)
.ToFullViewingKey()
.ToIncomingViewingKey()
.Address(0);
builder.AddOutput(ovk, dummyTo, 0, std::nullopt);
auto bundle = builder.Build();
if (!bundle.has_value()) {
librustzcash_sapling_proving_ctx_free(ctx);
throw new std::runtime_error("Failed to create shielded output for miner");
}
ComputeBindingSig(ctx, std::move(bundle));
librustzcash_sapling_proving_ctx_free(ctx);
}
// Create shielded output
void operator()(const libzcash::SaplingPaymentAddress &pa) const {
auto ctx = librustzcash_sapling_proving_ctx_init();
@ -258,7 +312,7 @@ public:
}
mtx.vShieldedOutput.push_back(odesc.value());
ComputeBindingSig(ctx);
ComputeBindingSig(ctx, std::nullopt);
librustzcash_sapling_proving_ctx_free(ctx);
}
@ -277,7 +331,7 @@ public:
mtx.vout[0] = CTxOut(value, coinbaseScript->reserveScript);
if (mtx.vShieldedOutput.size() > 0) {
ComputeBindingSig(ctx);
ComputeBindingSig(ctx, std::nullopt);
}
librustzcash_sapling_proving_ctx_free(ctx);
@ -717,23 +771,38 @@ std::optional<MinerAddress> ExtractMinerAddress::operator()(const libzcash::Sapl
return addr;
}
std::optional<MinerAddress> ExtractMinerAddress::operator()(const libzcash::UnifiedAddress &addr) const {
for (const auto& receiver: addr) {
if (std::holds_alternative<libzcash::SaplingPaymentAddress>(receiver)) {
return std::get<libzcash::SaplingPaymentAddress>(receiver);
}
auto preferred = addr.GetPreferredRecipientAddress(consensus, height);
if (preferred.has_value()) {
std::optional<MinerAddress> ret;
std::visit(match {
[&](const libzcash::OrchardRawAddress addr) { ret = MinerAddress(addr); },
[&](const libzcash::SaplingPaymentAddress addr) { ret = MinerAddress(addr); },
[&](const CKeyID keyID) { ret = operator()(keyID); },
[&](const auto other) { ret = std::nullopt; }
}, preferred.value());
return ret;
} else {
return std::nullopt;
}
return std::nullopt;
}
void GetMinerAddress(MinerAddress &minerAddress)
void GetMinerAddress(std::optional<MinerAddress> &minerAddress)
{
KeyIO keyIO(Params());
// If the user sets a UA miner address with an Orchard component, we want to ensure we
// start using it once we reach that height.
int height;
{
LOCK(cs_main);
height = chainActive.Height() + 1;
}
auto mAddrArg = GetArg("-mineraddress", "");
auto zaddr0 = keyIO.DecodePaymentAddress(mAddrArg);
if (zaddr0.has_value()) {
auto zaddr = std::visit(ExtractMinerAddress(), zaddr0.value());
auto zaddr = std::visit(ExtractMinerAddress(Params().GetConsensus(), height), zaddr0.value());
if (zaddr.has_value()) {
minerAddress = zaddr.value();
}
@ -804,8 +873,8 @@ void static BitcoinMiner(const CChainParams& chainparams)
// Each thread has its own counter
unsigned int nExtraNonce = 0;
MinerAddress minerAddress;
GetMainSignals().AddressForMining(minerAddress);
std::optional<MinerAddress> maybeMinerAddress;
GetMainSignals().AddressForMining(maybeMinerAddress);
unsigned int n = chainparams.GetConsensus().nEquihashN;
unsigned int k = chainparams.GetConsensus().nEquihashK;
@ -826,9 +895,10 @@ void static BitcoinMiner(const CChainParams& chainparams)
try {
// Throw an error if no address valid for mining was provided.
if (!std::visit(IsValidMinerAddress(), minerAddress)) {
if (!(maybeMinerAddress.has_value() && std::visit(IsValidMinerAddress(), maybeMinerAddress.value()))) {
throw std::runtime_error("No miner address available (mining requires a wallet or -mineraddress)");
}
auto minerAddress = maybeMinerAddress.value();
while (true) {
if (chainparams.MiningRequiresPeers()) {

View File

@ -24,13 +24,18 @@ static const int DEFAULT_GENERATE_THREADS = 1;
static const bool DEFAULT_PRINTPRIORITY = false;
typedef std::variant<
libzcash::OrchardRawAddress,
libzcash::SaplingPaymentAddress,
boost::shared_ptr<CReserveScript>> MinerAddress;
class ExtractMinerAddress
{
const Consensus::Params& consensus;
int height;
public:
ExtractMinerAddress() {}
ExtractMinerAddress(const Consensus::Params& consensus, int height) :
consensus(consensus), height(height) {}
std::optional<MinerAddress> operator()(const CKeyID &keyID) const;
std::optional<MinerAddress> operator()(const CScriptID &addr) const;
@ -44,6 +49,7 @@ class KeepMinerAddress
public:
KeepMinerAddress() {}
void operator()(const libzcash::OrchardRawAddress &addr) const {}
void operator()(const libzcash::SaplingPaymentAddress &pa) const {}
void operator()(const boost::shared_ptr<CReserveScript> &coinbaseScript) const {
coinbaseScript->KeepScript();
@ -57,6 +63,9 @@ class IsValidMinerAddress
public:
IsValidMinerAddress() {}
bool operator()(const libzcash::OrchardRawAddress &addr) const {
return true;
}
bool operator()(const libzcash::SaplingPaymentAddress &pa) const {
return true;
}
@ -89,7 +98,7 @@ CBlockTemplate* CreateNewBlock(const CChainParams& chainparams, const MinerAddre
#ifdef ENABLE_MINING
/** Get -mineraddress */
void GetMinerAddress(MinerAddress &minerAddress);
void GetMinerAddress(std::optional<MinerAddress> &minerAddress);
/** Modify the extranonce in a block */
void IncrementExtraNonce(
CBlockTemplate* pblocktemplate,

View File

@ -9,11 +9,15 @@
#include <amount.h>
#include <rust/orchard.h>
#include <rust/orchard/wallet.h>
#include "zcash/address/orchard.hpp"
class OrchardMerkleFrontier;
class OrchardWallet;
namespace orchard { class UnauthorizedBundle; }
/**
* The Orchard component of a transaction.
* The Orchard component of an authorized transaction.
*/
class OrchardBundle
{
@ -22,7 +26,11 @@ private:
/// Memory is allocated by Rust.
std::unique_ptr<OrchardBundlePtr, decltype(&orchard_bundle_free)> inner;
OrchardBundle(OrchardBundlePtr* bundle) : inner(bundle, orchard_bundle_free) {}
friend class OrchardMerkleFrontier;
friend class OrchardWallet;
friend class orchard::UnauthorizedBundle;
public:
OrchardBundle() : inner(nullptr, orchard_bundle_free) {}

View File

@ -81,6 +81,11 @@ std::string SaplingOutPoint::ToString() const
return strprintf("SaplingOutPoint(%s, %u)", hash.ToString().substr(0, 10), n);
}
std::string OrchardOutPoint::ToString() const
{
return strprintf("OrchardOutPoint(%s, %u)", hash.ToString().substr(0, 10), n);
}
CTxIn::CTxIn(COutPoint prevoutIn, CScript scriptSigIn, uint32_t nSequenceIn)
{
prevout = prevoutIn;

View File

@ -523,6 +523,16 @@ public:
std::string ToString() const;
};
/** An outpoint - a combination of a txid and an index n into its orchard
* actions */
class OrchardOutPoint : public BaseOutPoint
{
public:
OrchardOutPoint() : BaseOutPoint() {};
OrchardOutPoint(uint256 hashIn, uint32_t nIn) : BaseOutPoint(hashIn, nIn) {};
std::string ToString() const;
};
/** An input of a transaction. It contains the location of the previous
* transaction's output that it claims and a signature that matches the
* output's public key.

View File

@ -134,7 +134,6 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "z_sendmany", 1},
{ "z_sendmany", 2},
{ "z_sendmany", 3},
{ "z_sendmany", 4},
{ "z_shieldcoinbase", 2},
{ "z_shieldcoinbase", 3},
{ "z_getoperationstatus", 0},

View File

@ -186,19 +186,25 @@ UniValue generate(const UniValue& params, bool fHelp)
int nHeight = 0;
int nGenerate = params[0].get_int();
MinerAddress minerAddress;
GetMainSignals().AddressForMining(minerAddress);
// If the keypool is exhausted, no script is returned at all. Catch this.
auto resv = std::get_if<boost::shared_ptr<CReserveScript>>(&minerAddress);
if (resv && !resv->get()) {
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first");
}
std::optional<MinerAddress> maybeMinerAddress;
GetMainSignals().AddressForMining(maybeMinerAddress);
// Throw an error if no address valid for mining was provided.
if (!std::visit(IsValidMinerAddress(), minerAddress)) {
if (!maybeMinerAddress.has_value()) {
throw JSONRPCError(RPC_INTERNAL_ERROR, "No miner address available (mining requires a wallet or -mineraddress)");
} else {
// Detect and handle keypool exhaustion separately from IsValidMinerAddress().
auto resv = std::get_if<boost::shared_ptr<CReserveScript>>(&maybeMinerAddress.value());
if (resv && !resv->get()) {
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first");
}
// Catch any other invalid miner address issues.
if (!std::visit(IsValidMinerAddress(), maybeMinerAddress.value())) {
throw JSONRPCError(RPC_INTERNAL_ERROR, "Miner address is invalid");
}
}
auto minerAddress = maybeMinerAddress.value();
{ // Don't keep cs_main locked
LOCK(cs_main);
@ -569,9 +575,14 @@ UniValue getblocktemplate(const UniValue& params, bool fHelp)
if (IsInitialBlockDownload(Params().GetConsensus()))
throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "Zcash is downloading blocks...");
std::optional<MinerAddress> maybeMinerAddress;
GetMainSignals().AddressForMining(maybeMinerAddress);
MinerAddress minerAddress;
GetMainSignals().AddressForMining(minerAddress);
// Throw an error if no address valid for mining was provided.
if (!(maybeMinerAddress.has_value() && std::visit(IsValidMinerAddress(), maybeMinerAddress.value()))) {
throw JSONRPCError(RPC_INTERNAL_ERROR, "No miner address available (getblocktemplate requires a wallet or -mineraddress)");
}
auto minerAddress = maybeMinerAddress.value();
static unsigned int nTransactionsUpdatedLast;
static std::optional<CMutableTransaction> cached_next_cb_mtx;

View File

@ -5,10 +5,13 @@
#ifndef ZCASH_RUST_INCLUDE_RUST_ADDRESS_H
#define ZCASH_RUST_INCLUDE_RUST_ADDRESS_H
#include "rust/orchard/keys.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef bool (*orchard_receiver_t)(void* ua, OrchardRawAddressPtr* addr);
typedef bool (*raw_to_receiver_t)(void* ua, const unsigned char* raw);
typedef bool (*unknown_receiver_t)(
void* ua,
@ -24,6 +27,7 @@ bool zcash_address_parse_unified(
const char* str,
const char* network,
void* ua,
orchard_receiver_t orchard_cb,
raw_to_receiver_t sapling_cb,
raw_to_receiver_t p2sh_cb,
raw_to_receiver_t p2pkh_cb,

View File

@ -0,0 +1,105 @@
// 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 .
#ifndef ZCASH_RUST_INCLUDE_RUST_BUILDER_H
#define ZCASH_RUST_INCLUDE_RUST_BUILDER_H
#include "rust/orchard.h"
#include "rust/orchard/keys.h"
#include "rust/transaction.h"
#ifdef __cplusplus
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;
/// Pointer to Rust-allocated Orchard bundle without proofs
/// or authorizing data.
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.
OrchardBuilderPtr* orchard_builder_new(
bool spends_enabled,
bool outputs_enabled,
const unsigned char* anchor);
/// 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,
/// or `null` to make the recipient unrecoverable by the sender.
///
/// `memo` is a pointer to the 512-byte memo field encoding, or `null` for "no memo".
bool orchard_builder_add_recipient(
OrchardBuilderPtr* ptr,
const unsigned char* ovk,
const OrchardRawAddressPtr* recipient,
uint64_t value,
const unsigned char* memo);
/// Builds a bundle containing the given spent notes and recipients.
///
/// Returns `null` if an error occurs.
///
/// `builder` is always freed by this method.
OrchardUnauthorizedBundlePtr* orchard_builder_build(OrchardBuilderPtr* builder);
/// Frees an Orchard bundle returned from `orchard_bundle_build`.
void orchard_unauthorized_bundle_free(OrchardUnauthorizedBundlePtr* bundle);
/// Adds proofs and signatures to the bundle.
///
/// Returns `null` if an error occurs.
///
/// `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
/// transaction.
///
/// Returns `false` if any of the parameters are invalid; in this case, `sighash_ret`
/// will be unaltered.
///
/// `preTx` is always freed by this method.
bool zcash_builder_zip244_shielded_signature_digest(
PrecomputedTxParts* preTx,
const OrchardUnauthorizedBundlePtr* bundle,
unsigned char* sighash_ret);
#ifdef __cplusplus
}
#endif
#endif // ZCASH_RUST_INCLUDE_RUST_BUILDER_H

View File

@ -13,6 +13,7 @@
extern "C" {
#endif
/// Typesafe pointer to a Rust-allocated orchard::bundle::Bundle value
struct OrchardBundlePtr;
typedef struct OrchardBundlePtr OrchardBundlePtr;

View File

@ -69,7 +69,7 @@ void orchard_merkle_frontier_root(
// The total number of leaves that have been appended to obtain
// the current state of the frontier. Subtract 1 from this value
// to obtain the position of the most recently appended leaf.
size_t orchard_merkle_frontier_num_leaves(
uint64_t orchard_merkle_frontier_num_leaves(
const OrchardMerkleFrontierPtr* tree_ptr);
// Estimate the amount of memory consumed by the merkle frontier.

View File

@ -32,6 +32,43 @@ OrchardRawAddressPtr* orchard_address_clone(
*/
void orchard_address_free(OrchardRawAddressPtr* ptr);
/**
* Parses Orchard raw address bytes from the given stream.
*
* - If the key does not parse correctly, the returned pointer will be null.
*/
OrchardRawAddressPtr* orchard_raw_address_parse(
void* stream,
read_callback_t read_cb);
/**
* Serializes Orchard raw address bytes to the given stream.
*
* This will return `false` and leave the stream unmodified if
* `raw_address == nullptr`;
*/
bool orchard_raw_address_serialize(
const OrchardRawAddressPtr* raw_address,
void* stream,
write_callback_t write_cb);
/**
* Implements the "equal" operation for comparing two Orchard addresses.
*/
bool orchard_address_eq(
const OrchardRawAddressPtr* k0,
const OrchardRawAddressPtr* k1);
/**
* Implements the "less than" operation `k0 < k1` for comparing two Orchard
* addresses. This is a comparison of the raw bytes, only useful for cases
* where a semantically irrelevant ordering is needed (such as for map keys).
*/
bool orchard_address_lt(
const OrchardRawAddressPtr* k0,
const OrchardRawAddressPtr* k1);
//
// Incoming Viewing Keys
//
@ -60,6 +97,18 @@ OrchardRawAddressPtr* orchard_incoming_viewing_key_to_address(
const OrchardIncomingViewingKeyPtr* incoming_viewing_key,
const unsigned char* j);
/**
* Decrypts the diversifier component of an Orchard raw address with the
* specified IVK, and verifies that the address was derived from that IVK.
*
* Returns `false` and leaves the `j_ret` parameter unmodified if the address
* was not derived from the specified IVK.
*/
bool orchard_incoming_viewing_key_decrypt_diversifier(
const OrchardIncomingViewingKeyPtr* incoming_viewing_key,
const OrchardRawAddressPtr* addr,
uint8_t *j_ret);
/**
* Parses an Orchard incoming viewing key from the given stream.
*
@ -69,8 +118,12 @@ OrchardIncomingViewingKeyPtr* orchard_incoming_viewing_key_parse(
void* stream,
read_callback_t read_cb);
/**
* Serializes an Orchard incoming viewing key to the given stream.
*
* This will return `false` and leave the stream unmodified if
* `incoming_viewing_key == nullptr`.
*/
bool orchard_incoming_viewing_key_serialize(
const OrchardIncomingViewingKeyPtr* incoming_viewing_key,
@ -123,6 +176,9 @@ OrchardFullViewingKeyPtr* orchard_full_viewing_key_parse(
/**
* Serializes an Orchard full viewing key to the given stream.
*
* This will return `false` and leave the stream unmodified if
* `full_viewing_key == nullptr`.
*/
bool orchard_full_viewing_key_serialize(
const OrchardFullViewingKeyPtr* full_viewing_key,
@ -135,6 +191,30 @@ bool orchard_full_viewing_key_serialize(
OrchardIncomingViewingKeyPtr* orchard_full_viewing_key_to_incoming_viewing_key(
const OrchardFullViewingKeyPtr* key);
/**
* Returns the internal incoming viewing key for the specified full 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.
*/
@ -173,25 +253,11 @@ OrchardSpendingKeyPtr* orchard_spending_key_clone(
*/
void orchard_spending_key_free(OrchardSpendingKeyPtr* ptr);
/**
* Parses an Orchard spending key from the given stream.
*
* - If the key does not parse correctly, the returned pointer will be null.
*/
OrchardSpendingKeyPtr* orchard_spending_key_parse(
void* stream,
read_callback_t read_cb);
/**
* Serializes an Orchard spending key to the given stream.
*/
bool orchard_spending_key_serialize(
const OrchardSpendingKeyPtr* spending_key,
void* stream,
write_callback_t write_cb);
/**
* Returns the full viewing key for the specified spending key.
*
* The resulting pointer must be ultimately freed by the caller
* using `orchard_full_viewing_key_free`.
*/
OrchardFullViewingKeyPtr* orchard_spending_key_to_full_viewing_key(
const OrchardSpendingKeyPtr* key);

View File

@ -0,0 +1,378 @@
// Copyright (c) 2021 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
#ifndef ZCASH_RUST_INCLUDE_RUST_ORCHARD_WALLET_H
#define ZCASH_RUST_INCLUDE_RUST_ORCHARD_WALLET_H
#include "rust/orchard/keys.h"
#include "rust/builder.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* A type-safe pointer type for an Orchard wallet.
*/
struct OrchardWalletPtr;
typedef struct OrchardWalletPtr OrchardWalletPtr;
/**
* Constructs a new empty Orchard wallet and return a pointer to it.
* Memory is allocated by Rust and must be manually freed using
* `orchard_wallet_free`.
*/
OrchardWalletPtr* orchard_wallet_new();
/**
* Frees the memory associated with an Orchard wallet that was allocated
* by Rust.
*/
void orchard_wallet_free(OrchardWalletPtr* wallet);
/**
* Reset the state of the wallet to be suitable for rescan from the NU5 activation
* height. This removes all witness and spentness information from the wallet. The
* keystore is unmodified and decrypted note, nullifier, and conflict data are left
* in place with the expectation that they will be overwritten and/or updated in
* the rescan process.
*/
bool orchard_wallet_reset(OrchardWalletPtr* wallet);
/**
* Checkpoint the note commitment tree. This returns `false` and leaves the note
* commitment tree unmodified if the block height specified is not the successor
* to the last block height checkpointed.
*/
bool orchard_wallet_checkpoint(
OrchardWalletPtr* wallet,
uint32_t blockHeight
);
/**
* Returns whether or not the wallet has any checkpointed state to which it can rewind.
* If so, `blockHeightRet` will be modified to contain the last block height at which a
* checkpoint was created.
*/
bool orchard_wallet_get_last_checkpoint(
const OrchardWalletPtr* wallet,
uint32_t* blockHeightRet);
/**
* Rewinds to the most recent checkpoint, and marks as unspent any notes previously
* identified as having been spent by transactions in the latest block.
*
* The `blockHeight` argument provides the height to which the witness tree should be
* rewound, such that after the rewind this height corresponds to the latest block
* appended to the tree. The number of blocks that were removed from the witness
* tree in the rewind process is returned via `blocksRewoundRet`.
*
* Returns `true` if the rewind is successful, in which case the number of blocks that were
* removed from the witness tree in the rewind process is returned via `blocksRewoundRet`;
* this returns `false` and leaves `blocksRewoundRet` unmodified.
*/
bool orchard_wallet_rewind(
OrchardWalletPtr* wallet,
uint32_t blockHeight,
uint32_t* blocksRewoundRet
);
/**
* A C struct used to transfer action_idx/IVK pairs back from Rust across the FFI
* boundary. This must have the same in-memory representation as the `FFIActionIVK` type
* in orchard_ffi/wallet.rs.
*
* Values of the `ivk` pointer must be freed manually; the best way to do this is to
* wrap this pointer in an `OrchardIncomingViewingKey` which handles deallocation
* in the object destructor.
*/
struct RawOrchardActionIVK {
uint64_t actionIdx;
OrchardIncomingViewingKeyPtr* ivk;
};
static_assert(
sizeof(RawOrchardActionIVK) == 16,
"RawOrchardActionIVK struct should have exactly a 128-bit in-memory representation.");
static_assert(alignof(RawOrchardActionIVK) == 8, "RawOrchardActionIVK struct alignment is not 64 bits.");
typedef void (*push_action_ivk_callback_t)(void* rec, const RawOrchardActionIVK actionIvk);
typedef void (*push_spend_action_idx_callback_t)(void* rec, uint32_t actionIdx);
/**
* Searches the provided bundle for notes that are visible to the specified wallet's
* incoming viewing keys, and adds those notes to the wallet. For each note decryptable
* by one of the wallet's keys, this method will insert a `RawOrchardActionIVK` value into
* the provided `callbackReceiver` referent using the `push_cb` callback. Note that
* this callback can perform transformations on the provided RawOrchardActionIVK in this
* process. For each action spending one of the wallet's notes, this method will pass
* a `uint32_t` action index corresponding to that action to the `callbackReceiver` referent;
* using the specified callback; usually, this will push the value into a result vector owned
* by the caller.
*
* The provided bundle must be a component of the transaction from which `txid` was
* derived.
*
* Returns `true` if the bundle is involved with the wallet; i.e. if it contains
* notes spendable by the wallet, or spends any of the wallet's notes.
*/
bool orchard_wallet_add_notes_from_bundle(
OrchardWalletPtr* wallet,
const unsigned char txid[32],
const OrchardBundlePtr* bundle,
void* callbackReceiver,
push_action_ivk_callback_t push_cb,
push_spend_action_idx_callback_t spend_cb
);
/**
* Decrypts a selection of notes from the bundle with specified incoming viewing
* keys, and adds those notes to the wallet.
*
* The provided bundle must be a component of the transaction from which
* `txid` was derived.
*
* The value the `blockHeight` pointer points to be set to the height at which the
* transaction was mined, or `nullptr` if the transaction is not in the main chain.
*/
bool orchard_wallet_load_bundle(
OrchardWalletPtr* wallet,
const uint32_t* blockHeight,
const unsigned char txid[32],
const OrchardBundlePtr* bundle,
const RawOrchardActionIVK* actionIvks,
size_t actionIvksLen,
const uint32_t* actionsSpendingWalletNotes,
size_t actionsSpendingWalletNotesLen
);
/**
* Add the note commitment values for the specified bundle to the wallet's note
* commitment tree, and mark any Orchard notes that belong to the wallet so
* that we can construct authentication paths to these notes in the future.
*
* This requires the block height and the index of the block within the
* transaction in order to guarantee that note commitments are appended in the
* correct order. Returns `false` if the provided bundle is not in the correct
* position to have its note commitments appended to the note commitment tree.
*/
bool orchard_wallet_append_bundle_commitments(
OrchardWalletPtr* wallet,
const uint32_t block_height,
const size_t block_tx_idx,
const unsigned char txid[32],
const OrchardBundlePtr* bundle
);
/**
* Returns the root of the wallet's note commitment tree.
*/
void orchard_wallet_commitment_tree_root(
const OrchardWalletPtr* wallet,
unsigned char* root_ret);
/**
* Returns whether the specified transaction involves any Orchard notes that belong to
* this wallet.
*/
bool orchard_wallet_tx_involves_my_notes(
const OrchardWalletPtr* wallet,
const unsigned char txid[32]);
/**
* Add the specified spending key to the wallet's key store. This will also compute and
* add the associated full and incoming viewing keys.
*/
void orchard_wallet_add_spending_key(
OrchardWalletPtr* wallet,
const OrchardSpendingKeyPtr* sk);
/**
* Add the specified full viewing key to the wallet's key store. This will also compute
* and add the associated incoming viewing key.
*/
void orchard_wallet_add_full_viewing_key(
OrchardWalletPtr* wallet,
const OrchardFullViewingKeyPtr* fvk);
/**
* Add the specified raw address to the wallet's key store, associated with the incoming
* viewing key from which that address was derived.
*/
bool orchard_wallet_add_raw_address(
OrchardWalletPtr* wallet,
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.
*
* Memory is allocated by Rust and must be manually freed using
* `orchard_incoming_viewing_key_free`.
*/
OrchardIncomingViewingKeyPtr* orchard_wallet_get_ivk_for_address(
const OrchardWalletPtr* wallet,
const OrchardRawAddressPtr* addr);
/**
* A C struct used to transfer note metadata information across the Rust FFI boundary.
* This must have the same in-memory representation as the `FFINoteMetadata` type in
* orchard_ffi/wallet.rs.
*/
struct RawOrchardNoteMetadata {
unsigned char txid[32];
uint32_t actionIdx;
OrchardRawAddressPtr* addr;
CAmount noteValue;
unsigned char memo[512];
};
typedef void (*push_note_callback_t)(void* resultVector, const RawOrchardNoteMetadata noteMeta);
/**
* Finds notes that belong to the wallet that were sent to addresses derived from the
* specified incoming viewing key, subject to the specified flags, and uses the provided
* callback to push RawOrchardNoteMetadata values corresponding to those notes on to the
* provided result vector. Note that the push_cb callback can perform any necessary
* conversion from a RawOrchardNoteMetadata value in addition to modifying the provided
* result vector.
*
* If `ivk` is null, all notes belonging to the wallet will be returned. The
* `RawOrchardNoteMetadata::addr` pointers for values provided to the callback must be
* manually freed by the caller.
*/
void orchard_wallet_get_filtered_notes(
const OrchardWalletPtr* wallet,
const OrchardIncomingViewingKeyPtr* ivk,
bool ignoreMined,
bool requireSpendingKey,
void* resultVector,
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;
OrchardRawAddressPtr* receivedAt;
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-decrypts the specfied Orchard bundle, and uses the provided callbacks to pass
* `RawOrchardActionSpend` and `RawOrchardActionOutput` values (corresponding to the
* actions of that bundle) to the provided result receiver.
*
* Note that the callbacks can perform any necessary conversion from a
* `RawOrchardActionSpend` or `RawOrchardActionOutput` value in addition to modifying the
* provided result receiver.
*
* `raw_ovks` must be a pointer to an array of `unsigned char[32]`.
*
* The `addr` pointer on each `RawOrchardActionOutput` value must be freed using
* `orchard_address_free`.
*/
bool orchard_wallet_get_txdata(
const OrchardWalletPtr* wallet,
const OrchardBundlePtr* bundle,
const unsigned char* raw_ovks,
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]);
/**
* Returns a vector of transaction IDs for transactions that have been observed as
* spending the given outpoint (transaction ID and action index) by using the `push_cb`
* callback to push transaction IDs onto the provided result vector.
*/
void orchard_wallet_get_potential_spends(
const OrchardWalletPtr* wallet,
const unsigned char txid[32],
const uint32_t action_idx,
void* resultVector,
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);
/**
* Run the garbage collection operation on the wallet's note commitment
* tree.
*/
void orchard_wallet_gc_note_commitment_tree(OrchardWalletPtr* wallet);
/**
* Write the wallet's note commitment tree to the provided stream.
*/
bool orchard_wallet_write_note_commitment_tree(
const OrchardWalletPtr* wallet,
void* stream,
write_callback_t write_cb);
/**
* Read a note commitment tree from the provided stream, and update the wallet's internal
* note commitment tree state to equal the value that was read.
*/
bool orchard_wallet_load_note_commitment_tree(
OrchardWalletPtr* wallet,
void* stream,
read_callback_t read_cb);
#ifdef __cplusplus
}
#endif
#endif // ZCASH_RUST_INCLUDE_RUST_ORCHARD_WALLET_H

View File

@ -84,6 +84,22 @@ bool unified_full_viewing_key_read_sapling(
const UnifiedFullViewingKeyPtr* full_viewing_key,
unsigned char* skeyout);
/**
* Reads the Orchard component of a unified full viewing key.
*
* `skeyout` must be of length 96.
*
* Returns `true` if the UFVK contained an Orchard component, `false` otherwise.
* The bytes of the Orchard Raw Full Viewing Key, in the encoding given in
* section 5.6.4.4 of the Zcash Protocol Specification, will be copied to
* `skeyout` if `true` is returned.
*
* If `false` is returned then `skeyout` will be unchanged.
*/
bool unified_full_viewing_key_read_orchard(
const UnifiedFullViewingKeyPtr* full_viewing_key,
unsigned char* skeyout);
/**
* Constructs a unified full viewing key from the binary encodings
* of its constituent parts.
@ -101,7 +117,8 @@ bool unified_full_viewing_key_read_sapling(
*/
UnifiedFullViewingKeyPtr* unified_full_viewing_key_from_components(
const unsigned char* t_key,
const unsigned char* sapling_key);
const unsigned char* sapling_key,
const unsigned char* orchard_key);
/**
* Derive the internal and external OVKs for the binary encoding

View File

@ -12,6 +12,8 @@ use zcash_address::{
use zcash_primitives::sapling;
pub type UnifiedAddressObj = NonNull<c_void>;
pub type AddOrchardReceiverCb =
unsafe extern "C" fn(ua: Option<UnifiedAddressObj>, orchard: *const orchard::Address) -> bool;
pub type AddReceiverCb =
unsafe extern "C" fn(ua: Option<UnifiedAddressObj>, raw: *const u8) -> bool;
pub type UnknownReceiverCb = unsafe extern "C" fn(
@ -53,10 +55,12 @@ impl FromAddress for UnifiedAddressHelper {
}
impl UnifiedAddressHelper {
#[allow(clippy::too_many_arguments)]
fn into_cpp(
self,
network: Network,
ua_obj: Option<UnifiedAddressObj>,
orchard_cb: Option<AddOrchardReceiverCb>,
sapling_cb: Option<AddReceiverCb>,
p2sh_cb: Option<AddReceiverCb>,
p2pkh_cb: Option<AddReceiverCb>,
@ -79,16 +83,13 @@ impl UnifiedAddressHelper {
// ZIP 316: Consumers MUST reject Unified Addresses/Viewing Keys in
// which any constituent Item does not meet the validation
// requirements of its encoding.
if orchard::Address::from_raw_address_bytes(&data)
.is_none()
.into()
{
let addr = orchard::Address::from_raw_address_bytes(&data);
if addr.is_none().into() {
tracing::error!("Unified Address contains invalid Orchard receiver");
false
} else {
unsafe {
// TODO: Replace with Orchard support.
(unknown_cb.unwrap())(ua_obj, 0x03, data.as_ptr(), data.len())
(orchard_cb.unwrap())(ua_obj, Box::into_raw(Box::new(addr.unwrap())))
}
}
}
@ -122,6 +123,7 @@ pub extern "C" fn zcash_address_parse_unified(
encoded: *const c_char,
network: *const c_char,
ua_obj: Option<UnifiedAddressObj>,
orchard_cb: Option<AddOrchardReceiverCb>,
sapling_cb: Option<AddReceiverCb>,
p2sh_cb: Option<AddReceiverCb>,
p2pkh_cb: Option<AddReceiverCb>,
@ -149,7 +151,9 @@ pub extern "C" fn zcash_address_parse_unified(
}
};
ua.into_cpp(network, ua_obj, sapling_cb, p2sh_cb, p2pkh_cb, unknown_cb)
ua.into_cpp(
network, ua_obj, orchard_cb, sapling_cb, p2sh_cb, p2pkh_cb, unknown_cb,
)
}
#[no_mangle]
@ -171,14 +175,9 @@ pub extern "C" fn zcash_address_serialize_unified(
Ok(
match unsafe { (typecode_cb.unwrap())(ua_obj, i) }.try_into()? {
unified::Typecode::Orchard => {
// TODO: Replace with Orchard support.
let data_len = unsafe { (receiver_len_cb.unwrap())(ua_obj, i) };
let mut data = vec![0; data_len];
unsafe { (receiver_cb.unwrap())(ua_obj, i, data.as_mut_ptr(), data_len) };
unified::Receiver::Unknown {
typecode: 0x03,
data,
}
let mut data = [0; 43];
unsafe { (receiver_cb.unwrap())(ua_obj, i, data.as_mut_ptr(), data.len()) };
unified::Receiver::Orchard(data)
}
unified::Typecode::Sapling => {
let mut data = [0; 43];

229
src/rust/src/builder_ffi.rs Normal file
View File

@ -0,0 +1,229 @@
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::{FullViewingKey, OutgoingViewingKey},
tree::{MerkleHashOrchard, MerklePath},
value::NoteValue,
Bundle, Note,
};
use rand_core::OsRng;
use tracing::error;
use zcash_primitives::transaction::{
components::{sapling, transparent, Amount},
sighash::{SignableInput, SIGHASH_ALL},
sighash_v5::v5_signature_hash,
txid::TxIdDigester,
Authorization, TransactionData, TxVersion,
};
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,
outputs_enabled: bool,
anchor: *const [u8; 32],
) -> *mut Builder {
let anchor = unsafe { anchor.as_ref() }
.map(|a| orchard::Anchor::from_bytes(*a).unwrap())
.unwrap_or_else(|| MerkleHashOrchard::empty_root(32.into()).into());
Box::into_raw(Box::new(Builder::new(
Flags::from_parts(spends_enabled, outputs_enabled),
anchor,
)))
}
#[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,
ovk: *const [u8; 32],
recipient: *const orchard::Address,
value: u64,
memo: *const [u8; 512],
) -> bool {
let builder = unsafe { builder.as_mut() }.expect("Builder may not be null.");
let ovk = unsafe { ovk.as_ref() }
.copied()
.map(OutgoingViewingKey::from);
let recipient = unsafe { recipient.as_ref() }.expect("Recipient may not be null.");
let value = NoteValue::from_raw(value);
let memo = unsafe { memo.as_ref() }.copied();
match builder.add_recipient(ovk, *recipient, value, memo) {
Ok(()) => true,
Err(e) => {
error!("Failed to add Orchard recipient: {}", e);
false
}
}
}
#[no_mangle]
pub extern "C" fn orchard_builder_free(builder: *mut Builder) {
if !builder.is_null() {
drop(unsafe { Box::from_raw(builder) });
}
}
#[no_mangle]
pub extern "C" fn orchard_builder_build(
builder: *mut Builder,
) -> *mut Bundle<InProgress<Unproven, Unauthorized>, Amount> {
if builder.is_null() {
error!("Called with null builder");
return ptr::null_mut();
}
let builder = unsafe { Box::from_raw(builder) };
match builder.build(OsRng) {
Ok(bundle) => Box::into_raw(Box::new(bundle)),
Err(e) => {
error!("Failed to build Orchard bundle: {:?}", e);
ptr::null_mut()
}
}
}
#[no_mangle]
pub extern "C" fn orchard_unauthorized_bundle_free(
bundle: *mut Bundle<InProgress<Unproven, Unauthorized>, Amount>,
) {
if !bundle.is_null() {
drop(unsafe { Box::from_raw(bundle) });
}
}
#[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, &signing_keys));
match res {
Ok(signed) => Box::into_raw(Box::new(signed)),
Err(e) => {
error!(
"An error occurred while authorizing the orchard bundle: {:?}",
e
);
std::ptr::null_mut()
}
}
}
/// Calculates a ZIP 244 shielded signature digest for the given under-construction
/// transaction.
///
/// Returns `false` if any of the parameters are invalid; in this case, `sighash_ret`
/// will be unaltered.
#[no_mangle]
pub extern "C" fn zcash_builder_zip244_shielded_signature_digest(
precomputed_tx: *mut PrecomputedTxParts,
bundle: *const Bundle<InProgress<Unproven, Unauthorized>, Amount>,
sighash_ret: *mut [u8; 32],
) -> bool {
let precomputed_tx = if !precomputed_tx.is_null() {
unsafe { Box::from_raw(precomputed_tx) }
} else {
error!("Invalid precomputed transaction");
return false;
};
if matches!(
precomputed_tx.tx.version(),
TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling,
) {
error!("Cannot calculate ZIP 244 digest for pre-v5 transaction");
return false;
}
let bundle = unsafe { bundle.as_ref().unwrap() };
struct Signable {}
impl Authorization for Signable {
type TransparentAuth = transparent::Authorized;
type SaplingAuth = sapling::Authorized;
type OrchardAuth = InProgress<Unproven, Unauthorized>;
}
let txdata: TransactionData<Signable> =
precomputed_tx
.tx
.into_data()
.map_bundles(|b| b, |b| b, |_| Some(bundle.clone()));
let txid_parts = txdata.digest(TxIdDigester);
let sighash = v5_signature_hash(&txdata, SIGHASH_ALL, &SignableInput::Shielded, &txid_parts);
// `v5_signature_hash` output is always 32 bytes.
*unsafe { &mut *sighash_ret } = sighash.as_ref().try_into().unwrap();
true
}

View File

@ -0,0 +1,115 @@
use byteorder::{ReadBytesExt, WriteBytesExt};
use std::io::{self, Read, Write};
use incrementalmerkletree::{
bridgetree::{BridgeTree, Checkpoint},
Hashable,
};
use zcash_encoding::Vector;
use zcash_primitives::merkle_tree::{
incremental::{
read_bridge_v1, read_leu64_usize, read_position, write_bridge_v1, write_position,
write_usize_leu64, SER_V1,
},
HashSer,
};
pub fn write_checkpoint_v1<H: HashSer + Ord, W: Write>(
mut writer: W,
checkpoint: &Checkpoint<H>,
) -> io::Result<()> {
write_usize_leu64(&mut writer, checkpoint.bridges_len())?;
writer.write_u8(if checkpoint.is_witnessed() { 1 } else { 0 })?;
Vector::write_sized(
&mut writer,
checkpoint.forgotten().iter(),
|mut w, ((pos, leaf_value), idx)| {
write_position(&mut w, *pos)?;
leaf_value.write(&mut w)?;
write_usize_leu64(&mut w, *idx)
},
)?;
Ok(())
}
pub fn read_checkpoint_v1<H: HashSer + Ord, R: Read>(mut reader: R) -> io::Result<Checkpoint<H>> {
Ok(Checkpoint::from_parts(
read_leu64_usize(&mut reader)?,
reader.read_u8()? == 1,
Vector::read_collected(&mut reader, |mut r| {
Ok((
(read_position(&mut r)?, H::read(&mut r)?),
read_leu64_usize(&mut r)?,
))
})?,
))
}
pub fn write_tree_v1<H: Hashable + HashSer + Ord, W: Write>(
mut writer: W,
tree: &BridgeTree<H, 32>,
) -> io::Result<()> {
Vector::write(&mut writer, tree.bridges(), |w, b| write_bridge_v1(w, b))?;
Vector::write_sized(
&mut writer,
tree.witnessed_indices().iter(),
|mut w, ((pos, a), i)| {
write_position(&mut w, *pos)?;
a.write(&mut w)?;
write_usize_leu64(&mut w, *i)
},
)?;
Vector::write(&mut writer, tree.checkpoints(), |w, c| {
write_checkpoint_v1(w, c)
})?;
write_usize_leu64(&mut writer, tree.max_checkpoints())?;
Ok(())
}
#[allow(clippy::redundant_closure)]
pub fn read_tree_v1<H: Hashable + HashSer + Ord + Clone, R: Read>(
mut reader: R,
) -> io::Result<BridgeTree<H, 32>> {
BridgeTree::from_parts(
Vector::read(&mut reader, |r| read_bridge_v1(r))?,
Vector::read_collected(&mut reader, |mut r| {
Ok((
(read_position(&mut r)?, H::read(&mut r)?),
read_leu64_usize(&mut r)?,
))
})?,
Vector::read(&mut reader, |r| read_checkpoint_v1(r))?,
read_leu64_usize(&mut reader)?,
)
.map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Consistency violation found when attempting to deserialize Merkle tree: {:?}",
err
),
)
})
}
pub fn write_tree<H: Hashable + HashSer + Ord, W: Write>(
mut writer: W,
tree: &BridgeTree<H, 32>,
) -> io::Result<()> {
writer.write_u8(SER_V1)?;
write_tree_v1(&mut writer, tree)
}
pub fn read_tree<H: Hashable + HashSer + Ord + Clone, R: Read>(
mut reader: R,
) -> io::Result<BridgeTree<H, 32>> {
match reader.read_u8()? {
SER_V1 => read_tree_v1(&mut reader),
flag => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Unrecognized tree serialization version: {:?}", flag),
)),
}
}

View File

@ -123,13 +123,13 @@ pub extern "C" fn orchard_merkle_frontier_root(
#[no_mangle]
pub extern "C" fn orchard_merkle_frontier_num_leaves(
tree: *const bridgetree::Frontier<MerkleHashOrchard, MERKLE_DEPTH>,
) -> usize {
) -> u64 {
let tree = unsafe {
tree.as_ref()
.expect("Orchard note commitment tree pointer may not be null.")
};
tree.position().map_or(0, |p| (<u64>::from(p) + 1) as usize)
tree.position().map_or(0, |p| <u64>::from(p) + 1)
}
#[no_mangle]

View File

@ -2,10 +2,15 @@ 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};
use crate::{
streams_ffi::{CppStreamReader, CppStreamWriter, ReadCb, StreamObj, WriteCb},
zcashd_orchard::OrderedAddress,
};
//
// Addresses
@ -25,6 +30,63 @@ pub extern "C" fn orchard_address_free(addr: *mut Address) {
}
}
#[no_mangle]
pub extern "C" fn orchard_raw_address_parse(
stream: Option<StreamObj>,
read_cb: Option<ReadCb>,
) -> *mut Address {
let mut reader = CppStreamReader::from_raw_parts(stream, read_cb.unwrap());
let mut buf = [0u8; 43];
match reader.read_exact(&mut buf) {
Err(e) => {
error!("Stream failure reading bytes of Orchard raw address: {}", e);
std::ptr::null_mut()
}
Ok(()) => {
let read = Address::from_raw_address_bytes(&buf);
if read.is_some().into() {
Box::into_raw(Box::new(read.unwrap()))
} else {
error!("Failed to parse Orchard raw address.");
std::ptr::null_mut()
}
}
}
}
#[no_mangle]
pub extern "C" fn orchard_raw_address_serialize(
key: *const Address,
stream: Option<StreamObj>,
write_cb: Option<WriteCb>,
) -> bool {
let key = unsafe { key.as_ref() }.expect("Orchard raw address pointer may not be null.");
let mut writer = CppStreamWriter::from_raw_parts(stream, write_cb.unwrap());
match writer.write_all(&key.to_raw_address_bytes()) {
Ok(()) => true,
Err(e) => {
error!("Stream failure writing Orchard raw address: {}", e);
false
}
}
}
#[no_mangle]
pub extern "C" fn orchard_address_eq(a0: *const Address, a1: *const Address) -> bool {
let a0 = unsafe { a0.as_ref() };
let a1 = unsafe { a1.as_ref() };
a0 == a1
}
#[no_mangle]
pub extern "C" fn orchard_address_lt(a0: *const Address, a1: *const Address) -> bool {
let a0 = unsafe { a0.as_ref() };
let a1 = unsafe { a1.as_ref() };
a0.map(|a| OrderedAddress::new(*a)) < a1.map(|a| OrderedAddress::new(*a))
}
//
// Incoming viewing keys
//
@ -85,6 +147,26 @@ pub extern "C" fn orchard_incoming_viewing_key_to_address(
Box::into_raw(Box::new(key.address_at(diversifier_index)))
}
#[no_mangle]
pub extern "C" fn orchard_incoming_viewing_key_decrypt_diversifier(
key: *const IncomingViewingKey,
addr: *const Address,
j_ret: *mut [u8; 11],
) -> bool {
let key =
unsafe { key.as_ref() }.expect("Orchard incoming viewing key pointer may not be null.");
let addr = unsafe { addr.as_ref() }.expect("Orchard raw address pointer may not be null.");
let j_ret = unsafe { j_ret.as_mut() }.expect("j_ret may not be null.");
match key.diversifier_index(addr) {
Some(j) => {
j_ret.copy_from_slice(j.to_bytes());
true
}
None => false,
}
}
#[no_mangle]
pub extern "C" fn orchard_incoming_viewing_key_serialize(
key: *const IncomingViewingKey,
@ -190,6 +272,43 @@ pub extern "C" fn orchard_full_viewing_key_to_incoming_viewing_key(
.unwrap_or(std::ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn orchard_full_viewing_key_to_internal_incoming_viewing_key(
fvk: *const FullViewingKey,
) -> *mut IncomingViewingKey {
unsafe { fvk.as_ref() }
.map(|fvk| {
let internal_fvk = fvk.derive_internal();
Box::into_raw(Box::new(IncomingViewingKey::from(&internal_fvk)))
})
.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

@ -70,14 +70,18 @@ mod ed25519;
mod metrics_ffi;
mod streams_ffi;
mod tracing_ffi;
mod zcashd_orchard;
mod address_ffi;
mod builder_ffi;
mod history_ffi;
mod incremental_merkle_tree;
mod incremental_merkle_tree_ffi;
mod orchard_ffi;
mod orchard_keys_ffi;
mod transaction_ffi;
mod unified_keys_ffi;
mod wallet;
mod zip339_ffi;
mod test_harness_ffi;

View File

@ -56,8 +56,8 @@ pub extern "C" fn zcash_transaction_digests(
}
pub struct PrecomputedTxParts {
tx: Transaction,
txid_parts: TxDigests<Hash>,
pub(crate) tx: Transaction,
pub(crate) txid_parts: TxDigests<Hash>,
}
/// Precomputes the `TxDigest` struct for the given transaction.

View File

@ -139,13 +139,33 @@ pub extern "C" fn unified_full_viewing_key_read_sapling(
false
}
#[no_mangle]
pub extern "C" fn unified_full_viewing_key_read_orchard(
key: *const Ufvk,
out: *mut [u8; 96],
) -> bool {
let key = unsafe { key.as_ref() }.expect("Unified full viewing key pointer may not be null.");
let out = unsafe { &mut *out };
for r in &key.items() {
if let Fvk::Orchard(data) = r {
*out = *data;
return true;
}
}
false
}
#[no_mangle]
pub extern "C" fn unified_full_viewing_key_from_components(
t_key: *const [u8; 65],
sapling_key: *const [u8; 128],
orchard_key: *const [u8; 96],
) -> *mut Ufvk {
let t_key = unsafe { t_key.as_ref() };
let sapling_key = unsafe { sapling_key.as_ref() };
let orchard_key = unsafe { orchard_key.as_ref() };
let mut items = vec![];
if let Some(t_bytes) = t_key {
@ -154,6 +174,9 @@ pub extern "C" fn unified_full_viewing_key_from_components(
if let Some(sapling_bytes) = sapling_key {
items.push(Fvk::Sapling(*sapling_bytes));
}
if let Some(orchard_bytes) = orchard_key {
items.push(Fvk::Orchard(*orchard_bytes));
}
match Ufvk::try_from_items(items) {
Ok(ufvk) => Box::into_raw(Box::new(ufvk)),

1212
src/rust/src/wallet.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
use orchard::Address;
use std::cmp::Ordering;
/// Internal newtype wrapper that allows us to use addresses as
/// BTreeMap keys.
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub(crate) struct OrderedAddress(Address);
impl std::ops::Deref for OrderedAddress {
type Target = Address;
#[inline]
fn deref(&self) -> &Address {
&self.0
}
}
impl OrderedAddress {
pub(crate) fn new(addr: Address) -> Self {
OrderedAddress(addr)
}
}
impl PartialOrd for OrderedAddress {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OrderedAddress {
fn cmp(&self, other: &Self) -> Ordering {
(&self.to_raw_address_bytes()).cmp(&other.to_raw_address_bytes())
}
}

View File

@ -13,7 +13,9 @@
#include "undo.h"
#include "primitives/transaction.h"
#include "pubkey.h"
#include "transaction_builder.h"
#include "zcash/Note.hpp"
#include "zcash/address/mnemonic.h"
#include <vector>
#include <map>
@ -267,6 +269,7 @@ public:
CTransaction tx;
uint256 sproutNullifier;
uint256 saplingNullifier;
uint256 orchardNullifier;
TxWithNullifiers()
{
@ -282,7 +285,13 @@ public:
sd.nullifier = saplingNullifier;
mutableTx.vShieldedSpend.push_back(sd);
// TODO: Orchard nullifier
// The Orchard bundle builder always pads to two Actions, so we can just
// use an empty builder to create a dummy Orchard bundle.
uint256 orchardAnchor;
uint256 dataToBeSigned;
auto builder = orchard::Builder(true, true, orchardAnchor);
mutableTx.orchardBundle = builder.Build().value().ProveAndSign({}, dataToBeSigned).value();
orchardNullifier = mutableTx.orchardBundle.GetNullifiers()[0];
tx = CTransaction(mutableTx);
}
@ -302,21 +311,43 @@ uint256 appendRandomSproutCommitment(SproutMerkleTree &tree)
return cm;
}
template<typename Tree> void AppendRandomLeaf(Tree &tree);
template<> void AppendRandomLeaf(SproutMerkleTree &tree) { tree.append(GetRandHash()); }
template<> void AppendRandomLeaf(SaplingMerkleTree &tree) { tree.append(GetRandHash()); }
template<> void AppendRandomLeaf(OrchardMerkleFrontier &tree) {
// OrchardMerkleFrontier only has APIs to append entire bundles, but
// fortunately the tests only require that the tree root change.
// TODO: Remove the need to create proofs by having a testing-only way to
// append a random leaf to OrchardMerkleFrontier.
uint256 orchardAnchor;
uint256 dataToBeSigned;
auto builder = orchard::Builder(true, true, orchardAnchor);
auto bundle = builder.Build().value().ProveAndSign({}, dataToBeSigned).value();
tree.AppendBundle(bundle);
}
template<typename Tree> bool GetAnchorAt(const CCoinsViewCacheTest &cache, const uint256 &rt, Tree &tree);
template<> bool GetAnchorAt(const CCoinsViewCacheTest &cache, const uint256 &rt, SproutMerkleTree &tree) { return cache.GetSproutAnchorAt(rt, tree); }
template<> bool GetAnchorAt(const CCoinsViewCacheTest &cache, const uint256 &rt, SaplingMerkleTree &tree) { return cache.GetSaplingAnchorAt(rt, tree); }
template<> bool GetAnchorAt(const CCoinsViewCacheTest &cache, const uint256 &rt, OrchardMerkleFrontier &tree) { return cache.GetOrchardAnchorAt(rt, tree); }
BOOST_FIXTURE_TEST_SUITE(coins_tests, BasicTestingSetup)
BOOST_FIXTURE_TEST_SUITE(coins_tests, JoinSplitTestingSetup)
void checkNullifierCache(const CCoinsViewCacheTest &cache, const TxWithNullifiers &txWithNullifiers, bool shouldBeInCache) {
// Make sure the nullifiers have not gotten mixed up
BOOST_CHECK(!cache.GetNullifier(txWithNullifiers.sproutNullifier, SAPLING));
BOOST_CHECK(!cache.GetNullifier(txWithNullifiers.sproutNullifier, ORCHARD));
BOOST_CHECK(!cache.GetNullifier(txWithNullifiers.saplingNullifier, SPROUT));
BOOST_CHECK(!cache.GetNullifier(txWithNullifiers.saplingNullifier, ORCHARD));
BOOST_CHECK(!cache.GetNullifier(txWithNullifiers.orchardNullifier, SPROUT));
BOOST_CHECK(!cache.GetNullifier(txWithNullifiers.orchardNullifier, SAPLING));
// Check if the nullifiers either are or are not in the cache
bool containsSproutNullifier = cache.GetNullifier(txWithNullifiers.sproutNullifier, SPROUT);
bool containsSaplingNullifier = cache.GetNullifier(txWithNullifiers.saplingNullifier, SAPLING);
bool containsOrchardNullifier = cache.GetNullifier(txWithNullifiers.orchardNullifier, ORCHARD);
BOOST_CHECK(containsSproutNullifier == shouldBeInCache);
BOOST_CHECK(containsSaplingNullifier == shouldBeInCache);
BOOST_CHECK(containsOrchardNullifier == shouldBeInCache);
}
BOOST_AUTO_TEST_CASE(nullifier_regression_test)
@ -416,7 +447,7 @@ template<typename Tree> void anchorPopRegressionTestImpl(ShieldedType type)
// Create dummy anchor/commitment
Tree tree;
tree.append(GetRandHash());
AppendRandomLeaf(tree);
// Add the anchor
cache1.PushAnchor(tree);
@ -445,7 +476,7 @@ template<typename Tree> void anchorPopRegressionTestImpl(ShieldedType type)
// Create dummy anchor/commitment
Tree tree;
tree.append(GetRandHash());
AppendRandomLeaf(tree);
// Add the anchor and flush to disk
cache1.PushAnchor(tree);
@ -488,6 +519,9 @@ BOOST_AUTO_TEST_CASE(anchor_pop_regression_test)
BOOST_TEST_CONTEXT("Sapling") {
anchorPopRegressionTestImpl<SaplingMerkleTree>(SAPLING);
}
BOOST_TEST_CONTEXT("Orchard") {
anchorPopRegressionTestImpl<OrchardMerkleFrontier>(ORCHARD);
}
}
template<typename Tree> void anchorRegressionTestImpl(ShieldedType type)
@ -499,7 +533,7 @@ template<typename Tree> void anchorRegressionTestImpl(ShieldedType type)
// Insert anchor into base.
Tree tree;
tree.append(GetRandHash());
AppendRandomLeaf(tree);
cache1.PushAnchor(tree);
cache1.Flush();
@ -516,7 +550,7 @@ template<typename Tree> void anchorRegressionTestImpl(ShieldedType type)
// Insert anchor into base.
Tree tree;
tree.append(GetRandHash());
AppendRandomLeaf(tree);
cache1.PushAnchor(tree);
cache1.Flush();
@ -533,7 +567,7 @@ template<typename Tree> void anchorRegressionTestImpl(ShieldedType type)
// Insert anchor into base.
Tree tree;
tree.append(GetRandHash());
AppendRandomLeaf(tree);
cache1.PushAnchor(tree);
cache1.Flush();
@ -556,7 +590,7 @@ template<typename Tree> void anchorRegressionTestImpl(ShieldedType type)
// Insert anchor into base.
Tree tree;
tree.append(GetRandHash());
AppendRandomLeaf(tree);
cache1.PushAnchor(tree);
cache1.Flush();
@ -580,6 +614,9 @@ BOOST_AUTO_TEST_CASE(anchor_regression_test)
BOOST_TEST_CONTEXT("Sapling") {
anchorRegressionTestImpl<SaplingMerkleTree>(SAPLING);
}
BOOST_TEST_CONTEXT("Orchard") {
anchorRegressionTestImpl<OrchardMerkleFrontier>(ORCHARD);
}
}
BOOST_AUTO_TEST_CASE(nullifiers_test)
@ -613,7 +650,7 @@ template<typename Tree> void anchorsFlushImpl(ShieldedType type)
CCoinsViewCacheTest cache(&base);
Tree tree;
BOOST_CHECK(GetAnchorAt(cache, cache.GetBestAnchor(type), tree));
tree.append(GetRandHash());
AppendRandomLeaf(tree);
newrt = tree.root();
@ -643,6 +680,9 @@ BOOST_AUTO_TEST_CASE(anchors_flush_test)
BOOST_TEST_CONTEXT("Sapling") {
anchorsFlushImpl<SaplingMerkleTree>(SAPLING);
}
BOOST_TEST_CONTEXT("Orchard") {
anchorsFlushImpl<OrchardMerkleFrontier>(ORCHARD);
}
}
BOOST_AUTO_TEST_CASE(chained_joinsplits)
@ -738,13 +778,13 @@ template<typename Tree> void anchorsTestImpl(ShieldedType type)
BOOST_CHECK(GetAnchorAt(cache, cache.GetBestAnchor(type), tree));
BOOST_CHECK(cache.GetBestAnchor(type) == tree.root());
tree.append(GetRandHash());
tree.append(GetRandHash());
tree.append(GetRandHash());
tree.append(GetRandHash());
tree.append(GetRandHash());
tree.append(GetRandHash());
tree.append(GetRandHash());
AppendRandomLeaf(tree);
AppendRandomLeaf(tree);
AppendRandomLeaf(tree);
AppendRandomLeaf(tree);
AppendRandomLeaf(tree);
AppendRandomLeaf(tree);
AppendRandomLeaf(tree);
Tree save_tree_for_later;
save_tree_for_later = tree;
@ -762,8 +802,8 @@ template<typename Tree> void anchorsTestImpl(ShieldedType type)
BOOST_CHECK(confirm_same.root() == newrt);
}
tree.append(GetRandHash());
tree.append(GetRandHash());
AppendRandomLeaf(tree);
AppendRandomLeaf(tree);
newrt2 = tree.root();
@ -801,6 +841,9 @@ BOOST_AUTO_TEST_CASE(anchors_test)
BOOST_TEST_CONTEXT("Sapling") {
anchorsTestImpl<SaplingMerkleTree>(SAPLING);
}
BOOST_TEST_CONTEXT("Orchard") {
anchorsTestImpl<OrchardMerkleFrontier>(ORCHARD);
}
}
static const unsigned int NUM_SIMULATION_ITERATIONS = 40000;

View File

@ -15,6 +15,106 @@
#include <librustzcash.h>
#include <rust/ed25519.h>
uint256 ProduceZip244SignatureHash(
const CTransaction& tx,
const orchard::UnauthorizedBundle& orchardBundle)
{
uint256 dataToBeSigned;
PrecomputedTransactionData local(tx);
if (!zcash_builder_zip244_shielded_signature_digest(
local.preTx.release(),
orchardBundle.inner.get(),
dataToBeSigned.begin()))
{
throw std::logic_error("ZIP 225 signature hash failed");
}
return dataToBeSigned;
}
namespace orchard {
Builder::Builder(
bool spendsEnabled,
bool outputsEnabled,
uint256 anchor) : inner(nullptr, orchard_builder_free)
{
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,
CAmount value,
const std::optional<std::array<unsigned char, ZC_MEMO_SIZE>>& memo)
{
if (!inner) {
throw std::logic_error("orchard::Builder has already been used");
}
orchard_builder_add_recipient(
inner.get(),
ovk.has_value() ? ovk->begin() : nullptr,
to.inner.get(),
value,
memo.has_value() ? memo->data() : nullptr);
hasActions = true;
}
std::optional<UnauthorizedBundle> Builder::Build() {
if (!inner) {
throw std::logic_error("orchard::Builder has already been used");
}
auto bundle = orchard_builder_build(inner.release());
if (bundle == nullptr) {
return std::nullopt;
} else {
return UnauthorizedBundle(bundle);
}
}
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(), pKeys.data(), pKeys.size(), sighash.begin());
if (authorizedBundle == nullptr) {
return std::nullopt;
} else {
return OrchardBundle(authorizedBundle);
}
}
} // namespace orchard
SpendDescriptionInfo::SpendDescriptionInfo(
libzcash::SaplingExpandedSpendingKey expsk,
libzcash::SaplingNote note,
@ -150,10 +250,10 @@ std::string TransactionBuilderResult::GetError() {
TransactionBuilder::TransactionBuilder(
const Consensus::Params& consensusParams,
int nHeight,
std::optional<uint256> orchardAnchor,
CKeyStore* keystore,
CCoinsViewCache* coinsView,
CCriticalSection* cs_coinsView) :
usingSprout(std::nullopt),
consensusParams(consensusParams),
nHeight(nHeight),
keystore(keystore),
@ -161,6 +261,11 @@ TransactionBuilder::TransactionBuilder(
cs_coinsView(cs_coinsView)
{
mtx = CreateNewContextualCMutableTransaction(consensusParams, nHeight);
// Ignore the Orchard anchor if we can't use it yet.
if (orchardAnchor.has_value() && mtx.nVersion >= ZIP225_MIN_TX_VERSION) {
orchardBuilder = orchard::Builder(true, true, orchardAnchor.value());
}
}
// This exception is thrown in certain scenarios when building JoinSplits fails.
@ -174,7 +279,6 @@ private:
std::string msg;
};
void TransactionBuilder::SetExpiryHeight(uint32_t nExpiryHeight)
{
if (nExpiryHeight < nHeight || nExpiryHeight <= 0 || nExpiryHeight >= TX_EXPIRY_HEIGHT_THRESHOLD) {
@ -183,6 +287,59 @@ void TransactionBuilder::SetExpiryHeight(uint32_t nExpiryHeight)
mtx.nExpiryHeight = nExpiryHeight;
}
bool TransactionBuilder::SupportsOrchard() const {
return orchardBuilder.has_value();
}
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,
CAmount value,
const std::optional<std::array<unsigned char, ZC_MEMO_SIZE>>& memo)
{
if (!orchardBuilder.has_value()) {
// Try to give a useful error.
if (!(jsInputs.empty() && jsOutputs.empty())) {
throw std::runtime_error("TransactionBuilder cannot add Orchard output to Sprout transaction");
} else if (mtx.nVersion < ZIP225_MIN_TX_VERSION) {
throw std::runtime_error("TransactionBuilder cannot add Orchard output before NU5 activation");
} else {
throw std::runtime_error("TransactionBuilder cannot add Orchard output without Orchard anchor");
}
}
orchardBuilder.value().AddOutput(ovk, to, value, memo);
valueBalanceOrchard -= value;
}
void TransactionBuilder::AddSaplingSpend(
libzcash::SaplingExpandedSpendingKey expsk,
libzcash::SaplingNote note,
@ -285,6 +442,7 @@ void TransactionBuilder::SendChangeTo(
const libzcash::RecipientAddress& changeAddr,
const uint256& ovk) {
tChangeAddr = std::nullopt;
orchardChangeAddr = std::nullopt;
saplingChangeAddr = std::nullopt;
sproutChangeAddr = std::nullopt;
@ -297,12 +455,16 @@ void TransactionBuilder::SendChangeTo(
},
[&](const libzcash::SaplingPaymentAddress& changeDest) {
saplingChangeAddr = std::make_pair(ovk, changeDest);
},
[&](const libzcash::OrchardRawAddress& changeDest) {
orchardChangeAddr = std::make_pair(ovk, changeDest);
}
}, changeAddr);
}
void TransactionBuilder::SendChangeToSprout(const libzcash::SproutPaymentAddress& zaddr) {
tChangeAddr = std::nullopt;
orchardChangeAddr = std::nullopt;
saplingChangeAddr = std::nullopt;
sproutChangeAddr = zaddr;
}
@ -314,7 +476,7 @@ TransactionBuilderResult TransactionBuilder::Build()
//
// Valid change
CAmount change = mtx.valueBalanceSapling - fee;
CAmount change = mtx.valueBalanceSapling + valueBalanceOrchard - fee;
for (auto jsInput : jsInputs) {
change += jsInput.note.value();
}
@ -340,13 +502,18 @@ TransactionBuilderResult TransactionBuilder::Build()
// was set, send change to the first Sapling address given as input
// if any; otherwise the first Sprout address given as input.
// (A t-address can only be used as the change address if explicitly set.)
if (saplingChangeAddr) {
if (orchardChangeAddr) {
AddOrchardOutput(orchardChangeAddr->first, orchardChangeAddr->second, change, std::nullopt);
} else if (saplingChangeAddr) {
AddSaplingOutput(saplingChangeAddr->first, saplingChangeAddr->second, change);
} else if (sproutChangeAddr) {
AddSproutOutput(sproutChangeAddr.value(), change);
} 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;
@ -360,6 +527,20 @@ TransactionBuilderResult TransactionBuilder::Build()
}
}
//
// Orchard
//
std::optional<orchard::UnauthorizedBundle> orchardBundle;
if (orchardBuilder.has_value() && orchardBuilder->HasActions()) {
auto bundle = orchardBuilder->Build();
if (bundle.has_value()) {
orchardBundle = std::move(bundle);
} else {
return TransactionBuilderResult("Failed to build Orchard bundle");
}
}
//
// Sapling spends and outputs
//
@ -449,14 +630,29 @@ TransactionBuilderResult TransactionBuilder::Build()
// Empty output script.
uint256 dataToBeSigned;
CScript scriptCode;
try {
dataToBeSigned = SignatureHash(scriptCode, mtx, NOT_AN_INPUT, SIGHASH_ALL, 0, consensusBranchId);
if (orchardBundle.has_value()) {
// Orchard is only usable with v5+ transactions.
dataToBeSigned = ProduceZip244SignatureHash(mtx, orchardBundle.value());
} else {
CScript scriptCode;
dataToBeSigned = SignatureHash(scriptCode, mtx, NOT_AN_INPUT, SIGHASH_ALL, 0, consensusBranchId);
}
} catch (std::logic_error ex) {
librustzcash_sapling_proving_ctx_free(ctx);
return TransactionBuilderResult("Could not construct signature hash: " + std::string(ex.what()));
}
if (orchardBundle.has_value()) {
auto authorizedBundle = orchardBundle.value().ProveAndSign(
orchardSpendingKeys, dataToBeSigned);
if (authorizedBundle.has_value()) {
mtx.orchardBundle = authorizedBundle.value();
} else {
return TransactionBuilderResult("Failed to create Orchard proof or signatures");
}
}
// Create Sapling spendAuth and binding signatures
for (size_t i = 0; i < spends.size(); i++) {
librustzcash_sapling_spend_sig(
@ -513,15 +709,11 @@ TransactionBuilderResult TransactionBuilder::Build()
void TransactionBuilder::CheckOrSetUsingSprout()
{
if (usingSprout.has_value()) {
if (!usingSprout.value()) {
throw JSONRPCError(RPC_WALLET_ERROR, "Can't use Sprout with a v5 transaction.");
}
if (orchardBuilder.has_value()) {
throw JSONRPCError(RPC_WALLET_ERROR, "Can't use Sprout with a v5 transaction.");
} else {
usingSprout = true;
// Switch if necessary to a Sprout-supporting transaction format.
auto txVersionInfo = CurrentTxVersionInfo(consensusParams, nHeight, usingSprout.value());
auto txVersionInfo = CurrentTxVersionInfo(consensusParams, nHeight, true);
mtx.nVersionGroupId = txVersionInfo.nVersionGroupId;
mtx.nVersion = txVersionInfo.nVersion;
mtx.nConsensusBranchId = std::nullopt;

View File

@ -21,8 +21,161 @@
#include <optional>
#include <rust/builder.h>
#define NO_MEMO {{0xF6}}
class OrchardWallet;
namespace orchard { class UnauthorizedBundle; }
uint256 ProduceZip244SignatureHash(
const CTransaction& tx,
const orchard::UnauthorizedBundle& orchardBundle);
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 {
private:
/// The Orchard builder. Memory is allocated by Rust. If this is `nullptr` then
/// `Builder::Build` has been called, and all subsequent operations will throw an
/// exception.
std::unique_ptr<OrchardBuilderPtr, decltype(&orchard_builder_free)> inner;
bool hasActions;
Builder() : inner(nullptr, orchard_builder_free), hasActions(false) { }
public:
Builder(bool spendsEnabled, bool outputsEnabled, uint256 anchor);
// Builder should never be copied
Builder(const Builder&) = delete;
Builder& operator=(const Builder&) = delete;
Builder(Builder&& builder) : inner(std::move(builder.inner)) {}
Builder& operator=(Builder&& builder)
{
if (this != &builder) {
inner = std::move(builder.inner);
}
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,
const libzcash::OrchardRawAddress& to,
CAmount value,
const std::optional<std::array<unsigned char, ZC_MEMO_SIZE>>& memo);
/// Returns `true` if any spends or outputs have been added to this builder. This can
/// be used to avoid calling `Build()` and creating a dummy Orchard bundle.
bool HasActions() {
return hasActions;
}
/// Builds a bundle containing the given spent notes and recipients.
///
/// Returns `std::nullopt` if an error occurs.
///
/// Calling this method invalidates this object; in particular, if an error occurs
/// this builder must be discarded and a new builder created. Subsequent usage of this
/// object in any way will cause an exception. This emulates Rust's compile-time move
/// semantics at runtime.
std::optional<UnauthorizedBundle> Build();
};
/// An unauthorized Orchard bundle, ready for its proof to be created and signatures
/// applied.
class UnauthorizedBundle {
private:
/// An optional Orchard bundle (with `nullptr` corresponding to `None`).
/// Memory is allocated by Rust.
std::unique_ptr<OrchardUnauthorizedBundlePtr, decltype(&orchard_unauthorized_bundle_free)> inner;
UnauthorizedBundle() : inner(nullptr, orchard_unauthorized_bundle_free) {}
UnauthorizedBundle(OrchardUnauthorizedBundlePtr* bundle) : inner(bundle, orchard_unauthorized_bundle_free) {}
friend class Builder;
// The parentheses here are necessary to avoid the following compilation error:
// error: C++ requires a type specifier for all declarations
// friend uint256 ::ProduceZip244SignatureHash(
// ~~~~~~ ^
friend uint256 (::ProduceZip244SignatureHash(
const CTransaction& tx,
const UnauthorizedBundle& orchardBundle));
public:
// UnauthorizedBundle should never be copied
UnauthorizedBundle(const UnauthorizedBundle&) = delete;
UnauthorizedBundle& operator=(const UnauthorizedBundle&) = delete;
UnauthorizedBundle(UnauthorizedBundle&& bundle) : inner(std::move(bundle.inner)) {}
UnauthorizedBundle& operator=(UnauthorizedBundle&& bundle)
{
if (this != &bundle) {
inner = std::move(bundle.inner);
}
return *this;
}
/// Adds proofs and signatures to this bundle.
///
/// Returns `std::nullopt` if an error occurs.
///
/// Calling this method invalidates this object; in particular, if an error occurs
/// 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(
const std::vector<libzcash::OrchardSpendingKey>& keys, uint256 sighash);
};
} // namespace orchard
struct SpendDescriptionInfo {
libzcash::SaplingExpandedSpendingKey expsk;
libzcash::SaplingNote note;
@ -107,7 +260,6 @@ public:
class TransactionBuilder
{
private:
std::optional<bool> usingSprout;
Consensus::Params consensusParams;
int nHeight;
const CKeyStore* keystore;
@ -115,7 +267,11 @@ private:
CCriticalSection* cs_coinsView;
CMutableTransaction mtx;
CAmount fee = 10000;
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;
@ -123,6 +279,7 @@ private:
std::vector<TransparentInputInfo> tIns;
std::optional<std::pair<uint256, libzcash::SaplingPaymentAddress>> saplingChangeAddr;
std::optional<std::pair<uint256, libzcash::OrchardRawAddress>> orchardChangeAddr;
std::optional<libzcash::SproutPaymentAddress> sproutChangeAddr;
std::optional<CTxDestination> tChangeAddr;
@ -131,14 +288,72 @@ public:
TransactionBuilder(
const Consensus::Params& consensusParams,
int nHeight,
std::optional<uint256> orchardAnchor,
CKeyStore* keyStore = nullptr,
CCoinsViewCache* coinsView = nullptr,
CCriticalSection* cs_coinsView = nullptr);
// TransactionBuilder should never be copied
TransactionBuilder(const TransactionBuilder&) = delete;
TransactionBuilder& operator=(const TransactionBuilder&) = delete;
TransactionBuilder(TransactionBuilder&& builder) :
consensusParams(std::move(builder.consensusParams)),
nHeight(std::move(builder.nHeight)),
keystore(std::move(builder.keystore)),
coinsView(std::move(builder.coinsView)),
cs_coinsView(std::move(builder.cs_coinsView)),
mtx(std::move(builder.mtx)),
fee(std::move(builder.fee)),
orchardBuilder(std::move(builder.orchardBuilder)),
valueBalanceOrchard(std::move(builder.valueBalanceOrchard)),
spends(std::move(builder.spends)),
outputs(std::move(builder.outputs)),
jsInputs(std::move(builder.jsInputs)),
jsOutputs(std::move(builder.jsOutputs)),
tIns(std::move(builder.tIns)),
saplingChangeAddr(std::move(builder.saplingChangeAddr)),
sproutChangeAddr(std::move(builder.sproutChangeAddr)),
tChangeAddr(std::move(builder.tChangeAddr)) {}
TransactionBuilder& operator=(TransactionBuilder&& builder)
{
if (this != &builder) {
consensusParams = std::move(builder.consensusParams);
nHeight = std::move(builder.nHeight);
keystore = std::move(builder.keystore);
coinsView = std::move(builder.coinsView);
cs_coinsView = std::move(builder.cs_coinsView);
mtx = std::move(builder.mtx);
fee = std::move(builder.fee);
orchardBuilder = std::move(builder.orchardBuilder);
valueBalanceOrchard = std::move(builder.valueBalanceOrchard);
spends = std::move(builder.spends);
outputs = std::move(builder.outputs);
jsInputs = std::move(builder.jsInputs);
jsOutputs = std::move(builder.jsOutputs);
tIns = std::move(builder.tIns);
saplingChangeAddr = std::move(builder.saplingChangeAddr);
sproutChangeAddr = std::move(builder.sproutChangeAddr);
tChangeAddr = std::move(builder.tChangeAddr);
}
return *this;
}
void SetExpiryHeight(uint32_t nExpiryHeight);
void SetFee(CAmount fee);
bool SupportsOrchard() const;
bool AddOrchardSpend(
libzcash::OrchardSpendingKey sk,
orchard::SpendInfo spendInfo);
void AddOrchardOutput(
const std::optional<uint256>& ovk,
const libzcash::OrchardRawAddress& to,
CAmount value,
const std::optional<std::array<unsigned char, ZC_MEMO_SIZE>>& memo);
// Throws if the anchor does not match the anchor used by
// previously-added Sapling spends.
void AddSaplingSpend(

View File

@ -8,6 +8,7 @@
#include "transaction_builder.h"
#include <array>
#include <optional>
#include <rust/ed25519.h>
@ -348,7 +349,7 @@ CWalletTx GetValidSaplingReceive(const Consensus::Params& consensusParams,
auto fvk = sk.expsk.full_viewing_key();
auto pa = sk.ToXFVK().DefaultAddress();
auto builder = TransactionBuilder(consensusParams, 1, &keyStore);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keyStore);
builder.SetFee(0);
builder.AddTransparentInput(COutPoint(), scriptPubKey, value);
builder.AddSaplingOutput(fvk.ovk, pa, value, {});

View File

@ -42,7 +42,7 @@ protected:
virtual void Inventory(const uint256 &hash) {}
virtual void ResendWalletTransactions(int64_t nBestBlockTime) {}
virtual void BlockChecked(const CBlock&, const CValidationState&) {}
virtual void GetAddressForMining(MinerAddress&) {};
virtual void GetAddressForMining(std::optional<MinerAddress>&) {};
virtual void ResetRequestCount(const uint256 &hash) {};
friend void ::RegisterValidationInterface(CValidationInterface*);
friend void ::UnregisterValidationInterface(CValidationInterface*);
@ -67,7 +67,7 @@ struct CMainSignals {
/** Notifies listeners of a block validation result */
boost::signals2::signal<void (const CBlock&, const CValidationState&)> BlockChecked;
/** Notifies listeners that an address for mining is required (coinbase) */
boost::signals2::signal<void (MinerAddress&)> AddressForMining;
boost::signals2::signal<void (std::optional<MinerAddress>&)> AddressForMining;
/** Notifies listeners that a block has been successfully mined */
boost::signals2::signal<void (const uint256 &)> BlockFound;
};

View File

@ -89,7 +89,7 @@ AsyncRPCOperation_mergetoaddress::AsyncRPCOperation_mergetoaddress(
isUsingBuilder_ = false;
if (builder) {
isUsingBuilder_ = true;
builder_ = builder.value();
builder_ = std::move(builder.value());
}
KeyIO keyIO(Params());

View File

@ -80,12 +80,13 @@ bool AsyncRPCOperation_saplingmigration::main_impl() {
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
std::vector<OrchardNoteMetadata> orchardEntries;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
// We set minDepth to 11 to avoid unconfirmed notes and in anticipation of specifying
// an anchor at height N-10 for each Sprout JoinSplit description
// Consider, should notes be sorted?
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 11);
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 11);
}
CAmount availableFunds = 0;
for (const SproutNoteEntry& sproutEntry : sproutEntries) {
@ -111,7 +112,7 @@ bool AsyncRPCOperation_saplingmigration::main_impl() {
CCoinsViewCache coinsView(pcoinsTip);
do {
CAmount amountToSend = chooseAmount(availableFunds);
auto builder = TransactionBuilder(consensusParams, targetHeight_, pwalletMain, &coinsView, &cs_main);
auto builder = TransactionBuilder(consensusParams, targetHeight_, std::nullopt, pwalletMain, &coinsView, &cs_main);
builder.SetExpiryHeight(targetHeight_ + MIGRATION_EXPIRY_DELTA);
LogPrint("zrpcunsafe", "%s: Beginning creating transaction with Sapling output amount=%s\n", getId(), FormatMoney(amountToSend - DEFAULT_FEE));
std::vector<SproutNoteEntry> fromNotes;
@ -122,6 +123,7 @@ bool AsyncRPCOperation_saplingmigration::main_impl() {
fromNoteAmount += sproutEntry.note.value();
}
availableFunds -= fromNoteAmount;
std::optional<libzcash::SproutPaymentAddress> changeAddr;
for (const SproutNoteEntry& sproutEntry : fromNotes) {
std::string data(sproutEntry.memo.begin(), sproutEntry.memo.end());
LogPrint("zrpcunsafe", "%s: Adding Sprout note input (txid=%s, vJoinSplit=%d, jsoutindex=%d, amount=%s, memo=%s)\n",
@ -142,11 +144,17 @@ bool AsyncRPCOperation_saplingmigration::main_impl() {
std::vector<std::optional<SproutWitness>> vInputWitnesses;
pwalletMain->GetSproutNoteWitnesses(vOutPoints, vInputWitnesses, inputAnchor);
builder.AddSproutInput(sproutSk, sproutEntry.note, vInputWitnesses[0].value());
// Send change to the address of the first input
if (!changeAddr.has_value()) {
changeAddr = sproutSk.address();
}
}
assert(changeAddr.has_value());
// The amount chosen *includes* the default fee for this transaction, i.e.
// the value of the Sapling output will be 0.00001 ZEC less.
builder.SetFee(DEFAULT_FEE);
builder.AddSaplingOutput(ovkForShieldingFromTaddr(seed), migrationDestAddress, amountToSend - DEFAULT_FEE);
builder.SendChangeToSprout(changeAddr.value());
CTransaction tx = builder.Build().GetTxOrThrow();
if (isCancelled()) {
LogPrint("zrpcunsafe", "%s: Canceled. Stopping.\n", getId());

View File

@ -48,11 +48,11 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
ZTXOSelector ztxoSelector,
std::vector<SendManyRecipient> recipients,
int minDepth,
TransactionStrategy strategy,
CAmount fee,
bool allowRevealedAmounts,
UniValue contextInfo) :
builder_(builder), ztxoSelector_(ztxoSelector), recipients_(recipients),
mindepth_(minDepth), fee_(fee), allowRevealedAmounts_(allowRevealedAmounts),
builder_(std::move(builder)), ztxoSelector_(ztxoSelector), recipients_(recipients),
mindepth_(minDepth), strategy_(strategy), fee_(fee),
contextinfo_(contextInfo)
{
assert(fee_ >= 0);
@ -62,38 +62,77 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
sendFromAccount_ = pwalletMain->FindAccountForSelector(ztxoSelector_).value_or(ZCASH_LEGACY_ACCOUNT);
// we always allow shielded change when not sending from the legacy account
if (sendFromAccount_ != ZCASH_LEGACY_ACCOUNT) {
allowedChangeTypes_.insert(libzcash::ChangeType::Sapling);
}
// calculate the target totals
// Determine the target totals and recipient pools
for (const SendManyRecipient& recipient : recipients_) {
std::visit(match {
[&](const CKeyID& addr) {
transparentRecipients_ += 1;
txOutputAmounts_.t_outputs_total += recipient.amount;
allowedChangeTypes_.insert(libzcash::ChangeType::Transparent);
recipientPools_.insert(OutputPool::Transparent);
},
[&](const CScriptID& addr) {
transparentRecipients_ += 1;
txOutputAmounts_.t_outputs_total += recipient.amount;
allowedChangeTypes_.insert(libzcash::ChangeType::Transparent);
recipientPools_.insert(OutputPool::Transparent);
},
[&](const libzcash::SaplingPaymentAddress& addr) {
txOutputAmounts_.sapling_outputs_total += recipient.amount;
if (ztxoSelector_.SelectsSprout() && !allowRevealedAmounts_) {
throw JSONRPCError(
recipientPools_.insert(OutputPool::Sapling);
if (!(ztxoSelector_.SelectsSapling() || strategy_.AllowRevealedAmounts())) {
if (ztxoSelector_.SelectsSprout()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Sending between shielded pools is not enabled by default because it will "
"Sending from the Sprout shielded pool to the Sapling "
"shielded pool is not enabled by default because it will "
"publicly reveal the transaction amount. THIS MAY AFFECT YOUR PRIVACY. "
"Resubmit with the `allowRevealedAmounts` parameter set to `true` if "
"you wish to allow this transaction to proceed anyway.");
"Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` "
"or weaker if you wish to allow this transaction to proceed anyway.");
}
if (builder_.SupportsOrchard() && ztxoSelector_.SelectsOrchard()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Sending from the Orchard shielded pool to the Sapling "
"shielded pool is not enabled by default because it will "
"publicly reveal the transaction amount. THIS MAY AFFECT YOUR PRIVACY. "
"Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` "
"or weaker if you wish to allow this transaction to proceed anyway.");
}
// If the source selects transparent then we don't show an
// error because we are necessarily revealing information.
}
},
[&](const libzcash::OrchardRawAddress& addr) {
txOutputAmounts_.orchard_outputs_total += recipient.amount;
recipientPools_.insert(OutputPool::Orchard);
// No transaction allows sends from Sprout to Orchard.
assert(!ztxoSelector_.SelectsSprout());
if (!((builder_.SupportsOrchard() && ztxoSelector_.SelectsOrchard()) || strategy_.AllowRevealedAmounts())) {
if (ztxoSelector_.SelectsSapling()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Sending from the Sapling shielded pool to the Orchard "
"shielded pool is not enabled by default because it will "
"publicly reveal the transaction amount. THIS MAY AFFECT YOUR PRIVACY. "
"Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` "
"or weaker if you wish to allow this transaction to proceed anyway.");
}
// If the source selects transparent then we don't show an
// error because we are necessarily revealing information.
}
}
}, recipient.address);
}
if (recipientPools_.count(OutputPool::Transparent) && !strategy_.AllowRevealedRecipients()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"This transaction would have transparent recipients, which is not "
"enabled by default because it will publicly reveal transaction "
"recipients and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit "
"with the `privacyPolicy` parameter set to `AllowRevealedRecipients` "
"or weaker if you wish to allow this transaction to proceed anyway.");
}
// Log the context info i.e. the call parameters to z_sendmany
if (LogAcceptCategory("zrpcunsafe")) {
LogPrint("zrpcunsafe", "%s: z_sendmany initialized (params=%s)\n", getId(), contextInfo.write());
@ -176,7 +215,10 @@ void AsyncRPCOperation_sendmany::main() {
//
// At least 4. and 5. differ from the Rust transaction builder.
uint256 AsyncRPCOperation_sendmany::main_impl() {
CAmount sendAmount = txOutputAmounts_.sapling_outputs_total + txOutputAmounts_.t_outputs_total;
CAmount sendAmount = (
txOutputAmounts_.orchard_outputs_total +
txOutputAmounts_.sapling_outputs_total +
txOutputAmounts_.t_outputs_total);
CAmount targetAmount = sendAmount + fee_;
builder_.SetFee(fee_);
@ -191,8 +233,12 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
// Find spendable inputs, and select a minimal set of them that
// can supply the required target amount.
auto spendable = pwalletMain->FindSpendableInputs(ztxoSelector_, allowTransparentCoinbase, mindepth_);
if (!spendable.LimitToAmount(targetAmount, dustThreshold)) {
SpendableInputs spendable;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
spendable = pwalletMain->FindSpendableInputs(ztxoSelector_, allowTransparentCoinbase, mindepth_);
}
if (!spendable.LimitToAmount(targetAmount, dustThreshold, recipientPools_)) {
CAmount changeAmount{spendable.Total() - targetAmount};
if (changeAmount > 0 && changeAmount < dustThreshold) {
// TODO: we should provide the option for the caller to explicitly
@ -211,6 +257,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
FormatMoney(changeAmount),
FormatMoney(dustThreshold)));
} else {
bool isFromUa = std::holds_alternative<libzcash::UnifiedAddress>(ztxoSelector_.GetPattern());
throw JSONRPCError(
RPC_WALLET_INSUFFICIENT_FUNDS,
strprintf(
@ -219,10 +266,27 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
+ (allowTransparentCoinbase ? "" :
"; note that coinbase outputs will not be selected if you specify "
"ANY_TADDR or if any transparent recipients are included.")
+ ((!isFromUa || strategy_.AllowLinkingAccountAddresses()) ? "" :
" (This transaction may require selecting transparent coins that were sent "
"to multiple Unified Addresses, which is not enabled by default because "
"it would create a public link between the transparent receivers of these "
"addresses. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` "
"parameter set to `AllowLinkingAccountAddresses` or weaker if you wish to "
"allow this transaction to proceed anyway.)")
);
}
}
if (!(spendable.utxos.empty() || strategy_.AllowRevealedSenders())) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"This transaction requires selecting transparent coins, which is "
"not enabled by default because it will publicly reveal transaction "
"senders and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit "
"with the `privacyPolicy` parameter set to `AllowRevealedSenders` "
"or weaker if you wish to allow this transaction to proceed anyway.");
}
spendable.LogInputs(getId());
CAmount t_inputs_total{0};
@ -236,6 +300,9 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
for (const auto& t : spendable.saplingNoteEntries) {
z_inputs_total += t.note.value();
}
for (const auto& t : spendable.orchardNoteMetadata) {
z_inputs_total += t.GetNoteValue();
}
if (z_inputs_total > 0 && mindepth_ == 0) {
throw JSONRPCError(
@ -268,21 +335,49 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
LogPrint("zrpcunsafe", "%s: total shielded input: %s (to choose from)\n", getId(), FormatMoney(z_inputs_total));
LogPrint("zrpc", "%s: total transparent output: %s\n", getId(), FormatMoney(txOutputAmounts_.t_outputs_total));
LogPrint("zrpcunsafe", "%s: total shielded Sapling output: %s\n", getId(), FormatMoney(txOutputAmounts_.sapling_outputs_total));
LogPrint("zrpcunsafe", "%s: total shielded Orchard output: %s\n", getId(), FormatMoney(txOutputAmounts_.orchard_outputs_total));
LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(fee_));
// Allow change to go to any pool for which we have recipients.
std::set<OutputPool> allowedChangeTypes = recipientPools_;
// We always allow shielded change when not sending from the legacy account.
if (sendFromAccount_ != ZCASH_LEGACY_ACCOUNT) {
allowedChangeTypes.insert(OutputPool::Sapling);
}
auto ovks = this->SelectOVKs(spendable);
auto allowChangeTypes = [&](const std::set<ReceiverType>& receiverTypes) {
for (ReceiverType rtype : receiverTypes) {
switch (rtype) {
case ReceiverType::P2PKH:
case ReceiverType::P2SH:
allowedChangeTypes.insert(OutputPool::Transparent);
break;
case ReceiverType::Sapling:
allowedChangeTypes.insert(OutputPool::Sapling);
break;
case ReceiverType::Orchard:
if (builder_.SupportsOrchard()) {
allowedChangeTypes.insert(OutputPool::Orchard);
}
break;
}
}
};
std::visit(match {
[&](const CKeyID& keyId) {
allowedChangeTypes_.insert(libzcash::ChangeType::Transparent);
allowedChangeTypes.insert(OutputPool::Transparent);
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount_, allowedChangeTypes_);
sendFromAccount_, allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
},
[&](const CScriptID& scriptId) {
allowedChangeTypes_.insert(libzcash::ChangeType::Transparent);
allowedChangeTypes.insert(OutputPool::Transparent);
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount_, allowedChangeTypes_);
sendFromAccount_, allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
},
@ -302,7 +397,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
builder_.SendChangeTo(addr, ovks.first);
} else {
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount_, allowedChangeTypes_);
sendFromAccount_, allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
}
@ -315,37 +410,45 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
builder_.SendChangeTo(fvk.DefaultAddress(), ovks.first);
} else {
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount_, allowedChangeTypes_);
sendFromAccount_, allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
}
},
[&](const libzcash::UnifiedFullViewingKey& fvk) {
auto zufvk = ZcashdUnifiedFullViewingKey::FromUnifiedFullViewingKey(Params(), fvk);
auto changeAddr = zufvk.GetChangeAddress();
[&](const libzcash::UnifiedAddress& ua) {
allowChangeTypes(ua.GetKnownReceiverTypes());
auto zufvk = pwalletMain->GetUFVKForAddress(ua);
if (!zufvk.has_value()) {
throw JSONRPCError(
RPC_WALLET_ERROR,
"Could not determine full viewing key for unified address.");
}
auto changeAddr = zufvk.value().GetChangeAddress(allowedChangeTypes);
if (!changeAddr.has_value()) {
throw JSONRPCError(
RPC_WALLET_ERROR,
"Could not generate a change address from the specified full viewing key ");
"Could not generate a change address from the inferred full viewing key.");
}
builder_.SendChangeTo(changeAddr.value(), ovks.first);
},
[&](const libzcash::UnifiedFullViewingKey& fvk) {
allowChangeTypes(fvk.GetKnownReceiverTypes());
auto zufvk = ZcashdUnifiedFullViewingKey::FromUnifiedFullViewingKey(Params(), fvk);
auto changeAddr = zufvk.GetChangeAddress(allowedChangeTypes);
if (!changeAddr.has_value()) {
throw JSONRPCError(
RPC_WALLET_ERROR,
"Could not generate a change address from the specified full viewing key.");
}
builder_.SendChangeTo(changeAddr.value(), ovks.first);
},
[&](const AccountZTXOPattern& acct) {
for (ReceiverType rtype : acct.GetReceiverTypes()) {
switch (rtype) {
case ReceiverType::P2PKH:
case ReceiverType::P2SH:
allowedChangeTypes_.insert(libzcash::ChangeType::Transparent);
break;
case ReceiverType::Sapling:
allowedChangeTypes_.insert(libzcash::ChangeType::Sapling);
break;
}
}
allowChangeTypes(acct.GetReceiverTypes());
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
acct.GetAccountId(),
allowedChangeTypes_);
allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
@ -375,12 +478,28 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
}
}
// Fetch Sapling anchor and witnesses
// Fetch Sapling anchor and witnesses, and Orchard Merkle paths.
uint256 anchor;
std::vector<std::optional<SaplingWitness>> witnesses;
std::vector<std::pair<libzcash::OrchardSpendingKey, orchard::SpendInfo>> orchardSpendInfo;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
pwalletMain->GetSaplingNoteWitnesses(saplingOutPoints, witnesses, anchor);
orchardSpendInfo = pwalletMain->GetOrchardSpendInfo(spendable.orchardNoteMetadata);
}
// Add Orchard spends
for (size_t i = 0; i < orchardSpendInfo.size(); i++) {
auto spendInfo = std::move(orchardSpendInfo[i]);
if (!builder_.AddOrchardSpend(
std::move(spendInfo.first),
std::move(spendInfo.second)))
{
throw JSONRPCError(
RPC_WALLET_ERROR,
"Failed to add Orchard note to transaction (check debug.log for details)"
);
}
}
// Add Sapling spends
@ -397,7 +516,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
builder_.AddSaplingSpend(saplingKeys[i].expsk, saplingNotes[i], anchor, witnesses[i].value());
}
// Add Sapling and transparent outputs
// Add outputs
for (const auto& r : recipients_) {
std::visit(match {
[&](const CKeyID& keyId) {
@ -408,9 +527,15 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
},
[&](const libzcash::SaplingPaymentAddress& addr) {
auto value = r.amount;
auto memo = get_memo_from_hex_string(r.memo.has_value() ? r.memo.value() : "");
auto memo = get_memo_from_hex_string(r.memo.value_or(""));
builder_.AddSaplingOutput(ovks.second, addr, value, memo);
},
[&](const libzcash::OrchardRawAddress& addr) {
auto value = r.amount;
auto memo = r.memo.has_value() ? std::optional(get_memo_from_hex_string(r.memo.value())) : std::nullopt;
builder_.AddOrchardOutput(ovks.second, addr, value, memo);
}
}, r.address);
}
@ -475,7 +600,37 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
std::pair<uint256, uint256> AsyncRPCOperation_sendmany::SelectOVKs(const SpendableInputs& spendable) const {
uint256 internalOVK;
uint256 externalOVK;
if (!spendable.saplingNoteEntries.empty()) {
if (!spendable.orchardNoteMetadata.empty()) {
std::optional<OrchardFullViewingKey> fvk;
std::visit(match {
[&](const UnifiedAddress& addr) {
auto ufvk = pwalletMain->GetUFVKForAddress(addr);
// This is safe because spending key checks will have ensured that we
// have a UFVK corresponding to this address, and Orchard notes will
// not have been selected if the UFVK does not contain an Orchard key.
fvk = ufvk.value().GetOrchardKey().value();
},
[&](const UnifiedFullViewingKey& ufvk) {
// Orchard notes will not have been selected if the UFVK does not contain
// an Orchard key.
fvk = ufvk.GetOrchardKey().value();
},
[&](const AccountZTXOPattern& acct) {
// By definition, we have a UFVK for every known account.
auto ufvk = pwalletMain->GetUnifiedFullViewingKeyByAccount(acct.GetAccountId());
// Orchard notes will not have been selected if the UFVK does not contain
// an Orchard key.
fvk = ufvk.value().GetOrchardKey().value();
},
[&](const auto& other) {
throw std::runtime_error("SelectOVKs: Selector cannot select Orchard notes.");
}
}, this->ztxoSelector_.GetPattern());
assert(fvk.has_value());
internalOVK = fvk.value().ToInternalOutgoingViewingKey();
externalOVK = fvk.value().ToExternalOutgoingViewingKey();
} else if (!spendable.saplingNoteEntries.empty()) {
std::optional<SaplingDiversifiableFullViewingKey> dfvk;
std::visit(match {
[&](const libzcash::SaplingPaymentAddress& addr) {
@ -483,12 +638,27 @@ std::pair<uint256, uint256> AsyncRPCOperation_sendmany::SelectOVKs(const Spendab
assert(pwalletMain->GetSaplingExtendedSpendingKey(addr, extsk));
dfvk = extsk.ToXFVK();
},
[&](const UnifiedAddress& addr) {
auto ufvk = pwalletMain->GetUFVKForAddress(addr);
// This is safe because spending key checks will have ensured that we
// have a UFVK corresponding to this address, and Sapling notes will
// not have been selected if the UFVK does not contain a Sapling key.
dfvk = ufvk.value().GetSaplingKey().value();
},
[&](const UnifiedFullViewingKey& ufvk) {
// Sapling notes will not have been selected if the UFVK does not contain
// a Sapling key.
dfvk = ufvk.GetSaplingKey().value();
},
[&](const AccountZTXOPattern& acct) {
// By definition, we have a UFVK for every known account.
auto ufvk = pwalletMain->GetUnifiedFullViewingKeyByAccount(acct.GetAccountId());
// Sapling notes will not have been selected if the UFVK does not contain
// a Sapling key.
dfvk = ufvk.value().GetSaplingKey().value();
},
[&](const auto& other) {
throw std::runtime_error("unreachable");
throw std::runtime_error("SelectOVKs: Selector cannot select Sapling notes.");
}
}, this->ztxoSelector_.GetPattern());
assert(dfvk.has_value());
@ -505,16 +675,31 @@ std::pair<uint256, uint256> AsyncRPCOperation_sendmany::SelectOVKs(const Spendab
[&](const CScriptID& keyId) {
tfvk = pwalletMain->GetLegacyAccountKey().ToAccountPubKey();
},
[&](const UnifiedAddress& addr) {
// This is safe because spending key checks will have ensured that we
// have a UFVK corresponding to this address, and transparent UTXOs will
// not have been selected if the UFVK does not contain a transparent key.
auto ufvk = pwalletMain->GetUFVKForAddress(addr);
tfvk = ufvk.value().GetTransparentKey().value();
},
[&](const UnifiedFullViewingKey& ufvk) {
// Transparent UTXOs will not have been selected if the UFVK does not contain
// a transparent key.
tfvk = ufvk.GetTransparentKey().value();
},
[&](const AccountZTXOPattern& acct) {
if (acct.GetAccountId() == ZCASH_LEGACY_ACCOUNT) {
tfvk = pwalletMain->GetLegacyAccountKey().ToAccountPubKey();
} else {
// By definition, we have a UFVK for every known account.
auto ufvk = pwalletMain->GetUnifiedFullViewingKeyByAccount(acct.GetAccountId()).value();
// Transparent UTXOs will not have been selected if the UFVK does not contain
// a transparent key.
tfvk = ufvk.GetTransparentKey().value();
}
},
[&](const auto& other) {
throw std::runtime_error("unreachable");
throw std::runtime_error("SelectOVKs: Selector cannot select transparent UTXOs.");
}
}, this->ztxoSelector_.GetPattern());
assert(tfvk.has_value());

View File

@ -38,6 +38,7 @@ class TxOutputAmounts {
public:
CAmount t_outputs_total{0};
CAmount sapling_outputs_total{0};
CAmount orchard_outputs_total{0};
};
class AsyncRPCOperation_sendmany : public AsyncRPCOperation {
@ -47,8 +48,8 @@ public:
ZTXOSelector ztxoSelector,
std::vector<SendManyRecipient> recipients,
int minDepth,
TransactionStrategy strategy,
CAmount fee = DEFAULT_FEE,
bool allowRevealedAmounts = false,
UniValue contextInfo = NullUniValue);
virtual ~AsyncRPCOperation_sendmany();
@ -76,10 +77,10 @@ private:
bool isfromsprout_{false};
bool isfromsapling_{false};
bool allowRevealedAmounts_{false};
TransactionStrategy strategy_;
uint32_t transparentRecipients_{0};
AccountId sendFromAccount_;
std::set<libzcash::ChangeType> allowedChangeTypes_;
std::set<OutputPool> recipientPools_;
TxOutputAmounts txOutputAmounts_;
/**

View File

@ -67,7 +67,7 @@ AsyncRPCOperation_shieldcoinbase::AsyncRPCOperation_shieldcoinbase(
PaymentAddress toAddress,
CAmount fee,
UniValue contextInfo) :
builder_(builder), tx_(contextualTx), inputs_(inputs), fee_(fee), contextinfo_(contextInfo)
builder_(std::move(builder)), tx_(contextualTx), inputs_(inputs), fee_(fee), contextinfo_(contextInfo)
{
assert(contextualTx.nVersion >= 2); // transaction format version must support vJoinSplit

View File

@ -0,0 +1,503 @@
#include <gtest/gtest.h>
#include "primitives/transaction.h"
#include "wallet/wallet.h"
#include "zcash/Note.hpp"
#include "zcash/address/sapling.hpp"
#include "zcash/address/sprout.hpp"
using namespace libzcash;
void PrintTo(const OutputPool& pool, std::ostream* os) {
switch (pool) {
case OutputPool::Orchard: *os << "Orchard"; break;
case OutputPool::Sapling: *os << "Sapling"; break;
case OutputPool::Transparent: *os << "Transparent"; break;
}
}
CWalletTx FakeWalletTx()
{
CMutableTransaction mtx;
mtx.vout.resize(1);
mtx.vout[0].nValue = 1;
return CWalletTx(nullptr, mtx);
}
SpendableInputs FakeSpendableInputs(
std::set<OutputPool> available,
bool includeSprout,
const CWalletTx* wtx)
{
SpendableInputs inputs;
if (available.count(OutputPool::Orchard)) {
auto seed = MnemonicSeed::Random(0);
auto sk = libzcash::OrchardSpendingKey::ForAccount(seed, 0, 0);
libzcash::diversifier_index_t j(0);
auto address = sk.ToFullViewingKey().ToIncomingViewingKey().Address(j);
for (int i = 0; i < 10; i++) {
OrchardOutPoint op;
inputs.orchardNoteMetadata.push_back(OrchardNoteMetadata{
op, address, 1, {}});
}
}
if (available.count(OutputPool::Sapling)) {
for (int i = 0; i < 10; i++) {
SaplingOutPoint op;
libzcash::SaplingPaymentAddress address;
libzcash::SaplingNote note(address, 1, libzcash::Zip212Enabled::AfterZip212);
inputs.saplingNoteEntries.push_back(SaplingNoteEntry{
op, address, note, {}, 100});
}
}
if (available.count(OutputPool::Transparent)) {
for (int i = 0; i < 10; i++) {
COutput utxo(wtx, 0, 100, true);
inputs.utxos.push_back(utxo);
}
}
if (includeSprout) {
for (int i = 0; i < 10; i++) {
JSOutPoint jsop;
libzcash::SproutPaymentAddress address;
libzcash::SproutNote note(uint256(), 1, uint256(), uint256());
inputs.sproutNoteEntries.push_back(SproutNoteEntry{
jsop, address, note, {}, 100});
}
}
return inputs;
}
class SpendableInputsTest :
public ::testing::TestWithParam<std::tuple<
// Pools with available inputs
std::set<OutputPool>,
// Recipient pools
std::set<OutputPool>,
// List of expected pool selection orders
std::vector<std::vector<OutputPool>>>> {
};
TEST_P(SpendableInputsTest, OrderListIsSequentiallyIncreasing)
{
// The list of selection orders encodes the "failover" as we exceed the
// funds available in the selected pools. For simplicity, we require these
// to be sequentially increasing in length.
auto order = std::get<2>(GetParam());
for (int i = 0; i < order.size(); i++) {
EXPECT_EQ(order[i].size(), i + 1);
}
}
TEST_P(SpendableInputsTest, SelectsSproutBeforeFirst)
{
auto available = std::get<0>(GetParam());
auto recipientPools = std::get<1>(GetParam());
auto order = std::get<2>(GetParam());
auto wtx = FakeWalletTx();
bool canSelectSprout = !(recipientPools.count(OutputPool::Orchard));
// Create a set of inputs from Sprout and the available pools.
auto inputs = FakeSpendableInputs(available, true, &wtx);
EXPECT_EQ(inputs.Total(), 10 * (available.size() + 1));
// We have Sprout notes along with the expected notes.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
for (auto pool : available) {
switch (pool) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
// Limit to 5 zatoshis (which can be satisfied by any pool).
EXPECT_TRUE(inputs.LimitToAmount(5, 1, recipientPools));
EXPECT_EQ(inputs.Total(), 5);
if (canSelectSprout) {
// We only have Sprout notes.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 5);
for (auto pool : order[0]) {
switch (pool) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 0); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 0); break;
}
}
} else {
// We never have Sprout notes.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
for (auto pool : order[0]) {
switch (pool) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 5); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 5); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 5); break;
}
}
}
}
TEST_P(SpendableInputsTest, SelectsSproutThenFirst)
{
auto available = std::get<0>(GetParam());
auto recipientPools = std::get<1>(GetParam());
auto order = std::get<2>(GetParam());
auto wtx = FakeWalletTx();
bool canSelectSprout = !(recipientPools.count(OutputPool::Orchard));
// Create a set of inputs from Sprout and the available pools.
auto inputs = FakeSpendableInputs(available, true, &wtx);
EXPECT_EQ(inputs.Total(), 10 * (available.size() + 1));
// We have Sprout notes along with the expected notes.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
for (auto pool : available) {
switch (pool) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
// Limit to 14 zatoshis (which requires two pools). If we only have one pool
// available and can't select Sprout, we won't have sufficient funds.
auto sufficientFunds = inputs.LimitToAmount(14, 1, std::get<1>(GetParam()));
if (available.size() == 1 && !canSelectSprout) {
EXPECT_FALSE(sufficientFunds);
EXPECT_EQ(inputs.Total(), 10);
// We have no Sprout notes and all from the first pool in the first order.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
for (int i = 0; i < order[0].size(); i++) {
auto expected = i == 0 ? 10 : 0;
switch (order[0][i]) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break;
}
}
} else {
EXPECT_TRUE(sufficientFunds);
EXPECT_EQ(inputs.Total(), 14);
if (canSelectSprout) {
// We have all Sprout notes and some from the first pool in the
// first order.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
for (int i = 0; i < order[0].size(); i++) {
auto expected = i == 0 ? 4 : 0;
switch (order[0][i]) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break;
}
}
} else {
// We have all notes from the first pool and some from the second,
// in the second order.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
for (int i = 0; i < order[1].size(); i++) {
auto expected = i == 0 ? 10 : i == 1 ? 4 : 0;
switch (order[1][i]) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break;
}
}
}
}
}
TEST_P(SpendableInputsTest, SelectsFirstBeforeSecond)
{
auto available = std::get<0>(GetParam());
auto recipientPools = std::get<1>(GetParam());
auto order = std::get<2>(GetParam());
auto wtx = FakeWalletTx();
// Create a set of inputs from the available pools.
auto inputs = FakeSpendableInputs(available, false, &wtx);
EXPECT_EQ(inputs.Total(), 10 * available.size());
// We have the expected notes.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
for (auto pool : available) {
switch (pool) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
// Limit to 8 zatoshis (which can be satisfied by any pool).
EXPECT_TRUE(inputs.LimitToAmount(8, 1, std::get<1>(GetParam())));
EXPECT_EQ(inputs.Total(), 8);
// We use the first order and only have the first pool selected.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
for (int i = 0; i < order[0].size(); i++) {
auto expected = i == 0 ? 8 : 0;
switch (order[0][i]) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break;
}
}
}
TEST_P(SpendableInputsTest, SelectsFirstThenSecond)
{
auto available = std::get<0>(GetParam());
auto recipientPools = std::get<1>(GetParam());
auto order = std::get<2>(GetParam());
auto wtx = FakeWalletTx();
// Create a set of inputs from the available pools.
auto inputs = FakeSpendableInputs(available, false, &wtx);
EXPECT_EQ(inputs.Total(), 10 * available.size());
// We have the expected notes.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
for (auto pool : available) {
switch (pool) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
// Limit to 13 zatoshis (which requires two pools).
// If we only have one pool available, we won't have sufficient funds.
auto sufficientFunds = inputs.LimitToAmount(13, 1, std::get<1>(GetParam()));
if (available.size() == 1) {
EXPECT_FALSE(sufficientFunds);
EXPECT_EQ(inputs.Total(), 10);
// We have selected all of the available pool.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
for (int i = 0; i < order[0].size(); i++) {
switch (order[0][i]) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
} else {
EXPECT_TRUE(sufficientFunds);
EXPECT_EQ(inputs.Total(), 13);
// We have all of the first pool and some of the second.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
for (int i = 0; i < order[1].size(); i++) {
auto expected = i == 0 ? 10 : i == 1 ? 3 : 0;
switch (order[1][i]) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break;
}
}
}
}
TEST_P(SpendableInputsTest, SelectsSproutAndFirstThenSecond)
{
auto available = std::get<0>(GetParam());
auto recipientPools = std::get<1>(GetParam());
auto order = std::get<2>(GetParam());
auto wtx = FakeWalletTx();
bool canSelectSprout = !(recipientPools.count(OutputPool::Orchard));
// Create a set of inputs from Sprout and the available pools.
auto inputs = FakeSpendableInputs(available, true, &wtx);
EXPECT_EQ(inputs.Total(), 10 * (available.size() + 1));
// We have Sprout notes along with the expected notes.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
for (auto pool : available) {
switch (pool) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
// Limit to 24 zatoshis. If we only have one pool available, or we have two
// pools but can't select Sprout, we won't have sufficient funds.
auto sufficientFunds = inputs.LimitToAmount(24, 1, std::get<1>(GetParam()));
if (available.size() == 1 || (available.size() == 2 && !canSelectSprout)) {
EXPECT_FALSE(sufficientFunds);
EXPECT_EQ(inputs.Total(), (canSelectSprout || available.size() == 2) ? 20 : 10);
// We have selected all of the available pool.
EXPECT_EQ(inputs.sproutNoteEntries.size(), canSelectSprout ? 10 : 0);
for (int i = 0; i < order[0].size(); i++) {
switch (order[0][i]) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
} else {
EXPECT_TRUE(sufficientFunds);
EXPECT_EQ(inputs.Total(), 24);
if (canSelectSprout) {
// We have all of Sprout and the first pool, and some of the second,
// in the second order.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
for (int i = 0; i < order[1].size(); i++) {
auto expected = i == 0 ? 10 : i == 1 ? 4 : 0;
switch (order[1][i]) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break;
}
}
} else {
// We have all notes from the first and second pools and some from
// the third, in the third order.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
for (int i = 0; i < order[2].size(); i++) {
auto expected = i <= 1 ? 10 : i == 2 ? 4 : 0;
switch (order[2][i]) {
case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break;
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break;
}
}
}
}
}
TEST_P(SpendableInputsTest, OpportunisticShielding)
{
auto available = std::get<0>(GetParam());
auto recipientPools = std::get<1>(GetParam());
auto order = std::get<2>(GetParam());
auto wtx = FakeWalletTx();
// If we don't have multiple pools of which one is transparent, this test
// doesn't apply.
if (!(available.size() > 1 && available.count(OutputPool::Transparent))) {
return;
}
// Create a set of inputs from the available pools.
auto inputs = FakeSpendableInputs(available, false, &wtx);
EXPECT_EQ(inputs.Total(), 10 * available.size());
// Remove notes from the shielded pools, so we have more transparent funds.
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
for (auto pool : available) {
switch (pool) {
case OutputPool::Orchard:
while (inputs.orchardNoteMetadata.size() > 3) {
inputs.orchardNoteMetadata.pop_back();
}
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 3);
break;
case OutputPool::Sapling:
while (inputs.saplingNoteEntries.size() > 3) {
inputs.saplingNoteEntries.pop_back();
}
EXPECT_EQ(inputs.saplingNoteEntries.size(), 3);
break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
// Limit to 7 zatoshis. We can't satisfy this with two shielded pools, so we
// will trigger the opportunistic shielding logic, which causes us to select
// all transparent notes. Because transparent is sufficient to reach the
// target amount, we don't select any shielded notes.
EXPECT_TRUE(inputs.LimitToAmount(7, 1, std::get<1>(GetParam())));
EXPECT_EQ(inputs.Total(), 10);
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 0);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
EXPECT_EQ(inputs.utxos.size(), 10);
}
const std::set<OutputPool> SET_T({OutputPool::Transparent});
const std::set<OutputPool> SET_S({OutputPool::Sapling});
const std::set<OutputPool> SET_O({OutputPool::Orchard});
const std::set<OutputPool> SET_TS({OutputPool::Transparent, OutputPool::Sapling});
const std::set<OutputPool> SET_TO({OutputPool::Transparent, OutputPool::Orchard});
const std::set<OutputPool> SET_SO({OutputPool::Sapling, OutputPool::Orchard});
const std::set<OutputPool> SET_TSO({OutputPool::Transparent, OutputPool::Sapling, OutputPool::Orchard});
const std::vector<OutputPool> VEC_T({OutputPool::Transparent});
const std::vector<OutputPool> VEC_S({OutputPool::Sapling});
const std::vector<OutputPool> VEC_O({OutputPool::Orchard});
const std::vector<OutputPool> VEC_TS({OutputPool::Transparent, OutputPool::Sapling});
const std::vector<OutputPool> VEC_TO({OutputPool::Transparent, OutputPool::Orchard});
const std::vector<OutputPool> VEC_SO({OutputPool::Sapling, OutputPool::Orchard});
const std::vector<OutputPool> VEC_TSO({OutputPool::Transparent, OutputPool::Sapling, OutputPool::Orchard});
INSTANTIATE_TEST_CASE_P(
ExhaustiveCases,
SpendableInputsTest,
::testing::Values(
// Available | Recipients | Order // Rationale
// ----------|---------------------|-------------------//----------
std::make_tuple(SET_T, SET_T, std::vector({VEC_T})), // N/A
std::make_tuple(SET_T, SET_S, std::vector({VEC_T})), // N/A
std::make_tuple(SET_T, SET_O, std::vector({VEC_T})), // N/A
std::make_tuple(SET_T, SET_TS, std::vector({VEC_T})), // N/A
std::make_tuple(SET_T, SET_TO, std::vector({VEC_T})), // N/A
std::make_tuple(SET_T, SET_SO, std::vector({VEC_T})), // N/A
std::make_tuple(SET_T, SET_TSO, std::vector({VEC_T})), // N/A
std::make_tuple(SET_S, SET_T, std::vector({VEC_S})), // N/A
std::make_tuple(SET_S, SET_S, std::vector({VEC_S})), // N/A
std::make_tuple(SET_S, SET_O, std::vector({VEC_S})), // N/A
std::make_tuple(SET_S, SET_TS, std::vector({VEC_S})), // N/A
std::make_tuple(SET_S, SET_TO, std::vector({VEC_S})), // N/A
std::make_tuple(SET_S, SET_SO, std::vector({VEC_S})), // N/A
std::make_tuple(SET_S, SET_TSO, std::vector({VEC_S})), // N/A
std::make_tuple(SET_O, SET_T, std::vector({VEC_O})), // N/A
std::make_tuple(SET_O, SET_S, std::vector({VEC_O})), // N/A
std::make_tuple(SET_O, SET_O, std::vector({VEC_O})), // N/A
std::make_tuple(SET_O, SET_TS, std::vector({VEC_O})), // N/A
std::make_tuple(SET_O, SET_TO, std::vector({VEC_O})), // N/A
std::make_tuple(SET_O, SET_SO, std::vector({VEC_O})), // N/A
std::make_tuple(SET_O, SET_TSO, std::vector({VEC_O})), // N/A
std::make_tuple(SET_TS, SET_T, std::vector({VEC_S, VEC_TS})), // Hide sender, opportunistic shielding
std::make_tuple(SET_TS, SET_S, std::vector({VEC_S, VEC_TS})), // Fully shielded, opportunistic shielding
std::make_tuple(SET_TS, SET_O, std::vector({VEC_S, VEC_TS})), // Hide sender, opportunistic shielding
std::make_tuple(SET_TS, SET_TS, std::vector({VEC_S, VEC_TS})), // Hide sender, opportunistic shielding
std::make_tuple(SET_TS, SET_TO, std::vector({VEC_S, VEC_TS})), // Hide sender, opportunistic shielding
std::make_tuple(SET_TS, SET_SO, std::vector({VEC_S, VEC_TS})), // Hide sender, opportunistic shielding
std::make_tuple(SET_TS, SET_TSO, std::vector({VEC_S, VEC_TS})), // Hide sender, opportunistic shielding
std::make_tuple(SET_TO, SET_T, std::vector({VEC_O, VEC_TO})), // Hide sender, opportunistic shielding
std::make_tuple(SET_TO, SET_S, std::vector({VEC_O, VEC_TO})), // Hide sender, opportunistic shielding
std::make_tuple(SET_TO, SET_O, std::vector({VEC_O, VEC_TO})), // Fully shielded, opportunistic shielding
std::make_tuple(SET_TO, SET_TS, std::vector({VEC_O, VEC_TO})), // Hide sender, opportunistic shielding
std::make_tuple(SET_TO, SET_TO, std::vector({VEC_O, VEC_TO})), // Hide sender, opportunistic shielding
std::make_tuple(SET_TO, SET_SO, std::vector({VEC_O, VEC_TO})), // Hide sender, opportunistic shielding
std::make_tuple(SET_TO, SET_TSO, std::vector({VEC_O, VEC_TO})), // Hide sender, opportunistic shielding
std::make_tuple(SET_SO, SET_T, std::vector({VEC_O, VEC_SO})), // Fewer pools, opportunistic migration
std::make_tuple(SET_SO, SET_S, std::vector({VEC_S, VEC_SO})), // Fully shielded
std::make_tuple(SET_SO, SET_O, std::vector({VEC_O, VEC_SO})), // Fully shielded, opportunistic migration
std::make_tuple(SET_SO, SET_TS, std::vector({VEC_S, VEC_SO})), // Fewer pools
std::make_tuple(SET_SO, SET_TO, std::vector({VEC_O, VEC_SO})), // Fewer pools, opportunistic migration
std::make_tuple(SET_SO, SET_SO, std::vector({VEC_S, VEC_SO})), // Opportunistic migration
std::make_tuple(SET_SO, SET_TSO, std::vector({VEC_S, VEC_SO})), // Opportunistic migration
std::make_tuple(SET_TSO, SET_T, std::vector({VEC_O, VEC_SO, VEC_TSO})), // Fewer pools, hide sender, opportunistic shielding
std::make_tuple(SET_TSO, SET_S, std::vector({VEC_S, VEC_SO, VEC_TSO})), // Fully shielded, hide sender, opportunistic shielding
std::make_tuple(SET_TSO, SET_O, std::vector({VEC_O, VEC_SO, VEC_TSO})), // Fully shielded, hide sender, opportunistic shielding
std::make_tuple(SET_TSO, SET_TS, std::vector({VEC_S, VEC_SO, VEC_TSO})), // Fewer pools, hide sender, opportunistic shielding
std::make_tuple(SET_TSO, SET_TO, std::vector({VEC_O, VEC_SO, VEC_TSO})), // Fewer pools, hide sender, opportunistic shielding
std::make_tuple(SET_TSO, SET_SO, std::vector({VEC_S, VEC_SO, VEC_TSO})), // Opportunistic migration, hide sender, opportunistic shielding
std::make_tuple(SET_TSO, SET_TSO, std::vector({VEC_S, VEC_SO, VEC_TSO})) // Opportunistic migration, hide sender, opportunistic shielding
)
);

View File

@ -0,0 +1,157 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "consensus/validation.h"
#include "random.h"
#include "transaction_builder.h"
#include "utiltest.h"
#include "wallet/orchard.h"
#include "zcash/Address.hpp"
#include "gtest/test_transaction_builder.h"
#include <optional>
using namespace libzcash;
OrchardSpendingKey RandomOrchardSpendingKey() {
auto coinType = Params().BIP44CoinType() ;
auto seed = MnemonicSeed::Random(coinType);
return OrchardSpendingKey::ForAccount(seed, coinType, 0);
}
CTransaction FakeOrchardTx(const OrchardSpendingKey& sk, libzcash::diversifier_index_t j) {
CBasicKeyStore keystore;
CKey tsk = AddTestCKeyToKeyStore(keystore);
auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID());
auto fvk = sk.ToFullViewingKey();
auto ivk = fvk.ToIncomingViewingKey();
auto recipient = ivk.Address(j);
TransactionBuilderCoinsViewDB fakeDB;
auto orchardAnchor = fakeDB.GetBestAnchor(ShieldedType::ORCHARD);
// Create a shielding transaction from transparent to Orchard
// 0.0005 t-ZEC in, 0.0004 z-ZEC out, default fee
auto builder = TransactionBuilder(Params().GetConsensus(), 1, orchardAnchor, &keystore);
builder.AddTransparentInput(COutPoint(uint256S("1234"), 0), scriptPubKey, 50000);
builder.AddOrchardOutput(std::nullopt, recipient, 40000, std::nullopt);
auto maybeTx = builder.Build();
EXPECT_TRUE(maybeTx.IsTx());
return maybeTx.GetTxOrThrow();
}
TEST(OrchardWalletTests, TxInvolvesMyNotes) {
auto consensusParams = RegtestActivateNU5();
OrchardWallet wallet;
// Add a new spending key to the wallet
auto sk = RandomOrchardSpendingKey();
wallet.AddSpendingKey(sk);
// Create a transaction sending to the default address for that
// spending key and add it to the wallet.
auto tx = FakeOrchardTx(sk, libzcash::diversifier_index_t(0));
wallet.AddNotesIfInvolvingMe(tx);
// Check that we detect the transaction as ours
EXPECT_TRUE(wallet.TxInvolvesMyNotes(tx.GetHash()));
// Create a transaction sending to a different diversified address
auto tx1 = FakeOrchardTx(sk, libzcash::diversifier_index_t(0xffffffffffffffff));
wallet.AddNotesIfInvolvingMe(tx1);
// Check that we also detect this transaction as ours
EXPECT_TRUE(wallet.TxInvolvesMyNotes(tx1.GetHash()));
// Now generate a new key, and send a transaction to it without adding
// the key to the wallet; it should not be detected as ours.
auto skNotOurs = RandomOrchardSpendingKey();
auto tx2 = FakeOrchardTx(skNotOurs, libzcash::diversifier_index_t(0));
wallet.AddNotesIfInvolvingMe(tx2);
EXPECT_FALSE(wallet.TxInvolvesMyNotes(tx2.GetHash()));
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();
}

View File

@ -33,19 +33,3 @@ TEST(OrchardZkeysTest, FVKSerializationRoundtrip) {
ASSERT_EQ(fvk, fvk0);
}
TEST(OrchardZkeysTest, SKSerializationRoundtrip) {
auto seed = MnemonicSeed::Random(1); //testnet coin type
auto sk = libzcash::OrchardSpendingKey::ForAccount(seed, 1, 0);
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << sk;
std::string skStr = ss.str();
auto sk0 = libzcash::OrchardSpendingKey::Read(ss);
CDataStream ss0(SER_NETWORK, PROTOCOL_VERSION);
ss0 << sk0;
std::string sk0Str = ss0.str();
ASSERT_EQ(skStr, sk0Str);
}

View File

@ -33,6 +33,7 @@ public:
MOCK_METHOD0(TxnAbort, bool());
MOCK_METHOD1(WriteTx, bool(const CWalletTx& wtx));
MOCK_METHOD1(WriteOrchardWitnesses, bool(const OrchardWallet& wallet));
MOCK_METHOD1(WriteWitnessCacheSize, bool(int64_t nWitnessCacheSize));
MOCK_METHOD1(WriteBestBlock, bool(const CBlockLocator& loc));
};
@ -44,6 +45,10 @@ class TestWallet : public CWallet {
public:
TestWallet(const CChainParams& params) : CWallet(params) { }
OrchardWallet& GetOrchardWallet() {
return orchardWallet;
}
bool EncryptKeys(CKeyingMaterial& vMasterKeyIn) {
return CCryptoKeyStore::EncryptKeys(vMasterKeyIn);
}
@ -52,14 +57,19 @@ public:
return CCryptoKeyStore::Unlock(vMasterKeyIn);
}
void IncrementNoteWitnesses(const CBlockIndex* pindex,
void IncrementNoteWitnesses(const Consensus::Params& consensus,
const CBlockIndex* pindex,
const CBlock* pblock,
SproutMerkleTree& sproutTree,
SaplingMerkleTree& saplingTree) {
CWallet::IncrementNoteWitnesses(pindex, pblock, sproutTree, saplingTree);
SaplingMerkleTree& saplingTree,
bool performOrchardWalletUpdates) {
CWallet::IncrementNoteWitnesses(
consensus, pindex, pblock, sproutTree, saplingTree, performOrchardWalletUpdates);
}
void DecrementNoteWitnesses(const CBlockIndex* pindex) {
CWallet::DecrementNoteWitnesses(pindex);
void DecrementNoteWitnesses(const Consensus::Params& consensus, const CBlockIndex* pindex) {
CWallet::DecrementNoteWitnesses(consensus, pindex);
}
void SetBestChain(MockWalletDB& walletdb, const CBlockLocator& loc) {
CWallet::SetBestChainINTERNAL(walletdb, loc);
@ -98,10 +108,10 @@ std::pair<JSOutPoint, SaplingOutPoint> CreateValidBlock(TestWallet& wallet,
noteData[jsoutpt] = nd;
wtx.SetSproutNoteData(noteData);
auto saplingNotes = SetSaplingNoteData(wtx);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
block.vtx.push_back(wtx);
wallet.IncrementNoteWitnesses(&index, &block, sproutTree, saplingTree);
wallet.IncrementNoteWitnesses(Params().GetConsensus(), &index, &block, sproutTree, saplingTree, true);
return std::make_pair(jsoutpt, saplingNotes[0]);
}
@ -201,17 +211,19 @@ TEST(WalletTests, FindUnspentSproutNotes) {
noteData[jsoutpt] = nd;
wtx.SetSproutNoteData(noteData);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
// We currently have an unspent and unconfirmed note in the wallet (depth of -1)
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 0);
std::vector<OrchardNoteMetadata> orchardEntries;
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0);
EXPECT_EQ(0, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, -1);
orchardEntries.clear();
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, -1);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
@ -229,28 +241,31 @@ TEST(WalletTests, FindUnspentSproutNotes) {
EXPECT_EQ(0, chainActive.Height());
wtx.SetMerkleBranch(block);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
// We now have an unspent and confirmed note in the wallet (depth of 1)
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 0);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 1);
orchardEntries.clear();
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 1);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 2);
orchardEntries.clear();
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2);
EXPECT_EQ(0, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// Let's spend the note.
auto wtx2 = GetValidSproutSpend(sk, note, 5);
wallet.AddToWallet(wtx2, true, NULL);
wallet.LoadWalletTx(wtx2);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
// Fake-mine a spend transaction
@ -268,29 +283,33 @@ TEST(WalletTests, FindUnspentSproutNotes) {
EXPECT_EQ(1, chainActive.Height());
wtx2.SetMerkleBranch(block2);
wallet.AddToWallet(wtx2, true, NULL);
wallet.LoadWalletTx(wtx2);
EXPECT_TRUE(wallet.IsSproutSpent(nullifier));
// The note has been spent. By default, GetFilteredNotes() ignores spent notes.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 0);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0);
EXPECT_EQ(0, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// Let's include spent notes to retrieve it.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 0, INT_MAX, false);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0, INT_MAX, false);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// The spent note has two confirmations.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 2, INT_MAX, false);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2, INT_MAX, false);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// It does not have 3 confirmations.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 3, INT_MAX, false);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 3, INT_MAX, false);
EXPECT_EQ(0, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// Let's receive a new note
@ -306,7 +325,7 @@ TEST(WalletTests, FindUnspentSproutNotes) {
noteData[jsoutpt] = nd;
wtx.SetSproutNoteData(noteData);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
wtx3 = wtx;
@ -327,28 +346,32 @@ TEST(WalletTests, FindUnspentSproutNotes) {
EXPECT_EQ(2, chainActive.Height());
wtx3.SetMerkleBranch(block3);
wallet.AddToWallet(wtx3, true, NULL);
wallet.LoadWalletTx(wtx3);
// We now have an unspent note which has one confirmation, in addition to our spent note.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 1);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 1);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// Let's return the spent note too.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 1, INT_MAX, false);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 1, INT_MAX, false);
EXPECT_EQ(2, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// Increasing number of confirmations will exclude our new unspent note.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 2, INT_MAX, false);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2, INT_MAX, false);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// If we also ignore spent notes at this depth, we won't find any notes.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 2, INT_MAX, true);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2, INT_MAX, true);
EXPECT_EQ(0, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// Tear down
chainActive.SetTip(NULL);
@ -404,7 +427,7 @@ TEST(WalletTests, SetSaplingNoteAddrsInCWalletTx) {
ASSERT_TRUE(nf);
uint256 nullifier = nf.value();
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(expsk, note, anchor, witness);
builder.AddSaplingOutput(fvk.ovk, pk, 50000, {});
builder.SetFee(0);
@ -541,7 +564,7 @@ TEST(WalletTests, FindMySaplingNotes) {
auto testNote = GetTestSaplingNote(pa, 50000);
// Generate transaction
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness());
builder.AddSaplingOutput(extfvk.fvk.ovk, pa, 25000, {});
auto tx = builder.Build().GetTxOrThrow();
@ -641,15 +664,15 @@ TEST(WalletTests, GetConflictedSproutNotes) {
// No conflicts for no spends
EXPECT_EQ(0, wallet.GetConflicts(hash2).size());
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
EXPECT_EQ(0, wallet.GetConflicts(hash2).size());
// No conflicts for one spend
wallet.AddToWallet(wtx2, true, NULL);
wallet.LoadWalletTx(wtx2);
EXPECT_EQ(0, wallet.GetConflicts(hash2).size());
// Conflicts for two spends
wallet.AddToWallet(wtx3, true, NULL);
wallet.LoadWalletTx(wtx3);
auto c3 = wallet.GetConflicts(hash2);
EXPECT_EQ(2, c3.size());
EXPECT_EQ(std::set<uint256>({hash2, hash3}), c3);
@ -688,7 +711,7 @@ TEST(WalletTests, GetConflictedSaplingNotes) {
auto witness = saplingTree.witness();
// Generate tx to create output note B
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(expsk, note, anchor, witness);
builder.AddSaplingOutput(extfvk.fvk.ovk, pk, 35000, {});
auto tx = builder.Build().GetTxOrThrow();
@ -712,10 +735,10 @@ TEST(WalletTests, GetConflictedSaplingNotes) {
ASSERT_TRUE(saplingNoteData.size() > 0);
wtx.SetSaplingNoteData(saplingNoteData);
wtx.SetMerkleBranch(block);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
// Simulate receiving new block and ChainTip signal
wallet.IncrementNoteWitnesses(&fakeIndex, &block, sproutTree, saplingTree);
wallet.IncrementNoteWitnesses(Params().GetConsensus(),&fakeIndex, &block, sproutTree, saplingTree, true);
wallet.UpdateSaplingNullifierNoteMapForBlock(&block);
// Retrieve the updated wtx from wallet
@ -744,13 +767,13 @@ TEST(WalletTests, GetConflictedSaplingNotes) {
anchor = saplingTree.root();
// Create transaction to spend note B
auto builder2 = TransactionBuilder(consensusParams, 2);
auto builder2 = TransactionBuilder(consensusParams, 2, std::nullopt);
builder2.AddSaplingSpend(expsk, note2, anchor, spend_note_witness);
builder2.AddSaplingOutput(extfvk.fvk.ovk, pk, 20000, {});
auto tx2 = builder2.Build().GetTxOrThrow();
// Create conflicting transaction which also spends note B
auto builder3 = TransactionBuilder(consensusParams, 2);
auto builder3 = TransactionBuilder(consensusParams, 2, std::nullopt);
builder3.AddSaplingSpend(expsk, note2, anchor, spend_note_witness);
builder3.AddSaplingOutput(extfvk.fvk.ovk, pk, 19999, {});
auto tx3 = builder3.Build().GetTxOrThrow();
@ -766,11 +789,11 @@ TEST(WalletTests, GetConflictedSaplingNotes) {
EXPECT_EQ(0, wallet.GetConflicts(hash3).size());
// No conflicts for one spend
wallet.AddToWallet(wtx2, true, NULL);
wallet.LoadWalletTx(wtx2);
EXPECT_EQ(0, wallet.GetConflicts(hash2).size());
// Conflicts for two spends
wallet.AddToWallet(wtx3, true, NULL);
wallet.LoadWalletTx(wtx3);
auto c3 = wallet.GetConflicts(hash2);
EXPECT_EQ(2, c3.size());
EXPECT_EQ(std::set<uint256>({hash2, hash3}), c3);
@ -797,11 +820,11 @@ TEST(WalletTests, SproutNullifierIsSpent) {
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
auto wtx2 = GetValidSproutSpend(sk, note, 5);
wallet.AddToWallet(wtx2, true, NULL);
wallet.LoadWalletTx(wtx2);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
// Fake-mine the transaction
@ -817,7 +840,7 @@ TEST(WalletTests, SproutNullifierIsSpent) {
EXPECT_EQ(0, chainActive.Height());
wtx2.SetMerkleBranch(block);
wallet.AddToWallet(wtx2, true, NULL);
wallet.LoadWalletTx(wtx2);
EXPECT_TRUE(wallet.IsSproutSpent(nullifier));
// Tear down
@ -841,7 +864,7 @@ TEST(WalletTests, SaplingNullifierIsSpent) {
auto testNote = GetTestSaplingNote(pa, 50000);
// Generate transaction
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness());
builder.AddSaplingOutput(extfvk.fvk.ovk, pa, 25000, {});
auto tx = builder.Build().GetTxOrThrow();
@ -871,7 +894,7 @@ TEST(WalletTests, SaplingNullifierIsSpent) {
EXPECT_EQ(0, chainActive.Height());
wtx.SetMerkleBranch(block);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
// Verify note has been spent
EXPECT_TRUE(wallet.IsSaplingSpent(nullifier));
@ -905,7 +928,7 @@ TEST(WalletTests, NavigateFromSproutNullifierToNote) {
EXPECT_EQ(0, wallet.mapSproutNullifiersToNotes.count(nullifier));
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
EXPECT_EQ(1, wallet.mapSproutNullifiersToNotes.count(nullifier));
EXPECT_EQ(wtx.GetHash(), wallet.mapSproutNullifiersToNotes[nullifier].hash);
EXPECT_EQ(0, wallet.mapSproutNullifiersToNotes[nullifier].js);
@ -928,7 +951,7 @@ TEST(WalletTests, NavigateFromSaplingNullifierToNote) {
auto testNote = GetTestSaplingNote(pa, 50000);
// Generate transaction
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness());
builder.AddSaplingOutput(extfvk.fvk.ovk, pa, 25000, {});
auto tx = builder.Build().GetTxOrThrow();
@ -963,7 +986,7 @@ TEST(WalletTests, NavigateFromSaplingNullifierToNote) {
auto saplingNoteData = wallet.FindMySaplingNotes(wtx, chainActive.Height()).first;
ASSERT_TRUE(saplingNoteData.size() > 0);
wtx.SetSaplingNoteData(saplingNoteData);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
// Verify dummy note is now spent, as AddToWallet invokes AddToSpends()
EXPECT_TRUE(wallet.IsSaplingSpent(nullifier));
@ -977,7 +1000,7 @@ TEST(WalletTests, NavigateFromSaplingNullifierToNote) {
}
// Simulate receiving new block and ChainTip signal
wallet.IncrementNoteWitnesses(&fakeIndex, &block, sproutTree, testNote.tree);
wallet.IncrementNoteWitnesses(Params().GetConsensus(), &fakeIndex, &block, sproutTree, testNote.tree, true);
wallet.UpdateSaplingNullifierNoteMapForBlock(&block);
// Retrieve the updated wtx from wallet
@ -1031,7 +1054,7 @@ TEST(WalletTests, SpentSproutNoteIsFromMe) {
EXPECT_FALSE(wallet.IsFromMe(wtx));
EXPECT_FALSE(wallet.IsFromMe(wtx2));
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
EXPECT_FALSE(wallet.IsFromMe(wtx));
EXPECT_TRUE(wallet.IsFromMe(wtx2));
}
@ -1066,7 +1089,7 @@ TEST(WalletTests, SpentSaplingNoteIsFromMe) {
auto witness = saplingTree.witness();
// Generate transaction, which sends funds to note B
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(expsk, note, anchor, witness);
builder.AddSaplingOutput(extfvk.fvk.ovk, pk, 25000, {});
auto tx = builder.Build().GetTxOrThrow();
@ -1092,12 +1115,12 @@ TEST(WalletTests, SpentSaplingNoteIsFromMe) {
ASSERT_TRUE(saplingNoteData.size() > 0);
wtx.SetSaplingNoteData(saplingNoteData);
wtx.SetMerkleBranch(block);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
// Simulate receiving new block and ChainTip signal.
// This triggers calculation of nullifiers for notes belonging to this wallet
// in the output descriptions of wtx.
wallet.IncrementNoteWitnesses(&fakeIndex, &block, sproutTree, saplingTree);
wallet.IncrementNoteWitnesses(Params().GetConsensus(), &fakeIndex, &block, sproutTree, saplingTree, true);
wallet.UpdateSaplingNullifierNoteMapForBlock(&block);
// Retrieve the updated wtx from wallet
@ -1138,7 +1161,7 @@ TEST(WalletTests, SpentSaplingNoteIsFromMe) {
anchor = saplingTree.root();
// Create transaction to spend note B
auto builder2 = TransactionBuilder(consensusParams, 2);
auto builder2 = TransactionBuilder(consensusParams, 2, std::nullopt);
builder2.AddSaplingSpend(expsk, note2, anchor, spend_note_witness);
builder2.AddSaplingOutput(extfvk.fvk.ovk, pk, 12500, {});
auto tx2 = builder2.Build().GetTxOrThrow();
@ -1169,9 +1192,9 @@ TEST(WalletTests, SpentSaplingNoteIsFromMe) {
ASSERT_TRUE(saplingNoteData2.size() > 0);
wtx2.SetSaplingNoteData(saplingNoteData2);
wtx2.SetMerkleBranch(block2);
wallet.AddToWallet(wtx2, true, NULL);
wallet.LoadWalletTx(wtx2);
// Verify note B is spent. AddToWallet invokes AddToSpends which updates mapTxSaplingNullifiers
// Verify note B is spent. LoadWalletTx invokes AddToSpends which updates mapTxSaplingNullifiers
EXPECT_TRUE(wallet.IsSaplingSpent(nullifier2));
// Verify note B belongs to wallet.
@ -1221,7 +1244,7 @@ TEST(WalletTests, CachedWitnessesEmptyChain) {
EXPECT_FALSE((bool) sproutWitnesses[1]);
EXPECT_FALSE((bool) saplingWitnesses[0]);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
::GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses);
@ -1234,7 +1257,7 @@ TEST(WalletTests, CachedWitnessesEmptyChain) {
CBlockIndex index(block);
SproutMerkleTree sproutTree;
SaplingMerkleTree saplingTree;
wallet.IncrementNoteWitnesses(&index, &block, sproutTree, saplingTree);
wallet.IncrementNoteWitnesses(Params().GetConsensus(), &index, &block, sproutTree, saplingTree, true);
::GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses);
@ -1243,7 +1266,7 @@ TEST(WalletTests, CachedWitnessesEmptyChain) {
EXPECT_TRUE((bool) saplingWitnesses[0]);
// Until #1302 is implemented, this should trigger an assertion
EXPECT_DEATH(wallet.DecrementNoteWitnesses(&index),
EXPECT_DEATH(wallet.DecrementNoteWitnesses(Params().GetConsensus(), &index),
".*nWitnessCacheSize > 0.*");
}
@ -1288,7 +1311,7 @@ TEST(WalletTests, CachedWitnessesChainTip) {
sproutNoteData[jsoutpt] = nd;
wtx.SetSproutNoteData(sproutNoteData);
std::vector<SaplingOutPoint> saplingNotes = SetSaplingNoteData(wtx);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
std::vector<JSOutPoint> sproutNotes {jsoutpt};
std::vector<std::optional<SproutWitness>> sproutWitnesses;
@ -1307,7 +1330,7 @@ TEST(WalletTests, CachedWitnessesChainTip) {
index2.nHeight = 2;
SproutMerkleTree sproutTree2 {sproutTree};
SaplingMerkleTree saplingTree2 {saplingTree};
wallet.IncrementNoteWitnesses(&index2, &block2, sproutTree2, saplingTree2);
wallet.IncrementNoteWitnesses(Params().GetConsensus(), &index2, &block2, sproutTree2, saplingTree2, true);
auto anchors2 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses);
EXPECT_NE(anchors2.first, anchors2.second);
@ -1318,7 +1341,7 @@ TEST(WalletTests, CachedWitnessesChainTip) {
EXPECT_NE(anchors1.second, anchors2.second);
// Decrementing should give us the previous anchor
wallet.DecrementNoteWitnesses(&index2);
wallet.DecrementNoteWitnesses(Params().GetConsensus(), &index2);
auto anchors3 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses);
EXPECT_FALSE((bool) sproutWitnesses[0]);
@ -1328,7 +1351,7 @@ TEST(WalletTests, CachedWitnessesChainTip) {
EXPECT_NE(anchors1.second, anchors3.second);
// Re-incrementing with the same block should give the same result
wallet.IncrementNoteWitnesses(&index2, &block2, sproutTree, saplingTree);
wallet.IncrementNoteWitnesses(Params().GetConsensus(), &index2, &block2, sproutTree, saplingTree, true);
auto anchors4 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses);
EXPECT_NE(anchors4.first, anchors4.second);
@ -1338,7 +1361,7 @@ TEST(WalletTests, CachedWitnessesChainTip) {
EXPECT_EQ(anchors2.second, anchors4.second);
// Incrementing with the same block again should not change the cache
wallet.IncrementNoteWitnesses(&index2, &block2, sproutTree, saplingTree);
wallet.IncrementNoteWitnesses(Params().GetConsensus(), &index2, &block2, sproutTree, saplingTree, true);
std::vector<std::optional<SproutWitness>> sproutWitnesses5;
std::vector<std::optional<SaplingWitness>> saplingWitnesses5;
@ -1400,7 +1423,7 @@ TEST(WalletTests, CachedWitnessesDecrementFirst) {
noteData[jsoutpt] = nd;
wtx.SetSproutNoteData(noteData);
std::vector<SaplingOutPoint> saplingNotes = SetSaplingNoteData(wtx);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
std::vector<JSOutPoint> sproutNotes {jsoutpt};
std::vector<std::optional<SproutWitness>> sproutWitnesses;
@ -1413,7 +1436,7 @@ TEST(WalletTests, CachedWitnessesDecrementFirst) {
// Decrementing (before the transaction has ever seen an increment)
// should give us the previous anchor
wallet.DecrementNoteWitnesses(&index2);
wallet.DecrementNoteWitnesses(Params().GetConsensus(), &index2);
auto anchors4 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses);
@ -1424,7 +1447,7 @@ TEST(WalletTests, CachedWitnessesDecrementFirst) {
EXPECT_NE(anchors2.second, anchors4.second);
// Re-incrementing with the same block should give the same result
wallet.IncrementNoteWitnesses(&index2, &block2, sproutTree, saplingTree);
wallet.IncrementNoteWitnesses(Params().GetConsensus(), &index2, &block2, sproutTree, saplingTree, true);
auto anchors5 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses);
@ -1484,7 +1507,7 @@ TEST(WalletTests, CachedWitnessesCleanIndex) {
for (size_t i = 0; i < numBlocks; i++) {
SproutMerkleTree sproutRiPrevTree {sproutRiTree};
SaplingMerkleTree saplingRiPrevTree {saplingRiTree};
wallet.IncrementNoteWitnesses(&(indices[i]), &(blocks[i]), sproutRiTree, saplingRiTree);
wallet.IncrementNoteWitnesses(Params().GetConsensus(), &(indices[i]), &(blocks[i]), sproutRiTree, saplingRiTree, true);
auto anchors = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses);
for (size_t j = 0; j < numBlocks; j++) {
@ -1498,7 +1521,7 @@ TEST(WalletTests, CachedWitnessesCleanIndex) {
if ((i == 5) || (i == 50)) {
// Pretend a reorg happened that was recorded in the block files
{
wallet.DecrementNoteWitnesses(&(indices[i]));
wallet.DecrementNoteWitnesses(Params().GetConsensus(), &(indices[i]));
auto anchors = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses);
for (size_t j = 0; j < numBlocks; j++) {
@ -1511,7 +1534,7 @@ TEST(WalletTests, CachedWitnessesCleanIndex) {
}
{
wallet.IncrementNoteWitnesses(&(indices[i]), &(blocks[i]), sproutRiPrevTree, saplingRiPrevTree);
wallet.IncrementNoteWitnesses(Params().GetConsensus(), &(indices[i]), &(blocks[i]), sproutRiPrevTree, saplingRiPrevTree, true);
auto anchors = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses);
for (size_t j = 0; j < numBlocks; j++) {
EXPECT_TRUE((bool) sproutWitnesses[j]);
@ -1557,7 +1580,7 @@ TEST(WalletTests, ClearNoteWitnessCache) {
wtx.mapSaplingNoteData[saplingNotes[0]].witnessHeight = 1;
wallet.nWitnessCacheSize = 2;
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
// For Sprout, we have two outputs in the one JSDescription, only one of
// which is in the wallet.
@ -1613,7 +1636,7 @@ TEST(WalletTests, WriteWitnessCache) {
SproutNoteData nd {sk.address(), nullifier};
noteData[jsoutpt] = nd;
wtx.SetSproutNoteData(noteData);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
// TxnBegin fails
EXPECT_CALL(walletdb, TxnBegin())
@ -1638,6 +1661,22 @@ TEST(WalletTests, WriteWitnessCache) {
EXPECT_CALL(walletdb, WriteTx(wtx))
.WillRepeatedly(Return(true));
// WriteOrchardWitnesses fails
EXPECT_CALL(walletdb, WriteOrchardWitnesses)
.WillOnce(Return(false));
EXPECT_CALL(walletdb, TxnAbort())
.Times(1);
wallet.SetBestChain(walletdb, loc);
// WriteOrchardWitnesses throws
EXPECT_CALL(walletdb, WriteOrchardWitnesses)
.WillOnce(ThrowLogicError());
EXPECT_CALL(walletdb, TxnAbort())
.Times(1);
wallet.SetBestChain(walletdb, loc);
EXPECT_CALL(walletdb, WriteOrchardWitnesses)
.WillRepeatedly(Return(true));
// WriteWitnessCacheSize fails
EXPECT_CALL(walletdb, WriteWitnessCacheSize(0))
.WillOnce(Return(false));
@ -1703,13 +1742,13 @@ TEST(WalletTests, SetBestChainIgnoresTxsWithoutShieldedData) {
t.vout[0].nValue = 90*CENT;
t.vout[0].scriptPubKey = scriptPubKey;
CWalletTx wtxTransparent {nullptr, t};
wallet.AddToWallet(wtxTransparent, true, nullptr);
wallet.LoadWalletTx(wtxTransparent);
// Generate a Sprout transaction that is ours
auto wtxSprout = GetValidSproutReceive(sk, 10, true);
auto noteMap = wallet.FindMySproutNotes(wtxSprout);
wtxSprout.SetSproutNoteData(noteMap);
wallet.AddToWallet(wtxSprout, true, nullptr);
wallet.LoadWalletTx(wtxSprout);
// Generate a Sprout transaction that only involves our transparent address
auto sk2 = libzcash::SproutSpendingKey::random();
@ -1720,7 +1759,7 @@ TEST(WalletTests, SetBestChainIgnoresTxsWithoutShieldedData) {
mtx.vout[0].scriptPubKey = scriptPubKey;
mtx.vout[0].nValue = CENT;
CWalletTx wtxSproutTransparent {nullptr, mtx};
wallet.AddToWallet(wtxSproutTransparent, true, nullptr);
wallet.LoadWalletTx(wtxSproutTransparent);
// Generate a fake Sapling transaction
CMutableTransaction mtxSapling;
@ -1731,7 +1770,7 @@ TEST(WalletTests, SetBestChainIgnoresTxsWithoutShieldedData) {
zcash_test_harness_random_jubjub_point(mtxSapling.vShieldedOutput[0].cv.begin());
CWalletTx wtxSapling {nullptr, mtxSapling};
SetSaplingNoteData(wtxSapling);
wallet.AddToWallet(wtxSapling, true, nullptr);
wallet.LoadWalletTx(wtxSapling);
// Generate a fake Sapling transaction that would only involve our transparent addresses
CMutableTransaction mtxSaplingTransparent;
@ -1741,7 +1780,7 @@ TEST(WalletTests, SetBestChainIgnoresTxsWithoutShieldedData) {
mtxSaplingTransparent.vShieldedOutput.resize(1);
zcash_test_harness_random_jubjub_point(mtxSaplingTransparent.vShieldedOutput[0].cv.begin());
CWalletTx wtxSaplingTransparent {nullptr, mtxSaplingTransparent};
wallet.AddToWallet(wtxSaplingTransparent, true, nullptr);
wallet.LoadWalletTx(wtxSaplingTransparent);
EXPECT_CALL(walletdb, TxnBegin())
.WillOnce(Return(true));
@ -1755,6 +1794,8 @@ TEST(WalletTests, SetBestChainIgnoresTxsWithoutShieldedData) {
.Times(1).WillOnce(Return(true));
EXPECT_CALL(walletdb, WriteTx(wtxSaplingTransparent))
.Times(0);
EXPECT_CALL(walletdb, WriteOrchardWitnesses)
.WillOnce(Return(true));
EXPECT_CALL(walletdb, WriteWitnessCacheSize(0))
.WillOnce(Return(true));
EXPECT_CALL(walletdb, WriteBestBlock(loc))
@ -1788,7 +1829,7 @@ TEST(WalletTests, UpdateSproutNullifierNoteMap) {
noteData[jsoutpt] = nd;
wtx.SetSproutNoteData(noteData);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
EXPECT_EQ(0, wallet.mapSproutNullifiersToNotes.count(nullifier));
EXPECT_FALSE(wallet.UpdateNullifierNoteMap());
@ -1875,7 +1916,7 @@ TEST(WalletTests, UpdatedSaplingNoteData) {
auto testNote = GetTestSaplingNote(pa, 50000);
// Generate transaction
auto builder = TransactionBuilder(consensusParams, 1);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness());
builder.AddSaplingOutput(extfvk.fvk.ovk, pa2, 25000, {});
auto tx = builder.Build().GetTxOrThrow();
@ -1904,10 +1945,10 @@ TEST(WalletTests, UpdatedSaplingNoteData) {
ASSERT_TRUE(saplingNoteData.size() == 1); // wallet only has key for change output
wtx.SetSaplingNoteData(saplingNoteData);
wtx.SetMerkleBranch(block);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
// Simulate receiving new block and ChainTip signal
wallet.IncrementNoteWitnesses(&fakeIndex, &block, sproutTree, testNote.tree);
wallet.IncrementNoteWitnesses(Params().GetConsensus(), &fakeIndex, &block, sproutTree, testNote.tree, true);
wallet.UpdateSaplingNullifierNoteMapForBlock(&block);
// Retrieve the updated wtx from wallet
@ -1983,7 +2024,7 @@ TEST(WalletTests, MarkAffectedSproutTransactionsDirty) {
noteData[jsoutpt] = nd;
wtx.SetSproutNoteData(noteData);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
wallet.MarkAffectedTransactionsDirty(wtx);
// After getting a cached value, the first tx should be clean
@ -1991,7 +2032,7 @@ TEST(WalletTests, MarkAffectedSproutTransactionsDirty) {
EXPECT_TRUE(wallet.mapWallet[hash].fDebitCached);
// After adding the note spend, the first tx should be dirty
wallet.AddToWallet(wtx2, true, NULL);
wallet.LoadWalletTx(wtx2);
wallet.MarkAffectedTransactionsDirty(wtx2);
EXPECT_FALSE(wallet.mapWallet[hash].fDebitCached);
}
@ -2020,7 +2061,7 @@ TEST(WalletTests, MarkAffectedSaplingTransactionsDirty) {
// Generate shielding tx from transparent to Sapling
// 0.0005 t-ZEC in, 0.0004 z-ZEC out, default fee
auto builder = TransactionBuilder(consensusParams, 1, &keystore);
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore);
builder.AddTransparentInput(COutPoint(), scriptPubKey, 50000);
builder.AddSaplingOutput(extfvk.fvk.ovk, pk, 40000, {});
auto tx1 = builder.Build().GetTxOrThrow();
@ -2053,10 +2094,10 @@ TEST(WalletTests, MarkAffectedSaplingTransactionsDirty) {
ASSERT_TRUE(saplingNoteData.size() > 0);
wtx.SetSaplingNoteData(saplingNoteData);
wtx.SetMerkleBranch(block);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
// Simulate receiving new block and ChainTip signal
wallet.IncrementNoteWitnesses(&fakeIndex, &block, sproutTree, saplingTree);
wallet.IncrementNoteWitnesses(Params().GetConsensus(), &fakeIndex, &block, sproutTree, saplingTree, true);
wallet.UpdateSaplingNullifierNoteMapForBlock(&block);
// Retrieve the updated wtx from wallet
@ -2074,7 +2115,7 @@ TEST(WalletTests, MarkAffectedSaplingTransactionsDirty) {
// Create a Sapling-only transaction
// 0.0004 z-ZEC in, 0.00025 z-ZEC out, default fee, 0.00005 z-ZEC change
auto builder2 = TransactionBuilder(consensusParams, 2);
auto builder2 = TransactionBuilder(consensusParams, 2, std::nullopt);
builder2.AddSaplingSpend(expsk, note, anchor, witness);
builder2.AddSaplingOutput(extfvk.fvk.ovk, pk, 25000, {});
auto tx2 = builder2.Build().GetTxOrThrow();
@ -2096,7 +2137,7 @@ TEST(WalletTests, MarkAffectedSaplingTransactionsDirty) {
EXPECT_TRUE(wallet.mapWallet[hash].fDebitCached);
// After adding the note spend, the first tx should be dirty
wallet.AddToWallet(wtx2, true, NULL);
wallet.LoadWalletTx(wtx2);
wallet.MarkAffectedTransactionsDirty(wtx2);
EXPECT_FALSE(wallet.mapWallet[hash].fDebitCached);
@ -2202,15 +2243,19 @@ TEST(WalletTests, GenerateUnifiedAddress) {
expected = WalletUAGenerationError::NoSuchAccount;
EXPECT_EQ(uaResult, expected);
// lock required by GenerateNewUnifiedSpendingKey
LOCK(wallet.cs_wallet);
// Create an account, then generate an address for that account.
auto skpair = wallet.GenerateNewUnifiedSpendingKey();
uaResult = wallet.GenerateUnifiedAddress(skpair.second, {ReceiverType::P2PKH, ReceiverType::Sapling});
auto ufvkpair = wallet.GenerateNewUnifiedSpendingKey();
auto ufvk = ufvkpair.first;
auto account = ufvkpair.second;
uaResult = wallet.GenerateUnifiedAddress(account, {ReceiverType::P2PKH, ReceiverType::Sapling});
auto ua = std::get_if<std::pair<libzcash::UnifiedAddress, libzcash::diversifier_index_t>>(&uaResult);
EXPECT_NE(ua, nullptr);
auto uaSaplingReceiver = ua->first.GetSaplingReceiver();
EXPECT_TRUE(uaSaplingReceiver.has_value());
auto ufvk = skpair.first.ToFullViewingKey();
EXPECT_EQ(uaSaplingReceiver.value(), ufvk.GetSaplingKey().value().Address(ua->second));
auto u4r = wallet.FindUnifiedAddressByReceiver(uaSaplingReceiver.value());
@ -2247,3 +2292,46 @@ TEST(WalletTests, GenerateUnifiedAddress) {
// Revert to default
RegtestDeactivateSapling();
}
TEST(WalletTests, GenerateUnifiedSpendingKeyAddsOrchardAddresses) {
(void) RegtestActivateSapling();
TestWallet wallet(Params());
wallet.GenerateNewSeed();
// lock required by GenerateNewUnifiedSpendingKey
LOCK(wallet.cs_wallet);
// Create an account.
auto ufvkpair = wallet.GenerateNewUnifiedSpendingKey();
auto ufvk = ufvkpair.first;
auto account = ufvkpair.second;
auto fvk = ufvk.GetOrchardKey();
EXPECT_TRUE(fvk.has_value());
// At this point the Orchard wallet should contain the change address, but
// no other addresses. We detect this by trying to look up their IVKs.
auto changeAddr = fvk->ToInternalIncomingViewingKey().Address(libzcash::diversifier_index_t{0});
auto internalIvk = wallet.GetOrchardWallet().GetIncomingViewingKeyForAddress(changeAddr);
EXPECT_TRUE(internalIvk.has_value());
EXPECT_EQ(internalIvk.value(), fvk->ToInternalIncomingViewingKey());
auto externalAddr = fvk->ToIncomingViewingKey().Address(libzcash::diversifier_index_t{0});
auto externalIvk = wallet.GetOrchardWallet().GetIncomingViewingKeyForAddress(externalAddr);
EXPECT_FALSE(externalIvk.has_value());
// Generate an address.
auto uaResult = wallet.GenerateUnifiedAddress(account, {ReceiverType::Orchard});
auto ua = std::get_if<std::pair<libzcash::UnifiedAddress, libzcash::diversifier_index_t>>(&uaResult);
EXPECT_NE(ua, nullptr);
auto uaOrchardReceiver = ua->first.GetOrchardReceiver();
EXPECT_TRUE(uaOrchardReceiver.has_value());
EXPECT_EQ(uaOrchardReceiver.value(), externalAddr);
// Now we can look up the external IVK.
externalIvk = wallet.GetOrchardWallet().GetIncomingViewingKeyForAddress(externalAddr);
EXPECT_TRUE(externalIvk.has_value());
EXPECT_EQ(externalIvk.value(), fvk->ToIncomingViewingKey());
// Revert to default
RegtestDeactivateSapling();
}

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

504
src/wallet/orchard.h Normal file
View File

@ -0,0 +1,504 @@
// Copyright (c) 2021 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
#ifndef ZCASH_WALLET_ORCHARD_H
#define ZCASH_WALLET_ORCHARD_H
#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"
class OrchardWallet;
class OrchardWalletNoteCommitmentTreeWriter;
class OrchardWalletNoteCommitmentTreeLoader;
class OrchardNoteMetadata
{
private:
OrchardOutPoint op;
libzcash::OrchardRawAddress address;
CAmount noteValue;
std::array<uint8_t, ZC_MEMO_SIZE> memo;
int confirmations;
public:
OrchardNoteMetadata(
OrchardOutPoint op,
const libzcash::OrchardRawAddress& address,
CAmount noteValue,
const std::array<unsigned char, ZC_MEMO_SIZE>& memo):
op(op), address(address), noteValue(noteValue), memo(memo), confirmations(0) {}
const OrchardOutPoint& GetOutPoint() const {
return op;
}
const libzcash::OrchardRawAddress& GetAddress() const {
return address;
}
void SetConfirmations(int c) {
confirmations = c;
}
int GetConfirmations() const {
return confirmations;
}
CAmount GetNoteValue() const {
return noteValue;
}
const std::array<uint8_t, ZC_MEMO_SIZE>& GetMemo() const {
return memo;
}
};
/**
* A container and serialization wrapper for storing information derived from
* a transaction that is relevant to restoring Orchard wallet caches.
*/
class OrchardWalletTxMeta
{
private:
// A map from action index to the IVK belonging to our wallet that decrypts
// that action
std::map<uint32_t, libzcash::OrchardIncomingViewingKey> mapOrchardActionData;
// A vector of the action indices that spend notes belonging to our wallet
std::vector<uint32_t> vActionsSpendingMyNotes;
friend class OrchardWallet;
public:
OrchardWalletTxMeta() {}
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action) {
int nVersion = s.GetVersion();
if (!(s.GetType() & SER_GETHASH)) {
READWRITE(nVersion);
}
READWRITE(mapOrchardActionData);
READWRITE(vActionsSpendingMyNotes);
}
const std::map<uint32_t, libzcash::OrchardIncomingViewingKey>& GetMyActionIVKs() const {
return mapOrchardActionData;
}
const std::vector<uint32_t>& GetActionsSpendingMyNotes() const {
return vActionsSpendingMyNotes;
}
bool empty() const {
return (mapOrchardActionData.empty() && vActionsSpendingMyNotes.empty());
}
friend bool operator==(const OrchardWalletTxMeta& a, const OrchardWalletTxMeta& b) {
return (a.mapOrchardActionData == b.mapOrchardActionData &&
a.vActionsSpendingMyNotes == b.vActionsSpendingMyNotes);
}
friend bool operator!=(const OrchardWalletTxMeta& a, const OrchardWalletTxMeta& b) {
return !(a == b);
}
};
class OrchardActionSpend {
private:
OrchardOutPoint outPoint;
libzcash::OrchardRawAddress receivedAt;
CAmount noteValue;
public:
OrchardActionSpend(OrchardOutPoint outPoint, libzcash::OrchardRawAddress receivedAt, CAmount noteValue):
outPoint(outPoint), receivedAt(receivedAt), noteValue(noteValue) { }
OrchardOutPoint GetOutPoint() const {
return outPoint;
}
const libzcash::OrchardRawAddress& GetReceivedAt() const {
return receivedAt;
}
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() const {
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
{
private:
std::unique_ptr<OrchardWalletPtr, decltype(&orchard_wallet_free)> inner;
friend class ::orchard::UnauthorizedBundle;
friend class OrchardWalletNoteCommitmentTreeWriter;
friend class OrchardWalletNoteCommitmentTreeLoader;
public:
OrchardWallet() : inner(orchard_wallet_new(), orchard_wallet_free) {}
OrchardWallet(OrchardWallet&& wallet_data) : inner(std::move(wallet_data.inner)) {}
OrchardWallet& operator=(OrchardWallet&& wallet)
{
if (this != &wallet) {
inner = std::move(wallet.inner);
}
return *this;
}
// OrchardWallet should never be copied
OrchardWallet(const OrchardWallet&) = delete;
OrchardWallet& operator=(const OrchardWallet&) = delete;
/**
* Reset the state of the wallet to be suitable for rescan from the NU5 activation
* height. This removes all witness and spentness information from the wallet. The
* keystore is unmodified and decrypted note, nullifier, and conflict data are left
* in place with the expectation that they will be overwritten and/or updated in the
* rescan process.
*/
bool Reset() {
return orchard_wallet_reset(inner.get());
}
/**
* Checkpoint the note commitment tree. This returns `false` and leaves the note
* commitment tree unmodified if the block height specified is not the successor
* to the last block height checkpointed.
*/
bool CheckpointNoteCommitmentTree(int nBlockHeight) {
assert(nBlockHeight >= 0);
return orchard_wallet_checkpoint(inner.get(), (uint32_t) nBlockHeight);
}
/**
* Return whether the orchard note commitment tree contains any checkpoints.
*/
std::optional<int> GetLastCheckpointHeight() const {
uint32_t lastHeight{0};
if (orchard_wallet_get_last_checkpoint(inner.get(), &lastHeight)) {
return (int) lastHeight;
} else {
return std::nullopt;
}
}
/**
* Rewinds to the most recent checkpoint, and marks as unspent any notes
* previously identified as having been spent by transactions in the
* latest block.
*/
bool Rewind(int nBlockHeight, uint32_t& blocksRewoundRet) {
assert(nBlockHeight >= 0);
return orchard_wallet_rewind(inner.get(), (uint32_t) nBlockHeight, &blocksRewoundRet);
}
static void PushOrchardActionIVK(void* rec, RawOrchardActionIVK actionIVK) {
reinterpret_cast<OrchardWalletTxMeta*>(rec)->mapOrchardActionData.insert_or_assign(
actionIVK.actionIdx, libzcash::OrchardIncomingViewingKey(actionIVK.ivk)
);
}
static void PushSpendActionIdx(void* rec, uint32_t actionIdx) {
reinterpret_cast<OrchardWalletTxMeta*>(rec)->vActionsSpendingMyNotes.push_back(actionIdx);
}
/**
* Add notes that are decryptable with IVKs for which the wallet
* contains the full viewing key to the wallet, and return the
* metadata describing the wallet's involvement with this action,
* or std::nullopt if the transaction does not involve the wallet.
*/
std::optional<OrchardWalletTxMeta> AddNotesIfInvolvingMe(const CTransaction& tx) {
OrchardWalletTxMeta txMeta;
if (orchard_wallet_add_notes_from_bundle(
inner.get(),
tx.GetHash().begin(),
tx.GetOrchardBundle().inner.get(),
&txMeta,
PushOrchardActionIVK,
PushSpendActionIdx
)) {
return txMeta;
} else {
return std::nullopt;
}
}
/**
* Decrypts a selection of notes from the specified transaction's
* Orchard bundle with provided incoming viewing keys, and adds those
* notes to the wallet.
*/
bool LoadWalletTx(
const std::optional<int> nBlockHeight,
const CTransaction& tx,
const OrchardWalletTxMeta& txMeta
) {
std::vector<RawOrchardActionIVK> rawHints;
for (const auto& [action_idx, ivk] : txMeta.mapOrchardActionData) {
rawHints.push_back({ action_idx, ivk.inner.get() });
}
uint32_t blockHeight = nBlockHeight.has_value() ? (uint32_t) nBlockHeight.value() : 0;
return orchard_wallet_load_bundle(
inner.get(),
nBlockHeight.has_value() ? &blockHeight : nullptr,
tx.GetHash().begin(),
tx.GetOrchardBundle().inner.get(),
rawHints.data(),
rawHints.size(),
txMeta.vActionsSpendingMyNotes.data(),
txMeta.vActionsSpendingMyNotes.size());
}
/**
* Append each Orchard note commitment from the specified block to the
* wallet's note commitment tree.
*
* Returns `false` if the caller attempts to insert a block out-of-order.
*/
bool AppendNoteCommitments(const int nBlockHeight, const CBlock& block) {
assert(nBlockHeight >= 0);
for (int txidx = 0; txidx < block.vtx.size(); txidx++) {
const CTransaction& tx = block.vtx[txidx];
if (!orchard_wallet_append_bundle_commitments(
inner.get(),
(uint32_t) nBlockHeight,
txidx,
tx.GetHash().begin(),
tx.GetOrchardBundle().inner.get()
)) {
return false;
}
}
return true;
}
uint256 GetLatestAnchor() const {
uint256 value;
orchard_wallet_commitment_tree_root(inner.get(), value.begin());
return value;
}
bool TxInvolvesMyNotes(const uint256& txid) {
return orchard_wallet_tx_involves_my_notes(
inner.get(),
txid.begin());
}
void AddSpendingKey(const libzcash::OrchardSpendingKey& sk) {
orchard_wallet_add_spending_key(inner.get(), sk.inner.get());
}
void AddFullViewingKey(const libzcash::OrchardFullViewingKey& fvk) {
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());
if (ivkPtr == nullptr) return std::nullopt;
return libzcash::OrchardIncomingViewingKey(ivkPtr);
}
/**
* Adds an address/IVK pair to the wallet, and returns `true` if the
* IVK corresponds to a full viewing key known to the wallet, `false`
* otherwise.
*/
bool AddRawAddress(
const libzcash::OrchardRawAddress& addr,
const libzcash::OrchardIncomingViewingKey& ivk) {
return orchard_wallet_add_raw_address(inner.get(), addr.inner.get(), ivk.inner.get());
}
static void PushOrchardNoteMeta(void* orchardNotesRet, RawOrchardNoteMetadata rawNoteMeta) {
uint256 txid;
std::move(std::begin(rawNoteMeta.txid), std::end(rawNoteMeta.txid), txid.begin());
OrchardOutPoint op(txid, rawNoteMeta.actionIdx);
std::array<uint8_t, ZC_MEMO_SIZE> memo;
std::move(std::begin(rawNoteMeta.memo), std::end(rawNoteMeta.memo), memo.begin());
OrchardNoteMetadata noteMeta(
op,
libzcash::OrchardRawAddress(rawNoteMeta.addr),
rawNoteMeta.noteValue,
memo);
reinterpret_cast<std::vector<OrchardNoteMetadata>*>(orchardNotesRet)->push_back(noteMeta);
}
void GetFilteredNotes(
std::vector<OrchardNoteMetadata>& orchardNotesRet,
const std::optional<libzcash::OrchardIncomingViewingKey>& ivk,
bool ignoreMined,
bool requireSpendingKey) const {
orchard_wallet_get_filtered_notes(
inner.get(),
ivk.has_value() ? ivk.value().inner.get() : nullptr,
ignoreMined,
requireSpendingKey,
&orchardNotesRet,
PushOrchardNoteMeta
);
}
static void PushTxId(void* txidsRet, unsigned char txid[32]) {
uint256 txid_out;
std::copy(txid, txid + 32, txid_out.begin());
reinterpret_cast<std::vector<uint256>*>(txidsRet)->push_back(txid_out);
}
std::vector<uint256> GetPotentialSpends(const OrchardOutPoint& outPoint) const {
std::vector<uint256> result;
orchard_wallet_get_potential_spends(
inner.get(),
outPoint.hash.begin(),
outPoint.n,
&result,
PushTxId
);
return result;
}
std::vector<std::pair<libzcash::OrchardSpendingKey, orchard::SpendInfo>> GetSpendInfo(
const std::vector<OrchardNoteMetadata>& noteMetadata) const;
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),
libzcash::OrchardRawAddress(rawSpend.receivedAt),
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
{
private:
const OrchardWallet& wallet;
public:
OrchardWalletNoteCommitmentTreeWriter(const OrchardWallet& wallet): wallet(wallet) {}
template<typename Stream>
void Serialize(Stream& s) const {
RustStream rs(s);
if (!orchard_wallet_write_note_commitment_tree(
wallet.inner.get(),
&rs, RustStream<Stream>::write_callback)) {
throw std::ios_base::failure("Failed to serialize Orchard note commitment tree.");
}
}
};
class OrchardWalletNoteCommitmentTreeLoader
{
private:
OrchardWallet& wallet;
public:
OrchardWalletNoteCommitmentTreeLoader(OrchardWallet& wallet): wallet(wallet) {}
template<typename Stream>
void Unserialize(Stream& s) {
RustStream rs(s);
if (!orchard_wallet_load_note_commitment_tree(
wallet.inner.get(),
&rs, RustStream<Stream>::read_callback)) {
throw std::ios_base::failure("Failed to load Orchard note commitment tree.");
}
}
};
#endif // ZCASH_ORCHARD_WALLET_H

View File

@ -91,7 +91,7 @@ UniValue importprivkey(const UniValue& params, bool fHelp)
"1. \"zcashprivkey\" (string, required) The private key (see dumpprivkey)\n"
"2. \"label\" (string, optional, default=\"\") An optional label\n"
"3. rescan (boolean, optional, default=true) Rescan the wallet for transactions\n"
"\nNote: This call can take minutes to complete if rescan is true.\n"
"\nNote: This call can take a long time to complete if rescan is true.\n"
"\nExamples:\n"
"\nDump a private key\n"
+ HelpExampleCli("dumpprivkey", "\"myaddress\"") +
@ -120,7 +120,8 @@ UniValue importprivkey(const UniValue& params, bool fHelp)
if (params.size() > 2)
fRescan = params[2].get_bool();
KeyIO keyIO(Params());
const auto& chainparams = Params();
KeyIO keyIO(chainparams);
CKey key = keyIO.DecodeSecret(strSecret);
if (!key.IsValid()) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key encoding");
@ -146,7 +147,7 @@ UniValue importprivkey(const UniValue& params, bool fHelp)
pwalletMain->nTimeFirstKey = 1; // 0 would be considered 'no value'
if (fRescan) {
pwalletMain->ScanForWalletTransactions(chainActive.Genesis(), true);
pwalletMain->ScanForWalletTransactions(chainActive.Genesis(), true, false);
}
}
@ -201,7 +202,7 @@ UniValue importaddress(const UniValue& params, bool fHelp)
"2. \"label\" (string, optional, default=\"\") An optional label\n"
"3. rescan (boolean, optional, default=true) Rescan the wallet for transactions\n"
"4. p2sh (boolean, optional, default=false) Add the P2SH version of the script as well\n"
"\nNote: This call can take minutes to complete if rescan is true.\n"
"\nNote: This call can take a long time to complete if rescan is true.\n"
"If you have the full public key, you should call importpubkey instead of this.\n"
"\nNote: If you import a non-standard raw script in hex form, outputs sending to it will be treated\n"
"as change, and not show up in many RPCs.\n"
@ -233,7 +234,8 @@ UniValue importaddress(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
KeyIO keyIO(Params());
const auto& chainparams = Params();
KeyIO keyIO(chainparams);
CTxDestination dest = keyIO.DecodeDestination(params[0].get_str());
if (IsValidDestination(dest)) {
if (fP2SH) {
@ -249,7 +251,7 @@ UniValue importaddress(const UniValue& params, bool fHelp)
if (fRescan)
{
pwalletMain->ScanForWalletTransactions(chainActive.Genesis(), true);
pwalletMain->ScanForWalletTransactions(chainActive.Genesis(), true, false);
pwalletMain->ReacceptWalletTransactions();
}
@ -269,7 +271,7 @@ UniValue importpubkey(const UniValue& params, bool fHelp)
"1. \"pubkey\" (string, required) The hex-encoded public key\n"
"2. \"label\" (string, optional, default=\"\") An optional label\n"
"3. rescan (boolean, optional, default=true) Rescan the wallet for transactions\n"
"\nNote: This call can take minutes to complete if rescan is true.\n"
"\nNote: This call can take a long time to complete if rescan is true.\n"
"\nExamples:\n"
"\nImport a public key with rescan\n"
+ HelpExampleCli("importpubkey", "\"mypubkey\"") +
@ -305,7 +307,7 @@ UniValue importpubkey(const UniValue& params, bool fHelp)
if (fRescan)
{
pwalletMain->ScanForWalletTransactions(chainActive.Genesis(), true);
pwalletMain->ScanForWalletTransactions(chainActive.Genesis(), true, false);
pwalletMain->ReacceptWalletTransactions();
}
@ -380,7 +382,8 @@ UniValue importwallet_impl(const UniValue& params, bool fImportZKeys)
int64_t nFilesize = std::max((int64_t)1, (int64_t)file.tellg());
file.seekg(0, file.beg);
KeyIO keyIO(Params());
const auto& chainparams = Params();
KeyIO keyIO(chainparams);
pwalletMain->ShowProgress(_("Importing..."), 0); // show progress dialog in GUI
while (file.good()) {
@ -404,7 +407,7 @@ UniValue importwallet_impl(const UniValue& params, bool fImportZKeys)
std::optional<std::string> seedFpStr = (vstr.size() > 3) ? std::optional<std::string>(vstr[3]) : std::nullopt;
if (spendingkey.has_value()) {
auto addResult = std::visit(
AddSpendingKeyToWallet(pwalletMain, Params().GetConsensus(), nTime, hdKeypath, seedFpStr, true, true), spendingkey.value());
AddSpendingKeyToWallet(pwalletMain, chainparams.GetConsensus(), nTime, hdKeypath, seedFpStr, true, true), spendingkey.value());
if (addResult == KeyAlreadyExists){
LogPrint("zrpc", "Skipping import of zaddr (key already present)\n");
} else if (addResult == KeyNotAdded) {
@ -465,7 +468,7 @@ UniValue importwallet_impl(const UniValue& params, bool fImportZKeys)
pwalletMain->nTimeFirstKey = nTimeBegin;
LogPrintf("Rescanning last %i blocks\n", chainActive.Height() - pindex->nHeight + 1);
pwalletMain->ScanForWalletTransactions(pindex);
pwalletMain->ScanForWalletTransactions(pindex, false, false);
pwalletMain->MarkDirty();
if (!fGood)
@ -646,13 +649,20 @@ UniValue dumpwallet_impl(const UniValue& params, bool fDumpZKeys)
std::string strAddr = keyIO.EncodeDestination(keyid);
CKey key;
if (pwalletMain->GetKey(keyid, key)) {
CKeyMetadata keyMeta = pwalletMain->mapKeyMetadata[keyid];
file << strprintf("%s %s ", keyIO.EncodeSecret(key), strTime);
if (pwalletMain->mapAddressBook.count(keyid)) {
file << strprintf("%s %s label=%s # addr=%s\n", keyIO.EncodeSecret(key), strTime, EncodeDumpString(pwalletMain->mapAddressBook[keyid].name), strAddr);
file << strprintf("label=%s", EncodeDumpString(pwalletMain->mapAddressBook[keyid].name));
} else if (setKeyPool.count(keyid)) {
file << strprintf("%s %s reserve=1 # addr=%s\n", keyIO.EncodeSecret(key), strTime, strAddr);
file << "reserve=1";
} else {
file << strprintf("%s %s change=1 # addr=%s\n", keyIO.EncodeSecret(key), strTime, strAddr);
file << "change=1";
}
file << strprintf(" # addr=%s", strAddr);
if (!(keyMeta.hdKeypath.empty() || keyMeta.seedFp.IsNull())) {
file << strprintf(" hdkeypath=%s seedfp=%s", keyMeta.hdKeypath, keyMeta.seedFp.GetHex());
}
file << "\n";
}
}
file << "\n";
@ -681,12 +691,12 @@ UniValue dumpwallet_impl(const UniValue& params, bool fDumpZKeys)
auto ivk = extsk.expsk.full_viewing_key().in_viewing_key();
CKeyMetadata keyMeta = pwalletMain->mapSaplingZKeyMetadata[ivk];
std::string strTime = EncodeDumpTime(keyMeta.nCreateTime);
file << strprintf("%s %s", keyIO.EncodeSpendingKey(extsk), strTime);
// Keys imported with z_importkey do not have zip32 metadata
if (keyMeta.hdKeypath.empty() || keyMeta.seedFp.IsNull()) {
file << strprintf("%s %s # zaddr=%s\n", keyIO.EncodeSpendingKey(extsk), strTime, keyIO.EncodePaymentAddress(addr));
} else {
file << strprintf("%s %s %s %s # zaddr=%s\n", keyIO.EncodeSpendingKey(extsk), strTime, keyMeta.hdKeypath, keyMeta.seedFp.GetHex(), keyIO.EncodePaymentAddress(addr));
if (!(keyMeta.hdKeypath.empty() || keyMeta.seedFp.IsNull())) {
file << strprintf(" %s %s", keyMeta.hdKeypath, keyMeta.seedFp.GetHex());
}
file << strprintf(" # zaddr=%s\n", keyIO.EncodePaymentAddress(addr));
}
}
file << "\n";
@ -707,12 +717,13 @@ UniValue z_importkey(const UniValue& params, bool fHelp)
if (fHelp || params.size() < 1 || params.size() > 3)
throw runtime_error(
"z_importkey \"zkey\" ( rescan startHeight )\n"
"\nAdds a zkey (as returned by z_exportkey) to your wallet.\n"
"\nAdds a zkey (as returned by z_exportkey) to your wallet."
"\nImport of Orchard keys is not supported.\n"
"\nArguments:\n"
"1. \"zkey\" (string, required) The zkey (see z_exportkey)\n"
"2. rescan (string, optional, default=\"whenkeyisnew\") Rescan the wallet for transactions - can be \"yes\", \"no\" or \"whenkeyisnew\"\n"
"3. startHeight (numeric, optional, default=0) Block height to start rescan from\n"
"\nNote: This call can take minutes to complete if rescan is true.\n"
"\nNote: This call can take a long time to complete if rescan is true.\n"
"\nResult:\n"
"{\n"
" \"type\" : \"xxxx\", (string) \"sprout\" or \"sapling\"\n"
@ -771,7 +782,8 @@ UniValue z_importkey(const UniValue& params, bool fHelp)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Block height out of range");
}
KeyIO keyIO(Params());
const auto& chainparams = Params();
KeyIO keyIO(chainparams);
string strSecret = params[0].get_str();
auto spendingkey = keyIO.DecodeSpendingKey(strSecret);
if (!spendingkey.has_value()) {
@ -784,7 +796,7 @@ UniValue z_importkey(const UniValue& params, bool fHelp)
result.pushKV("address", keyIO.EncodePaymentAddress(addrInfo.second));
// Sapling support
auto addResult = std::visit(AddSpendingKeyToWallet(pwalletMain, Params().GetConsensus()), spendingkey.value());
auto addResult = std::visit(AddSpendingKeyToWallet(pwalletMain, chainparams.GetConsensus()), spendingkey.value());
if (addResult == KeyAlreadyExists && fIgnoreExistingKey) {
return result;
}
@ -798,7 +810,7 @@ UniValue z_importkey(const UniValue& params, bool fHelp)
// We want to scan for transactions and notes
if (fRescan) {
pwalletMain->ScanForWalletTransactions(chainActive[nRescanHeight], true);
pwalletMain->ScanForWalletTransactions(chainActive[nRescanHeight], true, false);
}
return result;
@ -817,7 +829,7 @@ UniValue z_importviewingkey(const UniValue& params, bool fHelp)
"1. \"vkey\" (string, required) The viewing key (see z_exportviewingkey)\n"
"2. rescan (string, optional, default=\"whenkeyisnew\") Rescan the wallet for transactions - can be \"yes\", \"no\" or \"whenkeyisnew\"\n"
"3. startHeight (numeric, optional, default=0) Block height to start rescan from\n"
"\nNote: This call can take minutes to complete if rescan is true.\n"
"\nNote: This call can take a long time to complete if rescan is true. Import of Unified viewing keys is not yet supported.\n"
"\nResult:\n"
"{\n"
" \"type\" : \"xxxx\", (string) \"sprout\" or \"sapling\"\n"
@ -866,14 +878,15 @@ UniValue z_importviewingkey(const UniValue& params, bool fHelp)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Block height out of range");
}
KeyIO keyIO(Params());
const auto& chainparams = Params();
KeyIO keyIO(chainparams);
string strVKey = params[0].get_str();
auto viewingkey = keyIO.DecodeViewingKey(strVKey);
if (!viewingkey.has_value()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid viewing key");
}
auto addrInfo = std::visit(libzcash::AddressInfoFromViewingKey(Params()), viewingkey.value());
auto addrInfo = std::visit(libzcash::AddressInfoFromViewingKey(chainparams), viewingkey.value());
UniValue result(UniValue::VOBJ);
const string strAddress = keyIO.EncodePaymentAddress(addrInfo.second);
result.pushKV("type", addrInfo.first);
@ -894,7 +907,7 @@ UniValue z_importviewingkey(const UniValue& params, bool fHelp)
// We want to scan for transactions and notes
if (fRescan) {
pwalletMain->ScanForWalletTransactions(chainActive[nRescanHeight], true);
pwalletMain->ScanForWalletTransactions(chainActive[nRescanHeight], true, false);
}
return result;
@ -984,8 +997,10 @@ UniValue z_exportviewingkey(const UniValue& params, bool fHelp)
if (fHelp || params.size() != 1)
throw runtime_error(
"z_exportviewingkey \"zaddr\"\n"
"\nReveals the viewing key corresponding to 'zaddr'.\n"
"Then the z_importviewingkey can be used with this output\n"
"\nReturns the full viewing key corresponding to 'zaddr' in the format specified by\n"
"https://zips.z.cash/protocol/protocol.pdf#addressandkeyencoding.\n"
"z_importviewingkey can be used to import Sprout and Sapling exported in this format;\n"
"import of unified viewing keys is not yet supported.\n"
"\nArguments:\n"
"1. \"zaddr\" (string, required) The zaddr for the viewing key\n"
"\nResult:\n"

View File

@ -59,6 +59,7 @@ using namespace libzcash;
const std::string ADDR_TYPE_SPROUT = "sprout";
const std::string ADDR_TYPE_SAPLING = "sapling";
const std::string ADDR_TYPE_ORCHARD = "orchard";
extern UniValue TxJoinSplitToJSON(const CTransaction& tx);
@ -2318,7 +2319,7 @@ UniValue z_listunspent(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
std::optional<AddrSet> noteFilter = std::nullopt;
std::optional<NoteFilter> noteFilter = std::nullopt;
std::set<std::pair<libzcash::SproutPaymentAddress, uint256>> sproutNullifiers;
std::set<std::pair<libzcash::SaplingPaymentAddress, uint256>> saplingNullifiers;
@ -2345,7 +2346,7 @@ UniValue z_listunspent(const UniValue& params, bool fHelp)
sourceAddrs.push_back(zaddr.value());
}
noteFilter = AddrSet::ForPaymentAddresses(sourceAddrs);
noteFilter = NoteFilter::ForPaymentAddresses(sourceAddrs);
sproutNullifiers = pwalletMain->GetSproutNullifiers(noteFilter.value().GetSproutAddresses());
saplingNullifiers = pwalletMain->GetSaplingNullifiers(noteFilter.value().GetSaplingAddresses());
@ -2370,7 +2371,8 @@ UniValue z_listunspent(const UniValue& params, bool fHelp)
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, noteFilter, nMinDepth, nMaxDepth, true, !fIncludeWatchonly, false);
std::vector<OrchardNoteMetadata> orchardEntries;
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, nMinDepth, nMaxDepth, true, !fIncludeWatchonly, false);
for (auto & entry : sproutEntries) {
UniValue obj(UniValue::VOBJ);
@ -2415,6 +2417,8 @@ UniValue z_listunspent(const UniValue& params, bool fHelp)
results.push_back(obj);
}
// TODO ORCHARD #5683
return results;
}
@ -2985,7 +2989,7 @@ UniValue z_getnewaddress(const UniValue& params, bool fHelp)
if (fHelp || params.size() > 1)
throw runtime_error(
"z_getnewaddress ( type )\n"
"\nDEPRECATED\n"
"\nDEPRECATED. Use z_getnewaccount and z_getaddressforaccount instead.\n"
"\nReturns a new shielded address for receiving payments.\n"
"\nWith no arguments, returns a Sapling address.\n"
"Generating a Sprout address is not allowed after Canopy has activated.\n"
@ -3061,8 +3065,8 @@ UniValue z_getnewaccount(const UniValue& params, bool fHelp)
EnsureWalletIsUnlocked();
// Generate the new account.
auto skNew = pwalletMain->GenerateNewUnifiedSpendingKey();
const auto& account = skNew.second;
auto ufvkNew = pwalletMain->GenerateNewUnifiedSpendingKey();
const auto& account = ufvkNew.second;
UniValue result(UniValue::VOBJ);
result.pushKV("account", (uint64_t)account);
@ -3123,14 +3127,16 @@ UniValue z_getaddressforaccount(const UniValue& params, bool fHelp)
receivers.insert(ReceiverType::P2PKH);
} else if (p == "sapling") {
receivers.insert(ReceiverType::Sapling);
} else if (p == "orchard") {
receivers.insert(ReceiverType::Orchard);
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER, "pool arguments must be \"transparent\", or \"sapling\"");
throw JSONRPCError(RPC_INVALID_PARAMETER, "pool arguments must be \"transparent\", \"sapling\", or \"orchard\"");
}
}
}
if (receivers.empty()) {
// Default is the best and second-best shielded pools, and the transparent pool.
receivers = {ReceiverType::P2PKH, ReceiverType::Sapling};
receivers = CWallet::DefaultReceiverTypes();
}
std::optional<libzcash::diversifier_index_t> j = std::nullopt;
@ -3224,6 +3230,9 @@ UniValue z_getaddressforaccount(const UniValue& params, bool fHelp)
case ReceiverType::Sapling:
pools.push_back("sapling");
break;
case ReceiverType::Orchard:
pools.push_back("orchard");
break;
default:
// Unreachable
assert(false);
@ -3335,6 +3344,12 @@ UniValue z_listunifiedreceivers(const UniValue& params, bool fHelp)
UniValue result(UniValue::VOBJ);
for (const auto& receiver : ua) {
std::visit(match {
[&](const libzcash::OrchardRawAddress& addr) {
// Create a single-receiver UA that just contains this Orchard receiver.
UnifiedAddress singleReceiver;
singleReceiver.AddReceiver(addr);
result.pushKV("orchard", keyIO.EncodePaymentAddress(singleReceiver));
},
[&](const libzcash::SaplingPaymentAddress& addr) {
result.pushKV("sapling", keyIO.EncodePaymentAddress(addr));
},
@ -3387,20 +3402,24 @@ CAmount getBalanceZaddr(std::optional<libzcash::PaymentAddress> address, int min
CAmount balance = 0;
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
std::vector<OrchardNoteMetadata> orchardEntries;
LOCK2(cs_main, pwalletMain->cs_wallet);
std::optional<AddrSet> noteFilter = std::nullopt;
std::optional<NoteFilter> noteFilter = std::nullopt;
if (address.has_value()) {
noteFilter = AddrSet::ForPaymentAddresses(std::vector({address.value()}));
noteFilter = NoteFilter::ForPaymentAddresses(std::vector({address.value()}));
}
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, noteFilter, minDepth, maxDepth, true, ignoreUnspendable);
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, minDepth, maxDepth, true, ignoreUnspendable);
for (auto & entry : sproutEntries) {
balance += CAmount(entry.note.value());
}
for (auto & entry : saplingEntries) {
balance += CAmount(entry.note.value());
}
for (auto & entry : orchardEntries) {
balance += entry.GetNoteValue();
}
return balance;
}
@ -3506,8 +3525,10 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp)
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
auto noteFilter = AddrSet::ForPaymentAddresses(std::vector({decoded.value()}));
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, noteFilter, nMinDepth, INT_MAX, false, false);
std::vector<OrchardNoteMetadata> orchardEntries;
auto noteFilter = NoteFilter::ForPaymentAddresses(std::vector({decoded.value()}));
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, nMinDepth, INT_MAX, false, false);
auto push_transparent_result = [&](const CTxDestination& dest) -> void {
const CScript scriptPubKey{GetScriptForDestination(dest)};
@ -3635,11 +3656,10 @@ UniValue z_getbalance(const UniValue& params, bool fHelp)
if (fHelp || params.size() == 0 || params.size() > 3)
throw runtime_error(
"z_getbalance \"address\" ( minconf inZat )\n"
"\nDEPRECATED\n"
"\nDEPRECATED; please use z_getbalanceforviewingkey instead.`\n"
"\nReturns the balance of a taddr or zaddr belonging to the node's wallet.\n"
"\nCAUTION: If the wallet has only an incoming viewing key for this address, then spends cannot be"
"\ndetected, and so the returned balance may be larger than the actual balance."
"\nThe argument address may not be a Unified Address; please use z_getbalanceforviewingkey instead.\n"
"\nArguments:\n"
"1. \"address\" (string) The selected address. It may be a transparent or shielded address.\n"
"2. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n"
@ -3692,9 +3712,23 @@ UniValue z_getbalance(const UniValue& params, bool fHelp)
nBalance = getBalanceZaddr(addr, nMinDepth, INT_MAX, false);
},
[&](const libzcash::UnifiedAddress& addr) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Unified addresses are not yet supported for z_getbalance.");
auto selector = pwalletMain->ZTXOSelectorForAddress(addr, true, false);
if (!selector.has_value()) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Unified address does not correspond to an account in the wallet");
}
auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, nMinDepth);
for (const auto& t : spendableInputs.utxos) {
nBalance += t.Value();
}
for (const auto& t : spendableInputs.saplingNoteEntries) {
nBalance += t.note.value();
}
for (const auto& t : spendableInputs.orchardNoteMetadata) {
nBalance += t.GetNoteValue();
}
},
}, pa.value());
@ -3787,6 +3821,7 @@ UniValue z_getbalanceforviewingkey(const UniValue& params, bool fHelp)
CAmount transparentBalance = 0;
CAmount sproutBalance = 0;
CAmount saplingBalance = 0;
CAmount orchardBalance = 0;
for (const auto& t : spendableInputs.utxos) {
transparentBalance += t.Value();
}
@ -3796,6 +3831,9 @@ UniValue z_getbalanceforviewingkey(const UniValue& params, bool fHelp)
for (const auto& t : spendableInputs.saplingNoteEntries) {
saplingBalance += t.note.value();
}
for (const auto& t : spendableInputs.orchardNoteMetadata) {
orchardBalance += t.GetNoteValue();
}
UniValue pools(UniValue::VOBJ);
auto renderBalance = [&](std::string poolName, CAmount balance) {
@ -3808,6 +3846,7 @@ UniValue z_getbalanceforviewingkey(const UniValue& params, bool fHelp)
renderBalance("transparent", transparentBalance);
renderBalance("sprout", sproutBalance);
renderBalance("sapling", saplingBalance);
renderBalance("orchard", orchardBalance);
UniValue result(UniValue::VOBJ);
result.pushKV("pools", pools);
@ -3888,12 +3927,16 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp)
CAmount transparentBalance = 0;
CAmount saplingBalance = 0;
CAmount orchardBalance = 0;
for (const auto& t : spendableInputs.utxos) {
transparentBalance += t.Value();
}
for (const auto& t : spendableInputs.saplingNoteEntries) {
saplingBalance += t.note.value();
}
for (const auto& t : spendableInputs.orchardNoteMetadata) {
orchardBalance += t.GetNoteValue();
}
UniValue pools(UniValue::VOBJ);
auto renderBalance = [&](std::string poolName, CAmount balance) {
@ -3905,6 +3948,7 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp)
};
renderBalance("transparent", transparentBalance);
renderBalance("sapling", saplingBalance);
renderBalance("orchard", orchardBalance);
UniValue result(UniValue::VOBJ);
result.pushKV("pools", pools);
@ -3993,7 +4037,7 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
" \"js\" : n, (numeric, sprout) the index of the JSDescription within vJoinSplit\n"
" \"jsSpend\" : n, (numeric, sprout) the index of the spend within the JSDescription\n"
" \"spend\" : n, (numeric, sapling) the index of the spend within vShieldedSpend\n"
" \"actionspend\" : n, (numeric, orchard) the index of the action within orchard bundle\n"
" \"action\" : n, (numeric, orchard) the index of the action within orchard bundle\n"
" \"txidPrev\" : \"transactionid\", (string) The id for the transaction this note was created in\n"
" \"jsPrev\" : n, (numeric, sprout) the index of the JSDescription within vJoinSplit\n"
" \"jsOutputPrev\" : n, (numeric, sprout) the index of the output within the JSDescription\n"
@ -4011,9 +4055,10 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
" \"js\" : n, (numeric, sprout) the index of the JSDescription within vJoinSplit\n"
" \"jsOutput\" : n, (numeric, sprout) the index of the output within the JSDescription\n"
" \"output\" : n, (numeric, sapling) the index of the output within the vShieldedOutput\n"
" \"actionoutput\" : n, (numeric, orchard) the index of the action within the orchard bundle\n"
" \"address\" : \"zcashaddress\", (string) The Zcash address involved in the transaction\n"
" \"outgoing\" : true|false (boolean, sapling) True if the output is not for an address in the wallet\n"
" \"action\" : n, (numeric, orchard) the index of the action within the orchard bundle\n"
" \"address\" : \"zcashaddress\", (string) The Zcash address involved in the transaction. Not included for change outputs.\n"
" \"outgoing\" : true|false (boolean) True if the output is not for an address in the wallet\n"
" \"walletInternal\" : true|false (boolean) True if this is a change output.\n"
" \"value\" : x.xxx (numeric) The amount in " + CURRENCY_UNIT + "\n"
" \"valueZat\" : xxxx (numeric) The amount in zatoshis\n"
" \"memo\" : \"hexmemo\", (string) hexadecimal string representation of the memo field\n"
@ -4030,15 +4075,15 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
uint256 hash;
hash.SetHex(params[0].get_str());
uint256 txid;
txid.SetHex(params[0].get_str());
UniValue entry(UniValue::VOBJ);
if (!pwalletMain->mapWallet.count(hash))
if (!pwalletMain->mapWallet.count(txid))
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id");
const CWalletTx& wtx = pwalletMain->mapWallet[hash];
const CWalletTx& wtx = pwalletMain->mapWallet[txid];
entry.pushKV("txid", hash.GetHex());
entry.pushKV("txid", txid.GetHex());
UniValue spends(UniValue::VARR);
UniValue outputs(UniValue::VARR);
@ -4125,6 +4170,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
@ -4153,23 +4222,22 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
assert(pwalletMain->GetSaplingFullViewingKey(wtxPrev.mapSaplingNoteData.at(op).ivk, extfvk));
ovks.insert(extfvk.fvk.ovk);
// If the note belongs to a Sapling address that is part of an account in the
// wallet, show the corresponding Unified Address.
std::string address = keyIO.EncodePaymentAddress([&]() {
auto ua = pwalletMain->FindUnifiedAddressByReceiver(pa);
if (ua.has_value()) {
return libzcash::PaymentAddress{ua.value()};
} else {
return libzcash::PaymentAddress{pa};
}
}());
// Show the address that was cached at transaction construction as the
// recipient.
std::optional<std::string> addrStr;
if (!pwalletMain->IsInternalRecipient(pa)) {
auto addr = pwalletMain->GetPaymentAddressForRecipient(txid, pa);
addrStr = keyIO.EncodePaymentAddress(addr);
}
UniValue entry(UniValue::VOBJ);
entry.pushKV("type", ADDR_TYPE_SAPLING);
entry.pushKV("spend", (int)i);
entry.pushKV("txidPrev", op.hash.GetHex());
entry.pushKV("outputPrev", (int)op.n);
entry.pushKV("address", address);
if (addrStr.has_value()) {
entry.pushKV("address", addrStr.value());
}
entry.pushKV("value", ValueFromAmount(notePt.value()));
entry.pushKV("valueZat", notePt.value());
spends.push_back(entry);
@ -4177,7 +4245,7 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
// Sapling outputs
for (uint32_t i = 0; i < wtx.vShieldedOutput.size(); ++i) {
auto op = SaplingOutPoint(hash, i);
auto op = SaplingOutPoint(txid, i);
SaplingNotePlaintext notePt;
SaplingPaymentAddress pa;
@ -4207,28 +4275,88 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
}
auto memo = notePt.memo();
// If the note belongs to a Sapling address that is part of an account in the
// wallet, show the corresponding Unified Address.
std::string address = keyIO.EncodePaymentAddress([&]() {
auto ua = pwalletMain->FindUnifiedAddressByReceiver(pa);
if (ua.has_value()) {
return libzcash::PaymentAddress{ua.value()};
} else {
return libzcash::PaymentAddress{pa};
}
}());
// Show the address that was cached at transaction construction as the
// recipient.
std::optional<std::string> addrStr;
bool isInternal = pwalletMain->IsInternalRecipient(pa);
if (!isInternal) {
auto addr = pwalletMain->GetPaymentAddressForRecipient(txid, pa);
addrStr = keyIO.EncodePaymentAddress(addr);
}
UniValue entry(UniValue::VOBJ);
entry.pushKV("type", ADDR_TYPE_SAPLING);
entry.pushKV("output", (int)op.n);
entry.pushKV("outgoing", isOutgoing);
entry.pushKV("address", address);
entry.pushKV("walletInternal", isInternal);
if (addrStr.has_value()) {
entry.pushKV("address", addrStr.value());
}
entry.pushKV("value", ValueFromAmount(notePt.value()));
entry.pushKV("valueZat", notePt.value());
addMemo(entry, memo);
outputs.push_back(entry);
}
// TODO unified addresses, orchard, see #5186
std::vector<uint256> ovksVector(ovks.begin(), ovks.end());
OrchardActions orchardActions = wtx.RecoverOrchardActions(ovksVector);
// Orchard spends
for (auto & pair : orchardActions.GetSpends()) {
auto actionIdx = pair.first;
OrchardActionSpend orchardActionSpend = pair.second;
auto outpoint = orchardActionSpend.GetOutPoint();
auto receivedAt = orchardActionSpend.GetReceivedAt();
auto noteValue = orchardActionSpend.GetNoteValue();
std::optional<std::string> addrStr;
if (!pwalletMain->IsInternalRecipient(receivedAt)) {
auto ua = pwalletMain->FindUnifiedAddressByReceiver(receivedAt);
assert(ua.has_value());
addrStr = keyIO.EncodePaymentAddress(ua.value());
}
UniValue entry(UniValue::VOBJ);
entry.pushKV("type", ADDR_TYPE_ORCHARD);
entry.pushKV("action", (int) actionIdx);
entry.pushKV("txidPrev", outpoint.hash.GetHex());
entry.pushKV("actionPrev", (int) outpoint.n);
if (addrStr.has_value()) {
entry.pushKV("address", addrStr.value());
}
entry.pushKV("value", ValueFromAmount(noteValue));
entry.pushKV("valueZat", noteValue);
spends.push_back(entry);
}
// Orchard outputs
for (const auto& [actionIdx, orchardActionOutput] : orchardActions.GetOutputs()) {
auto noteValue = orchardActionOutput.GetNoteValue();
auto recipient = orchardActionOutput.GetRecipient();
auto memo = orchardActionOutput.GetMemo();
// Show the address that was cached at transaction construction as the
// recipient.
std::optional<std::string> addrStr;
bool isInternal = pwalletMain->IsInternalRecipient(recipient);
if (!isInternal) {
auto addr = pwalletMain->GetPaymentAddressForRecipient(txid, recipient);
addrStr = keyIO.EncodePaymentAddress(addr);
}
UniValue entry(UniValue::VOBJ);
entry.pushKV("type", ADDR_TYPE_ORCHARD);
entry.pushKV("action", (int) actionIdx);
entry.pushKV("outgoing", orchardActionOutput.IsOutgoing());
entry.pushKV("walletInternal", isInternal);
if (addrStr.has_value()) {
entry.pushKV("address", addrStr.value());
}
entry.pushKV("value", ValueFromAmount(noteValue));
entry.pushKV("valueZat", noteValue);
addMemo(entry, memo);
outputs.push_back(entry);
}
entry.pushKV("spends", spends);
entry.pushKV("outputs", outputs);
@ -4428,7 +4556,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
if (fHelp || params.size() < 2 || params.size() > 5)
throw runtime_error(
"z_sendmany \"fromaddress\" [{\"address\":... ,\"amount\":...},...] ( minconf ) ( fee ) ( allowRevealedAmounts )\n"
"z_sendmany \"fromaddress\" [{\"address\":... ,\"amount\":...},...] ( minconf ) ( fee ) ( privacyPolicy )\n"
"\nSend multiple times. Amounts are decimal numbers with at most 8 digits of precision."
"\nChange generated from one or more transparent addresses flows to a new transparent"
"\naddress, while change generated from a shielded address returns to itself."
@ -4450,7 +4578,26 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
" }, ... ]\n"
"3. minconf (numeric, optional, default=1) Only use funds confirmed at least this many times.\n"
"4. fee (numeric, optional, default=" + strprintf("%s", FormatMoney(DEFAULT_FEE)) + ") The fee amount to attach to this transaction.\n"
"5. allowRevealedAmounts (bool, optional, default=false) Permit cross-shielded-pool transfers, which will publicly reveal the amount(s) crossing pool boundaries.\n"
"5. privacyPolicy (string, optional, default=\"LegacyCompat\") Policy for what information leakage is acceptable.\n"
" One of the following strings:\n"
" - \"FullPrivacy\": Only allow fully-shielded transactions (involving a single shielded pool).\n"
" - \"LegacyCompat\": If the transaction involves any Unified Addressess, this is equivalent to\n"
" \"FullPrivacy\". Otherwise, this is equivalent to \"AllowFullyTransparent\".\n"
" - \"AllowRevealedAmounts\": Allow funds to cross between shielded pools, revealing the amount\n"
" that crosses pools.\n"
" - \"AllowRevealedRecipients\": Allow transparent recipients. This also implies revealing\n"
" information described under \"AllowRevealedAmounts\".\n"
" - \"AllowRevealedSenders\": Allow transparent funds to be spent, revealing the sending\n"
" addresses and amounts. This implies revealing information described under \"AllowRevealedAmounts\".\n"
" - \"AllowFullyTransparent\": Allow transaction to both spend transparent funds and have\n"
" transparent recipients. This implies revealing information described under \"AllowRevealedSenders\"\n"
" and \"AllowRevealedRecipients\".\n"
" - \"AllowLinkingAccountAddresses\": Allow selecting transparent coins from the full account,\n"
" rather than just the funds sent to the transparent receiver in the provided Unified Address.\n"
" This implies revealing information described under \"AllowRevealedSenders\".\n"
" - \"NoPrivacy\": Allow the transaction to reveal any information necessary to create it.\n"
" This implies revealing information described under \"AllowFullyTransparent\" and\n"
" \"AllowLinkingAccountAddresses\".\n"
"\nResult:\n"
"\"operationid\" (string) An operationid to pass to z_getoperationstatus to get the result of the operation.\n"
"\nExamples:\n"
@ -4472,6 +4619,28 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
KeyIO keyIO(chainparams);
// We need to know the privacy policy before we construct the ZTXOSelector,
// but we can't determine the default privacy policy without knowing whether
// any UAs are involved. We break this cycle by parsing the privacy policy
// argument first, and then resolving it to the default after parsing the
// rest of the arguments. This works because all possible defaults for the
// privacy policy have the same effect on ZTXOSelector construction (in that
// they don't include AllowLinkingAccountAddresses).
std::optional<TransactionStrategy> maybeStrategy;
if (params.size() > 4) {
auto strategyName = params[4].get_str();
if (strategyName != "LegacyCompat") {
maybeStrategy = TransactionStrategy::FromString(strategyName);
if (!maybeStrategy.has_value()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf("Unknown privacy policy name '%s'", strategyName));
}
}
}
bool involvesUnifiedAddress = false;
// Check that the from address is valid.
// Unified address (UA) allowed here (#5185)
auto fromaddress = params[0].get_str();
@ -4486,7 +4655,11 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
"Invalid from address: should be a taddr, zaddr, UA, or the string 'ANY_TADDR'.");
}
auto ztxoSelectorOpt = pwalletMain->ZTXOSelectorForAddress(decoded.value(), true);
auto ztxoSelectorOpt = pwalletMain->ZTXOSelectorForAddress(
decoded.value(),
true,
// LegacyCompat does not include AllowLinkingAccountAddresses.
maybeStrategy.has_value() ? maybeStrategy.value().AllowLinkingAccountAddresses() : false);
if (!ztxoSelectorOpt.has_value()) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
@ -4501,6 +4674,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
RPC_INVALID_ADDRESS_OR_KEY,
"Invalid from address, UA does not correspond to a known account.");
}
involvesUnifiedAddress = true;
},
[&](const auto& other) {
if (selectorAccount.has_value() && selectorAccount.value() != ZCASH_LEGACY_ACCOUNT) {
@ -4541,7 +4715,9 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
std::string("Invalid parameter, unknown address format: ") + addrStr);
}
std::optional<RecipientAddress> addr = std::visit(SelectRecipientAddress(), decoded.value());
std::optional<RecipientAddress> addr = std::visit(
SelectRecipientAddress(chainparams.GetConsensus(), nextBlockHeight),
decoded.value());
if (!addr.has_value()) {
bool toSprout = std::holds_alternative<libzcash::SproutPaymentAddress>(decoded.value());
if (toSprout) {
@ -4556,14 +4732,14 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
}
if (!recipientAddrs.insert(addr.value()).second) {
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated recipient from address: ") + addrStr);
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated recipient address: ") + addrStr);
}
UniValue memoValue = find_value(o, "memo");
std::optional<std::string> memo;
if (!memoValue.isNull()) {
memo = memoValue.get_str();
if (!std::visit(libzcash::HasShieldedRecipient(), addr.value())) {
if (!std::visit(libzcash::IsShieldedRecipient(), addr.value())) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, memos cannot be sent to transparent addresses.");
} else if (!IsHex(memo.value())) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected memo data in hexadecimal format.");
@ -4583,6 +4759,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
std::optional<libzcash::UnifiedAddress> ua = std::nullopt;
if (std::holds_alternative<libzcash::UnifiedAddress>(decoded.value())) {
ua = std::get<libzcash::UnifiedAddress>(decoded.value());
involvesUnifiedAddress = true;
}
recipients.push_back(SendManyRecipient(ua, addr.value(), nAmount, memo));
@ -4592,6 +4769,15 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
throw JSONRPCError(RPC_INVALID_PARAMETER, "No recipients");
}
// Now that we've set involvesUnifiedAddress correctly, we can finish
// evaluating the strategy.
TransactionStrategy strategy = maybeStrategy.value_or(
// Default privacy policy is "LegacyCompat".
involvesUnifiedAddress ?
TransactionStrategy(PrivacyPolicy::FullPrivacy) :
TransactionStrategy(PrivacyPolicy::AllowFullyTransparent)
);
// Sanity check for transaction size
// TODO: move this to the builder?
auto txsize = EstimateTxSize(ztxoSelector, recipients, nextBlockHeight);
@ -4638,11 +4824,6 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
}
}
bool allowRevealedAmounts{false};
if (params.size() > 4) {
allowRevealedAmounts = params[4].get_bool();
}
// Use input parameters as the optional context info to be returned by z_getoperationstatus and z_getoperationresult.
UniValue o(UniValue::VOBJ);
o.pushKV("fromaddress", params[0]);
@ -4651,12 +4832,21 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
o.pushKV("fee", std::stod(FormatMoney(nFee)));
UniValue contextInfo = o;
TransactionBuilder builder(chainparams.GetConsensus(), nextBlockHeight, pwalletMain);
std::optional<uint256> orchardAnchor;
if (!ztxoSelector.SelectsSprout()) {
// Allow Orchard recipients by setting an Orchard anchor.
// TODO: Add an orchardAnchorHeight field to ZTXOSelector so we can ensure the
// same anchor is used for witnesses of any selected Orchard note.
auto orchardAnchorHeight = nextBlockHeight - nOrchardAnchorConfirmations;
orchardAnchor = chainActive[orchardAnchorHeight]->hashFinalOrchardRoot;
}
TransactionBuilder builder(chainparams.GetConsensus(), nextBlockHeight, orchardAnchor, pwalletMain);
// Create operation and add to global queue
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> operation(
new AsyncRPCOperation_sendmany(builder, ztxoSelector, recipients, nMinDepth, nFee, allowRevealedAmounts, contextInfo)
new AsyncRPCOperation_sendmany(
std::move(builder), ztxoSelector, recipients, nMinDepth, strategy, nFee, contextInfo)
);
q->addOperation(operation);
AsyncRPCOperationId operationId = operation->getId();
@ -4723,9 +4913,10 @@ UniValue z_getmigrationstatus(const UniValue& params, bool fHelp) {
{
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
std::vector<OrchardNoteMetadata> orchardEntries;
// Here we are looking for any and all Sprout notes for which we have the spending key, including those
// which are locked and/or only exist in the mempool, as they should be included in the unmigrated amount.
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 0, INT_MAX, true, true, false);
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0, INT_MAX, true, true, false);
CAmount unmigratedAmount = 0;
for (const auto& sproutEntry : sproutEntries) {
unmigratedAmount += sproutEntry.note.value();
@ -5000,8 +5191,14 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp)
contextInfo.pushKV("fee", ValueFromAmount(nFee));
// Builder (used if Sapling addresses are involved)
std::optional<uint256> orchardAnchor;
if (canopyActive) {
// Allow Orchard recipients by setting an Orchard anchor.
auto orchardAnchorHeight = nextBlockHeight - nOrchardAnchorConfirmations;
orchardAnchor = chainActive[orchardAnchorHeight]->hashFinalOrchardRoot;
}
TransactionBuilder builder = TransactionBuilder(
Params().GetConsensus(), nextBlockHeight, pwalletMain);
Params().GetConsensus(), nextBlockHeight, orchardAnchor, pwalletMain);
// Contextual transaction we will build on
// (used if no Sapling addresses are involved)
@ -5013,7 +5210,8 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp)
// Create operation and add to global queue
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_shieldcoinbase(builder, contextualTx, inputs, destaddress.value(), nFee, contextInfo) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_shieldcoinbase(
std::move(builder), contextualTx, inputs, destaddress.value(), nFee, contextInfo) );
q->addOperation(operation);
AsyncRPCOperationId operationId = operation->getId();
@ -5326,11 +5524,12 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
// Get available notes
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
std::optional<AddrSet> noteFilter =
std::vector<OrchardNoteMetadata> orchardEntries;
std::optional<NoteFilter> noteFilter =
useAnySprout || useAnySapling ?
std::nullopt :
std::optional(AddrSet::ForPaymentAddresses(zaddrs));
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, noteFilter);
std::optional(NoteFilter::ForPaymentAddresses(zaddrs));
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter);
// If Sapling is not active, do not allow sending from a sapling addresses.
if (!saplingActive && saplingEntries.size() > 0) {
@ -5467,12 +5666,21 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
// Builder (used if Sapling addresses are involved)
std::optional<TransactionBuilder> builder;
if (isToSaplingZaddr || saplingNoteInputs.size() > 0) {
builder = TransactionBuilder(Params().GetConsensus(), nextBlockHeight, pwalletMain);
std::optional<uint256> orchardAnchor;
if (!isSproutShielded) {
// Allow Orchard recipients by setting an Orchard anchor.
// TODO: Add an orchardAnchorHeight field to ZTXOSelector so we can ensure the
// same anchor is used for witnesses of any selected Orchard note.
auto orchardAnchorHeight = nextBlockHeight - nOrchardAnchorConfirmations;
orchardAnchor = chainActive[orchardAnchorHeight]->hashFinalOrchardRoot;
}
builder = TransactionBuilder(Params().GetConsensus(), nextBlockHeight, orchardAnchor, pwalletMain);
}
// Create operation and add to global queue
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> operation(
new AsyncRPCOperation_mergetoaddress(builder, contextualTx, utxoInputs, sproutNoteInputs, saplingNoteInputs, recipient, nFee, contextInfo) );
new AsyncRPCOperation_mergetoaddress(
std::move(builder), contextualTx, utxoInputs, sproutNoteInputs, saplingNoteInputs, recipient, nFee, contextInfo) );
q->addOperation(operation);
AsyncRPCOperationId operationId = operation->getId();
@ -5549,11 +5757,12 @@ UniValue z_getnotescount(const UniValue& params, bool fHelp)
"z_getnotescount\n"
"\nArguments:\n"
"1. minconf (numeric, optional, default=1) Only include notes in transactions confirmed at least this many times.\n"
"\nReturns the number of sprout and sapling notes available in the wallet.\n"
"\nReturns the number of shielded notes of each pool available in the wallet.\n"
"\nResult:\n"
"{\n"
" \"sprout\" (numeric) the number of sprout notes in the wallet\n"
" \"sapling\" (numeric) the number of sapling notes in the wallet\n"
" \"sprout\" (numeric) the number of Sprout notes in the wallet\n"
" \"sapling\" (numeric) the number of Sapling notes in the wallet\n"
" \"orchard\" (numeric) the number of Orchard notes in the wallet\n"
"}\n"
"\nExamples:\n"
+ HelpExampleCli("z_getnotescount", "0")
@ -5568,15 +5777,18 @@ UniValue z_getnotescount(const UniValue& params, bool fHelp)
int sprout = 0;
int sapling = 0;
int orchard = 0;
for (auto& wtx : pwalletMain->mapWallet) {
if (wtx.second.GetDepthInMainChain() >= nMinDepth) {
sprout += wtx.second.mapSproutNoteData.size();
sapling += wtx.second.mapSaplingNoteData.size();
orchard += wtx.second.orchardTxMeta.GetMyActionIVKs().size();
}
}
UniValue ret(UniValue::VOBJ);
ret.pushKV("sprout", sprout);
ret.pushKV("sapling", sapling);
ret.pushKV("orchard", orchard);
return ret;
}

View File

@ -799,7 +799,7 @@ void CheckHaveAddr(const std::optional<libzcash::PaymentAddress>& addr) {
auto addr_of_type = std::get_if<ADDR_TYPE>(&(addr.value()));
BOOST_ASSERT(addr_of_type != nullptr);
BOOST_CHECK(pwalletMain->ZTXOSelectorForAddress(*addr_of_type, true).has_value());
BOOST_CHECK(pwalletMain->ZTXOSelectorForAddress(*addr_of_type, true, false).has_value());
}
BOOST_AUTO_TEST_CASE(rpc_wallet_z_getnewaddress) {
@ -1191,7 +1191,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_parameters)
// Mutable tx containing contextual information we need to build tx
UniValue retValue = CallRPC("getblockcount");
int nHeight = retValue.get_int();
TransactionBuilder builder(Params().GetConsensus(), nHeight + 1, pwalletMain);
TransactionBuilder builder(Params().GetConsensus(), nHeight + 1, std::nullopt, pwalletMain);
}
BOOST_AUTO_TEST_CASE(asyncrpcoperation_sign_send_raw_transaction) {
@ -1233,10 +1233,11 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// there are no utxos to spend
{
auto selector = pwalletMain->ZTXOSelectorForAddress(taddr1, true).value();
TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain);
auto selector = pwalletMain->ZTXOSelectorForAddress(taddr1, true, false).value();
TransactionBuilder builder(consensusParams, nHeight + 1, std::nullopt, pwalletMain);
std::vector<SendManyRecipient> recipients = { SendManyRecipient(std::nullopt, zaddr1, 100*COIN, "DEADBEEF") };
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, selector, recipients, 1));
TransactionStrategy strategy;
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, strategy));
operation->main();
BOOST_CHECK(operation->isFailed());
std::string msg = operation->getErrorMessage();
@ -1245,10 +1246,11 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// there are no unspent notes to spend
{
auto selector = pwalletMain->ZTXOSelectorForAddress(zaddr1, true).value();
TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain);
auto selector = pwalletMain->ZTXOSelectorForAddress(zaddr1, true, false).value();
TransactionBuilder builder(consensusParams, nHeight + 1, std::nullopt, pwalletMain);
std::vector<SendManyRecipient> recipients = { SendManyRecipient(std::nullopt, taddr1, 100*COIN, "DEADBEEF") };
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, selector, recipients, 1));
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedRecipients);
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, strategy));
operation->main();
BOOST_CHECK(operation->isFailed());
std::string msg = operation->getErrorMessage();
@ -1257,10 +1259,11 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// get_memo_from_hex_string())
{
auto selector = pwalletMain->ZTXOSelectorForAddress(zaddr1, true).value();
TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain);
auto selector = pwalletMain->ZTXOSelectorForAddress(zaddr1, true, false).value();
TransactionBuilder builder(consensusParams, nHeight + 1, std::nullopt, pwalletMain);
std::vector<SendManyRecipient> recipients = { SendManyRecipient(std::nullopt, zaddr1, 100*COIN, "DEADBEEF") };
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, selector, recipients, 1));
TransactionStrategy strategy;
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, strategy));
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr);
@ -1336,7 +1339,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_taddr_to_sapling)
CScript scriptPubKey = CScript() << OP_DUP << OP_HASH160 << ToByteVector(taddr) << OP_EQUALVERIFY << OP_CHECKSIG;
mtx.vout.push_back(CTxOut(5 * COIN, scriptPubKey));
CWalletTx wtx(pwalletMain, mtx);
pwalletMain->AddToWallet(wtx, true, NULL);
pwalletMain->LoadWalletTx(wtx);
// Fake-mine the transaction
BOOST_CHECK_EQUAL(0, chainActive.Height());
@ -1352,15 +1355,16 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_taddr_to_sapling)
BOOST_CHECK(chainActive.Contains(&fakeIndex));
BOOST_CHECK_EQUAL(1, chainActive.Height());
wtx.SetMerkleBranch(block);
pwalletMain->AddToWallet(wtx, true, NULL);
pwalletMain->LoadWalletTx(wtx);
// Context that z_sendmany requires
auto builder = TransactionBuilder(consensusParams, nextBlockHeight, pwalletMain);
auto builder = TransactionBuilder(consensusParams, nextBlockHeight, std::nullopt, pwalletMain);
mtx = CreateNewContextualCMutableTransaction(consensusParams, nextBlockHeight);
auto selector = pwalletMain->ZTXOSelectorForAddress(taddr, true).value();
auto selector = pwalletMain->ZTXOSelectorForAddress(taddr, true, false).value();
std::vector<SendManyRecipient> recipients = { SendManyRecipient(std::nullopt, pa, 1*COIN, "ABCD") };
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, selector, recipients, 0));
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedSenders);
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 0, strategy));
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
// Enable test mode so tx is not sent
@ -1368,7 +1372,9 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_taddr_to_sapling)
// Generate the Sapling shielding transaction
operation->main();
BOOST_CHECK(operation->isSuccess());
if (!operation->isSuccess()) {
BOOST_FAIL(operation->getErrorMessage());
}
// Get the transaction
auto result = operation->getResult();
@ -1990,7 +1996,7 @@ void TestWTxStatus(const Consensus::Params consensusParams, const int delta) {
CScript scriptPubKey = CScript() << OP_DUP << OP_HASH160 << ToByteVector(taddr) << OP_EQUALVERIFY << OP_CHECKSIG;
mtx.vout.push_back(CTxOut(5 * COIN, scriptPubKey));
CWalletTx wtx(pwalletMain, mtx);
pwalletMain->AddToWallet(wtx, true, NULL);
pwalletMain->LoadWalletTx(wtx);
return wtx;
};
@ -2011,7 +2017,7 @@ void TestWTxStatus(const Consensus::Params consensusParams, const int delta) {
if (has_trx) {
wtx.SetMerkleBranch(block);
pwalletMain->AddToWallet(wtx, true, NULL);
pwalletMain->LoadWalletTx(wtx);
}
hashes.push_back(blockHash);

File diff suppressed because it is too large Load Diff

View File

@ -15,12 +15,14 @@
#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"
#include "validationinterface.h"
#include "script/ismine.h"
#include "wallet/crypter.h"
#include "wallet/orchard.h"
#include "wallet/walletdb.h"
#include "wallet/rpcwallet.h"
#include "zcash/address/unified.h"
@ -51,6 +53,7 @@ extern unsigned int nTxConfirmTarget;
extern bool bSpendZeroConfChange;
extern bool fSendFreeTransactions;
extern bool fPayAtLeastCustomFee;
extern unsigned int nOrchardAnchorConfirmations;
static const unsigned int DEFAULT_KEYPOOL_SIZE = 100;
//! -paytxfee default
@ -75,6 +78,8 @@ static const unsigned int WITNESS_CACHE_SIZE = MAX_REORG_LENGTH + 1;
//! Amount of entropy used in generation of the mnemonic seed, in bytes.
static const size_t WALLET_MNEMONIC_ENTROPY_LENGTH = 32;
//! -orchardanchorconfirmations default
static const unsigned int DEFAULT_ORCHARD_ANCHOR_CONFIRMATIONS = 1;
extern const char * DEFAULT_WALLET_DAT;
@ -458,6 +463,8 @@ public:
mapValue_t mapValue;
mapSproutNoteData_t mapSproutNoteData;
mapSaplingNoteData_t mapSaplingNoteData;
OrchardWalletTxMeta orchardTxMeta;
std::vector<std::pair<std::string, std::string> > vOrderForm;
unsigned int fTimeReceivedIsTxTime;
unsigned int nTimeReceived; //!< time received by this node
@ -570,6 +577,10 @@ public:
READWRITE(mapSaplingNoteData);
}
if (fOverwintered && nVersion >= ZIP225_TX_VERSION) {
READWRITE(orchardTxMeta);
}
if (ser_action.ForRead())
{
ReadOrderPos(nOrderPos, mapValue);
@ -602,8 +613,9 @@ public:
MarkDirty();
}
void SetSproutNoteData(mapSproutNoteData_t &noteData);
void SetSaplingNoteData(mapSaplingNoteData_t &noteData);
void SetSproutNoteData(const mapSproutNoteData_t& noteData);
void SetSaplingNoteData(const mapSaplingNoteData_t& noteData);
void SetOrchardTxMeta(OrchardWalletTxMeta actionData);
std::pair<libzcash::SproutNotePlaintext, libzcash::SproutPaymentAddress> DecryptSproutNote(
JSOutPoint jsop) const;
@ -620,6 +632,7 @@ public:
std::optional<std::pair<
libzcash::SaplingNotePlaintext,
libzcash::SaplingPaymentAddress>> RecoverSaplingNoteWithoutLeadByteCheck(SaplingOutPoint op, std::set<uint256>& ovks) const;
OrchardActions RecoverOrchardActions(const std::vector<uint256>& ovks) const;
//! filter decides which addresses will count towards the debit
CAmount GetDebit(const isminefilter& filter) const;
@ -644,15 +657,16 @@ public:
std::set<uint256> GetConflicts() const;
};
class AddrSet {
class NoteFilter {
private:
std::set<libzcash::SproutPaymentAddress> sproutAddresses;
std::set<libzcash::SaplingPaymentAddress> saplingAddresses;
std::set<libzcash::OrchardRawAddress> orchardAddresses;
AddrSet() {}
NoteFilter() {}
public:
static AddrSet Empty() { return AddrSet(); }
static AddrSet ForPaymentAddresses(const std::vector<libzcash::PaymentAddress>& addrs);
static NoteFilter Empty() { return NoteFilter(); }
static NoteFilter ForPaymentAddresses(const std::vector<libzcash::PaymentAddress>& addrs);
const std::set<libzcash::SproutPaymentAddress>& GetSproutAddresses() const {
return sproutAddresses;
@ -662,8 +676,15 @@ public:
return saplingAddresses;
}
const std::set<libzcash::OrchardRawAddress>& GetOrchardAddresses() const {
return orchardAddresses;
}
bool IsEmpty() const {
return sproutAddresses.empty() && saplingAddresses.empty();
return
sproutAddresses.empty() &&
saplingAddresses.empty() &&
orchardAddresses.empty();
}
bool HasSproutAddress(libzcash::SproutPaymentAddress addr) const {
@ -673,6 +694,10 @@ public:
bool HasSaplingAddress(libzcash::SaplingPaymentAddress addr) const {
return saplingAddresses.count(addr) > 0;
}
bool HasOrchardAddress(libzcash::OrchardRawAddress addr) const {
return orchardAddresses.count(addr) > 0;
}
};
class COutput
@ -691,6 +716,35 @@ public:
std::string ToString() const;
};
/**
* A strategy to use for managing privacy when constructing a transaction.
*/
enum class PrivacyPolicy {
FullPrivacy,
AllowRevealedAmounts,
AllowRevealedRecipients,
AllowRevealedSenders,
AllowFullyTransparent,
AllowLinkingAccountAddresses,
NoPrivacy,
};
class TransactionStrategy {
PrivacyPolicy privacy;
public:
TransactionStrategy() : privacy(PrivacyPolicy::FullPrivacy) {}
TransactionStrategy(const TransactionStrategy& strategy) : privacy(strategy.privacy) {}
TransactionStrategy(PrivacyPolicy privacyPolicy) : privacy(privacyPolicy) {}
static std::optional<TransactionStrategy> FromString(std::string privacyPolicy);
bool AllowRevealedAmounts();
bool AllowRevealedRecipients();
bool AllowRevealedSenders();
bool AllowLinkingAccountAddresses();
};
/**
* A class representing the ZIP 316 unified spending authority associated with
* a ZIP 32 account and this wallet's mnemonic seed. This is intended to be
@ -734,6 +788,10 @@ public:
return receiverTypes.empty() || receiverTypes.count(libzcash::ReceiverType::Sapling) > 0;
}
bool IncludesOrchard() const {
return receiverTypes.empty() || receiverTypes.count(libzcash::ReceiverType::Orchard) > 0;
}
friend bool operator==(const AccountZTXOPattern &a, const AccountZTXOPattern &b) {
return a.accountId == b.accountId && a.receiverTypes == b.receiverTypes;
}
@ -750,6 +808,7 @@ typedef std::variant<
libzcash::SproutViewingKey,
libzcash::SaplingPaymentAddress,
libzcash::SaplingExtendedFullViewingKey,
libzcash::UnifiedAddress,
libzcash::UnifiedFullViewingKey,
AccountZTXOPattern> ZTXOPattern;
@ -774,20 +833,34 @@ public:
bool SelectsTransparent() const;
bool SelectsSprout() const;
bool SelectsSapling() const;
bool SelectsOrchard() const;
};
class SpendableInputs {
private:
bool limited = false;
public:
std::vector<COutput> utxos;
std::vector<SproutNoteEntry> sproutNoteEntries;
std::vector<SaplingNoteEntry> saplingNoteEntries;
std::vector<OrchardNoteMetadata> orchardNoteMetadata;
/**
* Selectively discard notes that are not required to obtain the desired
* amount. Returns `false` if the available inputs do not add up to the
* desired amount.
*
* `recipientPools` is the set of `OutputPool`s to which the caller intends
* to send funds. This is used during note selection to minimise information
* leakage. The empty set is short-hand for "all pools".
*
* This method must only be called once.
*/
bool LimitToAmount(CAmount amount, CAmount dustThreshold);
bool LimitToAmount(
CAmount amount,
CAmount dustThreshold,
std::set<libzcash::OutputPool> recipientPools);
/**
* Compute the total ZEC amount of spendable inputs.
@ -803,6 +876,9 @@ public:
for (const auto& t : saplingNoteEntries) {
result += t.note.value();
}
for (const auto& t : orchardNoteMetadata) {
result += t.GetNoteValue();
}
return result;
}
@ -930,6 +1006,8 @@ public:
class CWallet : public CCryptoKeyStore, public CValidationInterface
{
private:
friend class CWalletTx;
/**
* Select a set of coins such that nValueRet >= nTargetValue and at least
* all coins from coinControl are selected; Never select unconfirmed coins
@ -997,14 +1075,21 @@ protected:
/**
* pindex is the new tip being connected.
*/
void IncrementNoteWitnesses(const CBlockIndex* pindex,
const CBlock* pblock,
SproutMerkleTree& sproutTree,
SaplingMerkleTree& saplingTree);
void IncrementNoteWitnesses(
const Consensus::Params& consensus,
const CBlockIndex* pindex,
const CBlock* pblock,
SproutMerkleTree& sproutTree,
SaplingMerkleTree& saplingTree,
bool performOrchardWalletUpdates
);
/**
* pindex is the old tip being disconnected.
*/
void DecrementNoteWitnesses(const CBlockIndex* pindex);
void DecrementNoteWitnesses(
const Consensus::Params& consensus,
const CBlockIndex* pindex
);
template <typename WalletDB>
void SetBestChainINTERNAL(WalletDB& walletdb, const CBlockLocator& loc) {
@ -1029,6 +1114,13 @@ protected:
}
}
}
// Add persistence of Orchard incremental witness tree
orchardWallet.GarbageCollect();
if (!walletdb.WriteOrchardWitnesses(orchardWallet)) {
LogPrintf("SetBestChain(): Failed to write Orchard witnesses, aborting atomic write\n");
walletdb.TxnAbort();
return;
}
if (!walletdb.WriteWitnessCacheSize(nWitnessCacheSize)) {
LogPrintf("SetBestChain(): Failed to write nWitnessCacheSize, aborting atomic write\n");
walletdb.TxnAbort();
@ -1056,7 +1148,12 @@ protected:
private:
template <class T>
void SyncMetaData(std::pair<typename TxSpendMap<T>::iterator, typename TxSpendMap<T>::iterator>);
void ChainTipAdded(const CBlockIndex *pindex, const CBlock *pblock, SproutMerkleTree sproutTree, SaplingMerkleTree saplingTree);
void ChainTipAdded(
const CBlockIndex *pindex,
const CBlock *pblock,
SproutMerkleTree sproutTree,
SaplingMerkleTree saplingTree,
bool performOrchardWalletUpdates);
/* Add a transparent secret key to the wallet. Internal use only. */
CPubKey AddTransparentSecretKey(
@ -1064,6 +1161,8 @@ private:
const CKey& secret,
const HDKeyPath& keyPath);
std::map<libzcash::OrchardIncomingViewingKey, CKeyMetadata> mapOrchardZKeyMetadata;
protected:
bool UpdatedNoteData(const CWalletTx& wtxIn, CWalletTx& wtx);
void MarkAffectedTransactionsDirty(const CTransaction& tx);
@ -1074,6 +1173,11 @@ protected:
/* the network ID string for the network for which this wallet was created */
std::string networkIdString;
/* The Orchard subset of wallet data. As many operations as possible are
* delegated to the Orchard wallet.
*/
OrchardWallet orchardWallet;
public:
/*
* Main wallet lock.
@ -1190,6 +1294,8 @@ public:
std::map<uint256, CWalletTx> mapWallet;
std::map<uint256, std::vector<RecipientMapping>> sendRecipients;
typedef std::multimap<int64_t, CWalletTx*> TxItems;
TxItems wtxOrdered;
@ -1253,7 +1359,8 @@ public:
*/
std::optional<ZTXOSelector> ZTXOSelectorForAddress(
const libzcash::PaymentAddress& addr,
bool requireSpendingKey) const;
bool requireSpendingKey,
bool allowAddressLinkability) const;
/**
* Returns the ZTXO selector for the specified viewing key, if that key
@ -1291,11 +1398,11 @@ public:
* and return the associated transparent change address.
*
* Returns `std::nullopt` if the account does not have an internal spending
* key matching the requested `ChangeType`.
* key matching the requested `OutputPool`.
*/
std::optional<libzcash::RecipientAddress> GenerateChangeAddressForAccount(
libzcash::AccountId accountId,
std::set<libzcash::ChangeType> changeOptions);
std::set<libzcash::OutputPool> changeOptions);
SpendableInputs FindSpendableInputs(
ZTXOSelector paymentSource,
@ -1309,6 +1416,7 @@ public:
bool IsSpent(const uint256& hash, unsigned int n) const;
bool IsSproutSpent(const uint256& nullifier) const;
bool IsSaplingSpent(const uint256& nullifier) const;
bool IsOrchardSpent(const OrchardOutPoint& outpoint) const;
bool IsLockedCoin(uint256 hash, unsigned int n) const;
void LockCoin(COutPoint& output);
@ -1436,6 +1544,34 @@ public:
bool LoadCryptedSaplingZKey(const libzcash::SaplingExtendedFullViewingKey &extfvk,
const std::vector<unsigned char> &vchCryptedSecret);
//
// Orchard Support
//
bool AddOrchardZKey(const libzcash::OrchardSpendingKey &sk);
bool AddOrchardFullViewingKey(const libzcash::OrchardFullViewingKey &fvk);
/**
* Adds an address/ivk mapping to the in-memory wallet. Returns `false` if
* the mapping could not be persisted, or the IVK does not correspond to an
* FVK known by the wallet.
*/
bool AddOrchardRawAddress(
const libzcash::OrchardIncomingViewingKey &ivk,
const libzcash::OrchardRawAddress &addr);
/**
* Loads an address/ivk mapping to the in-memory wallet. Returns `true`
* if the provided IVK corresponds to an FVK known by the wallet.
*/
bool LoadOrchardRawAddress(
const libzcash::OrchardRawAddress &addr,
const libzcash::OrchardIncomingViewingKey &ivk);
/**
* Returns a loader that can be used to read an Orchard note commitment
* tree from a stream into the Orchard wallet.
*/
OrchardWalletNoteCommitmentTreeLoader GetOrchardNoteCommitmentTreeLoader();
//
// Unified keys, addresses, and accounts
//
@ -1446,7 +1582,7 @@ public:
//! Generate the unified spending key from the wallet's mnemonic seed
//! for the next unused account identifier.
std::pair<libzcash::ZcashdUnifiedSpendingKey, libzcash::AccountId>
std::pair<libzcash::UnifiedFullViewingKey, libzcash::AccountId>
GenerateNewUnifiedSpendingKey();
//! Generate the unified spending key for the specified ZIP-32/BIP-44
@ -1476,11 +1612,32 @@ public:
bool LoadUnifiedAccountMetadata(const ZcashdUnifiedAccountMetadata &skmeta);
bool LoadUnifiedAddressMetadata(const ZcashdUnifiedAddressMetadata &addrmeta);
std::optional<libzcash::UFVKId> FindUnifiedFullViewingKey(const libzcash::UnifiedAddress& addr) const;
libzcash::PaymentAddress GetPaymentAddressForRecipient(
const uint256& txid,
const libzcash::RecipientAddress& recipient) const;
bool IsInternalRecipient(
const libzcash::RecipientAddress& recipient) const;
void LoadRecipientMapping(const uint256& txid, const RecipientMapping& mapping);
//! Reconstructs (in memory) caches and mappings for unified accounts,
//! addresses and keying material. This should be called once, after the
//! remainder of the on-disk wallet data has been loaded.
//!
//! Returns true if and only if there were no detected inconsistencies or
//! failures in reconstructing the cache.
bool LoadCaches();
std::optional<libzcash::AccountId> GetUnifiedAccountId(const libzcash::UFVKId& ufvkId) const;
std::optional<libzcash::ZcashdUnifiedFullViewingKey> FindUFVKByReceiver(const libzcash::Receiver& receiver) const;
std::optional<libzcash::UnifiedAddress> FindUnifiedAddressByReceiver(const libzcash::Receiver& receiver) const;
/**
* Reconstructs a unified address by determining the UFVK that the receiver
* is associated with, combined with the set of receiver types that were
* associated with the diversifier index that the provided receiver
* corresponds to.
*/
std::optional<libzcash::UnifiedAddress> FindUnifiedAddressByReceiver(
const libzcash::Receiver& receiver) const;
/**
* Increment the next transaction order id
@ -1495,15 +1652,25 @@ public:
void UpdateNullifierNoteMapWithTx(const CWalletTx& wtx);
void UpdateSaplingNullifierNoteMapWithTx(CWalletTx& wtx);
void UpdateSaplingNullifierNoteMapForBlock(const CBlock* pblock);
bool AddToWallet(const CWalletTx& wtxIn, bool fFromLoadWallet, CWalletDB* pwalletdb);
void LoadWalletTx(const CWalletTx& wtxIn);
bool AddToWallet(const CWalletTx& wtxIn, CWalletDB* pwalletdb);
void SyncTransaction(const CTransaction& tx, const CBlock* pblock, const int nHeight);
bool AddToWalletIfInvolvingMe(const CTransaction& tx, const CBlock* pblock, const int nHeight, bool fUpdate);
bool AddToWalletIfInvolvingMe(
const Consensus::Params& consensus,
const CTransaction& tx,
const CBlock* pblock,
const int nHeight,
bool fUpdate
);
void EraseFromWallet(const uint256 &hash);
void WitnessNoteCommitment(
std::vector<uint256> commitments,
std::vector<std::optional<SproutWitness>>& witnesses,
uint256 &final_anchor);
int ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate = false);
int ScanForWalletTransactions(
CBlockIndex* pindexStart,
bool fUpdate,
bool isInitScan);
void ReacceptWalletTransactions();
void ResendWalletTransactions(int64_t nBestBlockTime);
std::vector<uint256> ResendWalletTransactionsBefore(int64_t nTime);
@ -1536,16 +1703,16 @@ public:
bool SaveRecipientMappings(const uint256& txid, const std::vector<RecipientMapping>& recipients)
{
LOCK2(cs_main, cs_wallet);
LogPrintf("SaveRecipientMappings:\n%s", txid.ToString());
for (const auto& recipient : recipients)
{
sendRecipients[txid].push_back(recipient);
if (recipient.ua.has_value()) {
CWalletDB(strWalletFile).WriteRecipientMapping(
assert(CWalletDB(strWalletFile).WriteRecipientMapping(
txid,
recipient.address,
recipient.ua.value()
);
));
}
}
@ -1566,6 +1733,12 @@ public:
*/
static CAmount GetRequiredFee(unsigned int nTxBytes);
/**
* The current set of default receiver types used when the wallet generates
* unified addresses
*/
static std::set<libzcash::ReceiverType> DefaultReceiverTypes();
private:
bool NewKeyPool();
public:
@ -1598,6 +1771,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;
@ -1653,7 +1828,7 @@ public:
}
}
void GetAddressForMining(MinerAddress &minerAddress);
void GetAddressForMining(std::optional<MinerAddress> &minerAddress);
void ResetRequestCount(const uint256 &hash)
{
LOCK(cs_wallet);
@ -1755,13 +1930,14 @@ public:
* Check whether the wallet contains spending keys for all the addresses
* contained in the given address set.
*/
bool HasSpendingKeys(const AddrSet& noteFilter) const;
bool HasSpendingKeys(const NoteFilter& noteFilter) const;
/* Find notes filtered by payment addresses, min depth, max depth, if they are spent,
if a spending key is required, and if they are locked */
void GetFilteredNotes(std::vector<SproutNoteEntry>& sproutEntries,
std::vector<SaplingNoteEntry>& saplingEntries,
const std::optional<AddrSet>& noteFilter,
void GetFilteredNotes(std::vector<SproutNoteEntry>& sproutEntriesRet,
std::vector<SaplingNoteEntry>& saplingEntriesRet,
std::vector<OrchardNoteMetadata>& orchardNotesRet,
const std::optional<NoteFilter>& noteFilter,
int minDepth=1,
int maxDepth=INT_MAX,
bool ignoreSpent=true,
@ -1807,6 +1983,7 @@ public:
// Shielded key and address generalizations
//
// PaymentAddressBelongsToWallet visitor :: (CWallet&, PaymentAddress) -> bool
class PaymentAddressBelongsToWallet
{
private:
@ -1821,6 +1998,7 @@ public:
bool operator()(const libzcash::UnifiedAddress &uaddr) const;
};
// GetViewingKeyForPaymentAddress visitor :: (CWallet&, PaymentAddress) -> std::optional<ViewingKey>
class GetViewingKeyForPaymentAddress
{
private:
@ -1844,6 +2022,7 @@ enum class PaymentAddressSource {
AddressNotFound,
};
// GetSourceForPaymentAddress visitor :: (CWallet&, PaymentAddress) -> PaymentAddressSource
class GetSourceForPaymentAddress
{
private:
@ -1867,6 +2046,7 @@ enum KeyAddResult {
KeyNotAdded,
};
// AddViewingKeyToWallet visitor :: (CWallet&, ViewingKey) -> KeyAddResult
class AddViewingKeyToWallet
{
private:
@ -1880,6 +2060,8 @@ public:
KeyAddResult operator()(const libzcash::UnifiedFullViewingKey &sk) const;
};
// AddSpendingKeyToWallet visitor ::
// (CWallet&, Consensus::Params, ..., ViewingKey) -> KeyAddResult
class AddSpendingKeyToWallet
{
private:
@ -1908,6 +2090,7 @@ public:
KeyAddResult operator()(const libzcash::SaplingExtendedSpendingKey &sk) const;
};
// UFVKForReceiver :: (CWallet&, Receiver) -> std::optional<ZcashdUnifiedFullViewingKey>
class UFVKForReceiver {
private:
const CWallet& wallet;
@ -1915,12 +2098,14 @@ private:
public:
UFVKForReceiver(const CWallet& wallet): wallet(wallet) {}
std::optional<libzcash::ZcashdUnifiedFullViewingKey> operator()(const libzcash::OrchardRawAddress& orchardAddr) const;
std::optional<libzcash::ZcashdUnifiedFullViewingKey> operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const;
std::optional<libzcash::ZcashdUnifiedFullViewingKey> operator()(const CScriptID& scriptId) const;
std::optional<libzcash::ZcashdUnifiedFullViewingKey> operator()(const CKeyID& keyId) const;
std::optional<libzcash::ZcashdUnifiedFullViewingKey> operator()(const libzcash::UnknownReceiver& receiver) const;
};
// UnifiedAddressForReceiver :: (CWallet&, Receiver) -> std::optional<UnifiedAddress>
class UnifiedAddressForReceiver {
private:
const CWallet& wallet;
@ -1928,6 +2113,7 @@ private:
public:
UnifiedAddressForReceiver(const CWallet& wallet): wallet(wallet) {}
std::optional<libzcash::UnifiedAddress> operator()(const libzcash::OrchardRawAddress& orchardAddr) const;
std::optional<libzcash::UnifiedAddress> operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const;
std::optional<libzcash::UnifiedAddress> operator()(const CScriptID& scriptId) const;
std::optional<libzcash::UnifiedAddress> operator()(const CKeyID& keyId) const;

View File

@ -218,6 +218,17 @@ bool CWalletDB::EraseSaplingExtendedFullViewingKey(
return Erase(std::make_pair(std::string("sapextfvk"), extfvk));
}
//
// Orchard wallet persistence
//
bool CWalletDB::WriteOrchardWitnesses(const OrchardWallet& wallet) {
nWalletDBUpdateCounter++;
return Write(
std::string("orchard_note_commitment_tree"),
OrchardWalletNoteCommitmentTreeWriter(wallet));
}
//
// Unified address & key storage
//
@ -413,7 +424,7 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
if (wtx.nOrderPos == -1)
wss.fAnyUnordered = true;
pwallet->AddToWallet(wtx, true, NULL);
pwallet->LoadWalletTx(wtx);
}
else if (strType == "watchs")
{
@ -881,6 +892,13 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
strErr = "Error in wallet database: recipientmapping UA does not contain recipient";
return false;
}
pwallet->LoadRecipientMapping(txid, RecipientMapping(ua.value(), recipient));
}
else if (strType == "orchard_note_commitment_tree")
{
auto loader = pwallet->GetOrchardNoteCommitmentTreeLoader();
ssValue >> loader;
}
} catch (...)
{
@ -963,6 +981,14 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
}
pcursor->close();
// Load unified address/account/key caches based on what was loaded
if (!pwallet->LoadCaches()) {
// We can be more permissive of certain kinds of failures during
// loading; for now we'll interpret failure to reconstruct the
// caches to be "as bad" as losing keys.
result = DB_CORRUPT;
}
// Run the Orchard batch validator; if it fails, treat it like a bad transaction record.
if (!wss.orchardAuth.Validate()) {
fNoncriticalErrors = true;
@ -1283,6 +1309,13 @@ bool CWalletDB::Recover(CDBEnv& dbenv, const std::string& filename, bool fOnlyKe
ptxn->commit(0);
pdbCopy->close(0);
// Try to load the wallet's caches, uncovering inconsistencies in wallet
// records like missing viewing key records despite existing account
// records.
if (!dummyWallet.LoadCaches()) {
LogPrintf("WARNING: wallet caches could not be reconstructed; salvaged wallet file may have omissions");
}
return fSuccess;
}

View File

@ -339,6 +339,10 @@ class CSerializeRecipientAddress {
[&](const libzcash::SaplingPaymentAddress& saplingAddr) {
ReceiverTypeSer(libzcash::ReceiverType::Sapling).Serialize(s);
s << saplingAddr;
},
[&](const libzcash::OrchardRawAddress& orchardAddr) {
ReceiverTypeSer(libzcash::ReceiverType::Orchard).Serialize(s);
s << orchardAddr;
}
}, recipient);
}
@ -367,6 +371,11 @@ class CSerializeRecipientAddress {
recipient = saplingAddr;
break;
}
case libzcash::ReceiverType::Orchard: {
auto orchardAddr = libzcash::OrchardRawAddress::Read(s);
recipient = orchardAddr;
break;
}
}
}
@ -457,6 +466,9 @@ public:
bool WriteSaplingExtendedFullViewingKey(const libzcash::SaplingExtendedFullViewingKey &extfvk);
bool EraseSaplingExtendedFullViewingKey(const libzcash::SaplingExtendedFullViewingKey &extfvk);
/// Orchard support.
bool WriteOrchardWitnesses(const OrchardWallet& wallet);
/// Unified key support.
bool WriteUnifiedAccountMetadata(const ZcashdUnifiedAccountMetadata& keymeta);

View File

@ -16,6 +16,12 @@ namespace libzcash {
// Unified Addresses
//
static bool AddOrchardReceiver(void* ua, OrchardRawAddressPtr* ptr)
{
return reinterpret_cast<libzcash::UnifiedAddress*>(ua)->AddReceiver(
libzcash::OrchardRawAddress::KeyIoOnlyFromReceiver(ptr));
}
/**
* `raw` MUST be 43 bytes.
*/
@ -73,6 +79,7 @@ std::optional<UnifiedAddress> UnifiedAddress::Parse(const KeyConstants& keyConst
str.c_str(),
keyConstants.NetworkIDString().c_str(),
&ua,
AddOrchardReceiver,
AddSaplingReceiver,
AddP2SHReceiver,
AddP2PKHReceiver,
@ -150,12 +157,27 @@ std::optional<SaplingPaymentAddress> UnifiedAddress::GetSaplingReceiver() const
return std::nullopt;
}
std::optional<RecipientAddress> UnifiedAddress::GetPreferredRecipientAddress() const {
std::optional<OrchardRawAddress> UnifiedAddress::GetOrchardReceiver() const {
for (const auto& r : receivers) {
if (std::holds_alternative<OrchardRawAddress>(r)) {
return std::get<OrchardRawAddress>(r);
}
}
return std::nullopt;
}
std::optional<RecipientAddress> UnifiedAddress::GetPreferredRecipientAddress(
const Consensus::Params& consensus, int height) const
{
bool nu5Active = consensus.NetworkUpgradeActive(height, Consensus::UPGRADE_NU5);
// Return the first receiver type we recognize; receivers are sorted in
// order from most-preferred to least.
std::optional<RecipientAddress> result;
for (const auto& receiver : *this) {
std::visit(match {
[&](const OrchardRawAddress& addr) { if (nu5Active) result = addr; },
[&](const SaplingPaymentAddress& addr) { result = addr; },
[&](const CScriptID& addr) { result = addr; },
[&](const CKeyID& addr) { result = addr; },
@ -171,6 +193,7 @@ std::optional<RecipientAddress> UnifiedAddress::GetPreferredRecipientAddress() c
bool HasKnownReceiverType(const Receiver& receiver) {
return std::visit(match {
[](const OrchardRawAddress& addr) { return true; },
[](const SaplingPaymentAddress& addr) { return true; },
[](const CScriptID& addr) { return true; },
[](const CKeyID& addr) { return true; },
@ -202,6 +225,12 @@ std::pair<std::string, PaymentAddress> AddressInfoFromViewingKey::operator()(con
} // namespace libzcash
uint32_t TypecodeForReceiver::operator()(
const libzcash::OrchardRawAddress &zaddr) const
{
return static_cast<uint32_t>(libzcash::ReceiverType::Orchard);
}
uint32_t TypecodeForReceiver::operator()(
const libzcash::SaplingPaymentAddress &zaddr) const
{
@ -251,6 +280,16 @@ std::string libzcash::UnifiedFullViewingKey::Encode(const KeyConstants& keyConst
return res;
}
std::optional<libzcash::OrchardFullViewingKey> libzcash::UnifiedFullViewingKey::GetOrchardKey() const {
std::vector<uint8_t> buffer(96);
if (unified_full_viewing_key_read_orchard(inner.get(), buffer.data())) {
CDataStream ss(buffer, SER_NETWORK, PROTOCOL_VERSION);
return OrchardFullViewingKey::Read(ss);
} else {
return std::nullopt;
}
}
std::optional<libzcash::SaplingDiversifiableFullViewingKey> libzcash::UnifiedFullViewingKey::GetSaplingKey() const {
std::vector<uint8_t> buffer(128);
if (unified_full_viewing_key_read_sapling(inner.get(), buffer.data())) {
@ -291,10 +330,21 @@ bool libzcash::UnifiedFullViewingKeyBuilder::AddSaplingKey(const SaplingDiversif
return true;
}
bool libzcash::UnifiedFullViewingKeyBuilder::AddOrchardKey(const OrchardFullViewingKey& key) {
if (orchard_bytes.has_value()) return false;
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << key;
assert(ss.size() == 96);
std::vector<uint8_t> ss_bytes(ss.begin(), ss.end());
orchard_bytes = ss_bytes;
return true;
}
std::optional<libzcash::UnifiedFullViewingKey> libzcash::UnifiedFullViewingKeyBuilder::build() const {
UnifiedFullViewingKeyPtr* ptr = unified_full_viewing_key_from_components(
t_bytes.has_value() ? t_bytes.value().data() : nullptr,
sapling_bytes.has_value() ? sapling_bytes.value().data() : nullptr);
sapling_bytes.has_value() ? sapling_bytes.value().data() : nullptr,
orchard_bytes.has_value() ? orchard_bytes.value().data() : nullptr);
if (ptr == nullptr) {
return std::nullopt;
@ -311,6 +361,9 @@ libzcash::UnifiedFullViewingKey libzcash::UnifiedFullViewingKey::FromZcashdUFVK(
if (key.GetSaplingKey().has_value()) {
builder.AddSaplingKey(key.GetSaplingKey().value());
}
if (key.GetOrchardKey().has_value()) {
builder.AddOrchardKey(key.GetOrchardKey().value());
}
auto result = builder.build();
if (!result.has_value()) {

View File

@ -1,6 +1,7 @@
#ifndef ZC_ADDRESS_H_
#define ZC_ADDRESS_H_
#include "consensus/params.h"
#include "key_constants.h"
#include "pubkey.h"
#include "key_constants.h"
@ -66,6 +67,12 @@ class UnifiedAddress {
public:
UnifiedAddress() {}
static UnifiedAddress ForSingleReceiver(Receiver receiver) {
UnifiedAddress ua;
ua.AddReceiver(receiver);
return ua;
}
static std::optional<UnifiedAddress> Parse(const KeyConstants& keyConstants, const std::string& str);
ADD_SERIALIZE_METHODS;
@ -95,6 +102,9 @@ public:
std::set<ReceiverType> result;
for (const auto& receiver : receivers) {
std::visit(match {
[&](const libzcash::OrchardRawAddress &zaddr) {
result.insert(ReceiverType::Orchard);
},
[&](const libzcash::SaplingPaymentAddress &zaddr) {
result.insert(ReceiverType::Sapling);
},
@ -127,11 +137,14 @@ public:
std::optional<SaplingPaymentAddress> GetSaplingReceiver() const;
std::optional<OrchardRawAddress> GetOrchardReceiver() const;
/**
* Return the most-preferred receiver from among the receiver types
* that we recognize.
* that we recognize and can use at the given block height.
*/
std::optional<RecipientAddress> GetPreferredRecipientAddress() const;
std::optional<RecipientAddress> GetPreferredRecipientAddress(
const Consensus::Params& consensus, int height) const;
friend inline bool operator==(const UnifiedAddress& a, const UnifiedAddress& b) {
return a.receivers == b.receivers;
@ -182,6 +195,8 @@ public:
std::string Encode(const KeyConstants& keyConstants) const;
std::optional<OrchardFullViewingKey> GetOrchardKey() const;
std::optional<SaplingDiversifiableFullViewingKey> GetSaplingKey() const;
std::optional<transparent::AccountPubKey> GetTransparentKey() const;
@ -196,6 +211,9 @@ public:
if (GetSaplingKey().has_value()) {
result.insert(ReceiverType::Sapling);
}
if (GetOrchardKey().has_value()) {
result.insert(ReceiverType::Orchard);
}
return result;
}
@ -220,11 +238,16 @@ class UnifiedFullViewingKeyBuilder {
private:
std::optional<std::vector<uint8_t>> t_bytes;
std::optional<std::vector<uint8_t>> sapling_bytes;
std::optional<std::vector<uint8_t>> orchard_bytes;
public:
UnifiedFullViewingKeyBuilder(): t_bytes(std::nullopt), sapling_bytes(std::nullopt) {}
UnifiedFullViewingKeyBuilder():
t_bytes(std::nullopt),
sapling_bytes(std::nullopt),
orchard_bytes(std::nullopt) {}
bool AddTransparentKey(const transparent::AccountPubKey&);
bool AddSaplingKey(const SaplingDiversifiableFullViewingKey&);
bool AddOrchardKey(const OrchardFullViewingKey&);
std::optional<UnifiedFullViewingKey> build() const;
};
@ -246,25 +269,29 @@ typedef std::variant<
SproutSpendingKey,
SaplingExtendedSpendingKey> SpendingKey;
class HasShieldedRecipient {
class IsShieldedRecipient {
public:
bool operator()(const CKeyID& p2pkh) { return false; }
bool operator()(const CScriptID& p2sh) { return false; }
bool operator()(const SproutPaymentAddress& addr) { return true; }
bool operator()(const SaplingPaymentAddress& addr) { return true; }
// unified addresses must contain a shielded receiver, so we
// consider this to be safe by construction
bool operator()(const UnifiedAddress& addr) { return true; }
bool operator()(const OrchardRawAddress& addr) { return true; }
};
class SelectRecipientAddress {
const Consensus::Params& consensus;
int height;
public:
SelectRecipientAddress(const Consensus::Params& consensus, int height) :
consensus(consensus), height(height) {}
std::optional<RecipientAddress> operator()(const CKeyID& p2pkh) { return p2pkh; }
std::optional<RecipientAddress> operator()(const CScriptID& p2sh) { return p2sh; }
std::optional<RecipientAddress> operator()(const SproutPaymentAddress& addr) { return std::nullopt; }
std::optional<RecipientAddress> operator()(const SaplingPaymentAddress& addr) { return addr; }
std::optional<RecipientAddress> operator()(const UnifiedAddress& addr) {
return addr.GetPreferredRecipientAddress();
return addr.GetPreferredRecipientAddress(consensus, height);
}
};
@ -294,6 +321,7 @@ class TypecodeForReceiver {
public:
TypecodeForReceiver() {}
uint32_t operator()(const libzcash::OrchardRawAddress &zaddr) const;
uint32_t operator()(const libzcash::SaplingPaymentAddress &zaddr) const;
uint32_t operator()(const CScriptID &p2sh) const;
uint32_t operator()(const CKeyID &p2pkh) const;

View File

@ -7,13 +7,46 @@
namespace libzcash {
OrchardRawAddress OrchardIncomingViewingKey::Address(const diversifier_index_t& j) const {
assert(inner.get() != nullptr);
return OrchardRawAddress(orchard_incoming_viewing_key_to_address(inner.get(), j.begin()));
}
std::optional<diversifier_index_t> OrchardIncomingViewingKey::DecryptDiversifier(const OrchardRawAddress& addr) const {
assert(inner.get() != nullptr);
diversifier_index_t j_ret;
if (orchard_incoming_viewing_key_decrypt_diversifier(inner.get(), addr.inner.get(), j_ret.begin())) {
return j_ret;
} else {
return std::nullopt;
}
}
OrchardIncomingViewingKey OrchardFullViewingKey::ToIncomingViewingKey() const {
assert(inner.get() != nullptr);
return OrchardIncomingViewingKey(orchard_full_viewing_key_to_incoming_viewing_key(inner.get()));
}
OrchardIncomingViewingKey OrchardFullViewingKey::ToInternalIncomingViewingKey() const {
assert(inner.get() != nullptr);
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;
}
OrchardRawAddress OrchardFullViewingKey::GetChangeAddress() const {
return ToInternalIncomingViewingKey().Address(0);
}
OrchardSpendingKey OrchardSpendingKey::ForAccount(
const HDSeed& seed,
uint32_t bip44CoinType,

View File

@ -9,9 +9,19 @@
#include "zcash/address/zip32.h"
#include <rust/orchard/keys.h>
#include <optional>
class OrchardWallet;
namespace orchard {
class Builder;
class UnauthorizedBundle;
}
namespace libzcash {
class OrchardFullViewingKey;
class OrchardIncomingViewingKey;
class OrchardSpendingKey;
class OrchardRawAddress
{
@ -23,7 +33,13 @@ private:
OrchardRawAddress(OrchardRawAddressPtr* ptr) : inner(ptr, orchard_address_free) {}
friend class OrchardIncomingViewingKey;
friend class ::OrchardWallet;
friend class ::orchard::Builder;
public:
static OrchardRawAddress KeyIoOnlyFromReceiver(OrchardRawAddressPtr* ptr) {
return OrchardRawAddress(ptr);
}
OrchardRawAddress(OrchardRawAddress&& key) : inner(std::move(key.inner)) {}
OrchardRawAddress(const OrchardRawAddress& key) :
@ -44,22 +60,56 @@ public:
}
return *this;
}
};
class OrchardFullViewingKey;
friend bool operator==(const OrchardRawAddress& c1, const OrchardRawAddress& c2) {
return orchard_address_eq(c1.inner.get(), c2.inner.get());
}
friend bool operator<(const OrchardRawAddress& c1, const OrchardRawAddress& c2) {
return orchard_address_lt(c1.inner.get(), c2.inner.get());
}
template<typename Stream>
void Serialize(Stream& s) const {
RustStream rs(s);
if (!orchard_raw_address_serialize(inner.get(), &rs, RustStream<Stream>::write_callback)) {
throw std::ios_base::failure("Failed to serialize Orchard raw address to bytes");
}
}
template<typename Stream>
void Unserialize(Stream& s) {
RustStream rs(s);
OrchardRawAddressPtr* addr = orchard_raw_address_parse(&rs, RustStream<Stream>::read_callback);
if (addr == nullptr) {
throw std::ios_base::failure("Failed to parse Orchard raw address bytes");
}
inner.reset(addr);
}
template<typename Stream>
static OrchardRawAddress Read(Stream& stream) {
OrchardRawAddress key;
stream >> key;
return key;
}
};
class OrchardIncomingViewingKey
{
private:
std::unique_ptr<OrchardIncomingViewingKeyPtr, decltype(&orchard_incoming_viewing_key_free)> inner;
OrchardIncomingViewingKey() : inner(nullptr, orchard_incoming_viewing_key_free) {}
OrchardIncomingViewingKey(OrchardIncomingViewingKeyPtr* key) :
inner(key, orchard_incoming_viewing_key_free) {}
friend class OrchardFullViewingKey;
friend class OrchardSpendingKey;
friend class ::OrchardWallet;
public:
// DO NOT USE - this is exposed for serialization purposes only.
OrchardIncomingViewingKey() :
inner(nullptr, orchard_incoming_viewing_key_free) {}
OrchardIncomingViewingKey(OrchardIncomingViewingKey&& key) : inner(std::move(key.inner)) {}
@ -68,6 +118,12 @@ public:
OrchardRawAddress Address(const diversifier_index_t& j) const;
/**
* Decrypts the diversifier for the given raw address, and returns it if that
* address was derived from this IVK; otherwise returns std::nullopt;
*/
std::optional<diversifier_index_t> DecryptDiversifier(const OrchardRawAddress& addr) const;
OrchardIncomingViewingKey& operator=(OrchardIncomingViewingKey&& key)
{
if (this != &key) {
@ -86,15 +142,20 @@ public:
friend bool operator==(const OrchardIncomingViewingKey& a, const OrchardIncomingViewingKey& b)
{
assert(a.inner.get() != nullptr);
assert(b.inner.get() != nullptr);
return orchard_incoming_viewing_key_eq(a.inner.get(), b.inner.get());
}
friend bool operator<(const OrchardIncomingViewingKey& c1, const OrchardIncomingViewingKey& c2) {
assert(c1.inner.get() != nullptr);
assert(c2.inner.get() != nullptr);
return orchard_incoming_viewing_key_lt(c1.inner.get(), c2.inner.get());
}
template<typename Stream>
void Serialize(Stream& s) const {
assert(inner.get() != nullptr);
RustStream rs(s);
if (!orchard_incoming_viewing_key_serialize(inner.get(), &rs, RustStream<Stream>::write_callback)) {
throw std::ios_base::failure("Failed to serialize Orchard incoming viewing key");
@ -119,8 +180,6 @@ public:
}
};
class OrchardSpendingKey;
class OrchardFullViewingKey
{
private:
@ -132,12 +191,23 @@ private:
inner(ptr, orchard_full_viewing_key_free) {}
friend class OrchardSpendingKey;
friend class ::OrchardWallet;
public:
OrchardFullViewingKey(OrchardFullViewingKey&& key) : inner(std::move(key.inner)) {}
OrchardFullViewingKey(const OrchardFullViewingKey& key) :
inner(orchard_full_viewing_key_clone(key.inner.get()), orchard_full_viewing_key_free) {}
OrchardIncomingViewingKey ToIncomingViewingKey() const;
OrchardIncomingViewingKey ToInternalIncomingViewingKey() const;
uint256 ToExternalOutgoingViewingKey() const;
uint256 ToInternalOutgoingViewingKey() const;
libzcash::OrchardRawAddress GetChangeAddress() const;
OrchardFullViewingKey& operator=(OrchardFullViewingKey&& key)
{
if (this != &key) {
@ -183,8 +253,6 @@ public:
stream >> key;
return key;
}
OrchardIncomingViewingKey ToIncomingViewingKey() const;
};
class OrchardSpendingKey
@ -196,6 +264,9 @@ 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)) {}
@ -207,6 +278,8 @@ public:
uint32_t bip44CoinType,
libzcash::AccountId accountId);
OrchardFullViewingKey ToFullViewingKey() const;
OrchardSpendingKey& operator=(OrchardSpendingKey&& key)
{
if (this != &key) {
@ -222,33 +295,6 @@ public:
}
return *this;
}
template<typename Stream>
void Serialize(Stream& s) const {
RustStream rs(s);
if (!orchard_spending_key_serialize(inner.get(), &rs, RustStream<Stream>::write_callback)) {
throw std::ios_base::failure("Failed to serialize Orchard spending key");
}
}
template<typename Stream>
void Unserialize(Stream& s) {
RustStream rs(s);
OrchardSpendingKeyPtr* key = orchard_spending_key_parse(&rs, RustStream<Stream>::read_callback);
if (key == nullptr) {
throw std::ios_base::failure("Failed to parse Orchard spending key");
}
inner.reset(key);
}
template<typename Stream>
static OrchardSpendingKey Read(Stream& stream) {
OrchardSpendingKey key;
stream >> key;
return key;
}
OrchardFullViewingKey ToFullViewingKey() const;
};
} // namespace libzcash

View File

@ -2,9 +2,11 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
#include "tinyformat.h"
#include "zcash/Address.hpp"
#include "unified.h"
#include "util/match.h"
#include "utilstrencodings.h"
#include <rust/unified_keys.h>
@ -17,7 +19,7 @@ using namespace libzcash;
bool libzcash::HasShielded(const std::set<ReceiverType>& receiverTypes) {
auto has_shielded = [](ReceiverType r) {
// TODO: update this as support for new shielded protocols is added.
return r == ReceiverType::Sapling;
return r == ReceiverType::Sapling || r == ReceiverType::Orchard;
};
return std::find_if(receiverTypes.begin(), receiverTypes.end(), has_shielded) != receiverTypes.end();
}
@ -31,14 +33,43 @@ bool libzcash::HasTransparent(const std::set<ReceiverType>& receiverTypes) {
}
Receiver libzcash::RecipientAddressToReceiver(const RecipientAddress& recipient) {
Receiver recipientReceiver;
std::visit(match {
[&](const CKeyID& key) { recipientReceiver = key; },
[&](const CScriptID& scriptId) { recipientReceiver = scriptId; },
[&](const libzcash::SaplingPaymentAddress& addr) { recipientReceiver = addr; }
return std::visit(match {
[](const CKeyID& key) { return Receiver(key); },
[](const CScriptID& scriptId) { return Receiver(scriptId); },
[](const libzcash::OrchardRawAddress& addr) { return Receiver(addr); },
[](const libzcash::SaplingPaymentAddress& addr) { return Receiver(addr); }
}, recipient);
}
return recipientReceiver;
std::string libzcash::DebugPrintReceiver(const Receiver& receiver) {
return std::visit(match {
[&](const OrchardRawAddress &zaddr) {
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << zaddr;
return tfm::format("Orchard(%s)", HexStr(ss.begin(), ss.end()));
},
[&](const SaplingPaymentAddress &zaddr) {
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << zaddr;
return tfm::format("Sapling(%s)", HexStr(ss.begin(), ss.end()));
},
[&](const CScriptID &p2sh) {
return tfm::format("P2SH(%s)", p2sh.GetHex());
},
[&](const CKeyID &p2pkh) {
return tfm::format("P2PKH(%s)", p2pkh.GetHex());
},
[&](const UnknownReceiver &unknown) {
return tfm::format(
"Unknown(%x, %s)",
unknown.typecode,
HexStr(unknown.data.begin(), unknown.data.end()));
}
}, receiver);
};
std::string libzcash::DebugPrintRecipientAddress(const RecipientAddress& addr) {
return DebugPrintReceiver(RecipientAddressToReceiver(addr));
}
std::optional<ZcashdUnifiedSpendingKey> ZcashdUnifiedSpendingKey::ForAccount(
@ -51,7 +82,9 @@ std::optional<ZcashdUnifiedSpendingKey> ZcashdUnifiedSpendingKey::ForAccount(
auto saplingKey = SaplingExtendedSpendingKey::ForAccount(seed, bip44CoinType, accountId);
return ZcashdUnifiedSpendingKey(transparentKey.value(), saplingKey.first);
auto orchardKey = OrchardSpendingKey::ForAccount(seed, bip44CoinType, accountId);
return ZcashdUnifiedSpendingKey(transparentKey.value(), saplingKey.first, orchardKey);
}
UnifiedFullViewingKey ZcashdUnifiedSpendingKey::ToFullViewingKey() const {
@ -59,6 +92,7 @@ UnifiedFullViewingKey ZcashdUnifiedSpendingKey::ToFullViewingKey() const {
builder.AddTransparentKey(transparentKey.ToAccountPubKey());
builder.AddSaplingKey(saplingKey.ToXFVK());
builder.AddOrchardKey(orchardKey.ToFullViewingKey());
// This call to .value() is safe as ZcashdUnifiedSpendingKey values are always
// constructed to contain all required components.
@ -81,6 +115,11 @@ ZcashdUnifiedFullViewingKey ZcashdUnifiedFullViewingKey::FromUnifiedFullViewingK
result.saplingKey = saplingKey.value();
}
auto orchardKey = ufvk.GetOrchardKey();
if (orchardKey.has_value()) {
result.orchardKey = orchardKey.value();
}
return result;
}
@ -97,6 +136,14 @@ UnifiedAddressGenerationResult ZcashdUnifiedFullViewingKey::Address(
}
UnifiedAddress ua;
if (receiverTypes.count(ReceiverType::Orchard) > 0) {
if (orchardKey.has_value()) {
ua.AddReceiver(orchardKey.value().ToIncomingViewingKey().Address(j));
} else {
return UnifiedAddressGenerationError::ReceiverTypeNotAvailable;
}
}
if (receiverTypes.count(ReceiverType::Sapling) > 0) {
if (saplingKey.has_value()) {
auto saplingAddress = saplingKey.value().Address(j);
@ -146,7 +193,7 @@ UnifiedAddressGenerationResult ZcashdUnifiedFullViewingKey::FindAddress(
UnifiedAddressGenerationResult ZcashdUnifiedFullViewingKey::FindAddress(
const diversifier_index_t& j) const {
return FindAddress(j, {ReceiverType::P2PKH, ReceiverType::Sapling});
return FindAddress(j, {ReceiverType::P2PKH, ReceiverType::Sapling, ReceiverType::Orchard});
}
std::optional<RecipientAddress> ZcashdUnifiedFullViewingKey::GetChangeAddress(const ChangeRequest& req) const {
@ -161,20 +208,28 @@ std::optional<RecipientAddress> ZcashdUnifiedFullViewingKey::GetChangeAddress(co
}
},
[&](const SaplingChangeRequest& req) {
// currently true by construction, as a UFVK must have a supported shielded component
if (saplingKey.has_value()) {
addr = saplingKey.value().GetChangeAddress();
}
},
[&](const OrchardChangeRequest& req) {
if (orchardKey.has_value()) {
addr = orchardKey.value().GetChangeAddress();
}
}
}, req);
return addr;
}
std::optional<RecipientAddress> ZcashdUnifiedFullViewingKey::GetChangeAddress() const {
if (saplingKey.has_value()) {
std::optional<RecipientAddress> ZcashdUnifiedFullViewingKey::GetChangeAddress(
const std::set<OutputPool>& allowedPools) const {
if (orchardKey.has_value() && allowedPools.count(OutputPool::Orchard) > 0) {
return orchardKey.value().GetChangeAddress();
}
if (saplingKey.has_value() && allowedPools.count(OutputPool::Sapling) > 0) {
return saplingKey.value().GetChangeAddress();
}
if (transparentKey.has_value()) {
if (transparentKey.has_value() && allowedPools.count(OutputPool::Transparent) > 0) {
auto changeAddr = transparentKey.value().GetChangeAddress(diversifier_index_t(0));
if (changeAddr.has_value()) {
return changeAddr.value();
@ -192,6 +247,9 @@ UnifiedFullViewingKey ZcashdUnifiedFullViewingKey::ToFullViewingKey() const {
if (saplingKey.has_value()) {
builder.AddSaplingKey(saplingKey.value());
}
if (orchardKey.has_value()) {
builder.AddOrchardKey(orchardKey.value());
}
// This call to .value() is safe as ZcashdUnifiedFullViewingKey values are always
// constructed to contain all required components.

View File

@ -8,6 +8,7 @@
#include "transparent.h"
#include "key_constants.h"
#include "script/script.h"
#include "zcash/address/orchard.hpp"
#include "zip32.h"
#include <variant>
@ -23,7 +24,18 @@ enum class ReceiverType: uint32_t {
P2PKH = 0x00,
P2SH = 0x01,
Sapling = 0x02,
//Orchard = 0x03
Orchard = 0x03
};
/**
* An enumeration of the fund pools for which a transaction may produce outputs.
* It is sorted in descending preference order, so that when iterating over a
* set of output pools the most-preferred pool is selected first.
*/
enum class OutputPool {
Orchard,
Sapling,
Transparent,
};
enum class UnifiedAddressGenerationError {
@ -42,17 +54,10 @@ typedef std::variant<
typedef std::variant<
CKeyID,
CScriptID,
libzcash::SaplingPaymentAddress> RecipientAddress;
libzcash::SaplingPaymentAddress,
libzcash::OrchardRawAddress> RecipientAddress;
/**
* An enumeration of the types of change that a transaction may produce. It is
* sorted in descending preference order, so that when iterating over a set of
* change types the most-preferred type is selected first.
*/
enum class ChangeType {
Sapling,
Transparent,
};
std::string DebugPrintRecipientAddress(const RecipientAddress& add);
class TransparentChangeRequest {
private:
@ -66,10 +71,12 @@ public:
};
class SaplingChangeRequest {};
class OrchardChangeRequest {};
typedef std::variant<
TransparentChangeRequest,
SaplingChangeRequest> ChangeRequest;
SaplingChangeRequest,
OrchardChangeRequest> ChangeRequest;
/**
* Test whether the specified list of receiver types contains a
@ -113,6 +120,7 @@ public:
* variants by `operator<` is equivalent to sorting by preference.
*/
typedef std::variant<
OrchardRawAddress,
SaplingPaymentAddress,
CScriptID,
CKeyID,
@ -120,6 +128,8 @@ typedef std::variant<
Receiver RecipientAddressToReceiver(const RecipientAddress& recipient);
std::string DebugPrintReceiver(const Receiver& receiver);
/**
* An internal identifier for a unified full viewing key, derived as a
* blake2b hash of the serialized form of the UFVK.
@ -141,6 +151,7 @@ private:
UFVKId keyId;
std::optional<transparent::AccountPubKey> transparentKey;
std::optional<SaplingDiversifiableFullViewingKey> saplingKey;
std::optional<OrchardFullViewingKey> orchardKey;
ZcashdUnifiedFullViewingKey() {}
@ -169,6 +180,10 @@ public:
return saplingKey;
}
const std::optional<OrchardFullViewingKey>& GetOrchardKey() const {
return orchardKey;
}
/**
* Creates a new unified address having the specified receiver types, at the specified
* diversifier index, unless the diversifer index would generate an invalid receiver.
@ -226,7 +241,7 @@ public:
* *any* shielded pool) in which case the change address returned will be
* associated with diversifier index 0.
*/
std::optional<RecipientAddress> GetChangeAddress() const;
std::optional<RecipientAddress> GetChangeAddress(const std::set<OutputPool>& allowedPools) const;
UnifiedFullViewingKey ToFullViewingKey() const;
@ -243,10 +258,12 @@ class ZcashdUnifiedSpendingKey {
private:
transparent::AccountKey transparentKey;
SaplingExtendedSpendingKey saplingKey;
OrchardSpendingKey orchardKey;
ZcashdUnifiedSpendingKey(
transparent::AccountKey tkey,
SaplingExtendedSpendingKey skey): transparentKey(tkey), saplingKey(skey) {}
SaplingExtendedSpendingKey skey,
OrchardSpendingKey okey): transparentKey(tkey), saplingKey(skey), orchardKey(okey) {}
public:
static std::optional<ZcashdUnifiedSpendingKey> ForAccount(
const HDSeed& seed,
@ -257,10 +274,14 @@ public:
return transparentKey;
}
const SaplingExtendedSpendingKey& GetSaplingExtendedSpendingKey() const {
const SaplingExtendedSpendingKey& GetSaplingKey() const {
return saplingKey;
}
const OrchardSpendingKey& GetOrchardKey() const {
return orchardKey;
}
UnifiedFullViewingKey ToFullViewingKey() const;
};

View File

@ -295,4 +295,4 @@ bool IsInternalKeyPath(uint32_t purpose, uint32_t coinType, const std::string& k
}
}
};
} //namespace libzcash

View File

@ -178,6 +178,17 @@ public:
return j;
}
// Attempts to construct a valid internal payment address with diversifier
// index `j`; returns std::nullopt if `j` does not result in a valid diversifier
// given this xfvk.
std::optional<libzcash::SaplingPaymentAddress> InternalAddress(diversifier_index_t j) const {
return GetInternalDFVK().Address(j);
}
diversifier_index_t DecryptInternalDiversifier(const diversifier_t& d) const {
return GetInternalDFVK().DecryptDiversifier(d);
}
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>

View File

@ -342,7 +342,7 @@ double benchmark_increment_sprout_note_witnesses(size_t nTxs)
CBlock block1;
for (int i = 0; i < nTxs; ++i) {
auto wtx = CreateSproutTxWithNoteData(sproutSpendingKey);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
block1.vtx.push_back(wtx);
}
@ -357,7 +357,7 @@ double benchmark_increment_sprout_note_witnesses(size_t nTxs)
block2.hashPrevBlock = block1.GetHash();
{
auto sproutTx = CreateSproutTxWithNoteData(sproutSpendingKey);
wallet.AddToWallet(sproutTx, true, NULL);
wallet.LoadWalletTx(sproutTx);
block2.vtx.push_back(sproutTx);
}
@ -404,7 +404,7 @@ double benchmark_increment_sapling_note_witnesses(size_t nTxs)
CBlock block1;
for (int i = 0; i < nTxs; ++i) {
auto wtx = CreateSaplingTxWithNoteData(consensusParams, wallet, saplingSpendingKey);
wallet.AddToWallet(wtx, true, NULL);
wallet.LoadWalletTx(wtx);
block1.vtx.push_back(wtx);
}
@ -419,7 +419,7 @@ double benchmark_increment_sapling_note_witnesses(size_t nTxs)
block2.hashPrevBlock = block1.GetHash();
{
auto saplingTx = CreateSaplingTxWithNoteData(consensusParams, wallet, saplingSpendingKey);
wallet.AddToWallet(saplingTx, true, NULL);
wallet.LoadWalletTx(saplingTx);
block1.vtx.push_back(saplingTx);
}