wallet: new {ex,im}port_key_images commands and RPC calls
They are used to export a signed set of key images from a wallet with a private spend key, so an auditor with the matching view key may see which of those are spent, and which are not.
This commit is contained in:
parent
47618c2710
commit
ebf97d76f0
|
@ -662,6 +662,8 @@ simple_wallet::simple_wallet()
|
|||
m_cmd_binder.set_handler("status", boost::bind(&simple_wallet::status, this, _1), tr("Show wallet status information"));
|
||||
m_cmd_binder.set_handler("sign", boost::bind(&simple_wallet::sign, this, _1), tr("Sign the contents of a file"));
|
||||
m_cmd_binder.set_handler("verify", boost::bind(&simple_wallet::verify, this, _1), tr("Verify a signature on the contents of a file"));
|
||||
m_cmd_binder.set_handler("export_key_images", boost::bind(&simple_wallet::export_key_images, this, _1), tr("Export a signed set of key images"));
|
||||
m_cmd_binder.set_handler("import_key_images", boost::bind(&simple_wallet::import_key_images, this, _1), tr("Import signed key images list and verify their spent status"));
|
||||
m_cmd_binder.set_handler("help", boost::bind(&simple_wallet::help, this, _1), tr("Show this help"));
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------
|
||||
|
@ -3480,6 +3482,111 @@ bool simple_wallet::verify(const std::vector<std::string> &args)
|
|||
return true;
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------
|
||||
bool simple_wallet::export_key_images(const std::vector<std::string> &args)
|
||||
{
|
||||
if (args.size() != 1)
|
||||
{
|
||||
fail_msg_writer() << tr("usage: export_key_images <filename>");
|
||||
return true;
|
||||
}
|
||||
if (m_wallet->watch_only())
|
||||
{
|
||||
fail_msg_writer() << tr("wallet is watch-only and cannot export key images");
|
||||
return true;
|
||||
}
|
||||
std::string filename = args[0];
|
||||
|
||||
try
|
||||
{
|
||||
std::vector<std::pair<crypto::key_image, crypto::signature>> ski = m_wallet->export_key_images();
|
||||
std::string data;
|
||||
for (const auto &i: ski)
|
||||
{
|
||||
data += epee::string_tools::pod_to_hex(i.first);
|
||||
data += epee::string_tools::pod_to_hex(i.second);
|
||||
}
|
||||
bool r = epee::file_io_utils::save_string_to_file(filename, data);
|
||||
if (!r)
|
||||
{
|
||||
fail_msg_writer() << tr("failed to save file ") << filename;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (std::exception &e)
|
||||
{
|
||||
LOG_ERROR("Error exporting key images: " << e.what());
|
||||
fail_msg_writer() << "Error exporting key images: " << e.what();
|
||||
return true;
|
||||
}
|
||||
|
||||
success_msg_writer() << tr("Signed key images exported to ") << filename;
|
||||
return true;
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------
|
||||
bool simple_wallet::import_key_images(const std::vector<std::string> &args)
|
||||
{
|
||||
if (args.size() != 1)
|
||||
{
|
||||
fail_msg_writer() << tr("usage: import_key_images <filename>");
|
||||
return true;
|
||||
}
|
||||
std::string filename = args[0];
|
||||
|
||||
std::string data;
|
||||
bool r = epee::file_io_utils::load_file_to_string(filename, data);
|
||||
if (!r)
|
||||
{
|
||||
fail_msg_writer() << tr("failed to read file ") << filename;
|
||||
return true;
|
||||
}
|
||||
|
||||
const size_t record_size = sizeof(crypto::key_image)*2 + sizeof(crypto::signature)*2;
|
||||
if (data.size() % record_size)
|
||||
{
|
||||
fail_msg_writer() << "Bad data size from file " << filename;
|
||||
return true;
|
||||
}
|
||||
size_t nki = data.size() / record_size;
|
||||
|
||||
std::vector<std::pair<crypto::key_image, crypto::signature>> ski;
|
||||
ski.reserve(nki);
|
||||
for (size_t n = 0; n < nki; ++n)
|
||||
{
|
||||
cryptonote::blobdata bd;
|
||||
|
||||
if(!epee::string_tools::parse_hexstr_to_binbuff(std::string(&data[n * record_size], sizeof(crypto::key_image)*2), bd))
|
||||
{
|
||||
fail_msg_writer() << tr("failed to parse key image");
|
||||
return false;
|
||||
}
|
||||
crypto::key_image key_image = *reinterpret_cast<const crypto::key_image*>(bd.data());
|
||||
|
||||
if(!epee::string_tools::parse_hexstr_to_binbuff(std::string(&data[n * record_size + sizeof(crypto::key_image)*2], sizeof(crypto::signature)*2), bd))
|
||||
{
|
||||
fail_msg_writer() << tr("failed to parse signature");
|
||||
return false;
|
||||
}
|
||||
crypto::signature signature = *reinterpret_cast<const crypto::signature*>(bd.data());
|
||||
|
||||
ski.push_back(std::make_pair(key_image, signature));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
uint64_t spent = 0, unspent = 0;
|
||||
uint64_t height = m_wallet->import_key_images(ski, spent, unspent);
|
||||
success_msg_writer() << "Signed key images imported to height " << height << ", "
|
||||
<< print_money(spent) << " spent, " << print_money(unspent) << " unspent";
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
fail_msg_writer() << "Failed to import key images: " << e.what();
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------
|
||||
bool simple_wallet::process_command(const std::vector<std::string> &args)
|
||||
{
|
||||
return m_cmd_binder.process_command_vec(args);
|
||||
|
|
|
@ -145,6 +145,8 @@ namespace cryptonote
|
|||
bool set_default_fee_multiplier(const std::vector<std::string> &args);
|
||||
bool sign(const std::vector<std::string> &args);
|
||||
bool verify(const std::vector<std::string> &args);
|
||||
bool export_key_images(const std::vector<std::string> &args);
|
||||
bool import_key_images(const std::vector<std::string> &args);
|
||||
|
||||
uint64_t get_daemon_blockchain_height(std::string& err);
|
||||
bool try_connect_to_daemon(bool silent = false);
|
||||
|
|
|
@ -3286,7 +3286,123 @@ bool wallet2::verify(const std::string &data, const cryptonote::account_public_a
|
|||
memcpy(&s, decoded.data(), sizeof(s));
|
||||
return crypto::check_signature(hash, address.m_spend_public_key, s);
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------
|
||||
std::vector<std::pair<crypto::key_image, crypto::signature>> wallet2::export_key_images() const
|
||||
{
|
||||
std::vector<std::pair<crypto::key_image, crypto::signature>> ski;
|
||||
|
||||
ski.reserve(m_transfers.size());
|
||||
for (size_t n = 0; n < m_transfers.size(); ++n)
|
||||
{
|
||||
const transfer_details &td = m_transfers[n];
|
||||
|
||||
crypto::hash hash;
|
||||
crypto::cn_fast_hash(&td.m_key_image, sizeof(td.m_key_image), hash);
|
||||
|
||||
// get ephemeral public key
|
||||
const cryptonote::tx_out &out = td.m_tx.vout[td.m_internal_output_index];
|
||||
THROW_WALLET_EXCEPTION_IF(out.target.type() != typeid(txout_to_key), error::wallet_internal_error,
|
||||
"Output is not txout_to_key");
|
||||
const cryptonote::txout_to_key &o = boost::get<const cryptonote::txout_to_key>(out.target);
|
||||
const crypto::public_key pkey = o.key;
|
||||
|
||||
// get tx pub key
|
||||
std::vector<tx_extra_field> tx_extra_fields;
|
||||
if(!parse_tx_extra(td.m_tx.extra, tx_extra_fields))
|
||||
{
|
||||
// Extra may only be partially parsed, it's OK if tx_extra_fields contains public key
|
||||
}
|
||||
tx_extra_pub_key pub_key_field;
|
||||
THROW_WALLET_EXCEPTION_IF(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field), error::wallet_internal_error,
|
||||
"Public key wasn't found in the transaction extra");
|
||||
crypto::public_key tx_pub_key = pub_key_field.pub_key;
|
||||
|
||||
// generate ephemeral secret key
|
||||
crypto::key_image ki;
|
||||
cryptonote::keypair in_ephemeral;
|
||||
cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, td.m_internal_output_index, in_ephemeral, ki);
|
||||
THROW_WALLET_EXCEPTION_IF(ki != td.m_key_image,
|
||||
error::wallet_internal_error, "key_image generated not matched with cached key image");
|
||||
THROW_WALLET_EXCEPTION_IF(in_ephemeral.pub != pkey,
|
||||
error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key");
|
||||
|
||||
// sign the key image with the output secret key
|
||||
crypto::signature signature;
|
||||
std::vector<const crypto::public_key*> key_ptrs;
|
||||
key_ptrs.push_back(&pkey);
|
||||
|
||||
crypto::generate_ring_signature((const crypto::hash&)td.m_key_image, td.m_key_image, key_ptrs, in_ephemeral.sec, 0, &signature);
|
||||
|
||||
ski.push_back(std::make_pair(td.m_key_image, signature));
|
||||
}
|
||||
return ski;
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------
|
||||
uint64_t wallet2::import_key_images(const std::vector<std::pair<crypto::key_image, crypto::signature>> &signed_key_images, uint64_t &spent, uint64_t &unspent)
|
||||
{
|
||||
COMMAND_RPC_IS_KEY_IMAGE_SPENT::request req = AUTO_VAL_INIT(req);
|
||||
COMMAND_RPC_IS_KEY_IMAGE_SPENT::response daemon_resp = AUTO_VAL_INIT(daemon_resp);
|
||||
|
||||
THROW_WALLET_EXCEPTION_IF(signed_key_images.size() > m_transfers.size(), error::wallet_internal_error,
|
||||
"The blockchain is out of date compared to the signed key images");
|
||||
|
||||
if (signed_key_images.empty())
|
||||
{
|
||||
spent = 0;
|
||||
unspent = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t n = 0; n < signed_key_images.size(); ++n)
|
||||
{
|
||||
const transfer_details &td = m_transfers[n];
|
||||
const crypto::key_image &key_image = signed_key_images[n].first;
|
||||
const crypto::signature &signature = signed_key_images[n].second;
|
||||
|
||||
// get ephemeral public key
|
||||
const cryptonote::tx_out &out = td.m_tx.vout[td.m_internal_output_index];
|
||||
THROW_WALLET_EXCEPTION_IF(out.target.type() != typeid(txout_to_key), error::wallet_internal_error,
|
||||
"Non txout_to_key output found");
|
||||
const cryptonote::txout_to_key &o = boost::get<cryptonote::txout_to_key>(out.target);
|
||||
const crypto::public_key pkey = o.key;
|
||||
|
||||
std::vector<const crypto::public_key*> pkeys;
|
||||
pkeys.push_back(&pkey);
|
||||
THROW_WALLET_EXCEPTION_IF(!crypto::check_ring_signature((const crypto::hash&)key_image, key_image, pkeys, &signature),
|
||||
error::wallet_internal_error, "Signature check failed: key image " + epee::string_tools::pod_to_hex(key_image)
|
||||
+ ", signature " + epee::string_tools::pod_to_hex(signature) + ", pubkey " + epee::string_tools::pod_to_hex(*pkeys[0]));
|
||||
|
||||
req.key_images.push_back(epee::string_tools::pod_to_hex(key_image));
|
||||
}
|
||||
|
||||
m_daemon_rpc_mutex.lock();
|
||||
bool r = epee::net_utils::invoke_http_json_remote_command2(m_daemon_address + "/is_key_image_spent", req, daemon_resp, m_http_client, 200000);
|
||||
m_daemon_rpc_mutex.unlock();
|
||||
THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "is_key_image_spent");
|
||||
THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "is_key_image_spent");
|
||||
THROW_WALLET_EXCEPTION_IF(daemon_resp.status != CORE_RPC_STATUS_OK, error::is_key_image_spent_error, daemon_resp.status);
|
||||
THROW_WALLET_EXCEPTION_IF(daemon_resp.spent_status.size() != signed_key_images.size(), error::wallet_internal_error,
|
||||
"daemon returned wrong response for is_key_image_spent, wrong amounts count = " +
|
||||
std::to_string(daemon_resp.spent_status.size()) + ", expected " + std::to_string(signed_key_images.size()));
|
||||
|
||||
spent = 0;
|
||||
unspent = 0;
|
||||
for (size_t n = 0; n < daemon_resp.spent_status.size(); ++n)
|
||||
{
|
||||
transfer_details &td = m_transfers[n];
|
||||
uint64_t amount = td.m_tx.vout[td.m_internal_output_index].amount;
|
||||
td.m_spent = daemon_resp.spent_status[n] != COMMAND_RPC_IS_KEY_IMAGE_SPENT::UNSPENT;
|
||||
if (td.m_spent)
|
||||
spent += amount;
|
||||
else
|
||||
unspent += amount;
|
||||
LOG_PRINT_L2("Transfer " << n << ": " << print_money(amount) << " (" << td.m_global_output_index << "): "
|
||||
<< (td.m_spent ? "spent" : "unspent") << " (key image " << req.key_images[n] << ")");
|
||||
}
|
||||
LOG_PRINT_L1("Total: " << print_money(spent) << " spent, " << print_money(unspent) << " unspent");
|
||||
|
||||
return m_transfers[signed_key_images.size() - 1].m_block_height;
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------
|
||||
void wallet2::generate_genesis(cryptonote::block& b) {
|
||||
if (m_testnet)
|
||||
|
|
|
@ -394,8 +394,10 @@ namespace tools
|
|||
std::string sign(const std::string &data) const;
|
||||
bool verify(const std::string &data, const cryptonote::account_public_address &address, const std::string &signature) const;
|
||||
|
||||
void update_pool_state();
|
||||
std::vector<std::pair<crypto::key_image, crypto::signature>> export_key_images() const;
|
||||
uint64_t import_key_images(const std::vector<std::pair<crypto::key_image, crypto::signature>> &signed_key_images, uint64_t &spent, uint64_t &unspent);
|
||||
|
||||
void update_pool_state();
|
||||
private:
|
||||
/*!
|
||||
* \brief Stores wallet information to wallet file.
|
||||
|
|
|
@ -995,5 +995,71 @@ namespace tools
|
|||
return true;
|
||||
}
|
||||
//------------------------------------------------------------------------------------------------------------------------------
|
||||
bool wallet_rpc_server::on_export_key_images(const wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES::request& req, wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES::response& res, epee::json_rpc::error& er)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::vector<std::pair<crypto::key_image, crypto::signature>> ski = m_wallet.export_key_images();
|
||||
res.signed_key_images.resize(ski.size());
|
||||
for (size_t n = 0; n < ski.size(); ++n)
|
||||
{
|
||||
res.signed_key_images[n].key_image = epee::string_tools::pod_to_hex(ski[n].first);
|
||||
res.signed_key_images[n].signature = epee::string_tools::pod_to_hex(ski[n].second);
|
||||
}
|
||||
}
|
||||
|
||||
catch (...)
|
||||
{
|
||||
er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
|
||||
er.message = "Failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
//------------------------------------------------------------------------------------------------------------------------------
|
||||
bool wallet_rpc_server::on_import_key_images(const wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES::request& req, wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES::response& res, epee::json_rpc::error& er)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::vector<std::pair<crypto::key_image, crypto::signature>> ski;
|
||||
ski.resize(req.signed_key_images.size());
|
||||
for (size_t n = 0; n < ski.size(); ++n)
|
||||
{
|
||||
cryptonote::blobdata bd;
|
||||
|
||||
if(!epee::string_tools::parse_hexstr_to_binbuff(req.signed_key_images[n].key_image, bd))
|
||||
{
|
||||
er.code = WALLET_RPC_ERROR_CODE_WRONG_KEY_IMAGE;
|
||||
er.message = "failed to parse key image";
|
||||
return false;
|
||||
}
|
||||
ski[n].first = *reinterpret_cast<const crypto::key_image*>(bd.data());
|
||||
|
||||
if(!epee::string_tools::parse_hexstr_to_binbuff(req.signed_key_images[n].signature, bd))
|
||||
{
|
||||
er.code = WALLET_RPC_ERROR_CODE_WRONG_SIGNATURE;
|
||||
er.message = "failed to parse signature";
|
||||
return false;
|
||||
}
|
||||
ski[n].second = *reinterpret_cast<const crypto::signature*>(bd.data());
|
||||
}
|
||||
uint64_t spent = 0, unspent = 0;
|
||||
uint64_t height = m_wallet.import_key_images(ski, spent, unspent);
|
||||
res.spent = spent;
|
||||
res.unspent = unspent;
|
||||
res.height = height;
|
||||
}
|
||||
|
||||
catch (...)
|
||||
{
|
||||
er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
|
||||
er.message = "Failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
//------------------------------------------------------------------------------------------------------------------------------
|
||||
}
|
||||
|
||||
|
|
|
@ -82,6 +82,8 @@ namespace tools
|
|||
MAP_JON_RPC_WE("get_transfers", on_get_transfers, wallet_rpc::COMMAND_RPC_GET_TRANSFERS)
|
||||
MAP_JON_RPC_WE("sign", on_sign, wallet_rpc::COMMAND_RPC_SIGN)
|
||||
MAP_JON_RPC_WE("verify", on_verify, wallet_rpc::COMMAND_RPC_VERIFY)
|
||||
MAP_JON_RPC_WE("export_key_images", on_export_key_images, wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES)
|
||||
MAP_JON_RPC_WE("import_key_images", on_import_key_images, wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES)
|
||||
END_JSON_RPC_MAP()
|
||||
END_URI_MAP2()
|
||||
|
||||
|
@ -107,6 +109,8 @@ namespace tools
|
|||
bool on_get_transfers(const wallet_rpc::COMMAND_RPC_GET_TRANSFERS::request& req, wallet_rpc::COMMAND_RPC_GET_TRANSFERS::response& res, epee::json_rpc::error& er);
|
||||
bool on_sign(const wallet_rpc::COMMAND_RPC_SIGN::request& req, wallet_rpc::COMMAND_RPC_SIGN::response& res, epee::json_rpc::error& er);
|
||||
bool on_verify(const wallet_rpc::COMMAND_RPC_VERIFY::request& req, wallet_rpc::COMMAND_RPC_VERIFY::response& res, epee::json_rpc::error& er);
|
||||
bool on_export_key_images(const wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES::request& req, wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES::response& res, epee::json_rpc::error& er);
|
||||
bool on_import_key_images(const wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES::request& req, wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES::response& res, epee::json_rpc::error& er);
|
||||
|
||||
bool handle_command_line(const boost::program_options::variables_map& vm);
|
||||
|
||||
|
|
|
@ -601,5 +601,70 @@ namespace wallet_rpc
|
|||
};
|
||||
};
|
||||
|
||||
struct COMMAND_RPC_EXPORT_KEY_IMAGES
|
||||
{
|
||||
struct request
|
||||
{
|
||||
BEGIN_KV_SERIALIZE_MAP()
|
||||
END_KV_SERIALIZE_MAP()
|
||||
};
|
||||
|
||||
struct signed_key_image
|
||||
{
|
||||
std::string key_image;
|
||||
std::string signature;
|
||||
|
||||
BEGIN_KV_SERIALIZE_MAP()
|
||||
KV_SERIALIZE(key_image);
|
||||
KV_SERIALIZE(signature);
|
||||
END_KV_SERIALIZE_MAP()
|
||||
};
|
||||
|
||||
struct response
|
||||
{
|
||||
std::vector<signed_key_image> signed_key_images;
|
||||
|
||||
BEGIN_KV_SERIALIZE_MAP()
|
||||
KV_SERIALIZE(signed_key_images);
|
||||
END_KV_SERIALIZE_MAP()
|
||||
};
|
||||
};
|
||||
|
||||
struct COMMAND_RPC_IMPORT_KEY_IMAGES
|
||||
{
|
||||
struct signed_key_image
|
||||
{
|
||||
std::string key_image;
|
||||
std::string signature;
|
||||
|
||||
BEGIN_KV_SERIALIZE_MAP()
|
||||
KV_SERIALIZE(key_image);
|
||||
KV_SERIALIZE(signature);
|
||||
END_KV_SERIALIZE_MAP()
|
||||
};
|
||||
|
||||
struct request
|
||||
{
|
||||
std::vector<signed_key_image> signed_key_images;
|
||||
|
||||
BEGIN_KV_SERIALIZE_MAP()
|
||||
KV_SERIALIZE(signed_key_images);
|
||||
END_KV_SERIALIZE_MAP()
|
||||
};
|
||||
|
||||
struct response
|
||||
{
|
||||
uint64_t height;
|
||||
uint64_t spent;
|
||||
uint64_t unspent;
|
||||
|
||||
BEGIN_KV_SERIALIZE_MAP()
|
||||
KV_SERIALIZE(height)
|
||||
KV_SERIALIZE(spent)
|
||||
KV_SERIALIZE(unspent)
|
||||
END_KV_SERIALIZE_MAP()
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,3 +40,4 @@
|
|||
#define WALLET_RPC_ERROR_CODE_DENIED -7
|
||||
#define WALLET_RPC_ERROR_CODE_WRONG_TXID -8
|
||||
#define WALLET_RPC_ERROR_CODE_WRONG_SIGNATURE -9
|
||||
#define WALLET_RPC_ERROR_CODE_WRONG_KEY_IMAGE -10
|
||||
|
|
Loading…
Reference in New Issue