mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-14 12:43:40 +02:00
Descriptor support in PAK infrastructure and tests
This commit is contained in:
parent
6366734477
commit
1cb252e01d
7 changed files with 310 additions and 86 deletions
|
|
@ -25,6 +25,8 @@
|
|||
|
||||
#include <univalue.h>
|
||||
|
||||
#include <script/descriptor.h> // getwalletpakinfo
|
||||
|
||||
|
||||
int64_t static DecodeDumpTime(const std::string &str) {
|
||||
static const boost::posix_time::ptime epoch = boost::posix_time::from_time_t(0);
|
||||
|
|
@ -1276,8 +1278,6 @@ UniValue importmulti(const JSONRPCRequest& mainRequest)
|
|||
return response;
|
||||
}
|
||||
|
||||
extern CTxDestination DeriveBitcoinOfflineAddress(const CExtPubKey& xpub, const uint32_t counter);
|
||||
|
||||
UniValue getwalletpakinfo(const JSONRPCRequest& request)
|
||||
{
|
||||
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
|
||||
|
|
@ -1293,8 +1293,8 @@ UniValue getwalletpakinfo(const JSONRPCRequest& request)
|
|||
"\nReturns relevant pegout authorization key (PAK) information about this wallet. Throws an error if initpegoutwallet` has not been invoked on this wallet.\n"
|
||||
"\nResult:\n"
|
||||
"{\n"
|
||||
"\"derivation_path\" (string) The next index to be used by the wallet for `sendtomainchain`.\n"
|
||||
"\"bitcoin_xpub\" (string) The Bitcoin xpubkey loaded in the wallet for pegouts.\n"
|
||||
"\"bip32_counter\" (string) The next index to be used by the wallet for `sendtomainchain`.\n"
|
||||
"\"bitcoin_descriptor\" (string) The Bitcoin script descriptor loaded in the wallet for pegouts.\n"
|
||||
"\"liquid_pak\" (string) Pubkey in hex corresponding to the Liquid PAK loaded in the wallet for pegouts.\n"
|
||||
"\"liquid_pak_address\" (string) The corresponding address for `liquid_pak`. Useful for `dumpprivkey` for wallet backup or transfer.\n"
|
||||
"\"address_lookahead\"(array) The three next Bitcoin addresses the wallet will use for `sendtomainchain` based on the internal counter.\n"
|
||||
|
|
@ -1310,18 +1310,28 @@ UniValue getwalletpakinfo(const JSONRPCRequest& request)
|
|||
UniValue ret(UniValue::VOBJ);
|
||||
std::stringstream ss;
|
||||
ss << pwallet->offline_counter;
|
||||
ret.push_back(Pair("derivation_path", "/0/"+ss.str()));
|
||||
ret.push_back(Pair("bip32_counter", ss.str()));
|
||||
|
||||
CExtPubKey& xpub = pwallet->offline_xpub;
|
||||
const std::string desc_str = pwallet->offline_desc;
|
||||
|
||||
ret.pushKV("bitcoin_xpub", EncodeExtPubKey(xpub));
|
||||
FlatSigningProvider provider;
|
||||
const auto& desc = Parse(desc_str, provider);
|
||||
|
||||
ret.pushKV("bitcoin_descriptor", desc_str);
|
||||
ret.pushKV("liquid_pak", HexStr(pwallet->online_key));
|
||||
ret.pushKV("liquid_pak_address", EncodeDestination((pwallet->online_key.GetID())));
|
||||
|
||||
UniValue address_list(UniValue::VARR);
|
||||
for (unsigned int i = 0; i < 3; i++) {
|
||||
address_list.push_back(EncodeParentDestination(DeriveBitcoinOfflineAddress(xpub, pwallet->offline_counter+i)));
|
||||
std::vector<CScript> scripts;
|
||||
if (!desc->Expand(i, provider, scripts, provider)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Could not generate lookahead addresses with descriptor. This is a bug.");
|
||||
}
|
||||
CTxDestination destination;
|
||||
ExtractDestination(scripts[0], destination);
|
||||
address_list.push_back(EncodeParentDestination(destination));
|
||||
}
|
||||
|
||||
ret.push_back(Pair("address_lookahead", address_list));
|
||||
return ret;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@
|
|||
#include <functional>
|
||||
|
||||
#include <script/generic.hpp> // signblock
|
||||
#include <script/descriptor.h> // initpegoutwallet
|
||||
#include <span.h> // sendtomainchain_pak
|
||||
|
||||
static const std::string WALLET_ENDPOINT_BASE = "/wallet/";
|
||||
|
||||
|
|
@ -4936,38 +4938,6 @@ bool DerivePubTweak(const std::vector<uint32_t>& vPath, const CPubKey& keyMaster
|
|||
return true;
|
||||
}
|
||||
|
||||
CTxDestination DeriveBitcoinOfflineAddress(const CExtPubKey& xpub, const uint32_t counter)
|
||||
{
|
||||
std::vector<uint32_t> vPath;
|
||||
vPath.push_back(0);
|
||||
vPath.push_back(counter);
|
||||
|
||||
std::vector<unsigned char> tweakSum;
|
||||
if (!DerivePubTweak(vPath, xpub.pubkey, xpub.chaincode, tweakSum)) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Could not derive the pubkey tweak for given counter and xpub.");
|
||||
}
|
||||
|
||||
secp256k1_pubkey masterpub_secp;
|
||||
int ret = secp256k1_ec_pubkey_parse(secp256k1_ctx, &masterpub_secp, xpub.pubkey.begin(), xpub.pubkey.size());
|
||||
if (ret != 1) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Master pubkey could not be parsed.");
|
||||
}
|
||||
|
||||
ret = secp256k1_ec_pubkey_tweak_add(secp256k1_ctx, &masterpub_secp, tweakSum.data());
|
||||
assert(ret);
|
||||
|
||||
std::vector<unsigned char> btcpubkeybytes;
|
||||
btcpubkeybytes.resize(33);
|
||||
size_t btclen = 33;
|
||||
ret = secp256k1_ec_pubkey_serialize(secp256k1_ctx, &btcpubkeybytes[0], &btclen, &masterpub_secp, SECP256K1_EC_COMPRESSED);
|
||||
assert(ret == 1);
|
||||
assert(btclen == 33);
|
||||
assert(btcpubkeybytes.size() == 33);
|
||||
|
||||
CPubKey btcpub(btcpubkeybytes);
|
||||
return CTxDestination(btcpub.GetID());
|
||||
}
|
||||
|
||||
UniValue initpegoutwallet(const JSONRPCRequest& request)
|
||||
{
|
||||
|
||||
|
|
@ -4979,11 +4949,11 @@ UniValue initpegoutwallet(const JSONRPCRequest& request)
|
|||
|
||||
if (request.fHelp || request.params.size() < 1 || request.params.size() > 3)
|
||||
throw std::runtime_error(
|
||||
"initpegoutwallet bitcoin_xpub ( bip32_counter liquid_pak )\n"
|
||||
"\nThis call is for Liquid network initialization on the Liquid wallet. The wallet generates a new Liquid pegout authorization key (PAK) and stores it in the Liquid wallet. It then combines this with the `bitcoin_xpub` to finally create a PAK entry for the network. This allows the user to send Liquid coins directly to a secure offline Bitcoin wallet at the `/0/k` non-hardened derivation path from the bitcoin_xpub using the `sendtomainchain` command. Losing the Liquid PAK or offline Bitcoin root key will result in the inability to pegout funds, so immediate backup upon initialization is required.\n"
|
||||
"initpegoutwallet bitcoin_descriptor ( bip32_counter liquid_pak )\n"
|
||||
"\nThis call is for Liquid network initialization on the Liquid wallet. The wallet generates a new Liquid pegout authorization key (PAK) and stores it in the Liquid wallet. It then combines this with the `bitcoin_descriptor` to finally create a PAK entry for the network. This allows the user to send Liquid coins directly to a secure offline Bitcoin wallet at the derived path from the bitcoin_descriptor using the `sendtomainchain` command. Losing the Liquid PAK or offline Bitcoin root key will result in the inability to pegout funds, so immediate backup upon initialization is required.\n"
|
||||
"\nArguments:\n"
|
||||
"1. \"bitcoin_xpub\" (string, required) The Bitcoin extended pubkey to be used as the root for the Bitcoin destination wallet. The derivation path from this key will be `0/k`.\n"
|
||||
"2. \"bip32_counter\" (numeric, default=0) The `k` in `0/k` to be set as the next address to derive from the `bitcoin_xpub`. This will be stored in the wallet and incremented on each successful `sendtomainchain` invocation.\n"
|
||||
"1. \"bitcoin_descriptor\" (string, required) The Bitcoin descriptor that includes a single extended pubkey. Must be one of the following: pkh(<xpub>), sh(wpkh(<xpub>)), or wpkh(<xpub>). This is used as the root for the Bitcoin destination wallet. The derivation path from the xpub will be `0/k`, reflecting the external chain of the wallet. DEPRECATED: If a plain xpub is given, pkh(<xpub>) is assumed. See link for more details on script descriptors: https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md\n"
|
||||
"2. \"bip32_counter\" (numeric, default=0) The `k` in `0/k` to be set as the next address to derive from the `bitcoin_descriptor`. This will be stored in the wallet and incremented on each successful `sendtomainchain` invocation.\n"
|
||||
"3. \"liquid_pak\" (string, optional) The Liquid wallet pubkey in hex to be used as the Liquid PAK for pegout authorization. The private key must be in the wallet if argument is given. If this argument is not provided one will be generated and stored in the wallet automatically and returned.\n"
|
||||
+ HelpRequiringPassphrase(pwallet) +
|
||||
"\nResult:\n"
|
||||
|
|
@ -4993,8 +4963,8 @@ UniValue initpegoutwallet(const JSONRPCRequest& request)
|
|||
"\"liquid_pak_address\" (string) The corresponding address for `liquid_pak`. Useful for `dumpprivkey` for wallet backup or transfer.\n"
|
||||
"\"address_lookahead\"(array) The three next Bitcoin addresses the wallet will use for `sendtomainchain` based on `bip32_counter`.\n"
|
||||
"}\n"
|
||||
+ HelpExampleCli("initpegoutwallet", "tpubDAY5hwtonH4NE8zY46ZMFf6B6F3fqMis7cwfNihXXpAg6XzBZNoHAdAzAZx2peoU8nTWFqvUncXwJ9qgE5VxcnUKxdut8F6mptVmKjfiwDQ")
|
||||
+ HelpExampleRpc("initpegoutwallet", "tpubDAY5hwtonH4NE8zY46ZMFf6B6F3fqMis7cwfNihXXpAg6XzBZNoHAdAzAZx2peoU8nTWFqvUncXwJ9qgE5VxcnUKxdut8F6mptVmKjfiwDQ")
|
||||
+ HelpExampleCli("initpegoutwallet", "sh(wpkh(tpubDAY5hwtonH4NE8zY46ZMFf6B6F3fqMis7cwfNihXXpAg6XzBZNoHAdAzAZx2peoU8nTWFqvUncXwJ9qgE5VxcnUKxdut8F6mptVmKjfiwDQ/0/*))")
|
||||
+ HelpExampleRpc("initpegoutwallet", "sh(wpkh(tpubDAY5hwtonH4NE8zY46ZMFf6B6F3fqMis7cwfNihXXpAg6XzBZNoHAdAzAZx2peoU8nTWFqvUncXwJ9qgE5VxcnUKxdut8F6mptVmKjfiwDQ/0/*))")
|
||||
);
|
||||
|
||||
LOCK2(cs_main, pwallet->cs_wallet);
|
||||
|
|
@ -5037,11 +5007,49 @@ UniValue initpegoutwallet(const JSONRPCRequest& request)
|
|||
}
|
||||
}
|
||||
|
||||
//offline_xpub
|
||||
CExtPubKey xpub = DecodeExtPubKey(request.params[0].get_str());
|
||||
std::string bitcoin_desc = request.params[0].get_str();
|
||||
std::string xpub_str = "";
|
||||
|
||||
// First check for naked xpub, and impute it as pkh(<xpub>/0/*) for backwards compat
|
||||
CExtPubKey xpub = DecodeExtPubKey(bitcoin_desc);
|
||||
if (xpub.pubkey.IsFullyValid()) {
|
||||
bitcoin_desc = "pkh(" + bitcoin_desc + "/0/*)";
|
||||
}
|
||||
|
||||
FlatSigningProvider provider;
|
||||
auto desc = Parse(bitcoin_desc, provider);
|
||||
if (!desc) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "bitcoin_descriptor is not a valid descriptor string.");
|
||||
} else if (!desc->IsRange()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "bitcoin_descriptor must be a ranged descriptor.");
|
||||
}
|
||||
|
||||
// Three acceptable descriptors:
|
||||
if (bitcoin_desc.substr(0, 8) == "sh(wpkh("
|
||||
&& bitcoin_desc.substr(bitcoin_desc.size()-2, 2) == "))") {
|
||||
xpub_str = bitcoin_desc.substr(8, bitcoin_desc.size()-2);
|
||||
} else if (bitcoin_desc.substr(0, 5) == "wpkh("
|
||||
&& bitcoin_desc.substr(bitcoin_desc.size()-1, 1) == ")") {
|
||||
xpub_str = bitcoin_desc.substr(5, bitcoin_desc.size()-1);
|
||||
} else if (bitcoin_desc.substr(0, 4) == "pkh("
|
||||
&& bitcoin_desc.substr(bitcoin_desc.size()-1, 1) == ")") {
|
||||
xpub_str = bitcoin_desc.substr(4, bitcoin_desc.size()-1);
|
||||
} else {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "bitcoin_descriptor is not of any type supported: pkh(<xpub>), sh(wpkh(<xpub>)), wpkh(<xpub>), or <xpub>.");
|
||||
}
|
||||
|
||||
// Strip off leading key origin
|
||||
if (xpub_str.find("]") != std::string::npos) {
|
||||
xpub_str = xpub_str.substr(xpub_str.find("]"), std::string::npos);
|
||||
}
|
||||
|
||||
// Strip off following range
|
||||
xpub_str = xpub_str.substr(0, xpub_str.find("/"));
|
||||
|
||||
xpub = DecodeExtPubKey(xpub_str);
|
||||
|
||||
if (!xpub.pubkey.IsFullyValid()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "bitcoin_xpub is invalid for this network.");
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "bitcoin_descriptor does not contain a valid extended pubkey for this network.");
|
||||
}
|
||||
|
||||
// Parse master pubkey
|
||||
|
|
@ -5049,13 +5057,14 @@ UniValue initpegoutwallet(const JSONRPCRequest& request)
|
|||
secp256k1_pubkey masterpub_secp;
|
||||
int ret = secp256k1_ec_pubkey_parse(secp256k1_ctx, &masterpub_secp, masterpub.begin(), masterpub.size());
|
||||
if (ret != 1) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "bitcoin_xpub could not be parsed.");
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "bitcoin_descriptor could not be parsed.");
|
||||
}
|
||||
|
||||
// Store the keys and metadata
|
||||
if (!pwallet->SetOnlinePubKey(online_pubkey) ||
|
||||
!pwallet->SetOfflineXPubKey(xpub) ||
|
||||
!pwallet->SetOfflineCounter(counter)) {
|
||||
!pwallet->SetOfflineCounter(counter) ||
|
||||
!pwallet->SetOfflineDescriptor(bitcoin_desc)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error: Failure to initialize pegout wallet.");
|
||||
}
|
||||
|
||||
|
|
@ -5071,8 +5080,14 @@ UniValue initpegoutwallet(const JSONRPCRequest& request)
|
|||
assert(negatedpubkeybytes.size() == 33);
|
||||
|
||||
UniValue address_list(UniValue::VARR);
|
||||
for (unsigned int i = 0; i < 3; i++) {
|
||||
address_list.push_back(EncodeParentDestination(DeriveBitcoinOfflineAddress(xpub, counter+i)));
|
||||
for (int i = counter; i < counter+3; i++) {
|
||||
std::vector<CScript> scripts;
|
||||
if (!desc->Expand(i, provider, scripts, provider)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Could not generate lookahead addresses with descriptor. This is a bug.");
|
||||
}
|
||||
CTxDestination destination;
|
||||
ExtractDestination(scripts[0], destination);
|
||||
address_list.push_back(EncodeParentDestination(destination));
|
||||
}
|
||||
UniValue pak(UniValue::VOBJ);
|
||||
pak.push_back(Pair("pakentry", "pak=" + HexStr(negatedpubkeybytes) + ":" + HexStr(online_pubkey)));
|
||||
|
|
@ -5124,14 +5139,14 @@ UniValue sendtomainchain_base(const JSONRPCRequest& request)
|
|||
}
|
||||
|
||||
// Parse Bitcoin address for destination, embed script
|
||||
CScript scriptPubKeyMainchain(GetScriptForDestination(parent_address));
|
||||
CScript mainchain_script(GetScriptForDestination(parent_address));
|
||||
|
||||
uint256 genesisBlockHash = Params().ParentGenesisBlockHash();
|
||||
|
||||
// Asset type is implicit, no need to add to script
|
||||
NullData nulldata;
|
||||
nulldata << std::vector<unsigned char>(genesisBlockHash.begin(), genesisBlockHash.end());
|
||||
nulldata << std::vector<unsigned char>(scriptPubKeyMainchain.begin(), scriptPubKeyMainchain.end());
|
||||
nulldata << std::vector<unsigned char>(mainchain_script.begin(), mainchain_script.end());
|
||||
CTxDestination address(nulldata);
|
||||
|
||||
EnsureWalletIsUnlocked(pwallet);
|
||||
|
|
@ -5148,6 +5163,46 @@ UniValue sendtomainchain_base(const JSONRPCRequest& request)
|
|||
|
||||
}
|
||||
|
||||
// ELEMENTS: Copied from script/descriptor.cpp
|
||||
|
||||
typedef std::vector<uint32_t> KeyPath;
|
||||
|
||||
/** Split a string on every instance of sep, returning a vector. */
|
||||
std::vector<Span<const char>> Split(const Span<const char>& sp, char sep)
|
||||
{
|
||||
std::vector<Span<const char>> ret;
|
||||
auto it = sp.begin();
|
||||
auto start = it;
|
||||
while (it != sp.end()) {
|
||||
if (*it == sep) {
|
||||
ret.emplace_back(start, it);
|
||||
start = it + 1;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
ret.emplace_back(start, it);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/** Parse a key path, being passed a split list of elements (the first element is ignored). */
|
||||
bool ParseKeyPath(const std::vector<Span<const char>>& split, KeyPath& out)
|
||||
{
|
||||
for (size_t i = 1; i < split.size(); ++i) {
|
||||
Span<const char> elem = split[i];
|
||||
bool hardened = false;
|
||||
if (elem.size() > 0 && (elem[elem.size() - 1] == '\'' || elem[elem.size() - 1] == 'h')) {
|
||||
elem = elem.first(elem.size() - 1);
|
||||
hardened = true;
|
||||
}
|
||||
uint32_t p;
|
||||
if (!ParseUInt32(std::string(elem.begin(), elem.end()), &p) || p > 0x7FFFFFFFUL) return false;
|
||||
out.push_back(p | (((uint32_t)hardened) << 31));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
|
||||
UniValue sendtomainchain_pak(const JSONRPCRequest& request)
|
||||
{
|
||||
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
|
||||
|
|
@ -5159,7 +5214,7 @@ UniValue sendtomainchain_pak(const JSONRPCRequest& request)
|
|||
if (request.fHelp || request.params.size() < 2 || request.params.size() > 3)
|
||||
throw std::runtime_error(
|
||||
"sendtomainchain "" amount ( subtractfeefromamount ) \n"
|
||||
"\nSends Liquid funds to the Bitcoin mainchain, through the federated withdraw mechanism. The wallet internally generates the returned `bitcoin_address` via `bitcoin_xpub` and `bip32_counter` previously set in `initpegoutwallet`. The counter will be incremented upon successful send, avoiding address re-use.\n"
|
||||
"\nSends Liquid funds to the Bitcoin mainchain, through the federated withdraw mechanism. The wallet internally generates the returned `bitcoin_address` via `bitcoin_descriptor` and `bip32_counter` previously set in `initpegoutwallet`. The counter will be incremented upon successful send, avoiding address re-use.\n"
|
||||
+ HelpRequiringPassphrase(pwallet) +
|
||||
"\nArguments:\n"
|
||||
"1. \"address\" (string, required) Must be \"\". Only for non-PAK `sendtomainchain` compatibility.\n"
|
||||
|
|
@ -5170,8 +5225,8 @@ UniValue sendtomainchain_pak(const JSONRPCRequest& request)
|
|||
"{\n"
|
||||
"\"bitcoin_address\" (string) The destination address on Bitcoin mainchain."
|
||||
"\"txid\" (string) Transaction ID of the resulting Liquid transaction\n"
|
||||
"\"bitcoin_xpub\" (string) The xpubkey of the child destination address.\n"
|
||||
"\"derivation_path\" (string) The derivation path in text that leads to `bitcoin_address` from the `bitcoin_xpub`.\n"
|
||||
"\"bitcoin_descriptor\" (string) The xpubkey of the child destination address.\n"
|
||||
"\"bip32_counter\" (string) The derivation counter for the `bitcoin_descriptor`.\n"
|
||||
"}\n"
|
||||
"\nExamples:\n"
|
||||
+ HelpExampleCli("sendtomainchain", "\"\" 0.1")
|
||||
|
|
@ -5213,9 +5268,34 @@ UniValue sendtomainchain_pak(const JSONRPCRequest& request)
|
|||
throw JSONRPCError(RPC_WALLET_ERROR, "Pegout authorization for this wallet has not been set. Please call `initpegoutwallet` with the appropriate arguments first.");
|
||||
}
|
||||
|
||||
std::vector<uint32_t> vPath;
|
||||
vPath.push_back(0);
|
||||
vPath.push_back((uint32_t)counter);
|
||||
FlatSigningProvider provider;
|
||||
const auto descriptor = Parse(pwallet->offline_desc, provider);
|
||||
|
||||
// If descriptor not previously set, generate it
|
||||
if (!descriptor) {
|
||||
std::string offline_desc = "pkh(" + EncodeExtPubKey(xpub) + "0/*)";
|
||||
if (!pwallet->SetOfflineDescriptor(offline_desc)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Couldn't set wallet descriptor for peg-outs.");
|
||||
}
|
||||
}
|
||||
|
||||
std::string desc_str = pwallet->offline_desc;
|
||||
std::string xpub_str = EncodeExtPubKey(xpub);
|
||||
|
||||
// TODO: More properly expose key parsing functionality
|
||||
|
||||
// Strip last parenths(up to 2) and "/*" to let ParseKeyPath do its thing
|
||||
desc_str.erase(std::remove(desc_str.begin(), desc_str.end(), ')'), desc_str.end());
|
||||
desc_str = desc_str.substr(0, desc_str.size()-2);
|
||||
|
||||
// Since we know there are no key origin data, directly call inner parsing functions
|
||||
Span<const char> span(desc_str.data(), desc_str.size());
|
||||
auto split = Split(span, '/');
|
||||
KeyPath key_path;
|
||||
if (!ParseKeyPath(split, key_path)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Stored keypath in descriptor cannot be parsed.");
|
||||
}
|
||||
key_path.push_back(counter);
|
||||
|
||||
secp256k1_pubkey onlinepubkey_secp;
|
||||
if (secp256k1_ec_pubkey_parse(secp256k1_ctx, &onlinepubkey_secp, onlinepubkey.begin(), onlinepubkey.size()) != 1) {
|
||||
|
|
@ -5249,7 +5329,7 @@ UniValue sendtomainchain_pak(const JSONRPCRequest& request)
|
|||
|
||||
// Make sure negated master pubkey is in PAK list at same index as online_pubkey
|
||||
if (memcmp((void *)&paklist.OfflineKeys()[whitelistindex], (void *)&masterpub_secp, sizeof(secp256k1_pubkey)) != 0) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Given bitcoin_xpub cannot be found in same entry as known liquid_pak");
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Given bitcoin_descriptor cannot be found in same entry as known liquid_pak");
|
||||
}
|
||||
|
||||
// Get online PAK
|
||||
|
|
@ -5259,7 +5339,7 @@ UniValue sendtomainchain_pak(const JSONRPCRequest& request)
|
|||
|
||||
// Tweak offline pubkey by tweakSum aka sumkey to get bitcoin key
|
||||
std::vector<unsigned char> tweakSum;
|
||||
if (!DerivePubTweak(vPath, xpub.pubkey, xpub.chaincode, tweakSum)) {
|
||||
if (!DerivePubTweak(key_path, xpub.pubkey, xpub.chaincode, tweakSum)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Could not create xpub tweak to generate proof.");
|
||||
}
|
||||
ret = secp256k1_ec_pubkey_tweak_add(secp256k1_ctx, &btcpub_secp, tweakSum.data());
|
||||
|
|
@ -5292,14 +5372,20 @@ UniValue sendtomainchain_pak(const JSONRPCRequest& request)
|
|||
assert(outlen == expectedOutputSize);
|
||||
std::vector<unsigned char> whitelistproof(output, output + expectedOutputSize / sizeof(unsigned char));
|
||||
|
||||
// Bitcoin address
|
||||
CTxDestination bitcoin_address = DeriveBitcoinOfflineAddress(xpub, counter);
|
||||
CScript scriptPubKeyMainchain(GetScriptForDestination(bitcoin_address));
|
||||
// Derive the end address in mainchain
|
||||
std::vector<CScript> scripts;
|
||||
if (!descriptor->Expand(counter, provider, scripts, provider)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Could not generate mainchain destination with descriptor. This is a bug.");
|
||||
}
|
||||
assert(scripts.size() == 1);
|
||||
CScript mainchain_script = scripts[0];
|
||||
CTxDestination bitcoin_address;
|
||||
ExtractDestination(mainchain_script, bitcoin_address);
|
||||
|
||||
uint256 genesisBlockHash = Params().ParentGenesisBlockHash();
|
||||
NullData nulldata;
|
||||
nulldata << std::vector<unsigned char>(genesisBlockHash.begin(), genesisBlockHash.end());
|
||||
nulldata << std::vector<unsigned char>(scriptPubKeyMainchain.begin(), scriptPubKeyMainchain.end());
|
||||
nulldata << std::vector<unsigned char>(mainchain_script.begin(), mainchain_script.end());
|
||||
nulldata << btcpubkeybytes;
|
||||
nulldata << whitelistproof;
|
||||
CTxDestination address(nulldata);
|
||||
|
|
@ -5321,9 +5407,9 @@ UniValue sendtomainchain_pak(const JSONRPCRequest& request)
|
|||
|
||||
UniValue obj(UniValue::VOBJ);
|
||||
obj.push_back(Pair("txid", tx->GetHash().GetHex()));
|
||||
obj.push_back(Pair("bitcoin_address", EncodeDestination(bitcoin_address)));
|
||||
obj.push_back(Pair("derivation_path", "/0/"+ss.str()));
|
||||
obj.push_back(Pair("bitcoin_xpub", EncodeExtPubKey(xpub)));
|
||||
obj.push_back(Pair("bitcoin_address", EncodeParentDestination(bitcoin_address)));
|
||||
obj.push_back(Pair("bip32_counter", ss.str()));
|
||||
obj.push_back(Pair("bitcoin_descriptor", pwallet->offline_desc));
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
|
@ -5708,7 +5794,7 @@ static const CRPCCommand commands[] =
|
|||
{ "wallet", "claimpegin", &claimpegin, {"bitcoin_tx", "txoutproof", "claim_script"} },
|
||||
{ "wallet", "createrawpegin", &createrawpegin, {"bitcoin_tx", "txoutproof", "claim_script"} },
|
||||
{ "wallet", "sendtomainchain", &sendtomainchain, {"address", "amount", "subtractfeefromamount"} },
|
||||
{ "wallet", "initpegoutwallet", &initpegoutwallet, {"bitcoin_xpub", "bip32_counter", "liquid_pak"} },
|
||||
{ "wallet", "initpegoutwallet", &initpegoutwallet, {"bitcoin_descriptor", "bip32_counter", "liquid_pak"} },
|
||||
{ "wallet", "getwalletpakinfo", &getwalletpakinfo, {} },
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4492,6 +4492,16 @@ bool CWallet::SetOfflineXPubKey(const CExtPubKey& offline_xpub_in)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool CWallet::SetOfflineDescriptor(const std::string& offline_desc_in)
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
if (!WalletBatch(*database).WriteOfflineDescriptor(offline_desc_in)) {
|
||||
return false;
|
||||
}
|
||||
offline_desc = offline_desc_in;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CWallet::SetOfflineCounter(int counter) {
|
||||
LOCK(cs_wallet);
|
||||
if (!WalletBatch(*database).WriteOfflineCounter(counter)) {
|
||||
|
|
|
|||
|
|
@ -836,11 +836,16 @@ public:
|
|||
//! The online PAK aka `liquid_pak` in the wallet set by `initpegoutwallet`
|
||||
CPubKey online_key;
|
||||
|
||||
//! The offline xpub aka `bitcoin_xpub` in the wallet set by `initpegoutwallet`
|
||||
CExtPubKey offline_xpub;
|
||||
|
||||
//! The derivation counter for offline_xpub
|
||||
int offline_counter = -1;
|
||||
|
||||
//! The offline descriptor aka `bitcoind_descriptor` set by `initpegoutwallet`
|
||||
std::string offline_desc;
|
||||
|
||||
//The offline xpub aka `bitcoin_xpub` in the wallet set by `initpegoutwallet`
|
||||
CExtPubKey offline_xpub;
|
||||
|
||||
// END ELEMENTS
|
||||
|
||||
const CWalletTx* GetWalletTx(const uint256& hash) const;
|
||||
|
|
@ -1225,9 +1230,9 @@ public:
|
|||
// ELEMENTS
|
||||
//! Setters for online/offline pubkey pairs for PAK
|
||||
bool SetOnlinePubKey(const CPubKey& online_key_in);
|
||||
bool SetOfflineXPubKey(const CExtPubKey& offline_xpub_in);
|
||||
bool SetOfflineCounter(int counter);
|
||||
|
||||
bool SetOfflineDescriptor(const std::string& offline_desc_in);
|
||||
bool SetOfflineXPubKey(const CExtPubKey& offline_xpub_in);
|
||||
};
|
||||
|
||||
/** A key allocated from the key pool. */
|
||||
|
|
|
|||
|
|
@ -184,6 +184,11 @@ bool WalletBatch::WriteOfflineXPubKey(const CExtPubKey& offline_xpub)
|
|||
return WriteIC(std::string("offlinexpub"), vxpub);
|
||||
}
|
||||
|
||||
bool WalletBatch::WriteOfflineDescriptor(const std::string& offline_desc)
|
||||
{
|
||||
return WriteIC(std::string("offlinedesc"), offline_desc);
|
||||
}
|
||||
|
||||
bool WalletBatch::WriteOfflineCounter(int counter)
|
||||
{
|
||||
return WriteIC(std::string("offlinecounter"), counter);
|
||||
|
|
@ -547,6 +552,12 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
|||
int counter;
|
||||
ssValue >> counter;
|
||||
pwallet->offline_counter = counter;
|
||||
}
|
||||
else if (strType == "offlinedesc")
|
||||
{
|
||||
std::string descriptor;
|
||||
ssValue >> descriptor;
|
||||
pwallet->offline_desc = descriptor;
|
||||
} else if (strType != "bestblock" && strType != "bestblock_nomerkle") {
|
||||
wss.m_unknown_records++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -216,8 +216,10 @@ public:
|
|||
|
||||
/// ELEMENTS: Storage of PAK settings
|
||||
bool WriteOnlineKey(const CPubKey& online_key);
|
||||
bool WriteOfflineXPubKey(const CExtPubKey& offline_xpub);
|
||||
bool WriteOfflineCounter(int counter);
|
||||
bool WriteOfflineDescriptor(const std::string& offline_desc);
|
||||
// DEPRECATED
|
||||
bool WriteOfflineXPubKey(const CExtPubKey& offline_xpub);
|
||||
|
||||
DBErrors LoadWallet(CWallet* pwallet);
|
||||
DBErrors FindWalletTx(std::vector<uint256>& vTxHash, std::vector<CWalletTx>& vWtx);
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ pak2 = [("03767a74373b7207c5ae1214295197a88ec2abdf92e9e2a29daf024c322fae9fcb", "
|
|||
("02f4a7445f9c48ee8590a930d3fc4f0f5763e3d1d003fdf5fc822e7ba18f380632", "036b3786f029751ada9f02f519a86c7e02fb2963a7013e7e668eb5f7ec069b9e7e")]
|
||||
|
||||
# Args that will be re-used in slightly different ways across runs
|
||||
args = [["-acceptnonstdtxn=0", "-initialfreecoins=100000000"]] \
|
||||
+ [["-acceptnonstdtxn=0", "-enforce_pak=1", "-initialfreecoins=100000000"]]*4
|
||||
# TODO remove lol once parent chain hrp default is changed
|
||||
args = [["-acceptnonstdtxn=0", "-initialfreecoins=100000000", "-parent_bech32_hrp=lol"]] \
|
||||
+ [["-acceptnonstdtxn=0", "-enforce_pak=1", "-initialfreecoins=100000000", "-parent_bech32_hrp=lol"]]*4
|
||||
args[i_reject] = args[i_reject] + ['-pak=reject']
|
||||
# Novalidate has pak entry, should not act on it ever
|
||||
args[i_novalidate] = args[i_novalidate] + pak_to_option(pak1)
|
||||
|
|
@ -199,6 +200,7 @@ class PAKTest (BitcoinTestFramework):
|
|||
# We will re-use the same xpub, but each wallet will create its own online pak
|
||||
# so the lists will be incompatible, even if all else was synced
|
||||
xpub = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
|
||||
xpub_desc = "pkh("+xpub+"/0/*)" # Transform this into a descriptor
|
||||
init_results = []
|
||||
info_results = []
|
||||
for i in range(5):
|
||||
|
|
@ -213,8 +215,8 @@ class PAKTest (BitcoinTestFramework):
|
|||
assert_equal(init_results[i]["address_lookahead"], info_results[i]["address_lookahead"])
|
||||
assert_equal(init_results[i]["liquid_pak"], info_results[i]["liquid_pak"])
|
||||
assert_equal(init_results[i]["liquid_pak_address"], info_results[i]["liquid_pak_address"])
|
||||
assert_equal(info_results[i]["bitcoin_xpub"], xpub)
|
||||
assert_equal(info_results[i]["derivation_path"], "/0/0")
|
||||
assert_equal(info_results[i]["bitcoin_descriptor"], xpub_desc)
|
||||
assert_equal(info_results[i]["bip32_counter"], "0")
|
||||
|
||||
# Use custom derivation counter values, check if stored correctly,
|
||||
# address lookahead looks correct and that new liquid_pak was chosen
|
||||
|
|
@ -224,7 +226,7 @@ class PAKTest (BitcoinTestFramework):
|
|||
assert_raises_rpc_error(-8, "bip32_counter must be between 0 and 1,000,000,000, inclusive.", self.nodes[i_undefined].initpegoutwallet, xpub, 1000000001)
|
||||
|
||||
new_init = self.nodes[i_undefined].initpegoutwallet(xpub, 2)
|
||||
assert_equal(self.nodes[i_undefined].getwalletpakinfo()["derivation_path"], "/0/2")
|
||||
assert_equal(self.nodes[i_undefined].getwalletpakinfo()["bip32_counter"], "2")
|
||||
assert_equal(new_init["address_lookahead"][0], init_results[i_undefined]["address_lookahead"][2])
|
||||
assert(new_init["liquid_pak"] != init_results[i_undefined]["liquid_pak"])
|
||||
|
||||
|
|
@ -244,9 +246,9 @@ class PAKTest (BitcoinTestFramework):
|
|||
|
||||
# Check PAK settings persistance in wallet across restart
|
||||
restarted_info = self.nodes[i_undefined].getwalletpakinfo()
|
||||
assert_equal(restarted_info["bitcoin_xpub"], xpub)
|
||||
assert_equal(restarted_info["bitcoin_descriptor"], xpub_desc)
|
||||
assert_equal(restarted_info["liquid_pak"], new_init["liquid_pak"])
|
||||
assert_equal(restarted_info["derivation_path"], "/0/2")
|
||||
assert_equal(restarted_info["bip32_counter"], "2")
|
||||
|
||||
# Have nodes send pegouts, check it fails to enter mempool of other nodes with incompatible
|
||||
# PAK settings
|
||||
|
|
@ -260,10 +262,12 @@ class PAKTest (BitcoinTestFramework):
|
|||
|
||||
# pak1 will now create a pegout.
|
||||
pak1_pegout_txid = self.nodes[i_pak1].sendtomainchain("", 1)["txid"]
|
||||
assert_equal(self.nodes[i_pak1].getwalletpakinfo()["derivation_path"], "/0/1")
|
||||
assert_equal(self.nodes[i_pak1].getwalletpakinfo()["bip32_counter"], "1")
|
||||
# Also spend the change to make chained payment that will be rejected as well
|
||||
pak1_child_txid = self.nodes[i_pak1].sendtoaddress(self.nodes[i_pak1].getnewaddress(), self.nodes[i_pak1].getbalance(), "", "", True)
|
||||
|
||||
|
||||
# Wait for two nodes to get transaction in mempool only
|
||||
# Wait for node("follow the leader" conf-undefined) to get transaction in
|
||||
time_to_wait = 15
|
||||
while time_to_wait > 0:
|
||||
# novalidate doesn't allow >80 byte op_return outputs due to no enforce_pak
|
||||
|
|
@ -283,15 +287,111 @@ class PAKTest (BitcoinTestFramework):
|
|||
|
||||
assert_equal(pak1_pegout_txid in self.nodes[i_novalidate].getrawmempool(), False)
|
||||
assert_equal(pak1_pegout_txid in self.nodes[i_undefined].getrawmempool(), False)
|
||||
assert_equal(pak1_pegout_txid in self.nodes[i_pak1].getrawmempool(), True)
|
||||
assert_equal(pak1_pegout_txid in self.nodes[i_pak2].getrawmempool(), False)
|
||||
assert_equal(pak1_pegout_txid in self.nodes[i_reject].getrawmempool(), False)
|
||||
|
||||
assert_equal(self.nodes[i_pak1].gettransaction(pak1_pegout_txid)["confirmations"], 0)
|
||||
|
||||
# Make sure child payment also bumped from mempool
|
||||
assert_equal(pak1_child_txid in self.nodes[i_novalidate].getrawmempool(), False)
|
||||
assert_equal(pak1_child_txid in self.nodes[i_undefined].getrawmempool(), False)
|
||||
assert_equal(pak1_child_txid in self.nodes[i_pak1].getrawmempool(), True)
|
||||
assert_equal(pak1_child_txid in self.nodes[i_pak2].getrawmempool(), False)
|
||||
assert_equal(pak1_child_txid in self.nodes[i_reject].getrawmempool(), False)
|
||||
|
||||
assert_equal(self.nodes[i_pak1].gettransaction(pak1_child_txid)["confirmations"], 0)
|
||||
# Fail to peg-out too-small value
|
||||
assert_raises_rpc_error(-8, "Invalid amount for send, must send more than 0.0001 BTC", self.nodes[i_undefined].sendtomainchain, "", Decimal('0.0009'))
|
||||
|
||||
# Use wrong network's extended pubkey
|
||||
mainnetxpub = "xpub6AATBi58516uxLogbuaG3jkom7x1qyDoZzMN2AePBuQnMFKUV9xC2BW9vXsFJ9rELsvbeGQcFWhtbyM4qDeijM22u3AaSiSYEvuMZkJqtLn"
|
||||
assert_raises_rpc_error(-8, "bitcoin_xpub is invalid for this network", self.nodes[i_undefined].initpegoutwallet, mainnetxpub)
|
||||
assert_raises_rpc_error(-8, "bitcoin_descriptor is not a valid descriptor string.", self.nodes[i_undefined].initpegoutwallet, mainnetxpub)
|
||||
|
||||
# Test fixed online pubkey
|
||||
init_info = self.nodes[i_pak1].initpegoutwallet(xpub)
|
||||
init_info2 = self.nodes[i_pak1].initpegoutwallet(xpub, 0, init_info['liquid_pak'])
|
||||
assert_equal(init_info, init_info2)
|
||||
init_info3 = self.nodes[i_pak1].initpegoutwallet(xpub)
|
||||
assert(init_info != init_info3)
|
||||
|
||||
# Test Descriptor PAK Support
|
||||
|
||||
# Non-supported descriptors
|
||||
assert_raises_rpc_error(-8, "bitcoin_descriptor is not of any type supported: pkh(<xpub>), sh(wpkh(<xpub>)), wpkh(<xpub>), or <xpub>.", self.nodes[i_pak1].initpegoutwallet, "pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)")
|
||||
|
||||
assert_raises_rpc_error(-8, "bitcoin_descriptor must be a ranged descriptor.", self.nodes[i_pak1].initpegoutwallet, "pkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)")
|
||||
|
||||
# key origins aren't supported in 0.17
|
||||
assert_raises_rpc_error(-8, "bitcoin_descriptor is not a valid descriptor string.", self.nodes[i_pak1].initpegoutwallet, "pkh([d34db33f/44'/0'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/*)")
|
||||
|
||||
# Peg out with each new type, check that destination script matches
|
||||
wpkh_desc = "wpkh("+xpub+"/0/*)"
|
||||
wpkh_info = self.nodes[i_pak1].initpegoutwallet(wpkh_desc)
|
||||
wpkh_pak_info = self.nodes[i_pak1].getwalletpakinfo()
|
||||
|
||||
# Add to pak list for pak1, restart
|
||||
self.stop_nodes()
|
||||
extra_args = copy.deepcopy(args)
|
||||
extra_args[i_pak1] = extra_args[i_pak1]+["-"+wpkh_info["pakentry"]]
|
||||
self.start_nodes(extra_args)
|
||||
|
||||
# Make block commitment and get some block subsidy
|
||||
self.nodes[i_pak1].generate(101)
|
||||
wpkh_stmc = self.nodes[i_pak1].sendtomainchain("", 1)
|
||||
wpkh_txid = wpkh_stmc['txid']
|
||||
|
||||
# Also check some basic return fields of sendtomainchain with pak
|
||||
assert_equal(wpkh_stmc["bitcoin_address"], wpkh_info["address_lookahead"][0])
|
||||
validata = self.nodes[i_pak1].validateaddress(wpkh_stmc["bitcoin_address"])
|
||||
assert(not validata["isvalid"])
|
||||
assert(validata["isvalid_parent"])
|
||||
assert_equal(wpkh_pak_info["bip32_counter"], wpkh_stmc["bip32_counter"])
|
||||
assert_equal(wpkh_pak_info["bitcoin_descriptor"], wpkh_stmc["bitcoin_descriptor"])
|
||||
|
||||
sh_wpkh_desc = "sh(wpkh("+xpub+"/0/1/*))"
|
||||
sh_wpkh_info = self.nodes[i_pak1].initpegoutwallet(sh_wpkh_desc)
|
||||
|
||||
# Add to pak list for pak1, restart
|
||||
self.stop_nodes()
|
||||
extra_args = copy.deepcopy(args)
|
||||
extra_args[i_pak1] = extra_args[i_pak1]+["-"+sh_wpkh_info["pakentry"]]
|
||||
|
||||
# Restart and connect peers
|
||||
self.start_nodes(extra_args)
|
||||
connect_nodes_bi(self.nodes,0,1)
|
||||
connect_nodes_bi(self.nodes,1,2)
|
||||
connect_nodes_bi(self.nodes,2,3)
|
||||
connect_nodes_bi(self.nodes,3,4)
|
||||
|
||||
self.nodes[i_pak1].generate(1)
|
||||
sh_wpkh_txid = self.nodes[i_pak1].sendtomainchain("", 1)['txid']
|
||||
|
||||
# Make sure peg-outs look correct
|
||||
wpkh_raw = self.nodes[i_pak1].decoderawtransaction(self.nodes[i_pak1].gettransaction(wpkh_txid)['hex'])
|
||||
sh_wpkh_raw = self.nodes[i_pak1].decoderawtransaction(self.nodes[i_pak1].gettransaction(sh_wpkh_txid)['hex'])
|
||||
|
||||
peg_out_found = False
|
||||
for output in wpkh_raw["vout"]:
|
||||
if "pegout_addresses" in output["scriptPubKey"]:
|
||||
if output["scriptPubKey"]["pegout_addresses"][0] \
|
||||
== wpkh_info["address_lookahead"][0]:
|
||||
peg_out_found = True
|
||||
break
|
||||
else:
|
||||
raise Exception("Found unexpected peg-out output")
|
||||
assert(peg_out_found)
|
||||
|
||||
peg_out_found = False
|
||||
for output in sh_wpkh_raw["vout"]:
|
||||
if "pegout_addresses" in output["scriptPubKey"]:
|
||||
if output["scriptPubKey"]["pegout_addresses"][0] \
|
||||
== sh_wpkh_info["address_lookahead"][0]:
|
||||
peg_out_found = True
|
||||
break
|
||||
else:
|
||||
raise Exception("Found unexpected peg-out output")
|
||||
assert(peg_out_found)
|
||||
|
||||
if __name__ == '__main__':
|
||||
PAKTest ().main ()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue