Add payment disclosure as experimental feature.

This commit is contained in:
Simon 2017-11-14 13:29:05 -08:00
parent 086c6bed58
commit 45232b1961
27 changed files with 1324 additions and 14 deletions

View File

@ -11,6 +11,7 @@ export BITCOIND=${REAL_BITCOIND}
#Run the tests
testScripts=(
'paymentdisclosure.py'
'prioritisetransaction.py'
'wallet_treestate.py'
'wallet_protectcoinbase.py'

233
qa/rpc-tests/paymentdisclosure.py Executable file
View File

@ -0,0 +1,233 @@
#!/usr/bin/env python2
# Copyright (c) 2017 The Zcash developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from test_framework.test_framework import BitcoinTestFramework
from test_framework.authproxy import JSONRPCException
from test_framework.util import assert_equal, initialize_chain_clean, \
start_node, connect_nodes_bi
import time
from decimal import Decimal
class PaymentDisclosureTest (BitcoinTestFramework):
def setup_chain(self):
print("Initializing test directory "+self.options.tmpdir)
initialize_chain_clean(self.options.tmpdir, 4)
def setup_network(self, split=False):
args = ['-debug=zrpcunsafe,paymentdisclosure', '-experimentalfeatures', '-paymentdisclosure', '-txindex=1']
self.nodes = []
self.nodes.append(start_node(0, self.options.tmpdir, args))
self.nodes.append(start_node(1, self.options.tmpdir, args))
# node 2 does not enable payment disclosure
args2 = ['-debug=zrpcunsafe', '-experimentalfeatures', '-txindex=1']
self.nodes.append(start_node(2, self.options.tmpdir, args2))
connect_nodes_bi(self.nodes,0,1)
connect_nodes_bi(self.nodes,1,2)
connect_nodes_bi(self.nodes,0,2)
self.is_network_split=False
self.sync_all()
# Returns txid if operation was a success or None
def wait_and_assert_operationid_status(self, nodeid, myopid, in_status='success', in_errormsg=None):
print('waiting for async operation {}'.format(myopid))
opids = []
opids.append(myopid)
timeout = 300
status = None
errormsg = None
txid = None
for x in xrange(1, timeout):
results = self.nodes[nodeid].z_getoperationresult(opids)
if len(results)==0:
time.sleep(1)
else:
status = results[0]["status"]
if status == "failed":
errormsg = results[0]['error']['message']
elif status == "success":
txid = results[0]['result']['txid']
break
print('...returned status: {}'.format(status))
assert_equal(in_status, status)
if errormsg is not None:
assert(in_errormsg is not None)
assert(in_errormsg in errormsg)
print('...returned error: {}'.format(errormsg))
return txid
def run_test (self):
print "Mining blocks..."
self.nodes[0].generate(4)
walletinfo = self.nodes[0].getwalletinfo()
assert_equal(walletinfo['immature_balance'], 40)
assert_equal(walletinfo['balance'], 0)
self.sync_all()
self.nodes[2].generate(3)
self.sync_all()
self.nodes[1].generate(101)
self.sync_all()
assert_equal(self.nodes[0].getbalance(), 40)
assert_equal(self.nodes[1].getbalance(), 10)
assert_equal(self.nodes[2].getbalance(), 30)
mytaddr = self.nodes[0].getnewaddress()
myzaddr = self.nodes[0].z_getnewaddress()
# Check that Node 2 has payment disclosure disabled.
try:
self.nodes[2].z_getpaymentdisclosure("invalidtxid", 0, 0)
assert(False)
except JSONRPCException as e:
errorString = e.error['message']
assert("payment disclosure is disabled" in errorString)
# Check that Node 0 returns an error for an unknown txid
try:
self.nodes[0].z_getpaymentdisclosure("invalidtxid", 0, 0)
assert(False)
except JSONRPCException as e:
errorString = e.error['message']
assert("No information available about transaction" in errorString)
# Shield coinbase utxos from node 0 of value 40, standard fee of 0.00010000
recipients = [{"address":myzaddr, "amount":Decimal('40.0')-Decimal('0.0001')}]
myopid = self.nodes[0].z_sendmany(mytaddr, recipients)
txid = self.wait_and_assert_operationid_status(0, myopid)
# Check the tx has joinsplits
assert( len(self.nodes[0].getrawtransaction("" + txid, 1)["vjoinsplit"]) > 0 )
# Sync mempools
self.sync_all()
# Confirm that you can't create a payment disclosure for an unconfirmed tx
try:
self.nodes[0].z_getpaymentdisclosure(txid, 0, 0)
assert(False)
except JSONRPCException as e:
errorString = e.error['message']
assert("Transaction has not been confirmed yet" in errorString)
try:
self.nodes[1].z_getpaymentdisclosure(txid, 0, 0)
assert(False)
except JSONRPCException as e:
errorString = e.error['message']
assert("Transaction has not been confirmed yet" in errorString)
# Mine tx
self.nodes[0].generate(1)
self.sync_all()
# Confirm that Node 1 cannot create a payment disclosure for a transaction which does not impact its wallet
try:
self.nodes[1].z_getpaymentdisclosure(txid, 0, 0)
assert(False)
except JSONRPCException as e:
errorString = e.error['message']
assert("Transaction does not belong to the wallet" in errorString)
# Check that an invalid joinsplit index is rejected
try:
self.nodes[0].z_getpaymentdisclosure(txid, 1, 0)
assert(False)
except JSONRPCException as e:
errorString = e.error['message']
assert("Invalid js_index" in errorString)
try:
self.nodes[0].z_getpaymentdisclosure(txid, -1, 0)
assert(False)
except JSONRPCException as e:
errorString = e.error['message']
assert("Invalid js_index" in errorString)
# Check that an invalid output index is rejected
try:
self.nodes[0].z_getpaymentdisclosure(txid, 0, 2)
assert(False)
except JSONRPCException as e:
errorString = e.error['message']
assert("Invalid output_index" in errorString)
try:
self.nodes[0].z_getpaymentdisclosure(txid, 0, -1)
assert(False)
except JSONRPCException as e:
errorString = e.error['message']
assert("Invalid output_index" in errorString)
# Ask Node 0 to create and validate a payment disclosure for output 0
message = "Here is proof of my payment!"
pd = self.nodes[0].z_getpaymentdisclosure(txid, 0, 0, message)
result = self.nodes[0].z_validatepaymentdisclosure(pd)
assert(result["valid"])
output_value_sum = Decimal(result["value"])
# Ask Node 1 to confirm the payment disclosure is valid
result = self.nodes[1].z_validatepaymentdisclosure(pd)
assert(result["valid"])
assert_equal(result["message"], message)
assert_equal(result["value"], output_value_sum)
# Check that total value of output index 0 and index 1 should equal shielding amount of 40 less standard fee.
pd = self.nodes[0].z_getpaymentdisclosure(txid, 0, 1)
result = self.nodes[0].z_validatepaymentdisclosure(pd)
output_value_sum += Decimal(result["value"])
assert_equal(output_value_sum, Decimal('39.99990000'))
# Create a z->z transaction, sending shielded funds from node 0 to node 1
node1zaddr = self.nodes[1].z_getnewaddress()
recipients = [{"address":node1zaddr, "amount":Decimal('1')}]
myopid = self.nodes[0].z_sendmany(myzaddr, recipients)
txid = self.wait_and_assert_operationid_status(0, myopid)
self.sync_all()
self.nodes[0].generate(1)
self.sync_all()
# Confirm that Node 0 can create a valid payment disclosure
pd = self.nodes[0].z_getpaymentdisclosure(txid, 0, 0, "a message of your choice")
result = self.nodes[0].z_validatepaymentdisclosure(pd)
assert(result["valid"])
# Confirm that Node 1, even as recipient of shielded funds, cannot create a payment disclosure
# as the transaction was created by Node 0 and Node 1's payment disclosure database does not
# contain the necessary data to do so, where the data would only have been available on Node 0
# when executing z_shieldcoinbase.
try:
self.nodes[1].z_getpaymentdisclosure(txid, 0, 0)
assert(False)
except JSONRPCException as e:
errorString = e.error['message']
assert("Could not find payment disclosure info for the given joinsplit output" in errorString)
# Payment disclosures cannot be created for transparent transactions.
txid = self.nodes[2].sendtoaddress(mytaddr, 1.0)
self.sync_all()
# No matter the type of transaction, if it has not been confirmed, it is ignored.
try:
self.nodes[0].z_getpaymentdisclosure(txid, 0, 0)
assert(False)
except JSONRPCException as e:
errorString = e.error['message']
assert("Transaction has not been confirmed yet" in errorString)
self.nodes[0].generate(1)
self.sync_all()
# Confirm that a payment disclosure can only be generated for a shielded transaction.
try:
self.nodes[0].z_getpaymentdisclosure(txid, 0, 0)
assert(False)
except JSONRPCException as e:
errorString = e.error['message']
assert("Transaction is not a shielded transaction" in errorString)
if __name__ == '__main__':
PaymentDisclosureTest().main()

View File

@ -160,6 +160,8 @@ BITCOIN_CORE_H = \
net.h \
netbase.h \
noui.h \
paymentdisclosure.h \
paymentdisclosuredb.h \
policy/fees.h \
pow.h \
primitives/block.h \
@ -244,6 +246,8 @@ libbitcoin_server_a_SOURCES = \
miner.cpp \
net.cpp \
noui.cpp \
paymentdisclosure.cpp \
paymentdisclosuredb.cpp \
policy/fees.cpp \
pow.cpp \
rest.cpp \
@ -293,6 +297,9 @@ libbitcoin_wallet_a_SOURCES = \
wallet/asyncrpcoperation_shieldcoinbase.cpp \
wallet/crypter.cpp \
wallet/db.cpp \
paymentdisclosure.cpp \
paymentdisclosuredb.cpp \
wallet/rpcdisclosure.cpp \
wallet/rpcdump.cpp \
wallet/rpcwallet.cpp \
wallet/wallet.cpp \

View File

@ -38,6 +38,7 @@ zcash_gtest_SOURCES += \
gtest/test_txid.cpp \
gtest/test_libzcash_utils.cpp \
gtest/test_proofs.cpp \
gtest/test_paymentdisclosure.cpp \
gtest/test_checkblock.cpp
if ENABLE_WALLET
zcash_gtest_SOURCES += \

View File

@ -80,6 +80,8 @@ TEST(founders_reward_test, create_testnet_2of3multisig) {
std::cout << s << std::endl;
pWallet->Flush(true);
ECC_Stop();
}
#endif

View File

@ -0,0 +1,210 @@
#include <gtest/gtest.h>
#include "main.h"
#include "utilmoneystr.h"
#include "chainparams.h"
#include "utilstrencodings.h"
#include "zcash/Address.hpp"
#include "wallet/wallet.h"
#include "amount.h"
#include <memory>
#include <string>
#include <set>
#include <vector>
#include <boost/filesystem.hpp>
#include <iostream>
#include "util.h"
#include "paymentdisclosure.h"
#include "paymentdisclosuredb.h"
#include "sodium.h"
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
using namespace std;
/*
To run tests:
./zcash-gtest --gtest_filter="paymentdisclosure.*"
Note: As an experimental feature, writing your own tests may require option flags to be set.
mapArgs["-experimentalfeatures"] = true;
mapArgs["-paymentdisclosure"] = true;
*/
#define NUM_TRIES 10000
#define DUMP_DATABASE_TO_STDOUT false
static boost::uuids::random_generator uuidgen;
static uint256 random_uint256()
{
uint256 ret;
randombytes_buf(ret.begin(), 32);
return ret;
}
// Subclass of PaymentDisclosureDB to add debugging methods
class PaymentDisclosureDBTest : public PaymentDisclosureDB {
public:
PaymentDisclosureDBTest(const boost::filesystem::path& dbPath) : PaymentDisclosureDB(dbPath) {}
void DebugDumpAllStdout() {
ASSERT_NE(db, nullptr);
std::lock_guard<std::mutex> guard(lock_);
// Iterate over each item in the database and print them
leveldb::Iterator* it = db->NewIterator(leveldb::ReadOptions());
for (it->SeekToFirst(); it->Valid(); it->Next()) {
cout << it->key().ToString() << " : ";
// << it->value().ToString() << endl;
try {
std::string strValue = it->value().ToString();
PaymentDisclosureInfo info;
CDataStream ssValue(strValue.data(), strValue.data() + strValue.size(), SER_DISK, CLIENT_VERSION);
ssValue >> info;
cout << info.ToString() << std::endl;
} catch (const std::exception& e) {
cout << e.what() << std::endl;
}
}
if (false == it->status().ok()) {
cerr << "An error was found iterating over the database" << endl;
cerr << it->status().ToString() << endl;
}
delete it;
}
};
// This test creates random payment disclosure blobs and checks that they can be
// 1. inserted and retrieved from a database
// 2. serialized and deserialized without corruption
TEST(paymentdisclosure, mainnet) {
ECC_Start();
SelectParams(CBaseChainParams::MAIN);
boost::filesystem::path pathTemp = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
boost::filesystem::create_directories(pathTemp);
mapArgs["-datadir"] = pathTemp.string();
std::cout << "Test payment disclosure database created in folder: " << pathTemp.native() << std::endl;
PaymentDisclosureDBTest mydb(pathTemp);
for (int i=0; i<NUM_TRIES; i++) {
// Generate an ephemeral keypair for joinsplit sig.
uint256 joinSplitPubKey;
unsigned char buffer[crypto_sign_SECRETKEYBYTES] = {0};
crypto_sign_keypair(joinSplitPubKey.begin(), &buffer[0]);
// First 32 bytes contain private key, second 32 bytes contain public key.
ASSERT_EQ(0, memcmp(joinSplitPubKey.begin(), &buffer[0]+32, 32));
std::vector<unsigned char> vch(&buffer[0], &buffer[0] + 32);
uint256 joinSplitPrivKey = uint256(vch);
// Create payment disclosure key and info data to store in test database
size_t js = random_uint256().GetCheapHash() % std::numeric_limits<size_t>::max();
uint8_t n = random_uint256().GetCheapHash() % std::numeric_limits<uint8_t>::max();
PaymentDisclosureKey key { random_uint256(), js, n};
PaymentDisclosureInfo info;
info.esk = random_uint256();
info.joinSplitPrivKey = joinSplitPrivKey;
info.zaddr = libzcash::SpendingKey::random().address();
ASSERT_TRUE(mydb.Put(key, info));
// Retrieve info from test database into new local variable and test it matches
PaymentDisclosureInfo info2;
ASSERT_TRUE(mydb.Get(key, info2));
ASSERT_EQ(info, info2);
// Modify this local variable and confirm it no longer matches
info2.esk = random_uint256();
info2.joinSplitPrivKey = random_uint256();
info2.zaddr = libzcash::SpendingKey::random().address();
ASSERT_NE(info, info2);
// Using the payment info object, let's create a dummy payload
PaymentDisclosurePayload payload;
payload.version = PAYMENT_DISCLOSURE_VERSION_EXPERIMENTAL;
payload.esk = info.esk;
payload.txid = key.hash;
payload.js = key.js;
payload.n = key.n;
payload.message = "random-" + boost::uuids::to_string(uuidgen()); // random message
payload.zaddr = info.zaddr;
// Serialize and hash the payload to generate a signature
uint256 dataToBeSigned = SerializeHash(payload, SER_GETHASH, 0);
// Compute the payload signature
unsigned char payloadSig[64];
if (!(crypto_sign_detached(&payloadSig[0], NULL,
dataToBeSigned.begin(), 32,
&buffer[0] // buffer containing both private and public key required
) == 0))
{
throw std::runtime_error("crypto_sign_detached failed");
}
// Sanity check
if (!(crypto_sign_verify_detached(&payloadSig[0],
dataToBeSigned.begin(), 32,
joinSplitPubKey.begin()
) == 0))
{
throw std::runtime_error("crypto_sign_verify_detached failed");
}
// Convert signature buffer to boost array
boost::array<unsigned char, 64> arrayPayloadSig;
memcpy(arrayPayloadSig.data(), &payloadSig[0], 64);
// Payment disclosure blob to pass around
PaymentDisclosure pd = {payload, arrayPayloadSig};
// Test payment disclosure constructors
PaymentDisclosure pd2(payload, arrayPayloadSig);
ASSERT_EQ(pd, pd2);
PaymentDisclosure pd3(joinSplitPubKey, key, info, payload.message);
ASSERT_EQ(pd, pd3);
// Verify serialization and deserialization works
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << pd;
std::string ssHexString = HexStr(ss.begin(), ss.end());
PaymentDisclosure pdTmp;
CDataStream ssTmp(ParseHex(ssHexString), SER_NETWORK, PROTOCOL_VERSION);
ssTmp >> pdTmp;
ASSERT_EQ(pd, pdTmp);
CDataStream ss2(SER_NETWORK, PROTOCOL_VERSION);
ss2 << pdTmp;
std::string ss2HexString = HexStr(ss2.begin(), ss2.end());
ASSERT_EQ(ssHexString, ss2HexString);
// Verify marker
ASSERT_EQ(pd.payload.marker, PAYMENT_DISCLOSURE_PAYLOAD_MAGIC_BYTES);
ASSERT_EQ(pdTmp.payload.marker, PAYMENT_DISCLOSURE_PAYLOAD_MAGIC_BYTES);
ASSERT_EQ(0, ssHexString.find("706462ff")); // Little endian encoding of PAYMENT_DISCLOSURE_PAYLOAD_MAGIC_BYTES value
// Sanity check
PaymentDisclosure pdDummy;
ASSERT_NE(pd, pdDummy);
}
#if DUMP_DATABASE_TO_STDOUT == true
mydb.DebugDumpAllStdout();
#endif
ECC_Stop();
}

View File

@ -62,7 +62,7 @@ TEST(Transaction, JSDescriptionRandomized) {
*params, pubKeyHash, rt,
inputs, outputs,
inputMap, outputMap,
0, 0, false, GenZero);
0, 0, false, nullptr, GenZero);
boost::array<size_t, ZC_NUM_JS_INPUTS> expectedInputMap {1, 0};
boost::array<size_t, ZC_NUM_JS_OUTPUTS> expectedOutputMap {1, 0};
@ -75,7 +75,7 @@ TEST(Transaction, JSDescriptionRandomized) {
*params, pubKeyHash, rt,
inputs, outputs,
inputMap, outputMap,
0, 0, false, GenMax);
0, 0, false, nullptr, GenMax);
boost::array<size_t, ZC_NUM_JS_INPUTS> expectedInputMap {0, 1};
boost::array<size_t, ZC_NUM_JS_OUTPUTS> expectedOutputMap {0, 1};

