Add (working draft) subaddress support (#83)

This commit is contained in:
Lee *!* Clagett
2023-12-05 20:23:50 -05:00
committed by Lee *!* Clagett
parent e09d3d57e9
commit b4426b4a74
21 changed files with 1539 additions and 88 deletions

View File

@@ -94,16 +94,22 @@ namespace lws
std::atomic<bool> update;
};
struct options
{
net::ssl_verification_t webhook_verify;
bool enable_subaddresses;
};
struct thread_data
{
explicit thread_data(rpc::client client, db::storage disk, std::vector<lws::account> users, net::ssl_verification_t webhook_verify)
: client(std::move(client)), disk(std::move(disk)), users(std::move(users)), webhook_verify(webhook_verify)
explicit thread_data(rpc::client client, db::storage disk, std::vector<lws::account> users, options opts)
: client(std::move(client)), disk(std::move(disk)), users(std::move(users)), opts(opts)
{}
rpc::client client;
db::storage disk;
std::vector<lws::account> users;
net::ssl_verification_t webhook_verify;
options opts;
};
// until we have a signal-handler safe notification system
@@ -263,6 +269,23 @@ namespace lws
}
};
struct subaddress_reader
{
expect<db::storage_reader> reader;
db::cursor::subaddress_indexes cur;
subaddress_reader(db::storage const& disk, const bool enable_subaddresses)
: reader(common_error::kInvalidArgument), cur(nullptr)
{
if (enable_subaddresses)
{
reader = disk.start_read();
if (!reader)
MERROR("Subadress lookup failure: " << reader.error().message());
}
}
};
void scan_transaction_base(
epee::span<lws::account> users,
const db::block_id height,
@@ -270,6 +293,7 @@ namespace lws
crypto::hash const& tx_hash,
cryptonote::transaction const& tx,
std::vector<std::uint64_t> const& out_ids,
subaddress_reader& reader,
std::function<void(lws::account&, const db::spend&)> spend_action,
std::function<bool(lws::account&, const db::output&)> output_action)
{
@@ -280,6 +304,8 @@ namespace lws
boost::optional<crypto::hash> prefix_hash;
boost::optional<cryptonote::tx_extra_nonce> extra_nonce;
std::pair<std::uint8_t, db::output::payment_id_> payment_id;
cryptonote::tx_extra_additional_pub_keys additional_tx_pub_keys;
std::vector<crypto::key_derivation> additional_derivations;
{
std::vector<cryptonote::tx_extra_field> extra;
@@ -297,6 +323,10 @@ namespace lws
}
else
extra_nonce = boost::none;
// additional tx pub keys present when there are 3+ outputs in a tx involving subaddresses
if (reader.reader)
cryptonote::find_tx_extra_field_by_type(extra, additional_tx_pub_keys);
} // destruct `extra` vector
for (account& user : users)
@@ -308,6 +338,21 @@ namespace lws
if (!crypto::wallet::generate_key_derivation(key.pub_key, user.view_key(), derived))
continue; // to next user
if (reader.reader && additional_tx_pub_keys.data.size() == tx.vout.size())
{
additional_derivations.resize(tx.vout.size());
std::size_t index = -1;
for (auto const& out: tx.vout)
{
++index;
if (!crypto::wallet::generate_key_derivation(additional_tx_pub_keys.data[index], user.view_key(), additional_derivations[index]))
{
additional_derivations.clear();
break; // vout loop
}
}
}
db::extra ext{};
std::uint32_t mixin = 0;
for (auto const& in : tx.vin)
@@ -324,23 +369,26 @@ namespace lws
for (std::uint64_t offset : in_data->key_offsets)
{
goffset += offset;
if (user.has_spendable(db::output_id{in_data->amount, goffset}))
{
spend_action(
user,
db::spend{
db::transaction_link{height, tx_hash},
in_data->k_image,
db::output_id{in_data->amount, goffset},
timestamp,
tx.unlock_time,
mixin,
{0, 0, 0}, // reserved
payment_id.first,
payment_id.second.long_
}
);
}
const boost::optional<db::address_index> subaccount =
user.get_spendable(db::output_id{in_data->amount, goffset});
if (!subaccount)
continue; // to next input
spend_action(
user,
db::spend{
db::transaction_link{height, tx_hash},
in_data->k_image,
db::output_id{in_data->amount, goffset},
timestamp,
tx.unlock_time,
mixin,
{0, 0, 0}, // reserved
payment_id.first,
payment_id.second.long_,
*subaccount
}
);
}
}
else if (boost::get<cryptonote::txin_gen>(std::addressof(in)))
@@ -358,15 +406,57 @@ namespace lws
boost::optional<crypto::view_tag> view_tag_opt =
cryptonote::get_output_view_tag(out);
if (!cryptonote::out_can_be_to_acc(view_tag_opt, derived, index))
const bool found_tag =
(!additional_derivations.empty() && cryptonote::out_can_be_to_acc(view_tag_opt, additional_derivations.at(index), index)) ||
cryptonote::out_can_be_to_acc(view_tag_opt, derived, index);
if (!found_tag)
continue; // to next output
crypto::public_key derived_pub;
const bool received =
crypto::wallet::derive_subaddress_public_key(out_pub_key, derived, index, derived_pub) &&
derived_pub == user.spend_public();
bool found_pub = false;
db::address_index account_index{db::major_index::primary, db::minor_index::primary};
crypto::key_derivation active_derived;
if (!received)
// inspect the additional and traditional keys
for (std::size_t attempt = 0; attempt < 2; ++attempt)
{
if (attempt == 0)
active_derived = derived;
else if (!additional_derivations.empty())
active_derived = additional_derivations.at(index);
else
break; // inspection loop
crypto::public_key derived_pub;
if (!crypto::wallet::derive_subaddress_public_key(out_pub_key, active_derived, index, derived_pub))
continue; // to next available active_derived
if (user.spend_public() != derived_pub)
{
if (!reader.reader)
continue; // to next available active_derived
const expect<db::address_index> match =
reader.reader->find_subaddress(user.id(), derived_pub, reader.cur);
if (!match)
{
if (match != lmdb::error(MDB_NOTFOUND))
MERROR("Failure when doing subaddress search: " << match.error().message());
continue; // to next available active_derived
}
found_pub = true;
account_index = *match;
break; // additional_derivations loop
}
else
{
found_pub = true;
break; // additional_derivations loop
}
}
if (!found_pub)
continue; // to next output
if (!prefix_hash)
@@ -381,7 +471,7 @@ namespace lws
{
const bool bulletproof2 = (rct::RCTTypeBulletproof2 <= tx.rct_signatures.type);
const auto decrypted = lws::decode_amount(
tx.rct_signatures.outPk.at(index).mask, tx.rct_signatures.ecdhInfo.at(index), derived, index, bulletproof2
tx.rct_signatures.outPk.at(index).mask, tx.rct_signatures.ecdhInfo.at(index), active_derived, index, bulletproof2
);
if (!decrypted)
{
@@ -398,7 +488,7 @@ namespace lws
if (!payment_id.first && cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce->nonce, payment_id.second.short_))
{
payment_id.first = sizeof(crypto::hash8);
lws::decrypt_payment_id(payment_id.second.short_, derived);
lws::decrypt_payment_id(payment_id.second.short_, active_derived);
}
}
@@ -421,7 +511,8 @@ namespace lws
{0, 0, 0, 0, 0, 0, 0}, // reserved bytes
db::pack(ext, payment_id.first),
payment_id.second,
tx.rct_signatures.txnFee
tx.rct_signatures.txnFee,
account_index
}
);
@@ -437,12 +528,13 @@ namespace lws
const std::uint64_t timestamp,
crypto::hash const& tx_hash,
cryptonote::transaction const& tx,
std::vector<std::uint64_t> const& out_ids)
std::vector<std::uint64_t> const& out_ids,
subaddress_reader& reader)
{
scan_transaction_base(users, height, timestamp, tx_hash, tx, out_ids, add_spend{}, add_output{});
scan_transaction_base(users, height, timestamp, tx_hash, tx, out_ids, reader, add_spend{}, add_output{});
}
void scan_transactions(std::string&& txpool_msg, epee::span<lws::account> users, db::storage const& disk, rpc::client& client, const net::ssl_verification_t verify_mode)
void scan_transactions(std::string&& txpool_msg, epee::span<lws::account> users, db::storage const& disk, rpc::client& client, const options& opts)
{
// uint64::max is for txpool
static const std::vector<std::uint64_t> fake_outs(
@@ -459,9 +551,10 @@ namespace lws
const auto time =
boost::numeric_cast<std::uint64_t>(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));
send_webhook sender{disk, client, verify_mode};
subaddress_reader reader{disk, opts.enable_subaddresses};
send_webhook sender{disk, client, opts.webhook_verify};
for (const auto& tx : parsed->txes)
scan_transaction_base(users, db::block_id::txpool, time, crypto::hash{}, tx, fake_outs, null_spend{}, sender);
scan_transaction_base(users, db::block_id::txpool, time, crypto::hash{}, tx, fake_outs, reader, null_spend{}, sender);
}
void update_rates(rpc::context& ctx)
@@ -481,7 +574,7 @@ namespace lws
rpc::client client{std::move(data->client)};
db::storage disk{std::move(data->disk)};
std::vector<lws::account> users{std::move(data->users)};
const net::ssl_verification_t webhook_verify = data->webhook_verify;
const options opts = std::move(data->opts);
assert(!users.empty());
assert(std::is_sorted(users.begin(), users.end(), by_height{}));
@@ -568,7 +661,7 @@ namespace lws
{
if (message->first != rpc::client::topic::txpool)
break; // inner for loop
scan_transactions(std::move(message->second), epee::to_mut_span(users), disk, client, webhook_verify);
scan_transactions(std::move(message->second), epee::to_mut_span(users), disk, client, opts);
}
for ( ; message != new_pubs->end(); ++message)
@@ -605,6 +698,7 @@ namespace lws
else
fetched->start_height = 0;
subaddress_reader reader{disk, opts.enable_subaddresses};
for (auto block_data : boost::combine(blocks, indices))
{
++(fetched->start_height);
@@ -629,7 +723,8 @@ namespace lws
block.timestamp,
miner_tx_hash,
block.miner_tx,
*(indices.begin())
*(indices.begin()),
reader
);
indices.remove_prefix(1);
@@ -644,13 +739,15 @@ namespace lws
block.timestamp,
boost::get<0>(tx_data),
boost::get<1>(tx_data),
boost::get<2>(tx_data)
boost::get<2>(tx_data),
reader
);
}
blockchain.push_back(cryptonote::get_block_hash(block));
} // for each block
reader.reader = std::error_code{common_error::kInvalidArgument}; // cleanup reader before next write
auto updated = disk.update(
users.front().scan_height(), epee::to_span(blockchain), epee::to_span(users)
);
@@ -665,7 +762,7 @@ namespace lws
}
MINFO("Processed " << blocks.size() << " block(s) against " << users.size() << " account(s)");
send_payment_hook(client, epee::to_span(updated->second), webhook_verify);
send_payment_hook(client, epee::to_span(updated->second), opts.webhook_verify);
if (updated->first != users.size())
{
MWARNING("Only updated " << updated->first << " account(s) out of " << users.size() << ", resetting");
@@ -692,7 +789,7 @@ namespace lws
Launches `thread_count` threads to run `scan_loop`, and then polls for
active account changes in background
*/
void check_loop(db::storage disk, rpc::context& ctx, std::size_t thread_count, std::vector<lws::account> users, std::vector<db::account_id> active, const net::ssl_verification_t webhook_verify)
void check_loop(db::storage disk, rpc::context& ctx, std::size_t thread_count, std::vector<lws::account> users, std::vector<db::account_id> active, const options opts)
{
assert(0 < thread_count);
assert(0 < users.size());
@@ -754,7 +851,7 @@ namespace lws
client.watch_scan_signals();
auto data = std::make_shared<thread_data>(
std::move(client), disk.clone(), std::move(thread_users), webhook_verify
std::move(client), disk.clone(), std::move(thread_users), opts
);
threads.emplace_back(attrs, std::bind(&scan_loop, std::ref(self), std::move(data)));
}
@@ -765,7 +862,7 @@ namespace lws
client.watch_scan_signals();
auto data = std::make_shared<thread_data>(
std::move(client), disk.clone(), std::move(users), webhook_verify
std::move(client), disk.clone(), std::move(users), opts
);
threads.emplace_back(attrs, std::bind(&scan_loop, std::ref(self), std::move(data)));
}
@@ -908,7 +1005,7 @@ namespace lws
return {std::move(client)};
}
void scanner::run(db::storage disk, rpc::context ctx, std::size_t thread_count, const epee::net_utils::ssl_verification_t webhook_verify)
void scanner::run(db::storage disk, rpc::context ctx, std::size_t thread_count, const epee::net_utils::ssl_verification_t webhook_verify, const bool enable_subaddresses)
{
thread_count = std::max(std::size_t(1), thread_count);
@@ -931,7 +1028,7 @@ namespace lws
for (db::account user : accounts.make_range())
{
std::vector<db::output_id> receives{};
std::vector<std::pair<db::output_id, db::address_index>> receives{};
std::vector<crypto::public_key> pubs{};
auto receive_list = MONERO_UNWRAP(reader.get_outputs(user.id));
@@ -941,7 +1038,9 @@ namespace lws
for (auto output = receive_list.make_iterator(); !output.is_end(); ++output)
{
receives.emplace_back(output.get_value<MONERO_FIELD(db::output, spend_meta.id)>());
auto id = output.get_value<MONERO_FIELD(db::output, spend_meta.id)>();
auto subaddr = output.get_value<MONERO_FIELD(db::output, recipient)>();
receives.emplace_back(std::move(id), std::move(subaddr));
pubs.emplace_back(output.get_value<MONERO_FIELD(db::output, pub)>());
}
@@ -960,7 +1059,7 @@ namespace lws
checked_wait(account_poll_interval - (std::chrono::steady_clock::now() - last));
}
else
check_loop(disk.clone(), ctx, thread_count, std::move(users), std::move(active), webhook_verify);
check_loop(disk.clone(), ctx, thread_count, std::move(users), std::move(active), options{webhook_verify, enable_subaddresses});
if (!scanner::is_running())
return;