mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-08 10:31:50 -05:00
Merge bitcoin/bitcoin#20867: Support up to 20 keys for multisig under Segwit context
ebd4be43cc
doc: add release notes for 20867 (Antoine Poinsot)5aa50ab9cc
rpc/util: multisig: only check redeemScript size is <= 520 for P2SH (Antoine Poinsot)063df9e897
test/functional: standardness sanity checks for P2(W)SH multisig (Antoine Poinsot)ae0429d3af
script: allow up to 20 keys in wsh() descriptors (Antoine Poinsot)9fc68faf35
script: match multisigs with up to MAX_PUBKEYS_PER_MULTISIG keys (Antoine Poinsot) Pull request description: As described in https://github.com/bitcoin/bitcoin/issues/20620 multisigs are currently limited to 16 keys in descriptors and RPC helpers, even for P2WSH and P2SH-P2WSH. This adds support for multisig with up to 20 keys (which are already standard) for Segwit v0 context for descriptors (`wsh()`, `sh(wsh())`) and RPC helpers. Fixes https://github.com/bitcoin/bitcoin/issues/20620 ACKs for top commit: meshcollider: re-utACKebd4be43cc
instagibbs: re-ACKebd4be43cc
Tree-SHA512: 36141f10a8288010d17d5c4fe8d24878bcd4533b88a8aba3a44fa8f74ceb3182d70fee01427e0ab7f53ce7fab46c88c1cd3ac3b18ab8a10bd4a6b8b74ed79e46
This commit is contained in:
commit
60132382a7
8 changed files with 171 additions and 22 deletions
11
doc/release-notes-20867.md
Normal file
11
doc/release-notes-20867.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
Wallet
|
||||
------
|
||||
|
||||
- We now support up to 20 keys in `multi()` and `sortedmulti()` descriptors
|
||||
under `wsh()`. (#20867)
|
||||
|
||||
Updated RPCs
|
||||
------------
|
||||
|
||||
- `addmultisigaddress` and `createmultisig` now support up to 20 keys for
|
||||
Segwit addresses.
|
|
@ -231,16 +231,12 @@ CTxDestination AddAndGetMultisigDestination(const int required, const std::vecto
|
|||
if ((int)pubkeys.size() < required) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("not enough keys supplied (got %u keys, but need at least %d to redeem)", pubkeys.size(), required));
|
||||
}
|
||||
if (pubkeys.size() > 16) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Number of keys involved in the multisignature address creation > 16\nReduce the number");
|
||||
if (pubkeys.size() > MAX_PUBKEYS_PER_MULTISIG) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Number of keys involved in the multisignature address creation > %d\nReduce the number", MAX_PUBKEYS_PER_MULTISIG));
|
||||
}
|
||||
|
||||
script_out = GetScriptForMultisig(required, pubkeys);
|
||||
|
||||
if (script_out.size() > MAX_SCRIPT_ELEMENT_SIZE) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, (strprintf("redeemScript exceeds size limit: %d > %d", script_out.size(), MAX_SCRIPT_ELEMENT_SIZE)));
|
||||
}
|
||||
|
||||
// Check if any keys are uncompressed. If so, the type is legacy
|
||||
for (const CPubKey& pk : pubkeys) {
|
||||
if (!pk.IsCompressed()) {
|
||||
|
@ -249,6 +245,10 @@ CTxDestination AddAndGetMultisigDestination(const int required, const std::vecto
|
|||
}
|
||||
}
|
||||
|
||||
if (type == OutputType::LEGACY && script_out.size() > MAX_SCRIPT_ELEMENT_SIZE) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, (strprintf("redeemScript exceeds size limit: %d > %d", script_out.size(), MAX_SCRIPT_ELEMENT_SIZE)));
|
||||
}
|
||||
|
||||
// Make the address
|
||||
CTxDestination dest = AddAndGetDestinationForScript(keystore, script_out, type);
|
||||
|
||||
|
|
|
@ -998,8 +998,8 @@ std::unique_ptr<DescriptorImpl> ParseScript(uint32_t& key_exp_index, Span<const
|
|||
providers.emplace_back(std::move(pk));
|
||||
key_exp_index++;
|
||||
}
|
||||
if (providers.empty() || providers.size() > 16) {
|
||||
error = strprintf("Cannot have %u keys in multisig; must have between 1 and 16 keys, inclusive", providers.size());
|
||||
if (providers.empty() || providers.size() > MAX_PUBKEYS_PER_MULTISIG) {
|
||||
error = strprintf("Cannot have %u keys in multisig; must have between 1 and %d keys, inclusive", providers.size(), MAX_PUBKEYS_PER_MULTISIG);
|
||||
return nullptr;
|
||||
} else if (thres < 1) {
|
||||
error = strprintf("Multisig threshold cannot be %d, must be at least 1", thres);
|
||||
|
@ -1015,6 +1015,7 @@ std::unique_ptr<DescriptorImpl> ParseScript(uint32_t& key_exp_index, Span<const
|
|||
}
|
||||
}
|
||||
if (ctx == ParseScriptContext::P2SH) {
|
||||
// This limits the maximum number of compressed pubkeys to 15.
|
||||
if (script_size + 3 > MAX_SCRIPT_ELEMENT_SIZE) {
|
||||
error = strprintf("P2SH script is too large, %d bytes is larger than %d bytes", script_size + 3, MAX_SCRIPT_ELEMENT_SIZE);
|
||||
return nullptr;
|
||||
|
|
|
@ -225,7 +225,7 @@ bool static CheckPubKeyEncoding(const valtype &vchPubKey, unsigned int flags, co
|
|||
return true;
|
||||
}
|
||||
|
||||
bool static CheckMinimalPush(const valtype& data, opcodetype opcode) {
|
||||
bool CheckMinimalPush(const valtype& data, opcodetype opcode) {
|
||||
// Excludes OP_1NEGATE, OP_1-16 since they are by definition minimal
|
||||
assert(0 <= opcode && opcode <= OP_PUSHDATA4);
|
||||
if (data.size() == 0) {
|
||||
|
|
|
@ -316,6 +316,8 @@ bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const C
|
|||
|
||||
size_t CountWitnessSigOps(const CScript& scriptSig, const CScript& scriptPubKey, const CScriptWitness* witness, unsigned int flags);
|
||||
|
||||
bool CheckMinimalPush(const std::vector<unsigned char>& data, opcodetype opcode);
|
||||
|
||||
int FindAndDelete(CScript& script, const CScript& b);
|
||||
|
||||
#endif // BITCOIN_SCRIPT_INTERPRETER_H
|
||||
|
|
|
@ -88,21 +88,53 @@ static constexpr bool IsSmallInteger(opcodetype opcode)
|
|||
return opcode >= OP_1 && opcode <= OP_16;
|
||||
}
|
||||
|
||||
static bool MatchMultisig(const CScript& script, unsigned int& required, std::vector<valtype>& pubkeys)
|
||||
static constexpr bool IsPushdataOp(opcodetype opcode)
|
||||
{
|
||||
return opcode > OP_FALSE && opcode <= OP_PUSHDATA4;
|
||||
}
|
||||
|
||||
static constexpr bool IsValidMultisigKeyCount(int n_keys)
|
||||
{
|
||||
return n_keys > 0 && n_keys <= MAX_PUBKEYS_PER_MULTISIG;
|
||||
}
|
||||
|
||||
static bool GetMultisigKeyCount(opcodetype opcode, valtype data, int& count)
|
||||
{
|
||||
if (IsSmallInteger(opcode)) {
|
||||
count = CScript::DecodeOP_N(opcode);
|
||||
return IsValidMultisigKeyCount(count);
|
||||
}
|
||||
|
||||
if (IsPushdataOp(opcode)) {
|
||||
if (!CheckMinimalPush(data, opcode)) return false;
|
||||
try {
|
||||
count = CScriptNum(data, /* fRequireMinimal = */ true).getint();
|
||||
return IsValidMultisigKeyCount(count);
|
||||
} catch (const scriptnum_error&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool MatchMultisig(const CScript& script, int& required_sigs, std::vector<valtype>& pubkeys)
|
||||
{
|
||||
opcodetype opcode;
|
||||
valtype data;
|
||||
int num_keys;
|
||||
|
||||
CScript::const_iterator it = script.begin();
|
||||
if (script.size() < 1 || script.back() != OP_CHECKMULTISIG) return false;
|
||||
|
||||
if (!script.GetOp(it, opcode, data) || !IsSmallInteger(opcode)) return false;
|
||||
required = CScript::DecodeOP_N(opcode);
|
||||
if (!script.GetOp(it, opcode, data) || !GetMultisigKeyCount(opcode, data, required_sigs)) return false;
|
||||
while (script.GetOp(it, opcode, data) && CPubKey::ValidSize(data)) {
|
||||
pubkeys.emplace_back(std::move(data));
|
||||
}
|
||||
if (!IsSmallInteger(opcode)) return false;
|
||||
unsigned int keys = CScript::DecodeOP_N(opcode);
|
||||
if (pubkeys.size() != keys || keys < required) return false;
|
||||
if (!GetMultisigKeyCount(opcode, data, num_keys)) return false;
|
||||
|
||||
if (pubkeys.size() != static_cast<unsigned long>(num_keys) || num_keys < required_sigs) return false;
|
||||
|
||||
return (it + 1 == script.end());
|
||||
}
|
||||
|
||||
|
@ -163,12 +195,12 @@ TxoutType Solver(const CScript& scriptPubKey, std::vector<std::vector<unsigned c
|
|||
return TxoutType::PUBKEYHASH;
|
||||
}
|
||||
|
||||
unsigned int required;
|
||||
int required;
|
||||
std::vector<std::vector<unsigned char>> keys;
|
||||
if (MatchMultisig(scriptPubKey, required, keys)) {
|
||||
vSolutionsRet.push_back({static_cast<unsigned char>(required)}); // safe as required is in range 1..16
|
||||
vSolutionsRet.push_back({static_cast<unsigned char>(required)}); // safe as required is in range 1..20
|
||||
vSolutionsRet.insert(vSolutionsRet.end(), keys.begin(), keys.end());
|
||||
vSolutionsRet.push_back({static_cast<unsigned char>(keys.size())}); // safe as size is in range 1..16
|
||||
vSolutionsRet.push_back({static_cast<unsigned char>(keys.size())}); // safe as size is in range 1..20
|
||||
return TxoutType::MULTISIG;
|
||||
}
|
||||
|
||||
|
@ -318,10 +350,11 @@ CScript GetScriptForMultisig(int nRequired, const std::vector<CPubKey>& keys)
|
|||
{
|
||||
CScript script;
|
||||
|
||||
script << CScript::EncodeOP_N(nRequired);
|
||||
script << nRequired;
|
||||
for (const CPubKey& key : keys)
|
||||
script << ToByteVector(key);
|
||||
script << CScript::EncodeOP_N(keys.size()) << OP_CHECKMULTISIG;
|
||||
script << keys.size() << OP_CHECKMULTISIG;
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -459,6 +459,77 @@ class ImportDescriptorsTest(BitcoinTestFramework):
|
|||
assert_equal(tx_signed_2['complete'], True)
|
||||
self.nodes[1].sendrawtransaction(tx_signed_2['hex'])
|
||||
|
||||
self.log.info("We can create and use a huge multisig under P2WSH")
|
||||
self.nodes[1].createwallet(wallet_name='wmulti_priv_big', blank=True, descriptors=True)
|
||||
wmulti_priv_big = self.nodes[1].get_wallet_rpc('wmulti_priv_big')
|
||||
xkey = "tprv8ZgxMBicQKsPeZSeYx7VXDDTs3XrTcmZQpRLbAeSQFCQGgKwR4gKpcxHaKdoTNHniv4EPDJNdzA3KxRrrBHcAgth8fU5X4oCndkkxk39iAt/*"
|
||||
xkey_int = "tprv8ZgxMBicQKsPeZSeYx7VXDDTs3XrTcmZQpRLbAeSQFCQGgKwR4gKpcxHaKdoTNHniv4EPDJNdzA3KxRrrBHcAgth8fU5X4oCndkkxk39iAt/1/*"
|
||||
res = wmulti_priv_big.importdescriptors([
|
||||
{
|
||||
"desc": descsum_create(f"wsh(sortedmulti(20,{(xkey + ',') * 19}{xkey}))"),
|
||||
"active": True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"
|
||||
},
|
||||
{
|
||||
"desc": descsum_create(f"wsh(sortedmulti(20,{(xkey_int + ',') * 19}{xkey_int}))"),
|
||||
"active": True,
|
||||
"internal": True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"
|
||||
}])
|
||||
assert_equal(res[0]['success'], True)
|
||||
assert_equal(res[1]['success'], True)
|
||||
|
||||
addr = wmulti_priv_big.getnewaddress()
|
||||
w0.sendtoaddress(addr, 10)
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
# It is standard and would relay.
|
||||
txid = wmulti_priv_big.sendtoaddress(w0.getnewaddress(), 9.999)
|
||||
decoded = wmulti_priv_big.decoderawtransaction(wmulti_priv_big.gettransaction(txid)['hex'])
|
||||
# 20 sigs + dummy + witness script
|
||||
assert_equal(len(decoded['vin'][0]['txinwitness']), 22)
|
||||
|
||||
|
||||
self.log.info("Under P2SH, multisig are standard with up to 15 "
|
||||
"compressed keys")
|
||||
self.nodes[1].createwallet(wallet_name='multi_priv_big_legacy',
|
||||
blank=True, descriptors=True)
|
||||
multi_priv_big = self.nodes[1].get_wallet_rpc('multi_priv_big_legacy')
|
||||
res = multi_priv_big.importdescriptors([
|
||||
{
|
||||
"desc": descsum_create(f"sh(multi(15,{(xkey + ',') * 14}{xkey}))"),
|
||||
"active": True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"
|
||||
},
|
||||
{
|
||||
"desc": descsum_create(f"sh(multi(15,{(xkey_int + ',') * 14}{xkey_int}))"),
|
||||
"active": True,
|
||||
"internal": True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"
|
||||
}])
|
||||
assert_equal(res[0]['success'], True)
|
||||
assert_equal(res[1]['success'], True)
|
||||
|
||||
addr = multi_priv_big.getnewaddress("", "legacy")
|
||||
w0.sendtoaddress(addr, 10)
|
||||
self.nodes[0].generate(6)
|
||||
self.sync_all()
|
||||
# It is standard and would relay.
|
||||
txid = multi_priv_big.sendtoaddress(w0.getnewaddress(), 10, "", "",
|
||||
True)
|
||||
decoded = multi_priv_big.decoderawtransaction(
|
||||
multi_priv_big.gettransaction(txid)['hex']
|
||||
)
|
||||
|
||||
|
||||
self.log.info("Combo descriptors cannot be active")
|
||||
self.test_importdesc({"desc": descsum_create("combo(tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*)"),
|
||||
"active": True,
|
||||
|
|
Loading…
Add table
Reference in a new issue