View File

@ -791,6 +791,9 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
if (mapArgs.count("-developerencryptwallet")) {
return InitError(_("Wallet encryption requires -experimentalfeatures."));
}
else if (mapArgs.count("-paymentdisclosure")) {
return InitError(_("Payment disclosure requires -experimentalfeatures."));
}
}
// Set this early so that parameter interactions go to console

63
src/paymentdisclosure.cpp Normal file
View File

@ -0,0 +1,63 @@
// Copyright (c) 2017 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "paymentdisclosure.h"
#include "util.h"
std::string PaymentDisclosureInfo::ToString() const {
return strprintf("PaymentDisclosureInfo(version=%d, esk=%s, joinSplitPrivKey=<omitted>, address=%s)",
version, esk.ToString(), CZCPaymentAddress(zaddr).ToString());
}
std::string PaymentDisclosure::ToString() const {
std::string s = HexStr(payloadSig.begin(), payloadSig.end());
return strprintf("PaymentDisclosure(payload=%s, payloadSig=%s)", payload.ToString(), s);
}
std::string PaymentDisclosurePayload::ToString() const {
return strprintf("PaymentDisclosurePayload(version=%d, esk=%s, txid=%s, js=%d, n=%d, address=%s, message=%s)",
version, esk.ToString(), txid.ToString(), js, n, CZCPaymentAddress(zaddr).ToString(), message);
}
PaymentDisclosure::PaymentDisclosure(const uint256 &joinSplitPubKey, const PaymentDisclosureKey &key, const PaymentDisclosureInfo &info, const std::string &message)
{
// Populate payload member variable
payload.version = info.version; // experimental = 0, production = 1 etc.
payload.esk = info.esk;
payload.txid = key.hash;
payload.js = key.js;
payload.n = key.n;
payload.zaddr = info.zaddr;
payload.message = message;
// Serialize and hash the payload to generate a signature
uint256 dataToBeSigned = SerializeHash(payload, SER_GETHASH, 0);
LogPrint("paymentdisclosure", "Payment Disclosure: signing raw payload = %s\n", dataToBeSigned.ToString());
// Prepare buffer to store ed25519 key pair in libsodium-compatible format
unsigned char bufferKeyPair[64];
memcpy(&bufferKeyPair[0], info.joinSplitPrivKey.begin(), 32);
memcpy(&bufferKeyPair[32], joinSplitPubKey.begin(), 32);
// Compute payload signature member variable
if (!(crypto_sign_detached(payloadSig.data(), NULL,
dataToBeSigned.begin(), 32,
&bufferKeyPair[0]
) == 0))
{
throw std::runtime_error("crypto_sign_detached failed");
}
// Sanity check
if (!(crypto_sign_verify_detached(payloadSig.data(),
dataToBeSigned.begin(), 32,
joinSplitPubKey.begin()) == 0))
{
throw std::runtime_error("crypto_sign_verify_detached failed");
}
std::string sigString = HexStr(payloadSig.data(), payloadSig.data() + payloadSig.size());
LogPrint("paymentdisclosure", "Payment Disclosure: signature = %s\n", sigString);
}

