Merge pull request #5710 from zcash/feature/wallet_orchard
Add Orchard support to the zcashd wallet.
This commit is contained in:
commit
9974be75fb
|
@ -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]
|
||||
|
|
|
@ -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",
|
||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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__':
|
||||
|
|
|
@ -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()
|
|
@ -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")
|
||||
|
|
|
@ -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)")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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, {});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
188
src/keystore.cpp
188
src/keystore.cpp
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
110
src/miner.cpp
110
src/miner.cpp
|
@ -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()) {
|
||||
|
|
13
src/miner.h
13
src/miner.h
|
@ -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,
|
||||
|
|
|
@ -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) {}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -13,6 +13,7 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
|
||||
/// Typesafe pointer to a Rust-allocated orchard::bundle::Bundle value
|
||||
struct OrchardBundlePtr;
|
||||
typedef struct OrchardBundlePtr OrchardBundlePtr;
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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),
|
||||
)),
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)),
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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, {});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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_;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
);
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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 ¬eData);
|
||||
void SetSaplingNoteData(mapSaplingNoteData_t ¬eData);
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -295,4 +295,4 @@ bool IsInternalKeyPath(uint32_t purpose, uint32_t coinType, const std::string& k
|
|||
}
|
||||
}
|
||||
|
||||
};
|
||||
} //namespace libzcash
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue