rpc: a new 'broadcastspend' command
This commit is contained in:
parent
37ee93a1e6
commit
eff39ee35a
17
doc/API.md
17
doc/API.md
@ -12,6 +12,7 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`.
|
||||
| [`getnewaddress`](#getnewaddress) | Get a new receiving address |
|
||||
| [`listspendtxs`](#listspendtxs) | List all stored Spend transactions |
|
||||
| [`delspendtx`](#delspendtx) | Delete a stored Spend transaction |
|
||||
| [`broadcastspend`](#broadcastspend) | Finalize a stored Spend PSBT, and broadcast it |
|
||||
|
||||
# Reference
|
||||
|
||||
@ -182,3 +183,19 @@ This command does not return anything for now.
|
||||
|
||||
| Field | Type | Description |
|
||||
| -------------- | --------- | ---------------------------------------------------- |
|
||||
|
||||
|
||||
### `broadcastspend`
|
||||
|
||||
#### Request
|
||||
|
||||
| Field | Type | Description |
|
||||
| -------- | ------ | ------------------------------------------------------ |
|
||||
| `txid` | string | Hex encoded txid of the Spend transaction to broadcast |
|
||||
|
||||
#### Response
|
||||
|
||||
This command does not return anything for now.
|
||||
|
||||
| Field | Type | Description |
|
||||
| -------------- | --------- | ---------------------------------------------------- |
|
||||
|
||||
@ -72,9 +72,27 @@ fn delete_spend(control: &DaemonControl, params: Params) -> Result<serde_json::V
|
||||
Ok(serde_json::json!({}))
|
||||
}
|
||||
|
||||
fn broadcast_spend(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
|
||||
let txid = params
|
||||
.get(0, "txid")
|
||||
.ok_or_else(|| Error::invalid_params("Missing 'txid' parameter."))?
|
||||
.as_str()
|
||||
.and_then(|s| bitcoin::Txid::from_str(s).ok())
|
||||
.ok_or_else(|| Error::invalid_params("Invalid 'txid' parameter."))?;
|
||||
control.broadcast_spend(&txid)?;
|
||||
|
||||
Ok(serde_json::json!({}))
|
||||
}
|
||||
|
||||
/// Handle an incoming JSONRPC2 request.
|
||||
pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response, Error> {
|
||||
let result = match req.method.as_str() {
|
||||
"broadcastspend" => {
|
||||
let params = req
|
||||
.params
|
||||
.ok_or_else(|| Error::invalid_params("Missing 'txid' parameter."))?;
|
||||
broadcast_spend(control, params)?
|
||||
}
|
||||
"createspend" => {
|
||||
let params = req.params.ok_or_else(|| {
|
||||
Error::invalid_params(
|
||||
|
||||
@ -57,15 +57,14 @@ class Minisafed(TailableProc):
|
||||
|
||||
def sign_psbt(self, psbt):
|
||||
"""Sign a transaction using the owner's key.
|
||||
This creates a valid witness for all inputs in the transaction using the
|
||||
information contained in the PSBT.
|
||||
This will fill the 'partial_sigs' field of all inputs.
|
||||
|
||||
:param psbt: PSBT of the transaction to be signed.
|
||||
:returns: the serialized valid transaction, as hex.
|
||||
:returns: PSBT with a signature in each input for the owner's key.
|
||||
"""
|
||||
assert isinstance(psbt, PSBT)
|
||||
|
||||
# Create a witness for each input of the transaction.
|
||||
# Sign each input.
|
||||
for i, psbt_in in enumerate(psbt.inputs):
|
||||
# First, gather the needed information from the PSBT input.
|
||||
# 'hd_keypaths' is of the form {pubkey: (fingerprint, derivation index)}
|
||||
@ -82,13 +81,31 @@ class Minisafed(TailableProc):
|
||||
assert pubkey in psbt_in.hd_keypaths.keys()
|
||||
sig = privkey.sign(sighash, hasher=None) + b"\x01"
|
||||
logging.debug(f"Adding signature {sig.hex()} for pubkey {pubkey.hex()}")
|
||||
psbt_in.partial_sigs[pubkey] = sig
|
||||
|
||||
return psbt
|
||||
|
||||
def finalize_psbt(self, psbt):
|
||||
"""Create a valid witness for all inputs in the PSBT.
|
||||
This will fail if the PSBT input does not contain enough material.
|
||||
|
||||
:param psbt: PSBT of the transaction to be finalized.
|
||||
:returns: PSBT with finalized inputs.
|
||||
"""
|
||||
assert isinstance(psbt, PSBT)
|
||||
|
||||
# Create a witness for each input of the transaction.
|
||||
for i, psbt_in in enumerate(psbt.inputs):
|
||||
# First, gather the needed information from the PSBT input.
|
||||
# 'hd_keypaths' is of the form {pubkey: (fingerprint, derivation index)}
|
||||
der_index = next(iter(psbt_in.hd_keypaths.values()))[1]
|
||||
|
||||
# Create a copy of the descriptor to derive it at the index used in this input.
|
||||
# Then create a satisfaction for it using the signature we just created.
|
||||
desc = Descriptor.from_str(str(self.main_desc))
|
||||
desc.derive(der_index)
|
||||
sat_material = SatisfactionMaterial(
|
||||
signatures={pubkey: sig},
|
||||
signatures=psbt_in.partial_sigs,
|
||||
)
|
||||
stack = desc.satisfy(sat_material)
|
||||
logging.debug(f"Satisfaction for {desc} is {[e.hex() for e in stack]}")
|
||||
@ -98,9 +115,7 @@ class Minisafed(TailableProc):
|
||||
psbt_in.final_script_witness = CTxInWitness(CScriptWitness(stack))
|
||||
psbt.tx.wit.vtxinwit.append(psbt_in.final_script_witness)
|
||||
|
||||
tx = psbt.tx.serialize_with_witness().hex()
|
||||
logging.debug(f"Final transaction: {tx}")
|
||||
return tx
|
||||
return psbt
|
||||
|
||||
def start(self):
|
||||
TailableProc.start(self)
|
||||
|
||||
@ -69,7 +69,9 @@ def spend_coins(minisafed, bitcoind, coins):
|
||||
|
||||
psbt = PSBT()
|
||||
psbt.deserialize(res["psbt"])
|
||||
tx = minisafed.sign_psbt(psbt)
|
||||
signed_psbt = minisafed.sign_psbt(psbt)
|
||||
finalized_psbt = minisafed.finalize_psbt(signed_psbt)
|
||||
tx = finalized_psbt.tx.serialize_with_witness().hex()
|
||||
bitcoind.rpc.sendrawtransaction(tx)
|
||||
|
||||
return tx
|
||||
@ -184,6 +186,7 @@ class UnixDomainSocketRpc(object):
|
||||
We might still want to define the actual methods in the subclasses for
|
||||
documentation purposes.
|
||||
"""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
if len(args) != 0 and len(kwargs) != 0:
|
||||
raise RpcError(
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import pytest
|
||||
|
||||
from fixtures import *
|
||||
from test_framework.serializations import PSBT
|
||||
from test_framework.utils import wait_for, COIN, get_txid, spend_coins
|
||||
from test_framework.utils import wait_for, COIN, RpcError, get_txid, spend_coins
|
||||
|
||||
|
||||
def test_getinfo(minisafed):
|
||||
@ -108,8 +110,10 @@ def test_create_spend(minisafed, bitcoind):
|
||||
assert len(spend_psbt.tx.vout) == 4
|
||||
|
||||
# We can sign it and broadcast it.
|
||||
signed_tx_hex = minisafed.sign_psbt(spend_psbt)
|
||||
bitcoind.rpc.sendrawtransaction(signed_tx_hex)
|
||||
signed_psbt = minisafed.sign_psbt(spend_psbt)
|
||||
finalized_psbt = minisafed.finalize_psbt(signed_psbt)
|
||||
tx = finalized_psbt.tx.serialize_with_witness().hex()
|
||||
bitcoind.rpc.sendrawtransaction(tx)
|
||||
|
||||
|
||||
def test_list_spend(minisafed, bitcoind):
|
||||
@ -229,3 +233,33 @@ def test_update_spend(minisafed, bitcoind):
|
||||
assert len(psbt_merged.inputs[0].partial_sigs) == 2
|
||||
assert psbt_merged.inputs[0].partial_sigs[dummy_pk_a] == dummy_sig_a
|
||||
assert psbt_merged.inputs[0].partial_sigs[dummy_pk_b] == dummy_sig_b
|
||||
|
||||
|
||||
def test_broadcast_spend(minisafed, bitcoind):
|
||||
# Create a new coin and a spending tx for it.
|
||||
addr = minisafed.rpc.getnewaddress()["address"]
|
||||
bitcoind.rpc.sendtoaddress(addr, 0.2567)
|
||||
wait_for(lambda: len(minisafed.rpc.listcoins()["coins"]) > 0)
|
||||
outpoints = [c["outpoint"] for c in minisafed.rpc.listcoins()["coins"]]
|
||||
destinations = {
|
||||
bitcoind.rpc.getnewaddress(): 200_000,
|
||||
}
|
||||
res = minisafed.rpc.createspend(outpoints, destinations, 6)
|
||||
psbt = PSBT()
|
||||
psbt.deserialize(res["psbt"])
|
||||
txid = psbt.tx.txid().hex()
|
||||
|
||||
# We can't broadcast an unknown Spend
|
||||
with pytest.raises(RpcError, match="Unknown spend transaction.*"):
|
||||
minisafed.rpc.broadcastspend(txid)
|
||||
minisafed.rpc.updatespend(res["psbt"])
|
||||
|
||||
# We can't broadcast an unsigned transaction
|
||||
with pytest.raises(RpcError, match="Failed to finalize the spend transaction.*"):
|
||||
minisafed.rpc.broadcastspend(txid)
|
||||
signed_psbt = minisafed.sign_psbt(psbt)
|
||||
minisafed.rpc.updatespend(signed_psbt.serialize())
|
||||
|
||||
# Now we've signed and stored it, the daemon will take care of finalizing
|
||||
# the PSBT before broadcasting the transaction.
|
||||
minisafed.rpc.broadcastspend(txid)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user