145
src/paymentdisclosure.h Normal file
View File

@ -0,0 +1,145 @@
// Copyright (c) 2017 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef ZCASH_PAYMENTDISCLOSURE_H
#define ZCASH_PAYMENTDISCLOSURE_H
#include "uint256.h"
#include "clientversion.h"
#include "serialize.h"
#include "streams.h"
#include "version.h"
// For JSOutPoint
#include "wallet/wallet.h"
#include <cstdint>
#include <string>
// Ensure that the two different protocol messages, payment disclosure blobs and transactions,
// which are signed with the same key, joinSplitPrivKey, have disjoint encodings such that an
// encoding from one context will be rejected in the other. We know that the set of valid
// transaction versions is currently ({1..INT32_MAX}) so we will use a negative value for
// payment disclosure of -10328976 which in hex is 0xFF626470. Serialization is in little endian
// format, so a payment disclosure hex string begins 706462FF, which in ISO-8859-1 is "pdbÿ".
#define PAYMENT_DISCLOSURE_PAYLOAD_MAGIC_BYTES -10328976
#define PAYMENT_DISCLOSURE_VERSION_EXPERIMENTAL 0
typedef JSOutPoint PaymentDisclosureKey;
struct PaymentDisclosureInfo {
uint8_t version; // 0 = experimental, 1 = first production version, etc.
uint256 esk; // zcash/NoteEncryption.cpp
uint256 joinSplitPrivKey; // primitives/transaction.h
// ed25519 - not tied to implementation e.g. libsodium, see ed25519 rfc
libzcash::PaymentAddress zaddr;
PaymentDisclosureInfo() : version(PAYMENT_DISCLOSURE_VERSION_EXPERIMENTAL) {
}
PaymentDisclosureInfo(uint8_t v, uint256 esk, uint256 key, libzcash::PaymentAddress zaddr) : version(v), esk(esk), joinSplitPrivKey(key), zaddr(zaddr) { }
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) {
READWRITE(version);
READWRITE(esk);
READWRITE(joinSplitPrivKey);
READWRITE(zaddr);
}
std::string ToString() const;
friend bool operator==(const PaymentDisclosureInfo& a, const PaymentDisclosureInfo& b) {
return (a.version == b.version && a.esk == b.esk && a.joinSplitPrivKey == b.joinSplitPrivKey && a.zaddr == b.zaddr);
}
friend bool operator!=(const PaymentDisclosureInfo& a, const PaymentDisclosureInfo& b) {
return !(a == b);
}
};
struct PaymentDisclosurePayload {
int32_t marker = PAYMENT_DISCLOSURE_PAYLOAD_MAGIC_BYTES; // to be disjoint from transaction encoding
uint8_t version; // 0 = experimental, 1 = first production version, etc.
uint256 esk; // zcash/NoteEncryption.cpp
uint256 txid; // primitives/transaction.h
size_t js; // Index into CTransaction.vjoinsplit
uint8_t n; // Index into JSDescription fields of length ZC_NUM_JS_OUTPUTS
libzcash::PaymentAddress zaddr; // zcash/Address.hpp
std::string message; // parameter to RPC call
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) {
READWRITE(marker);
READWRITE(version);
READWRITE(esk);
READWRITE(txid);
READWRITE(js);
READWRITE(n);
READWRITE(zaddr);
READWRITE(message);
}
std::string ToString() const;
friend bool operator==(const PaymentDisclosurePayload& a, const PaymentDisclosurePayload& b) {
return (
a.version == b.version &&
a.esk == b.esk &&
a.txid == b.txid &&
a.js == b.js &&
a.n == b.n &&
a.zaddr == b.zaddr &&
a.message == b.message
);
}
friend bool operator!=(const PaymentDisclosurePayload& a, const PaymentDisclosurePayload& b) {
return !(a == b);
}
};
struct PaymentDisclosure {
PaymentDisclosurePayload payload;
boost::array<unsigned char, 64> payloadSig;
// We use boost array because serialize doesn't like char buffer, otherwise we could do: unsigned char payloadSig[64];
PaymentDisclosure() {};
PaymentDisclosure(const PaymentDisclosurePayload payload, const boost::array<unsigned char, 64> sig) : payload(payload), payloadSig(sig) {};
PaymentDisclosure(const uint256& joinSplitPubKey, const PaymentDisclosureKey& key, const PaymentDisclosureInfo& info, const std::string& message);
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) {
READWRITE(payload);
READWRITE(payloadSig);
}
std::string ToString() const;
friend bool operator==(const PaymentDisclosure& a, const PaymentDisclosure& b) {
return (a.payload == b.payload && a.payloadSig == b.payloadSig);
}
friend bool operator!=(const PaymentDisclosure& a, const PaymentDisclosure& b) {
return !(a == b);
}
};
typedef std::pair<PaymentDisclosureKey, PaymentDisclosureInfo> PaymentDisclosureKeyInfo;
#endif // ZCASH_PAYMENTDISCLOSURE_H

View File

@ -0,0 +1,93 @@
// Copyright (c) 2017 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "paymentdisclosuredb.h"
#include "util.h"
#include "leveldbwrapper.h"
#include <boost/filesystem.hpp>
using namespace std;
static boost::filesystem::path emptyPath;
/**
* Static method to return the shared/default payment disclosure database.
*/
shared_ptr<PaymentDisclosureDB> PaymentDisclosureDB::sharedInstance() {
// Thread-safe in C++11 and gcc 4.3
static shared_ptr<PaymentDisclosureDB> ptr = std::make_shared<PaymentDisclosureDB>();
return ptr;
}
// C++11 delegated constructor
PaymentDisclosureDB::PaymentDisclosureDB() : PaymentDisclosureDB(emptyPath) {
}
PaymentDisclosureDB::PaymentDisclosureDB(const boost::filesystem::path& dbPath) {
boost::filesystem::path path(dbPath);
if (path.empty()) {
path = GetDataDir() / "paymentdisclosure";
LogPrintf("PaymentDisclosure: using default path for database: %s\n", path.string());
} else {
LogPrintf("PaymentDisclosure: using custom path for database: %s\n", path.string());
}
TryCreateDirectory(path);
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options, path.string(), &db);
HandleError(status); // throws exception
LogPrintf("PaymentDisclosure: Opened LevelDB successfully\n");
}
PaymentDisclosureDB::~PaymentDisclosureDB() {
if (db != nullptr) {
delete db;
}
}
bool PaymentDisclosureDB::Put(const PaymentDisclosureKey& key, const PaymentDisclosureInfo& info)
{
if (db == nullptr) {
return false;
}
std::lock_guard<std::mutex> guard(lock_);
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
ssValue.reserve(ssValue.GetSerializeSize(info));
ssValue << info;
leveldb::Slice slice(&ssValue[0], ssValue.size());
leveldb::Status status = db->Put(writeOptions, key.ToString(), slice);
HandleError(status);
return true;
}
bool PaymentDisclosureDB::Get(const PaymentDisclosureKey& key, PaymentDisclosureInfo& info)
{
if (db == nullptr) {
return false;
}
std::lock_guard<std::mutex> guard(lock_);
std::string strValue;
leveldb::Status status = db->Get(readOptions, key.ToString(), &strValue);
if (!status.ok()) {
if (status.IsNotFound())
return false;
LogPrintf("PaymentDisclosure: LevelDB read failure: %s\n", status.ToString());
HandleError(status);
}
try {
CDataStream ssValue(strValue.data(), strValue.data() + strValue.size(), SER_DISK, CLIENT_VERSION);
ssValue >> info;
} catch (const std::exception&) {
return false;
}
return true;
}

42
src/paymentdisclosuredb.h Normal file
View File

@ -0,0 +1,42 @@
// Copyright (c) 2017 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef ZCASH_PAYMENTDISCLOSUREDB_H
#define ZCASH_PAYMENTDISCLOSUREDB_H
#include "paymentdisclosure.h"
#include <cstdint>
#include <string>
#include <mutex>
#include <future>
#include <memory>
#include <boost/optional.hpp>
#include <leveldb/db.h>
class PaymentDisclosureDB
{
protected:
leveldb::DB* db = nullptr;
leveldb::Options options;
leveldb::ReadOptions readOptions;
leveldb::WriteOptions writeOptions;
mutable std::mutex lock_;
public:
static std::shared_ptr<PaymentDisclosureDB> sharedInstance();
PaymentDisclosureDB();
PaymentDisclosureDB(const boost::filesystem::path& dbPath);
~PaymentDisclosureDB();
bool Put(const PaymentDisclosureKey& key, const PaymentDisclosureInfo& info);
bool Get(const PaymentDisclosureKey& key, PaymentDisclosureInfo& info);
};
#endif // ZCASH_PAYMENTDISCLOSUREDB_H

View File

@ -16,7 +16,9 @@ JSDescription::JSDescription(ZCJoinSplit& params,
const boost::array<libzcash::JSOutput, ZC_NUM_JS_OUTPUTS>& outputs,
CAmount vpub_old,
CAmount vpub_new,
bool computeProof) : vpub_old(vpub_old), vpub_new(vpub_new), anchor(anchor)
bool computeProof,
uint256 *esk // payment disclosure
) : vpub_old(vpub_old), vpub_new(vpub_new), anchor(anchor)
{
boost::array<libzcash::Note, ZC_NUM_JS_OUTPUTS> notes;
@ -37,7 +39,8 @@ JSDescription::JSDescription(ZCJoinSplit& params,
vpub_old,
vpub_new,
anchor,
computeProof
computeProof,
esk // payment disclosure
);
}
@ -52,7 +55,9 @@ JSDescription JSDescription::Randomized(
CAmount vpub_old,
CAmount vpub_new,
bool computeProof,
std::function<int(int)> gen)
uint256 *esk, // payment disclosure
std::function<int(int)> gen
)
{
// Randomize the order of the inputs and outputs
inputMap = {0, 1};
@ -65,7 +70,9 @@ JSDescription JSDescription::Randomized(
return JSDescription(
params, pubKeyHash, anchor, inputs, outputs,
vpub_old, vpub_new, computeProof);
vpub_old, vpub_new, computeProof,
esk // payment disclosure
);
}
bool JSDescription::Verify(

View File

@ -77,7 +77,8 @@ public:
const boost::array<libzcash::JSOutput, ZC_NUM_JS_OUTPUTS>& outputs,
CAmount vpub_old,
CAmount vpub_new,
bool computeProof = true // Set to false in some tests
bool computeProof = true, // Set to false in some tests
uint256 *esk = nullptr // payment disclosure
);
static JSDescription Randomized(
@ -91,6 +92,7 @@ public:
CAmount vpub_old,
CAmount vpub_new,
bool computeProof = true, // Set to false in some tests
uint256 *esk = nullptr, // payment disclosure
std::function<int(int)> gen = GetRandInt
);

View File

@ -114,6 +114,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "z_getoperationstatus", 0},
{ "z_getoperationresult", 0},
{ "z_importkey", 2 },
{ "z_getpaymentdisclosure", 1},
{ "z_getpaymentdisclosure", 2}
};
class CRPCConvertTable

View File

@ -396,7 +396,11 @@ static const CRPCCommand vRPCCommands[] =
{ "wallet", "z_exportkey", &z_exportkey, true },
{ "wallet", "z_importkey", &z_importkey, true },
{ "wallet", "z_exportwallet", &z_exportwallet, true },
{ "wallet", "z_importwallet", &z_importwallet, true }
{ "wallet", "z_importwallet", &z_importwallet, true },
// TODO: rearrange into another category
{ "disclosure", "z_getpaymentdisclosure", &z_getpaymentdisclosure, true },
{ "disclosure", "z_validatepaymentdisclosure", &z_validatepaymentdisclosure, true }
#endif // ENABLE_WALLET
};

View File

@ -292,6 +292,8 @@ extern UniValue z_getoperationstatus(const UniValue& params, bool fHelp); // in
extern UniValue z_getoperationresult(const UniValue& params, bool fHelp); // in rpcwallet.cpp
extern UniValue z_listoperationids(const UniValue& params, bool fHelp); // in rpcwallet.cpp
extern UniValue z_validateaddress(const UniValue& params, bool fHelp); // in rpcmisc.cpp
extern UniValue z_getpaymentdisclosure(const UniValue& params, bool fHelp); // in rpcdisclosure.cpp
extern UniValue z_validatepaymentdisclosure(const UniValue &params, bool fHelp); // in rpcdisclosure.cpp
bool StartRPC();
void InterruptRPC();

View File

