From 45232b19619a9df52af318289accc6b8ad4930e5 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 14 Nov 2017 13:29:05 -0800 Subject: [PATCH] Add payment disclosure as experimental feature. --- qa/pull-tester/rpc-tests.sh | 1 + qa/rpc-tests/paymentdisclosure.py | 233 ++++++++++++++ src/Makefile.am | 7 + src/Makefile.gtest.include | 1 + src/gtest/test_foundersreward.cpp | 2 + src/gtest/test_paymentdisclosure.cpp | 210 ++++++++++++ src/gtest/test_transaction.cpp | 4 +- src/init.cpp | 3 + src/paymentdisclosure.cpp | 63 ++++ src/paymentdisclosure.h | 145 +++++++++ src/paymentdisclosuredb.cpp | 93 ++++++ src/paymentdisclosuredb.h | 42 +++ src/primitives/transaction.cpp | 15 +- src/primitives/transaction.h | 4 +- src/rpcclient.cpp | 2 + src/rpcserver.cpp | 6 +- src/rpcserver.h | 2 + src/wallet/asyncrpcoperation_sendmany.cpp | 50 ++- src/wallet/asyncrpcoperation_sendmany.h | 5 + .../asyncrpcoperation_shieldcoinbase.cpp | 49 ++- src/wallet/asyncrpcoperation_shieldcoinbase.h | 7 + src/wallet/gtest/test_wallet_zkeys.cpp | 2 + src/wallet/rpcdisclosure.cpp | 299 ++++++++++++++++++ src/zcash/JoinSplit.cpp | 9 +- src/zcash/JoinSplit.hpp | 6 +- src/zcash/NoteEncryption.cpp | 48 +++ src/zcash/NoteEncryption.hpp | 30 ++ 27 files changed, 1324 insertions(+), 14 deletions(-) create mode 100755 qa/rpc-tests/paymentdisclosure.py create mode 100644 src/gtest/test_paymentdisclosure.cpp create mode 100644 src/paymentdisclosure.cpp create mode 100644 src/paymentdisclosure.h create mode 100644 src/paymentdisclosuredb.cpp create mode 100644 src/paymentdisclosuredb.h create mode 100644 src/wallet/rpcdisclosure.cpp diff --git a/qa/pull-tester/rpc-tests.sh b/qa/pull-tester/rpc-tests.sh index 5631f894f..a09edaf03 100755 --- a/qa/pull-tester/rpc-tests.sh +++ b/qa/pull-tester/rpc-tests.sh @@ -11,6 +11,7 @@ export BITCOIND=${REAL_BITCOIND} #Run the tests testScripts=( + 'paymentdisclosure.py' 'prioritisetransaction.py' 'wallet_treestate.py' 'wallet_protectcoinbase.py' diff --git a/qa/rpc-tests/paymentdisclosure.py b/qa/rpc-tests/paymentdisclosure.py new file mode 100755 index 000000000..60d6f188f --- /dev/null +++ b/qa/rpc-tests/paymentdisclosure.py @@ -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() diff --git a/src/Makefile.am b/src/Makefile.am index 4a7554c07..5b80b724f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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 \ diff --git a/src/Makefile.gtest.include b/src/Makefile.gtest.include index d92feaa20..02152f936 100644 --- a/src/Makefile.gtest.include +++ b/src/Makefile.gtest.include @@ -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 += \ diff --git a/src/gtest/test_foundersreward.cpp b/src/gtest/test_foundersreward.cpp index 9016cde54..b5e8acc18 100644 --- a/src/gtest/test_foundersreward.cpp +++ b/src/gtest/test_foundersreward.cpp @@ -80,6 +80,8 @@ TEST(founders_reward_test, create_testnet_2of3multisig) { std::cout << s << std::endl; pWallet->Flush(true); + + ECC_Stop(); } #endif diff --git a/src/gtest/test_paymentdisclosure.cpp b/src/gtest/test_paymentdisclosure.cpp new file mode 100644 index 000000000..e87c89297 --- /dev/null +++ b/src/gtest/test_paymentdisclosure.cpp @@ -0,0 +1,210 @@ +#include + +#include "main.h" +#include "utilmoneystr.h" +#include "chainparams.h" +#include "utilstrencodings.h" +#include "zcash/Address.hpp" +#include "wallet/wallet.h" +#include "amount.h" +#include +#include +#include +#include +#include +#include +#include "util.h" + +#include "paymentdisclosure.h" +#include "paymentdisclosuredb.h" + +#include "sodium.h" + +#include +#include +#include + +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 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 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::max(); + uint8_t n = random_uint256().GetCheapHash() % std::numeric_limits::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 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(); +} diff --git a/src/gtest/test_transaction.cpp b/src/gtest/test_transaction.cpp index a339f7652..fb68fd35c 100644 --- a/src/gtest/test_transaction.cpp +++ b/src/gtest/test_transaction.cpp @@ -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 expectedInputMap {1, 0}; boost::array 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 expectedInputMap {0, 1}; boost::array expectedOutputMap {0, 1}; diff --git a/src/init.cpp b/src/init.cpp index 59a59524d..fe3a61f5a 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -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 diff --git a/src/paymentdisclosure.cpp b/src/paymentdisclosure.cpp new file mode 100644 index 000000000..a33b1c604 --- /dev/null +++ b/src/paymentdisclosure.cpp @@ -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=, 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); +} diff --git a/src/paymentdisclosure.h b/src/paymentdisclosure.h new file mode 100644 index 000000000..b4f56eb45 --- /dev/null +++ b/src/paymentdisclosure.h @@ -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 +#include + + +// 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 + 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 + 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 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 sig) : payload(payload), payloadSig(sig) {}; + PaymentDisclosure(const uint256& joinSplitPubKey, const PaymentDisclosureKey& key, const PaymentDisclosureInfo& info, const std::string& message); + + ADD_SERIALIZE_METHODS; + + template + 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 PaymentDisclosureKeyInfo; + + +#endif // ZCASH_PAYMENTDISCLOSURE_H diff --git a/src/paymentdisclosuredb.cpp b/src/paymentdisclosuredb.cpp new file mode 100644 index 000000000..ef32f2845 --- /dev/null +++ b/src/paymentdisclosuredb.cpp @@ -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 + +using namespace std; + +static boost::filesystem::path emptyPath; + +/** + * Static method to return the shared/default payment disclosure database. + */ +shared_ptr PaymentDisclosureDB::sharedInstance() { + // Thread-safe in C++11 and gcc 4.3 + static shared_ptr ptr = std::make_shared(); + 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 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 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; +} diff --git a/src/paymentdisclosuredb.h b/src/paymentdisclosuredb.h new file mode 100644 index 000000000..9352cac8f --- /dev/null +++ b/src/paymentdisclosuredb.h @@ -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 +#include +#include +#include +#include + +#include + +#include + + +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 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 diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp index f6236a2f8..cdd299967 100644 --- a/src/primitives/transaction.cpp +++ b/src/primitives/transaction.cpp @@ -16,7 +16,9 @@ JSDescription::JSDescription(ZCJoinSplit& params, const boost::array& 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 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 gen) + uint256 *esk, // payment disclosure + std::function 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( diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index 111237cb7..f9723e399 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -77,7 +77,8 @@ public: const boost::array& 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 gen = GetRandInt ); diff --git a/src/rpcclient.cpp b/src/rpcclient.cpp index 2c5bd122f..c02c51991 100644 --- a/src/rpcclient.cpp +++ b/src/rpcclient.cpp @@ -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 diff --git a/src/rpcserver.cpp b/src/rpcserver.cpp index 0bd8fb1b8..7a3880902 100644 --- a/src/rpcserver.cpp +++ b/src/rpcserver.cpp @@ -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 }; diff --git a/src/rpcserver.h b/src/rpcserver.h index 4da515426..321568748 100644 --- a/src/rpcserver.h +++ b/src/rpcserver.h @@ -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 ¶ms, bool fHelp); // in rpcdisclosure.cpp bool StartRPC(); void InterruptRPC(); diff --git a/src/wallet/asyncrpcoperation_sendmany.cpp b/src/wallet/asyncrpcoperation_sendmany.cpp index b4e831d57..539d5d7d6 100644 --- a/src/wallet/asyncrpcoperation_sendmany.cpp +++ b/src/wallet/asyncrpcoperation_sendmany.cpp @@ -28,6 +28,8 @@ #include #include +#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 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 inputMap; boost::array 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 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)); diff --git a/src/wallet/asyncrpcoperation_sendmany.h b/src/wallet/asyncrpcoperation_sendmany.h index 6fac61160..69bdbe315 100644 --- a/src/wallet/asyncrpcoperation_sendmany.h +++ b/src/wallet/asyncrpcoperation_sendmany.h @@ -12,6 +12,7 @@ #include "zcash/JoinSplit.hpp" #include "zcash/Address.hpp" #include "wallet.h" +#include "paymentdisclosure.h" #include #include @@ -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 paymentDisclosureData_; }; diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp index 20d659cc2..a845c6085 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp @@ -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 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 inputMap; boost::array 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 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)); diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.h b/src/wallet/asyncrpcoperation_shieldcoinbase.h index 981b2fbe9..379aa5bd7 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.h +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.h @@ -18,6 +18,8 @@ #include +#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 paymentDisclosureData_; }; diff --git a/src/wallet/gtest/test_wallet_zkeys.cpp b/src/wallet/gtest/test_wallet_zkeys.cpp index c7912ae7a..554a4ee97 100644 --- a/src/wallet/gtest/test_wallet_zkeys.cpp +++ b/src/wallet/gtest/test_wallet_zkeys.cpp @@ -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(); } diff --git a/src/wallet/rpcdisclosure.cpp b/src/wallet/rpcdisclosure.cpp new file mode 100644 index 000000000..c1c8cb87c --- /dev/null +++ b/src/wallet/rpcdisclosure.cpp @@ -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 +#include + +#include +#include + +#include + +#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 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; +} diff --git a/src/zcash/JoinSplit.cpp b/src/zcash/JoinSplit.cpp index 590700cd9..9a7fe69db 100644 --- a/src/zcash/JoinSplit.cpp +++ b/src/zcash/JoinSplit.cpp @@ -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 diff --git a/src/zcash/JoinSplit.hpp b/src/zcash/JoinSplit.hpp index a8c08d21b..c6c256129 100644 --- a/src/zcash/JoinSplit.hpp +++ b/src/zcash/JoinSplit.hpp @@ -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( diff --git a/src/zcash/NoteEncryption.cpp b/src/zcash/NoteEncryption.cpp index a5ea2da15..9ae0ba5c3 100644 --- a/src/zcash/NoteEncryption.cpp +++ b/src/zcash/NoteEncryption.cpp @@ -135,6 +135,52 @@ typename NoteDecryption::Plaintext NoteDecryption::decrypt return plaintext; } +// +// Payment disclosure - decrypt with esk +// +template +typename PaymentDisclosureNoteDecryption::Plaintext PaymentDisclosureNoteDecryption::decryptWithEsk + (const PaymentDisclosureNoteDecryption::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::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::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::CLEN, + NULL, + 0, + cipher_nonce, K) != 0) { + throw note_decryption_failed(); + } + + return plaintext; +} + + + + template uint256 NoteEncryption::generate_privkey(const uint252 &a_sk) { @@ -176,4 +222,6 @@ uint252 random_uint252() template class NoteEncryption; template class NoteDecryption; +template class PaymentDisclosureNoteDecryption; + } diff --git a/src/zcash/NoteEncryption.hpp b/src/zcash/NoteEncryption.hpp index 11346ebc1..321d7dead 100644 --- a/src/zcash/NoteEncryption.hpp +++ b/src/zcash/NoteEncryption.hpp @@ -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 +class PaymentDisclosureNoteDecryption : public NoteDecryption { +protected: +public: + enum { CLEN=MLEN+NOTEENCRYPTION_AUTH_BYTES }; + typedef boost::array Ciphertext; + typedef boost::array Plaintext; + + PaymentDisclosureNoteDecryption() : NoteDecryption() {} + PaymentDisclosureNoteDecryption(uint256 sk_enc) : NoteDecryption(sk_enc) {} + + Plaintext decryptWithEsk( + const Ciphertext &ciphertext, + const uint256 &pk_enc, + const uint256 &esk, + const uint256 &hSig, + unsigned char nonce + ) const; +}; + } typedef libzcash::NoteEncryption ZCNoteEncryption; typedef libzcash::NoteDecryption ZCNoteDecryption; +typedef libzcash::PaymentDisclosureNoteDecryption ZCPaymentDisclosureNoteDecryption; + #endif /* ZC_NOTE_ENCRYPTION_H_ */