mirror of https://github.com/bitcoin/bitcoin.git
Merge 2a46e94a16
into b510893d00
This commit is contained in:
commit
c2fe151084
|
@ -27,12 +27,10 @@ These three wallets should not be used directly for privacy reasons (public key
|
|||
```bash
|
||||
for ((n=1;n<=3;n++))
|
||||
do
|
||||
./build/bin/bitcoin-cli -signet createwallet "participant_${n}"
|
||||
./build/bin/bitcoin rpc -signet createwallet "participant_${n}"
|
||||
done
|
||||
```
|
||||
|
||||
`bitcoin rpc` can also be substituted for `bitcoin-cli`.
|
||||
|
||||
Extract the xpub of each wallet. To do this, the `listdescriptors` RPC is used. By default, Bitcoin Core single-sig wallets are created using path `m/44'/1'/0'` for PKH, `m/84'/1'/0'` for WPKH, `m/49'/1'/0'` for P2WPKH-nested-in-P2SH and `m/86'/1'/0'` for P2TR based accounts. Each of them uses the chain 0 for external addresses and chain 1 for internal ones, as shown in the example below.
|
||||
|
||||
```
|
||||
|
@ -44,14 +42,14 @@ wpkh([1004658e/84'/1'/0']tpubDCBEcmVKbfC9KfdydyLbJ2gfNL88grZu1XcWSW9ytTM6fitvaRm
|
|||
The suffix (after #) is the checksum. Descriptors can optionally be suffixed with a checksum to protect against typos or copy-paste errors.
|
||||
All RPCs in Bitcoin Core will include the checksum in their output.
|
||||
|
||||
Note that previously at least two descriptors were usually used, one for external derivation paths and one for internal ones. Since https://github.com/bitcoin/bitcoin/pull/22838 this redundancy has been eliminated by a multipath descriptor with <code><0;1></code> at the [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#change) change level expanding to external and internal descriptors when imported.
|
||||
|
||||
```bash
|
||||
declare -A xpubs
|
||||
|
||||
for ((n=1;n<=3;n++))
|
||||
do
|
||||
xpubs["internal_xpub_${n}"]=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/1/*"))][0] | .desc' | grep -Po '(?<=\().*(?=\))')
|
||||
|
||||
xpubs["external_xpub_${n}"]=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/0/*") )][0] | .desc' | grep -Po '(?<=\().*(?=\))')
|
||||
xpubs["xpub_${n}"]=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/0/*") )][0] | .desc' | grep -Po '(?<=\().*(?=\))' | sed 's /0/\* /<0;1>/* ')
|
||||
done
|
||||
```
|
||||
|
||||
|
@ -65,61 +63,57 @@ for x in "${!xpubs[@]}"; do printf "[%s]=%s\n" "$x" "${xpubs[$x]}" ; done
|
|||
|
||||
As previously mentioned, this step extracts the `m/84'/1'/0'` account instead of the path defined in [BIP 45](https://github.com/bitcoin/bips/blob/master/bip-0045.mediawiki) or [BIP 87](https://github.com/bitcoin/bips/blob/master/bip-0087.mediawiki), since there is no way to extract a specific path in Bitcoin Core at the time of writing.
|
||||
|
||||
### 1.2 Define the Multisig Descriptors
|
||||
### 1.2 Define the Multisig Descriptor
|
||||
|
||||
Define the external and internal multisig descriptors, add the checksum and then, join both in a JSON array.
|
||||
Define the multisig descriptor, add the checksum and then, wrap it in a JSON array.
|
||||
|
||||
```bash
|
||||
external_desc="wsh(sortedmulti(2,${xpubs["external_xpub_1"]},${xpubs["external_xpub_2"]},${xpubs["external_xpub_3"]}))"
|
||||
internal_desc="wsh(sortedmulti(2,${xpubs["internal_xpub_1"]},${xpubs["internal_xpub_2"]},${xpubs["internal_xpub_3"]}))"
|
||||
desc="wsh(sortedmulti(2,${xpubs["xpub_1"]},${xpubs["xpub_2"]},${xpubs["xpub_3"]}))"
|
||||
|
||||
external_desc_sum=$(./build/bin/bitcoin-cli -signet getdescriptorinfo $external_desc | jq '.descriptor')
|
||||
internal_desc_sum=$(./build/bin/bitcoin-cli -signet getdescriptorinfo $internal_desc | jq '.descriptor')
|
||||
checksum=$(./build/bin/bitcoin rpc -signet getdescriptorinfo $desc | jq -r '.checksum')
|
||||
|
||||
multisig_ext_desc="{\"desc\": $external_desc_sum, \"active\": true, \"internal\": false, \"timestamp\": \"now\"}"
|
||||
multisig_int_desc="{\"desc\": $internal_desc_sum, \"active\": true, \"internal\": true, \"timestamp\": \"now\"}"
|
||||
|
||||
multisig_desc="[$multisig_ext_desc, $multisig_int_desc]"
|
||||
multisig_desc="[{\"desc\": \"${desc}#${checksum}\", \"active\": true, \"timestamp\": \"now\"}]"
|
||||
```
|
||||
|
||||
`external_desc` and `internal_desc` specify the output type (`wsh`, in this case) and the xpubs involved. They also use BIP 67 (`sortedmulti`), so the wallet can be recreated without worrying about the order of xpubs. Conceptually, descriptors describe a list of scriptPubKey (along with information for spending from it) [[source](https://github.com/bitcoin/bitcoin/issues/21199#issuecomment-780772418)].
|
||||
`desc` specifies the output type (`wsh`, in this case) and the xpubs involved. It also uses BIP 67 (`sortedmulti`), so the wallet can be recreated without worrying about the order of xpubs. Conceptually, descriptors describe a list of scriptPubKey (along with information for spending from it) [[source](https://github.com/bitcoin/bitcoin/issues/21199#issuecomment-780772418)].
|
||||
|
||||
Note that at least two descriptors are usually used, one for internal derivation paths and one for external ones. There are discussions about eliminating this redundancy, as can be seen in the issue [#17190](https://github.com/bitcoin/bitcoin/issues/17190).
|
||||
|
||||
After creating the descriptors, it is necessary to add the checksum, which is required by the `importdescriptors` RPC.
|
||||
After creating the descriptor, it is necessary to add the checksum, which is required by the `importdescriptors` RPC.
|
||||
|
||||
The checksum for a descriptor without one can be computed using the `getdescriptorinfo` RPC. The response has the `descriptor` field, which is the descriptor with the checksum added.
|
||||
|
||||
There are other fields that can be added to the descriptors:
|
||||
There are other fields that can be added to the descriptor:
|
||||
|
||||
* `active`: Sets the descriptor to be the active one for the corresponding output type (`wsh`, in this case).
|
||||
* `internal`: Indicates whether matching outputs should be treated as something other than incoming payments (e.g. change).
|
||||
* `timestamp`: Sets the time from which to start rescanning the blockchain for the descriptor, in UNIX epoch time.
|
||||
|
||||
Documentation for these and other parameters can be found by typing `./build/bin/bitcoin-cli help importdescriptors`.
|
||||
Note: when a multipath descriptor is imported, it is expanded into two descriptors which are imported separately, with the second implicitly used for internal (change) addresses.
|
||||
|
||||
`multisig_desc` concatenates external and internal descriptors in a JSON array and then it will be used to create the multisig wallet.
|
||||
Documentation for these and other parameters can be found by typing `./build/bin/bitcoin rpc -signet help importdescriptors`.
|
||||
|
||||
`multisig_desc` wraps the descriptor in a JSON array and will be used to create the multisig wallet.
|
||||
|
||||
### 1.3 Create the Multisig Wallet
|
||||
|
||||
To create the multisig wallet, first create an empty one (no keys, HD seed and private keys disabled).
|
||||
|
||||
Then import the descriptors created in the previous step using the `importdescriptors` RPC.
|
||||
Then import the descriptor created in the previous step using the `importdescriptors` RPC.
|
||||
|
||||
After that, `getwalletinfo` can be used to check if the wallet was created successfully.
|
||||
|
||||
```bash
|
||||
./build/bin/bitcoin-cli -signet -named createwallet wallet_name="multisig_wallet_01" disable_private_keys=true blank=true
|
||||
./build/bin/bitcoin rpc -signet createwallet "multisig_wallet_01" disable_private_keys=true blank=true
|
||||
|
||||
./build/bin/bitcoin-cli -signet -rpcwallet="multisig_wallet_01" importdescriptors "$multisig_desc"
|
||||
./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" importdescriptors "$multisig_desc"
|
||||
|
||||
./build/bin/bitcoin-cli -signet -rpcwallet="multisig_wallet_01" getwalletinfo
|
||||
./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" getwalletinfo
|
||||
```
|
||||
|
||||
Once the wallets have already been created and this tutorial needs to be repeated or resumed, it is not necessary to recreate them, just load them with the command below:
|
||||
|
||||
```bash
|
||||
for ((n=1;n<=3;n++)); do ./build/bin/bitcoin-cli -signet loadwallet "participant_${n}"; done
|
||||
for ((n=1;n<=3;n++)); do ./build/bin/bitcoin rpc -signet loadwallet "participant_${n}"; done
|
||||
./build/bin/bitcoin rpc -signet loadwallet "multisig_wallet_01"
|
||||
```
|
||||
|
||||
### 1.4 Fund the wallet
|
||||
|
@ -133,7 +127,7 @@ The url used by the script can also be accessed directly. At time of writing, th
|
|||
Coins received by the wallet must have at least 1 confirmation before they can be spent. It is necessary to wait for a new block to be mined before continuing.
|
||||
|
||||
```bash
|
||||
receiving_address=$(./build/bin/bitcoin-cli -signet -rpcwallet="multisig_wallet_01" getnewaddress)
|
||||
receiving_address=$(./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" getnewaddress)
|
||||
|
||||
./contrib/signet/getcoins.py -c ./build/bin/bitcoin-cli -a $receiving_address
|
||||
```
|
||||
|
@ -147,7 +141,7 @@ echo -n "$receiving_address" | xclip -sel clip
|
|||
The `getbalances` RPC may be used to check the balance. Coins with `trusted` status can be spent.
|
||||
|
||||
```bash
|
||||
./build/bin/bitcoin-cli -signet -rpcwallet="multisig_wallet_01" getbalances
|
||||
./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" getbalances
|
||||
```
|
||||
|
||||
### 1.5 Create a PSBT
|
||||
|
@ -163,13 +157,13 @@ For simplicity, the destination address is taken from the `participant_1` wallet
|
|||
The `walletcreatefundedpsbt` RPC is used to create and fund a transaction in the PSBT format. It is the first step in creating the PSBT.
|
||||
|
||||
```bash
|
||||
balance=$(./build/bin/bitcoin-cli -signet -rpcwallet="multisig_wallet_01" getbalance)
|
||||
balance=$(./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" getbalance)
|
||||
|
||||
amount=$(echo "$balance * 0.8" | bc -l | sed -e 's/^\./0./' -e 's/^-\./-0./')
|
||||
|
||||
destination_addr=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_1" getnewaddress)
|
||||
destination_addr=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_1" getnewaddress)
|
||||
|
||||
funded_psbt=$(./build/bin/bitcoin-cli -signet -named -rpcwallet="multisig_wallet_01" walletcreatefundedpsbt outputs="{\"$destination_addr\": $amount}" | jq -r '.psbt')
|
||||
funded_psbt=$(./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" walletcreatefundedpsbt outputs="{\"$destination_addr\": $amount}" | jq -r '.psbt')
|
||||
```
|
||||
|
||||
There is also the `createpsbt` RPC, which serves the same purpose, but it has no access to the wallet or to the UTXO set. It is functionally the same as `createrawtransaction` and just drops the raw transaction into an otherwise blank PSBT. [[source](https://bitcointalk.org/index.php?topic=5131043.msg50573609#msg50573609)] In most cases, `walletcreatefundedpsbt` solves the problem.
|
||||
|
@ -183,9 +177,9 @@ Optionally, the PSBT can be decoded to a JSON format using `decodepsbt` RPC.
|
|||
The `analyzepsbt` RPC analyzes and provides information about the current status of a PSBT and its inputs, e.g. missing signatures.
|
||||
|
||||
```bash
|
||||
./build/bin/bitcoin-cli -signet decodepsbt $funded_psbt
|
||||
./build/bin/bitcoin rpc -signet decodepsbt $funded_psbt
|
||||
|
||||
./build/bin/bitcoin-cli -signet analyzepsbt $funded_psbt
|
||||
./build/bin/bitcoin rpc -signet analyzepsbt $funded_psbt
|
||||
```
|
||||
|
||||
### 1.7 Update the PSBT
|
||||
|
@ -195,9 +189,9 @@ In the code above, two PSBTs are created. One signed by `participant_1` wallet a
|
|||
The `walletprocesspsbt` is used by the wallet to sign a PSBT.
|
||||
|
||||
```bash
|
||||
psbt_1=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_1" walletprocesspsbt $funded_psbt | jq '.psbt')
|
||||
psbt_1=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_1" walletprocesspsbt $funded_psbt | jq '.psbt')
|
||||
|
||||
psbt_2=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_2" walletprocesspsbt $funded_psbt | jq '.psbt')
|
||||
psbt_2=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_2" walletprocesspsbt $funded_psbt | jq '.psbt')
|
||||
```
|
||||
|
||||
### 1.8 Combine the PSBT
|
||||
|
@ -205,7 +199,7 @@ psbt_2=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_2" walletproces
|
|||
The PSBT, if signed separately by the co-signers, must be combined into one transaction before being finalized. This is done by `combinepsbt` RPC.
|
||||
|
||||
```bash
|
||||
combined_psbt=$(./build/bin/bitcoin-cli -signet combinepsbt "[$psbt_1, $psbt_2]")
|
||||
combined_psbt=$(./build/bin/bitcoin rpc -signet combinepsbt "[$psbt_1, $psbt_2]")
|
||||
```
|
||||
|
||||
There is an RPC called `joinpsbts`, but it has a different purpose than `combinepsbt`. `joinpsbts` joins the inputs from multiple distinct PSBTs into one PSBT.
|
||||
|
@ -219,9 +213,9 @@ The `finalizepsbt` RPC is used to produce a network serialized transaction which
|
|||
It checks that all inputs have complete scriptSigs and scriptWitnesses and, if so, encodes them into network serialized transactions.
|
||||
|
||||
```bash
|
||||
finalized_psbt_hex=$(./build/bin/bitcoin-cli -signet finalizepsbt $combined_psbt | jq -r '.hex')
|
||||
finalized_psbt_hex=$(./build/bin/bitcoin rpc -signet finalizepsbt $combined_psbt | jq -r '.hex')
|
||||
|
||||
./build/bin/bitcoin-cli -signet sendrawtransaction $finalized_psbt_hex
|
||||
./build/bin/bitcoin rpc -signet sendrawtransaction $finalized_psbt_hex
|
||||
```
|
||||
|
||||
### 1.10 Alternative Workflow (PSBT sequential signatures)
|
||||
|
@ -231,11 +225,11 @@ Instead of each wallet signing the original PSBT and combining them later, the w
|
|||
After that, the rest of the process is the same: the PSBT is finalized and transmitted to the network.
|
||||
|
||||
```bash
|
||||
psbt_1=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_1" walletprocesspsbt $funded_psbt | jq -r '.psbt')
|
||||
psbt_1=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_1" walletprocesspsbt $funded_psbt | jq -r '.psbt')
|
||||
|
||||
psbt_2=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_2" walletprocesspsbt $psbt_1 | jq -r '.psbt')
|
||||
psbt_2=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_2" walletprocesspsbt $psbt_1 | jq -r '.psbt')
|
||||
|
||||
finalized_psbt_hex=$(./build/bin/bitcoin-cli -signet finalizepsbt $psbt_2 | jq -r '.hex')
|
||||
finalized_psbt_hex=$(./build/bin/bitcoin rpc -signet finalizepsbt $psbt_2 | jq -r '.hex')
|
||||
|
||||
./build/bin/bitcoin-cli -signet sendrawtransaction $finalized_psbt_hex
|
||||
./build/bin/bitcoin rpc -signet sendrawtransaction $finalized_psbt_hex
|
||||
```
|
||||
|
|
|
@ -25,12 +25,13 @@ class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework):
|
|||
self.skip_if_no_wallet()
|
||||
|
||||
@staticmethod
|
||||
def _get_xpub(wallet, internal):
|
||||
def _get_xpub(wallet):
|
||||
"""Extract the wallet's xpubs using `listdescriptors` and pick the one from the `pkh` descriptor since it's least likely to be accidentally reused (legacy addresses)."""
|
||||
pkh_descriptor = next(filter(lambda d: d["desc"].startswith("pkh(") and d["internal"] == internal, wallet.listdescriptors()["descriptors"]))
|
||||
pkh_descriptor = next(filter(lambda d: d["desc"].startswith("pkh(") and not d["internal"], wallet.listdescriptors()["descriptors"]))
|
||||
# Keep all key origin information (master key fingerprint and all derivation steps) for proper support of hardware devices
|
||||
# See section 'Key origin identification' in 'doc/descriptors.md' for more details...
|
||||
return pkh_descriptor["desc"].split("pkh(")[1].split(")")[0]
|
||||
# Replace the change index with the multipath convention
|
||||
return pkh_descriptor["desc"].split("pkh(")[1].split(")")[0].replace("/0/*", "/<0;1>/*")
|
||||
|
||||
@staticmethod
|
||||
def _check_psbt(psbt, to, value, multisig):
|
||||
|
@ -44,26 +45,19 @@ class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework):
|
|||
amount += vout["value"]
|
||||
assert_approx(amount, float(value), vspan=0.001)
|
||||
|
||||
def participants_create_multisigs(self, external_xpubs, internal_xpubs):
|
||||
def participants_create_multisigs(self, xpubs):
|
||||
"""The multisig is created by importing the following descriptors. The resulting wallet is watch-only and every participant can do this."""
|
||||
for i, node in enumerate(self.nodes):
|
||||
node.createwallet(wallet_name=f"{self.name}_{i}", blank=True, disable_private_keys=True)
|
||||
multisig = node.get_wallet_rpc(f"{self.name}_{i}")
|
||||
external = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{','.join(external_xpubs)}))")
|
||||
internal = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{','.join(internal_xpubs)}))")
|
||||
multisig_desc = f"wsh(sortedmulti({self.M},{','.join(xpubs)}))"
|
||||
checksum = multisig.getdescriptorinfo(multisig_desc)["checksum"]
|
||||
result = multisig.importdescriptors([
|
||||
{ # receiving addresses (internal: False)
|
||||
"desc": external["descriptor"],
|
||||
{ # Multipath descriptor expands to receive and change
|
||||
"desc": f"{multisig_desc}#{checksum}",
|
||||
"active": True,
|
||||
"internal": False,
|
||||
"timestamp": "now",
|
||||
},
|
||||
{ # change addresses (internal: True)
|
||||
"desc": internal["descriptor"],
|
||||
"active": True,
|
||||
"internal": True,
|
||||
"timestamp": "now",
|
||||
},
|
||||
}
|
||||
])
|
||||
assert all(r["success"] for r in result)
|
||||
yield multisig
|
||||
|
@ -84,10 +78,10 @@ class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework):
|
|||
}
|
||||
|
||||
self.log.info("Generate and exchange xpubs...")
|
||||
external_xpubs, internal_xpubs = [[self._get_xpub(signer, internal) for signer in participants["signers"]] for internal in [False, True]]
|
||||
xpubs = [self._get_xpub(signer) for signer in participants["signers"]]
|
||||
|
||||
self.log.info("Every participant imports the following descriptors to create the watch-only multisig...")
|
||||
participants["multisigs"] = list(self.participants_create_multisigs(external_xpubs, internal_xpubs))
|
||||
participants["multisigs"] = list(self.participants_create_multisigs(xpubs))
|
||||
|
||||
self.log.info("Check that every participant's multisig generates the same addresses...")
|
||||
for _ in range(10): # we check that the first 10 generated addresses are the same for all participant's multisigs
|
||||
|
|
Loading…
Reference in New Issue