@ -28,6 +28,8 @@
#include <thread>
#include <string>
#include "paymentdisclosuredb.h"
using namespace libzcash;
int find_output(UniValue obj, int n) {
@ -103,6 +105,10 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
} else {
LogPrint("zrpc", "%s: z_sendmany initialized\n", getId());
}
// Enable payment disclosure if requested
paymentDisclosureMode = fExperimentalMode && GetBoolArg("-paymentdisclosure", false);
}
AsyncRPCOperation_sendmany::~AsyncRPCOperation_sendmany() {
@ -169,6 +175,21 @@ void AsyncRPCOperation_sendmany::main() {
s += strprintf(", error=%s)\n", getErrorMessage());
}
LogPrintf("%s",s);
// !!! Payment disclosure START
if (success && paymentDisclosureMode && paymentDisclosureData_.size()>0) {
uint256 txidhash = tx_.GetHash();
std::shared_ptr<PaymentDisclosureDB> db = PaymentDisclosureDB::sharedInstance();
for (PaymentDisclosureKeyInfo p : paymentDisclosureData_) {
p.first.hash = txidhash;
if (!db->Put(p.first, p.second)) {
LogPrint("paymentdisclosure", "%s: Payment Disclosure: Error writing entry to database for key %s\n", getId(), p.first.ToString());
} else {
LogPrint("paymentdisclosure", "%s: Payment Disclosure: Successfully added entry to database for key %s\n", getId(), p.first.ToString());
}
}
}
// !!! Payment disclosure END
}
// Notes:
@ -945,6 +966,9 @@ UniValue AsyncRPCOperation_sendmany::perform_joinsplit(
{info.vjsout[0], info.vjsout[1]};
boost::array<size_t, ZC_NUM_JS_INPUTS> inputMap;
boost::array<size_t, ZC_NUM_JS_OUTPUTS> outputMap;
uint256 esk; // payment disclosure - secret
JSDescription jsdesc = JSDescription::Randomized(
*pzcashParams,
joinSplitPubKey_,
@ -955,8 +979,8 @@ UniValue AsyncRPCOperation_sendmany::perform_joinsplit(
outputMap,
info.vpub_old,
info.vpub_new,
!this->testmode);
!this->testmode,
&esk); // parameter expects pointer to esk, so pass in address
{
auto verifier = libzcash::ProofVerifier::Strict();
if (!(jsdesc.Verify(*pzcashParams, verifier, joinSplitPubKey_))) {
@ -1025,6 +1049,28 @@ UniValue AsyncRPCOperation_sendmany::perform_joinsplit(
arrOutputMap.push_back(outputMap[i]);
}
// !!! Payment disclosure START
unsigned char buffer[32] = {0};
memcpy(&buffer[0], &joinSplitPrivKey_[0], 32); // private key in first half of 64 byte buffer
std::vector<unsigned char> vch(&buffer[0], &buffer[0] + 32);
uint256 joinSplitPrivKey = uint256(vch);
size_t js_index = tx_.vjoinsplit.size() - 1;
uint256 placeholder;
for (int i = 0; i < ZC_NUM_JS_OUTPUTS; i++) {
uint8_t mapped_index = outputMap[i];
// placeholder for txid will be filled in later when tx has been finalized and signed.
PaymentDisclosureKey pdKey = {placeholder, js_index, mapped_index};
JSOutput output = outputs[mapped_index];
libzcash::PaymentAddress zaddr = output.addr; // randomized output
PaymentDisclosureInfo pdInfo = {PAYMENT_DISCLOSURE_VERSION_EXPERIMENTAL, esk, joinSplitPrivKey, zaddr};
paymentDisclosureData_.push_back(PaymentDisclosureKeyInfo(pdKey, pdInfo));
CZCPaymentAddress address(zaddr);
LogPrint("paymentdisclosure", "%s: Payment Disclosure: js=%d, n=%d, zaddr=%s\n", getId(), js_index, int(mapped_index), address.ToString());
}
// !!! Payment disclosure END
UniValue obj(UniValue::VOBJ);
obj.push_back(Pair("encryptednote1", encryptedNote1));
obj.push_back(Pair("encryptednote2", encryptedNote2));

View File

@ -12,6 +12,7 @@
#include "zcash/JoinSplit.hpp"
#include "zcash/Address.hpp"
#include "wallet.h"
#include "paymentdisclosure.h"
#include <unordered_map>
#include <tuple>
@ -65,6 +66,8 @@ public:
bool testmode = false; // Set to true to disable sending txs and generating proofs
bool paymentDisclosureMode = false; // Set to true to save esk for encrypted notes in payment disclosure database.
private:
friend class TEST_FRIEND_AsyncRPCOperation_sendmany; // class for unit testing
@ -113,6 +116,8 @@ private:
void sign_send_raw_transaction(UniValue obj); // throws exception if there was an error
// payment disclosure!
std::vector<PaymentDisclosureKeyInfo> paymentDisclosureData_;
};

View File

@ -29,6 +29,9 @@
#include "asyncrpcoperation_shieldcoinbase.h"
#include "paymentdisclosure.h"
#include "paymentdisclosuredb.h"
using namespace libzcash;
static int find_output(UniValue obj, int n) {
@ -80,6 +83,9 @@ AsyncRPCOperation_shieldcoinbase::AsyncRPCOperation_shieldcoinbase(
// Lock UTXOs
lock_utxos();
// Enable payment disclosure if requested
paymentDisclosureMode = fExperimentalMode && GetBoolArg("-paymentdisclosure", false);
}
AsyncRPCOperation_shieldcoinbase::~AsyncRPCOperation_shieldcoinbase() {
@ -150,6 +156,21 @@ void AsyncRPCOperation_shieldcoinbase::main() {
LogPrintf("%s",s);
unlock_utxos(); // clean up
// !!! Payment disclosure START
if (success && paymentDisclosureMode && paymentDisclosureData_.size()>0) {
uint256 txidhash = tx_.GetHash();
std::shared_ptr<PaymentDisclosureDB> db = PaymentDisclosureDB::sharedInstance();
for (PaymentDisclosureKeyInfo p : paymentDisclosureData_) {
p.first.hash = txidhash;
if (!db->Put(p.first, p.second)) {
LogPrint("paymentdisclosure", "%s: Payment Disclosure: Error writing entry to database for key %s\n", getId(), p.first.ToString());
} else {
LogPrint("paymentdisclosure", "%s: Payment Disclosure: Successfully added entry to database for key %s\n", getId(), p.first.ToString());
}
}
}
// !!! Payment disclosure END
}
@ -319,6 +340,9 @@ UniValue AsyncRPCOperation_shieldcoinbase::perform_joinsplit(ShieldCoinbaseJSInf
{info.vjsout[0], info.vjsout[1]};
boost::array<size_t, ZC_NUM_JS_INPUTS> inputMap;
boost::array<size_t, ZC_NUM_JS_OUTPUTS> outputMap;
uint256 esk; // payment disclosure - secret
JSDescription jsdesc = JSDescription::Randomized(
*pzcashParams,
joinSplitPubKey_,
@ -329,8 +353,8 @@ UniValue AsyncRPCOperation_shieldcoinbase::perform_joinsplit(ShieldCoinbaseJSInf
outputMap,
info.vpub_old,
info.vpub_new,
!this->testmode);
!this->testmode,
&esk); // parameter expects pointer to esk, so pass in address
{
auto verifier = libzcash::ProofVerifier::Strict();
if (!(jsdesc.Verify(*pzcashParams, verifier, joinSplitPubKey_))) {
@ -399,6 +423,27 @@ UniValue AsyncRPCOperation_shieldcoinbase::perform_joinsplit(ShieldCoinbaseJSInf
arrOutputMap.push_back(outputMap[i]);
}
// !!! Payment disclosure START
unsigned char buffer[32] = {0};
memcpy(&buffer[0], &joinSplitPrivKey_[0], 32); // private key in first half of 64 byte buffer
std::vector<unsigned char> vch(&buffer[0], &buffer[0] + 32);
uint256 joinSplitPrivKey = uint256(vch);
size_t js_index = tx_.vjoinsplit.size() - 1;
uint256 placeholder;
for (int i = 0; i < ZC_NUM_JS_OUTPUTS; i++) {
uint8_t mapped_index = outputMap[i];
// placeholder for txid will be filled in later when tx has been finalized and signed.
PaymentDisclosureKey pdKey = {placeholder, js_index, mapped_index};
JSOutput output = outputs[mapped_index];
libzcash::PaymentAddress zaddr = output.addr; // randomized output
PaymentDisclosureInfo pdInfo = {PAYMENT_DISCLOSURE_VERSION_EXPERIMENTAL, esk, joinSplitPrivKey, zaddr};
paymentDisclosureData_.push_back(PaymentDisclosureKeyInfo(pdKey, pdInfo));
CZCPaymentAddress address(zaddr);
LogPrint("paymentdisclosure", "%s: Payment Disclosure: js=%d, n=%d, zaddr=%s\n", getId(), js_index, int(mapped_index), address.ToString());
}
// !!! Payment disclosure END
UniValue obj(UniValue::VOBJ);
obj.push_back(Pair("encryptednote1", encryptedNote1));
obj.push_back(Pair("encryptednote2", encryptedNote2));

View File

@ -18,6 +18,8 @@
#include <univalue.h>
#include "paymentdisclosure.h"
// Default transaction fee if caller does not specify one.
#define SHIELD_COINBASE_DEFAULT_MINERS_FEE 10000
@ -55,6 +57,8 @@ public:
bool testmode = false; // Set to true to disable sending txs and generating proofs
bool paymentDisclosureMode = false; // Set to true to save esk for encrypted notes in payment disclosure database.
private:
friend class TEST_FRIEND_AsyncRPCOperation_shieldcoinbase; // class for unit testing
@ -80,6 +84,9 @@ private:
void lock_utxos();
void unlock_utxos();
// payment disclosure!
std::vector<PaymentDisclosureKeyInfo> paymentDisclosureData_;
};

View File

@ -214,5 +214,7 @@ TEST(wallet_zkeys_tests, write_cryptedzkey_direct_to_db) {
wallet2.GetSpendingKey(paymentAddress2.Get(), keyOut);
ASSERT_EQ(paymentAddress2.Get(), keyOut.address());
ECC_Stop();
}

View File

@ -0,0 +1,299 @@
// Copyright (c) 2017 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "base58.h"
#include "rpcserver.h"
#include "init.h"
#include "main.h"
#include "script/script.h"
#include "script/standard.h"
#include "sync.h"
#include "util.h"
#include "utiltime.h"
#include "wallet.h"
#include <fstream>
#include <stdint.h>
#include <boost/algorithm/string.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <univalue.h>
#include "paymentdisclosure.h"
#include "paymentdisclosuredb.h"
#include "zcash/Note.hpp"
#include "zcash/NoteEncryption.hpp"
using namespace std;
using namespace libzcash;
// Function declaration for function implemented in wallet/rpcwallet.cpp
bool EnsureWalletIsAvailable(bool avoidException);
/**
* RPC call to generate a payment disclosure
*/
UniValue z_getpaymentdisclosure(const UniValue& params, bool fHelp)
{
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
auto fEnablePaymentDisclosure = fExperimentalMode && GetBoolArg("-paymentdisclosure", false);
string strPaymentDisclosureDisabledMsg = "";
if (!fEnablePaymentDisclosure) {
strPaymentDisclosureDisabledMsg = "\nWARNING: Payment disclosure is currently DISABLED. This call always fails.\n";
}
if (fHelp || params.size() < 3 || params.size() > 4 )
throw runtime_error(
"z_getpaymentdisclosure \"txid\" \"js_index\" \"output_index\" (\"message\") \n"
"\nGenerate a payment disclosure for a given joinsplit output.\n"
"\nEXPERIMENTAL FEATURE\n"
+ strPaymentDisclosureDisabledMsg +
"\nArguments:\n"
"1. \"txid\" (string, required) \n"
"2. \"js_index\" (string, required) \n"
"3. \"output_index\" (string, required) \n"
"4. \"message\" (string, optional) \n"
"\nResult:\n"
"\"paymentblob\" (string) Hex string of payment blob\n"
"\nExamples:\n"
+ HelpExampleCli("z_getpaymentdisclosure", "96f12882450429324d5f3b48630e3168220e49ab7b0f066e5c2935a6b88bb0f2 0 0 \"refund\"")
+ HelpExampleRpc("z_getpaymentdisclosure", "\"96f12882450429324d5f3b48630e3168220e49ab7b0f066e5c2935a6b88bb0f2\", 0, 0, \"refund\"")
);
if (!fEnablePaymentDisclosure) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error: payment disclosure is disabled.");
}
LOCK2(cs_main, pwalletMain->cs_wallet);
EnsureWalletIsUnlocked();
// Check wallet knows about txid
string txid = params[0].get_str();
uint256 hash;
hash.SetHex(txid);
CTransaction tx;
uint256 hashBlock;
// Check txid has been seen
if (!GetTransaction(hash, tx, hashBlock, true)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "No information available about transaction");
}
// Check tx has been confirmed
if (hashBlock.IsNull()) {
throw JSONRPCError(RPC_MISC_ERROR, "Transaction has not been confirmed yet");
}
// Check is mine
if (!pwalletMain->mapWallet.count(hash)) {
throw JSONRPCError(RPC_MISC_ERROR, "Transaction does not belong to the wallet");
}
const CWalletTx& wtx = pwalletMain->mapWallet[hash];
// Check if shielded tx
if (wtx.vjoinsplit.empty()) {
throw JSONRPCError(RPC_MISC_ERROR, "Transaction is not a shielded transaction");
}
// Check js_index
int js_index = params[1].get_int();
if (js_index < 0 || js_index >= wtx.vjoinsplit.size()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid js_index");
}
// Check output_index
int output_index = params[2].get_int();
if (output_index < 0 || output_index >= ZC_NUM_JS_OUTPUTS) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid output_index");
}
// Get message if it exists
string msg;
if (params.size() == 4) {
msg = params[3].get_str();
}
// Create PaymentDisclosureKey
PaymentDisclosureKey key = {hash, (size_t)js_index, (uint8_t)output_index };
// TODO: In future, perhaps init the DB in init.cpp
shared_ptr<PaymentDisclosureDB> db = PaymentDisclosureDB::sharedInstance();
PaymentDisclosureInfo info;
if (!db->Get(key, info)) {
throw JSONRPCError(RPC_DATABASE_ERROR, "Could not find payment disclosure info for the given joinsplit output");
}
PaymentDisclosure pd( wtx.joinSplitPubKey, key, info, msg );
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << pd;
string strHex = HexStr(ss.begin(), ss.end());
return strHex;
}
/**
* RPC call to validate a payment disclosure data blob.
*/
UniValue z_validatepaymentdisclosure(const UniValue& params, bool fHelp)
{
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
auto fEnablePaymentDisclosure = fExperimentalMode && GetBoolArg("-paymentdisclosure", false);
string strPaymentDisclosureDisabledMsg = "";
if (!fEnablePaymentDisclosure) {
strPaymentDisclosureDisabledMsg = "\nWARNING: Payment disclosure is curretly DISABLED. This call always fails.\n";
}
if (fHelp || params.size() != 1)
throw runtime_error(
"z_validatepaymentdisclosure \"paymentdisclosure\"\n"
"\nValidates a payment disclosure.\n"
"\nEXPERIMENTAL FEATURE\n"
+ strPaymentDisclosureDisabledMsg +
"\nArguments:\n"
"1. \"paymentdisclosure\" (string, required) Hex data string\n"
"\nExamples:\n"
+ HelpExampleCli("z_validatepaymentdisclosure", "\"hexblob\"")
+ HelpExampleRpc("z_validatepaymentdisclosure", "\"hexblob\"")
);
if (!fEnablePaymentDisclosure) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error: payment disclosure is disabled.");
}
LOCK2(cs_main, pwalletMain->cs_wallet);
EnsureWalletIsUnlocked();
string hexInput = params[0].get_str();
if (!IsHex(hexInput))
{
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected payment disclosure data in hexadecimal format.");
}
// Unserialize the payment disclosure data into an object
PaymentDisclosure pd;
CDataStream ss(ParseHex(hexInput), SER_NETWORK, PROTOCOL_VERSION);
try {
ss >> pd;
// too much data is ignored, but if not enough data, exception of type ios_base::failure is thrown,
// CBaseDataStream::read(): end of data: iostream error
} catch (const std::exception &e) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, payment disclosure data is malformed.");
}
if (pd.payload.marker != PAYMENT_DISCLOSURE_PAYLOAD_MAGIC_BYTES) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, payment disclosure marker not found.");
}
if (pd.payload.version != PAYMENT_DISCLOSURE_VERSION_EXPERIMENTAL) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Payment disclosure version is unsupported.");
}
uint256 hash = pd.payload.txid;
CTransaction tx;
uint256 hashBlock;
// Check if we have seen the transaction
if (!GetTransaction(hash, tx, hashBlock, true)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "No information available about transaction");
}
// Check if the transaction has been confirmed
if (hashBlock.IsNull()) {
throw JSONRPCError(RPC_MISC_ERROR, "Transaction has not been confirmed yet");
}
// Check if shielded tx
if (tx.vjoinsplit.empty()) {
throw JSONRPCError(RPC_MISC_ERROR, "Transaction is not a shielded transaction");
}
UniValue errs(UniValue::VARR);
UniValue o(UniValue::VOBJ);
o.push_back(Pair("txid", pd.payload.txid.ToString()));
// Check js_index
if (pd.payload.js >= tx.vjoinsplit.size()) {
errs.push_back("Payment disclosure refers to an invalid joinsplit index");
}
o.push_back(Pair("jsIndex", pd.payload.js));
if (pd.payload.n < 0 || pd.payload.n >= ZC_NUM_JS_OUTPUTS) {
errs.push_back("Payment disclosure refers to an invalid output index");
}
o.push_back(Pair("outputIndex", pd.payload.n));
o.push_back(Pair("version", pd.payload.version));
o.push_back(Pair("onetimePrivKey", pd.payload.esk.ToString()));
o.push_back(Pair("message", pd.payload.message));
o.push_back(Pair("joinSplitPubKey", tx.joinSplitPubKey.ToString()));
// Verify the payment disclosure was signed using the same key as the transaction i.e. the joinSplitPrivKey.
uint256 dataToBeSigned = SerializeHash(pd.payload, SER_GETHASH, 0);
bool sigVerified = (crypto_sign_verify_detached(pd.payloadSig.data(),
dataToBeSigned.begin(), 32,
tx.joinSplitPubKey.begin()) == 0);
o.push_back(Pair("signatureVerified", sigVerified));
if (!sigVerified) {
errs.push_back("Payment disclosure signature does not match transaction signature");
}
// Check the payment address is valid
PaymentAddress zaddr = pd.payload.zaddr;
CZCPaymentAddress address;
if (!address.Set(zaddr)) {
errs.push_back("Payment disclosure refers to an invalid payment address");
} else {
o.push_back(Pair("paymentAddress", address.ToString()));
try {
// Decrypt the note to get value and memo field
JSDescription jsdesc = tx.vjoinsplit[pd.payload.js];
uint256 h_sig = jsdesc.h_sig(*pzcashParams, tx.joinSplitPubKey);
ZCPaymentDisclosureNoteDecryption decrypter;
ZCNoteEncryption::Ciphertext ciphertext = jsdesc.ciphertexts[pd.payload.n];
uint256 pk_enc = zaddr.pk_enc;
auto plaintext = decrypter.decryptWithEsk(ciphertext, pk_enc, pd.payload.esk, h_sig, pd.payload.n);
CDataStream ssPlain(SER_NETWORK, PROTOCOL_VERSION);
ssPlain << plaintext;
NotePlaintext npt;
ssPlain >> npt;
string memoHexString = HexStr(npt.memo.data(), npt.memo.data() + npt.memo.size());
o.push_back(Pair("memo", memoHexString));
o.push_back(Pair("value", ValueFromAmount(npt.value)));
// Check the blockchain commitment matches decrypted note commitment
uint256 cm_blockchain = jsdesc.commitments[pd.payload.n];
Note note = npt.note(zaddr);
uint256 cm_decrypted = note.cm();
bool cm_match = (cm_decrypted == cm_blockchain);
o.push_back(Pair("commitmentMatch", cm_match));
if (!cm_match) {
errs.push_back("Commitment derived from payment disclosure does not match blockchain commitment");
}
} catch (const std::exception &e) {
errs.push_back(string("Error while decrypting payment disclosure note: ") + string(e.what()) );
}
}
bool isValid = errs.empty();
o.push_back(Pair("valid", isValid));
if (!isValid) {
o.push_back(Pair("errors", errs));
}
return o;
}

