basic structure for signing transactions

The keyring structure contains functions that will take a pending
transaction and fill out the cryptonote::transaction member with
the relevant signature fields ready for serialization.
This commit is contained in:
Sean Darcy 2022-02-17 09:27:27 +11:00
parent 3a1dfe3ea3
commit 44f565f085
11 changed files with 313 additions and 42 deletions

View File

@ -165,9 +165,9 @@ namespace cryptonote
struct tx_destination_entry
{
std::string original;
uint64_t amount; //money
account_public_address addr; //destination address
std::string original; // Cached string of the address. Access using address()
uint64_t amount; // Money
account_public_address addr; // Destination Address
bool is_subaddress;
bool is_integrated;

View File

@ -9,17 +9,19 @@ namespace wallet
struct Decoy
{
//outs - array of structure outkey as follows:
//height - unsigned int; block height of the output
//key - String; the public key of the output
//mask - String
//height - int; block height of the output
//key - crypto::public_key; the public key of the output
//mask - rct::key;
//txid - String; transaction id
//unlocked - boolean; States if output is locked (false) or not (true)
//index - int; absolute index of the decoy
int64_t height;
std::string key; // Hex public key of the output
std::string mask;
crypto::public_key key; // Hex public key of the output
rct::key mask;
std::string txid;
bool unlocked;
int64_t global_index;

View File

@ -316,11 +316,11 @@ namespace wallet
if (output_dict.key() != "key")
return;
o.key = output_dict.consume_string_view();
o.key = tools::make_from_guts<crypto::public_key>(output_dict.consume_string_view());
if (output_dict.key() != "mask")
return;
o.mask = output_dict.consume_string_view();
o.mask = tools::make_from_guts<rct::key>(output_dict.consume_string_view());
if (output_dict.key() != "txid")
return;

View File

@ -3,9 +3,37 @@
#include "wallet2½.hpp"
#include <stdexcept>
#include <cryptonote_core/cryptonote_tx_utils.h>
#include <cryptonote_basic/cryptonote_basic.h>
#include <cryptonote_basic/txtypes.h>
#include <cryptonote_basic/account.h>
#include <device/device.hpp>
namespace wallet
{
crypto::secret_key
Keyring::generate_tx_key(uint8_t hf_version)
{
// TODO sean make sure this is zero
crypto::secret_key tx_key{};
// TODO sean this should base itself on the hf version
//return key_device.open_tx(tx_key, transaction::get_max_version_for_hf(hf_version), txtype::standard);
if (!key_device.open_tx(tx_key, cryptonote::txversion::v4_tx_types, cryptonote::txtype::standard))
throw std::runtime_error("Could not generate transaction secret key");
return tx_key;
}
crypto::public_key
Keyring::secret_tx_key_to_public_tx_key(const crypto::secret_key a)
{
// TODO sean make sure this is zero
rct::key aG{};
if (!key_device.scalarmultBase(aG, rct::sk2rct(a)))
throw std::runtime_error("Could not convert secret tx key to public tx key");
return rct::rct2pk(aG);
}
crypto::key_derivation
Keyring::generate_key_derivation(const crypto::public_key& tx_pubkey) const
{
@ -97,4 +125,172 @@ namespace wallet
return wallet25::output_amount(rv, derivation, i, mask, key_device);
}
// This gets called for every output in the transaction, there is some complication for how the
// key gets generated for change address because the derivation is a*R or some simpler calc i guess
// set the bool for this_dst_is_change_addr to false and optional null for the actual thingo
crypto::public_key
Keyring::generate_output_ephemeral_keys(const crypto::secret_key& tx_key, const cryptonote::tx_destination_entry& dst_entr, const size_t output_index, std::vector<rct::key>& amount_keys)
{
crypto::public_key out_eph_public_key;
cryptonote::account_keys sender_account_keys{};
sender_account_keys.m_view_secret_key = view_private_key;
const auto tx_key_pub = secret_tx_key_to_public_tx_key(tx_key);
bool this_dst_is_change_addr = false;
//std::optional<cryptonote::tx_destination_entry> change_addr = std::nullopt;
bool need_additional_txkeys = false;
std::vector<crypto::secret_key> additional_tx_keys{};
std::vector<crypto::public_key> additional_tx_public_keys{};
key_device.generate_output_ephemeral_keys(
static_cast<uint16_t>(cryptonote::txversion::v4_tx_types), // size_t -> should be 4?
this_dst_is_change_addr, // bool -> found change. Return parameter?
sender_account_keys, // cryptonote::account_keys -> only uses view key i believe
tx_key_pub, // crypto::public_key -> public key of the transaction
tx_key, // crypto::secret_key -> secret key of the transaction
dst_entr, // cryptonote::tx_destination_entry -> data of the transaction
std::nullopt, // std::optional<cryptonote::tx_destination_entry> -> it will check if the data is the change because the one time address is different
output_index, // position the output is in the transaction, concatenated to generate consistently
need_additional_txkeys, // bool -> what are additional_txkeys ffs
additional_tx_keys, // std::vector<crypto::secret_key> more additional tx keys, this time secret keys
additional_tx_public_keys, // std::vector<crypto::public_key> public keys of additional keys. Return parameter?
amount_keys, // std::vector<rct::key> keys that committing to the amount. Device APPENDS to the vector, is essentially a return parameter
out_eph_public_key); // crypto::public_key -> Return parameter
//
return out_eph_public_key;
}
// This is called over a transaction input to produce the secret key that can spend an outputs funds.
// The key derivation is usually produced from calling generate_key_derivation().
crypto::secret_key
Keyring::derive_transaction_secret_key(const crypto::key_derivation& key_derivation, const size_t output_index)
{
crypto::secret_key output_secret_key;
key_device.derive_secret_key(key_derivation, output_index, spend_private_key, output_secret_key);
return output_secret_key;
}
crypto::hash
Keyring::get_transaction_prefix_hash(const cryptonote::transaction_prefix& tx)
{
crypto::hash h = crypto::null_hash;
key_device.get_transaction_prefix_hash(tx, h);
return h;
}
void
Keyring::sign_transaction(PendingTransaction& ptx)
{
uint8_t hf_version = cryptonote::network_version_19;
auto tx_key = generate_tx_key(hf_version);
rct::ctkeyV inSk;
rct::keyV dest_keys;
rct::ctkeyM mixRing(ptx.chosen_outputs.size());
uint64_t amount_in = 0, amount_out = 0;
std::vector<uint64_t> inamounts, outamounts;
std::vector<unsigned int> index;
inSk.reserve(ptx.chosen_outputs.size() + 1);
// Loop over inputs for the transaction to build the VIN array (Amount = 0, keyimage, array of offsets for ring)
// and collect all the transaction private keys so we can spend our outputs in this transaction.
int i = 0;
for(const wallet::Output& src_entr: ptx.chosen_outputs)
{
// This takes the source outputs public transaction and combines it with our secret view key
// to make a key derivation. This derivation can be used evaluate an output on the
// blockchain to see if it is ours to spend. We already know its ours because the wallet
// has collected them at an earlier point in time. Now we combine this derivation
// with the output index and our secret spend key to generate
// the actual transaction secret key which we can use to spend the output.
crypto::key_derivation key_derivation = generate_key_derivation(src_entr.key);
crypto::secret_key output_secret_key = derive_transaction_secret_key(key_derivation, src_entr.output_index);
// There is a input secret keys structure (inSk) that gets passed to the ringct library/module and it is
// essentially an array of our output secret keys. It also needs to know the mask which is
// another random number used to hide the amounts in our pederson commitments.
rct::ctkey ctkey;
ctkey.dest = rct::sk2rct(output_secret_key);
ctkey.mask = src_entr.rct_mask;
inSk.push_back(ctkey);
// Bookkeeping structures keeping track of how much $$ we are putting into the transaction
inamounts.push_back(src_entr.amount);
amount_in += src_entr.amount;
// Create the VIN structure of the transaction. This will just be a simple JSON without any crypto magic that
// shows the key images and a now redundant amount field which always says zero. We generated the key images
// when first scanning the outputs so we can just copy it straight from the database
cryptonote::txin_to_key input_to_key;
input_to_key.amount = 0;
input_to_key.k_image = src_entr.key_image;
// The outputs array in the VIN structure lists all the global indexs of the ring decoys,
// it uses offsets relative to the first output to save space on chain, so they need
// to be converted from absolute to relative afterwards using the utility function.
//
// At this point we also push the public keys of the decoys into our mixRing struct which will get
// passed to the ringct module which it actually will use to generate a ring signature.
mixRing[i].reserve(ptx.decoys[i].size());
for(const auto& decoy: ptx.decoys[i])
{
input_to_key.key_offsets.push_back(decoy.global_index);
mixRing[i].push_back(rct::ctkey{});
rct::ctkey& decoypk = mixRing[i].back();
decoypk.dest = rct::pk2rct(decoy.key);
decoypk.mask = decoy.mask;
}
input_to_key.key_offsets.push_back(src_entr.global_index);
index.push_back(src_entr.global_index);
input_to_key.key_offsets = cryptonote::absolute_output_offsets_to_relative(input_to_key.key_offsets);
i++;
}
// TODO sean the inputs should be sorted by key_image
std::vector<rct::key> amount_keys;
amount_keys.clear();
i = 0;
// Loop over destinations and generate one time destination keys (Output Ephemeral Key)
for(const cryptonote::tx_destination_entry& recipient: ptx.recipients)
{
//amount_keys is a return parameter here, generate_output_ephemeral keys appends to the vector as it goes
crypto::public_key out_eph_public_key = generate_output_ephemeral_keys(tx_key, recipient, i, amount_keys);
cryptonote::tx_out out;
out.amount = recipient.amount;
cryptonote::txout_to_key tk;
tk.key = out_eph_public_key;
out.target = tk;
dest_keys.push_back(rct::pk2rct(out_eph_public_key));
outamounts.push_back(recipient.amount);
amount_out += recipient.amount;
// TODO sean the output should be shuffled
// also a change address needs to be in here
i++;
}
crypto::hash tx_prefix_hash = get_transaction_prefix_hash(ptx.tx);
rct::ctkeyV outSk;
const rct::RCTConfig rct_config{rct::RangeProofType::PaddedBulletproof, 3/*CLSAG*/};
// This generates the bulletproofs and also the ring signature, pretty much does everything and adds
// the information for rct_signatures and rctsig_prunable to the transaction
ptx.tx.rct_signatures = rct::genRctSimple(
rct::hash2rct(tx_prefix_hash), // rct::key& message
inSk, // rct::ctkeyV inSk
dest_keys, // rct::keyV destinations
inamounts, // std::vector<xmr_amount>& inamounts
outamounts, // std::vector<xmr_amount>& outamounts
amount_in - amount_out, // xmr_amount txnFee
mixRing, // rct::ctkeyM& mixRing
amount_keys, // rct::keyV amount_keys -> Return Parameter
NULL, // std::vector<multisig_kLRki>* kLRki -> no multisig
nullptr, // rct::multisig_out* msout -> no multisig
index, // std::vector<unsigned int>& index -> array of real outputs within the mixRing keys
outSk, // rct::ctkeyV& outSk -> Return Parameter
rct_config, // rct::RCTConfig& rct_config
key_device); // hw::device& hwdev
}
} // namespace wallet

View File

@ -2,11 +2,14 @@
#include <crypto/crypto.h>
#include <cryptonote_basic/subaddress_index.h>
#include <cryptonote_basic/cryptonote_basic.h>
#include <device/device_default.hpp>
#include <ringct/rctSigs.h>
#include <optional>
#include "pending_transaction.hpp"
namespace wallet
{
class Keyring
@ -23,6 +26,9 @@ namespace wallet
, view_public_key(_view_public_key)
{}
virtual crypto::secret_key
generate_tx_key(uint8_t hf_version);
// Derivation = a*R where
// `a` is the private view key of the recipient
// `R` is the tx public key for the output
@ -35,6 +41,9 @@ namespace wallet
virtual crypto::key_derivation
generate_key_derivation(const crypto::public_key& tx_pubkey) const;
virtual crypto::public_key
secret_tx_key_to_public_tx_key(const crypto::secret_key tx_key);
virtual std::vector<crypto::key_derivation>
generate_key_derivations(const std::vector<crypto::public_key>& tx_pubkeys) const;
@ -64,6 +73,29 @@ namespace wallet
unsigned int i,
rct::key& mask);
virtual crypto::public_key
generate_output_ephemeral_keys(
const crypto::secret_key& tx_key,
const cryptonote::tx_destination_entry& dst_entr,
const size_t output_index,
std::vector<rct::key>& amount_keys);
virtual crypto::secret_key
derive_transaction_secret_key(
const crypto::key_derivation& key_derivation,
const size_t output_index
);
virtual crypto::hash
get_transaction_prefix_hash(
const cryptonote::transaction_prefix&
);
virtual void
sign_transaction(
PendingTransaction& ptx
);
private:
crypto::secret_key spend_private_key;
crypto::public_key spend_public_key;

View File

@ -4,15 +4,15 @@
namespace wallet
{
PendingTransaction::PendingTransaction(const std::vector<TransactionRecipient>& new_recipients)
PendingTransaction::PendingTransaction(const std::vector<cryptonote::tx_destination_entry>& new_recipients)
: recipients(new_recipients)
{
// TODO sean address of change needs to be creator
change = TransactionRecipient{{}, 0};
change = cryptonote::tx_destination_entry{};
int64_t sum_recipient_amounts = 0;
for (const auto& recipient : new_recipients)
{
if (sum_recipient_amounts < 0 || recipient.amount < 0)
if (sum_recipient_amounts < 0)
throw std::runtime_error("Transaction amounts must be positive");
sum_recipient_amounts += recipient.amount;
}
@ -43,7 +43,7 @@ namespace wallet
recipients.begin(),
recipients.end(),
0,
[](int64_t accumulator, const TransactionRecipient& recipient) {
[](int64_t accumulator, const cryptonote::tx_destination_entry& recipient) {
return accumulator + recipient.amount;
});
}
@ -116,6 +116,7 @@ namespace wallet
bool
PendingTransaction::finalise()
{
tx = cryptonote::transaction{};
if (sum_inputs() - sum_outputs() - fee - change.amount == 0)
return true;
else

View File

@ -1,6 +1,7 @@
#pragma once
#include <cryptonote_basic/cryptonote_basic.h>
#include <cryptonote_core/cryptonote_tx_utils.h>
#include "address.hpp"
#include "output.hpp"
#include "decoy.hpp"
@ -13,22 +14,13 @@ namespace wallet
struct version
{}; // XXX: placeholder type
struct TransactionRecipient
{
address recipient_address;
int64_t amount;
TransactionRecipient() = default;
TransactionRecipient(address addr, int64_t amt) : recipient_address(addr), amount(amt){};
};
struct PendingTransaction
{
version tx_version;
std::vector<TransactionRecipient> recipients; // does not include change
std::vector<cryptonote::tx_destination_entry> recipients; // does not include change
TransactionRecipient change;
cryptonote::tx_destination_entry change;
std::string memo;
@ -48,7 +40,7 @@ namespace wallet
PendingTransaction() = default;
PendingTransaction(const std::vector<TransactionRecipient>& new_recipients);
PendingTransaction(const std::vector<cryptonote::tx_destination_entry>& new_recipients);
int64_t
get_fee() const;

View File

@ -10,7 +10,7 @@ namespace wallet
// create_transaction will create a vanilla spend transaction without any special features.
PendingTransaction
TransactionConstructor::create_transaction(
const std::vector<TransactionRecipient>& recipients) const
const std::vector<cryptonote::tx_destination_entry>& recipients) const
{
PendingTransaction new_tx(recipients);
new_tx.fee_per_byte = fee_per_byte;

View File

@ -24,7 +24,7 @@ namespace wallet
};
PendingTransaction
create_transaction(const std::vector<TransactionRecipient>& recipients) const;
create_transaction(const std::vector<cryptonote::tx_destination_entry>& recipients) const;
uint64_t fee_per_byte = FEE_PER_BYTE_V13;
uint64_t fee_per_output = FEE_PER_OUTPUT_V18;

View File

@ -52,6 +52,26 @@ class MockWallet : public Wallet
store_transaction(hash, height, dummy_outputs);
db_tx.commit();
};
void
store_test_output(wallet::Output o)
{
height++;
wallet::Block b{};
b.height = height;
auto hash = debug_random_filled<crypto::hash>(height);
b.hash = hash;
add_block(b);
std::vector<wallet::Output> dummy_outputs;
o.block_height = height;
dummy_outputs.push_back(o);
SQLite::Transaction db_tx(db->db);
store_transaction(hash, height, dummy_outputs);
db_tx.commit();
};
};

View File

@ -7,6 +7,7 @@
#include <sqlitedb/database.hpp>
#include "mock_wallet.hpp"
#include "mock_keyring.hpp"
#include "mock_daemon_comms.hpp"
@ -19,8 +20,9 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
ctor.fee_per_output = 0;
SECTION("Expect Fail if database is empty")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 4);
std::vector<cryptonote::tx_destination_entry> recipients;
recipients.emplace_back(cryptonote::tx_destination_entry{});
recipients.back().amount = 4;
REQUIRE_THROWS(ctor.create_transaction(recipients));
}
@ -28,8 +30,9 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
SECTION("Creates a successful single transaction")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 4);
std::vector<cryptonote::tx_destination_entry> recipients;
recipients.emplace_back(cryptonote::tx_destination_entry{});
recipients.back().amount = 4;
wallet::PendingTransaction ptx = ctor.create_transaction(recipients);
REQUIRE(ptx.recipients.size() == 1);
REQUIRE(ptx.chosen_outputs.size() == 1);
@ -41,8 +44,9 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
SECTION("Fails to create a transaction if amount is not enough")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 6);
std::vector<cryptonote::tx_destination_entry> recipients;
recipients.emplace_back(cryptonote::tx_destination_entry{});
recipients.back().amount = 6;
REQUIRE_THROWS(ctor.create_transaction(recipients));
}
@ -50,8 +54,9 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
wallet.store_test_transaction(7);
SECTION("Creates a successful single transaction prefering to use a single input if possible")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 6);
std::vector<cryptonote::tx_destination_entry> recipients;
recipients.emplace_back(cryptonote::tx_destination_entry{});
recipients.back().amount = 6;
wallet::PendingTransaction ptx = ctor.create_transaction(recipients);
REQUIRE(ptx.recipients.size() == 1);
REQUIRE(ptx.chosen_outputs.size() == 1);
@ -63,8 +68,9 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
SECTION("Creates a successful transaction using 2 inputs")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 8);
std::vector<cryptonote::tx_destination_entry> recipients;
recipients.emplace_back(cryptonote::tx_destination_entry{});
recipients.back().amount = 8;
wallet::PendingTransaction ptx = ctor.create_transaction(recipients);
REQUIRE(ptx.recipients.size() == 1);
REQUIRE(ptx.chosen_outputs.size() == 2);
@ -79,8 +85,9 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
SECTION("Creates a successful transaction using 2 inputs, avoids creating dust and uses correct fee using 1 oxen per byte")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 4001);
std::vector<cryptonote::tx_destination_entry> recipients;
recipients.emplace_back(cryptonote::tx_destination_entry{});
recipients.back().amount = 4001;
wallet::PendingTransaction ptx = ctor.create_transaction(recipients);
REQUIRE(ptx.recipients.size() == 1);
REQUIRE(ptx.chosen_outputs.size() == 2);
@ -94,8 +101,9 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
ctor.fee_per_output = 50;
SECTION("Creates a successful transaction using 2 inputs, avoids creating dust and uses correct fee using 1 oxen per byte and 50 oxen per output")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 4001);
std::vector<cryptonote::tx_destination_entry> recipients;
recipients.emplace_back(cryptonote::tx_destination_entry{});
recipients.back().amount = 4001;
wallet::PendingTransaction ptx = ctor.create_transaction(recipients);
REQUIRE(ptx.recipients.size() == 1);
REQUIRE(ptx.chosen_outputs.size() == 2);
@ -105,4 +113,24 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
for (const auto& decoys : ptx.decoys)
REQUIRE(decoys.size() == 13);
}
SECTION("Creates a successful transaction then signs using the keyring successfully")
{
// Start a new wallet for real inputs to test signatures
auto wallet_with_valid_inputs = wallet::MockWallet();
auto ctor_for_signing = wallet::TransactionConstructor(wallet_with_valid_inputs.get_db(), comms);
wallet::Output o{};
wallet_with_valid_inputs.store_test_output(o);
std::vector<cryptonote::tx_destination_entry> recipients;
recipients.emplace_back(cryptonote::tx_destination_entry{});
recipients.back().amount = 4001;
wallet::PendingTransaction ptx = ctor_for_signing.create_transaction(recipients);
REQUIRE(ptx.finalise());
auto keys = std::make_unique<wallet::MockKeyring>();
REQUIRE_NOTHROW(keys->sign_transaction(ptx));
auto& signedtx = ptx.tx;
}
}