View File

@ -198,7 +198,8 @@ public:
uint64_t vpub_old,
uint64_t vpub_new,
const uint256& rt,
bool computeProof
bool computeProof,
uint256 *out_esk // Payment disclosure
) {
if (computeProof && !pk) {
throw std::runtime_error("JoinSplit proving key not loaded");
@ -303,6 +304,12 @@ public:
}
out_ephemeralKey = encryptor.get_epk();
// !!! Payment disclosure START
if (out_esk != nullptr) {
*out_esk = encryptor.get_esk();
}
// !!! Payment disclosure END
}
// Authenticate h_sig with each of the input

View File

@ -78,7 +78,11 @@ public:
uint64_t vpub_old,
uint64_t vpub_new,
const uint256& rt,
bool computeProof = true
bool computeProof = true,
// For paymentdisclosure, we need to retrieve the esk.
// Reference as non-const parameter with default value leads to compile error.
// So use pointer for simplicity.
uint256 *out_esk = nullptr
) = 0;
virtual bool verify(

View File

@ -135,6 +135,52 @@ typename NoteDecryption<MLEN>::Plaintext NoteDecryption<MLEN>::decrypt
return plaintext;
}
//
// Payment disclosure - decrypt with esk
//
template<size_t MLEN>
typename PaymentDisclosureNoteDecryption<MLEN>::Plaintext PaymentDisclosureNoteDecryption<MLEN>::decryptWithEsk
(const PaymentDisclosureNoteDecryption<MLEN>::Ciphertext &ciphertext,
const uint256 &pk_enc,
const uint256 &esk,
const uint256 &hSig,
unsigned char nonce
) const
{
uint256 dhsecret;
if (crypto_scalarmult(dhsecret.begin(), esk.begin(), pk_enc.begin()) != 0) {
throw std::logic_error("Could not create DH secret");
}
// Regenerate keypair
uint256 epk = NoteEncryption<MLEN>::generate_pubkey(esk);
unsigned char K[NOTEENCRYPTION_CIPHER_KEYSIZE];
KDF(K, dhsecret, epk, pk_enc, hSig, nonce);
// The nonce is zero because we never reuse keys
unsigned char cipher_nonce[crypto_aead_chacha20poly1305_IETF_NPUBBYTES] = {};
PaymentDisclosureNoteDecryption<MLEN>::Plaintext plaintext;
// Message length is always NOTEENCRYPTION_AUTH_BYTES less than
// the ciphertext length.
if (crypto_aead_chacha20poly1305_ietf_decrypt(plaintext.begin(), NULL,
NULL,
ciphertext.begin(), PaymentDisclosureNoteDecryption<MLEN>::CLEN,
NULL,
0,
cipher_nonce, K) != 0) {
throw note_decryption_failed();
}
return plaintext;
}
template<size_t MLEN>
uint256 NoteEncryption<MLEN>::generate_privkey(const uint252 &a_sk)
{
@ -176,4 +222,6 @@ uint252 random_uint252()
template class NoteEncryption<ZC_NOTEPLAINTEXT_SIZE>;
template class NoteDecryption<ZC_NOTEPLAINTEXT_SIZE>;
template class PaymentDisclosureNoteDecryption<ZC_NOTEPLAINTEXT_SIZE>;
}

View File

@ -31,6 +31,11 @@ public:
NoteEncryption(uint256 hSig);
// Gets the ephemeral secret key
uint256 get_esk() {
return esk;
}
// Gets the ephemeral public key
uint256 get_epk() {
return epk;
@ -87,9 +92,34 @@ public:
note_decryption_failed() : std::runtime_error("Could not decrypt message") { }
};
// Subclass PaymentDisclosureNoteDecryption provides a method to decrypt a note with esk.
template<size_t MLEN>
class PaymentDisclosureNoteDecryption : public NoteDecryption<MLEN> {
protected:
public:
enum { CLEN=MLEN+NOTEENCRYPTION_AUTH_BYTES };
typedef boost::array<unsigned char, CLEN> Ciphertext;
typedef boost::array<unsigned char, MLEN> Plaintext;
PaymentDisclosureNoteDecryption() : NoteDecryption<MLEN>() {}
PaymentDisclosureNoteDecryption(uint256 sk_enc) : NoteDecryption<MLEN>(sk_enc) {}
Plaintext decryptWithEsk(
const Ciphertext &ciphertext,
const uint256 &pk_enc,
const uint256 &esk,
const uint256 &hSig,
unsigned char nonce
) const;
};
}
typedef libzcash::NoteEncryption<ZC_NOTEPLAINTEXT_SIZE> ZCNoteEncryption;
typedef libzcash::NoteDecryption<ZC_NOTEPLAINTEXT_SIZE> ZCNoteDecryption;
typedef libzcash::PaymentDisclosureNoteDecryption<ZC_NOTEPLAINTEXT_SIZE> ZCPaymentDisclosureNoteDecryption;
#endif /* ZC_NOTE_ENCRYPTION_H_ */