mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-14 12:43:40 +02:00
Merge ElementsProject/elements#1042: Add 2-party blinded coinjoin PSET tutorial
048ca4a550pset: fix logic bug in `analyzepsbt` so that the "blinder" role may be returned (Andrew Poelstra)ec27e74f21pset_tutorial: write 2-party PSET coinjoin demo (Andrew Poelstra)a7643260a4assets_tutorial: move Daemon into its own file (Andrew Poelstra) Pull request description: Adds a second executable tutorial which uses the PSET API to produce a swap transaction. ACKs for top commit: achow101: ACK048ca4a550Tree-SHA512: 61720d905401a79dc9b6d6ae20a69b6216592e6b5b245c7e11c0b8323ff125a27a9d4eb1ba5b76b7024c91dc3dd0765c19ec7ae06315f5e719758d3569b0cb6f
This commit is contained in:
commit
27f9f0beab
4 changed files with 446 additions and 108 deletions
|
|
@ -1,14 +1,9 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from test_framework.authproxy import AuthServiceProxy, JSONRPCException
|
||||
from test_framework.authproxy import JSONRPCException
|
||||
from test_framework.daemon import Daemon, sync_all
|
||||
import argparse
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import subprocess
|
||||
import shutil
|
||||
from decimal import Decimal
|
||||
|
||||
## 0. Boilerplate to make the tutorial executable as a script
|
||||
|
|
@ -24,106 +19,6 @@ parser.add_argument('--no-cleanup', default=False, action="store_true")
|
|||
|
||||
args = parser.parse_args()
|
||||
|
||||
class Daemon():
|
||||
"""
|
||||
A class for representing a bitcoind or elementsd node.
|
||||
|
||||
Wraps the process management, creation and deletion of datadirs, and
|
||||
RPC connectivity, into a simple object that will clean itself up on
|
||||
exit. The `cleanup_on_exit` parameter can be set to False to prevent
|
||||
the node from deleting its datadir on restart (and can be set by
|
||||
passing --no-cleanup to the main program).
|
||||
"""
|
||||
|
||||
def __init__(self, name, daemon, path, conf_path, cleanup_on_exit = True):
|
||||
self.name = name
|
||||
self.daemon = daemon
|
||||
self.conf_path = path
|
||||
self.path = path
|
||||
self.cleanup_on_exit = cleanup_on_exit
|
||||
self.conf_path = conf_path
|
||||
self.datadir_path = None
|
||||
self.proc = None
|
||||
self.rpc = None
|
||||
|
||||
# Parse config
|
||||
self.config = {}
|
||||
with open(self.conf_path, encoding="utf8") as f:
|
||||
for line in f:
|
||||
if len(line) == 0 or line[0] == "#" or len(line.split("=")) != 2:
|
||||
continue
|
||||
self.config[line.split("=")[0]] = line.split("=")[1].strip()
|
||||
|
||||
def shutdown(self):
|
||||
if self.proc is not None:
|
||||
print ("Shutting down %s" % self.name)
|
||||
self.proc.terminate()
|
||||
## FIXME determine why we need 30+ seconds to shut down with a tiny regtest chain
|
||||
self.proc.wait(120)
|
||||
self.proc = None
|
||||
|
||||
if self.datadir_path is not None:
|
||||
if self.cleanup_on_exit:
|
||||
shutil.rmtree(self.datadir_path)
|
||||
else:
|
||||
print ("Leaving %s datadir at %s." % (self.name, self.datadir_path))
|
||||
|
||||
def start(self, ext_args = None, keep_datadir = False):
|
||||
if keep_datadir and self.datadir_path is not None:
|
||||
temp = self.datadir_path
|
||||
self.datadir_path = None
|
||||
self.shutdown()
|
||||
self.datadir_path = temp
|
||||
else:
|
||||
self.shutdown()
|
||||
# Create datadir and copy config into place
|
||||
self.datadir_path = tempfile.mkdtemp()
|
||||
shutil.copyfile(self.conf_path, self.datadir_path + '/' + self.daemon + '.conf')
|
||||
print("%s datadir: %s" % (self.name, self.datadir_path))
|
||||
|
||||
# Start process
|
||||
print ("Starting %s" % self.name)
|
||||
if ext_args is None:
|
||||
ext_args = []
|
||||
self.proc = subprocess.Popen([self.path, "-datadir=" + self.datadir_path] + ext_args)
|
||||
self.rpc = AuthServiceProxy("http://" + self.config["rpcuser"] + ":" + self.config["rpcpassword"] + "@127.0.0.1:" + self.config["rpcport"])
|
||||
|
||||
# Give daemon a moment to start up
|
||||
time.sleep(1)
|
||||
|
||||
def connect_to(self, other):
|
||||
self.addnode("localhost:%s" % other.config['port'], "onetry")
|
||||
|
||||
def restart(self, ext_args = None, keep_datadir = False):
|
||||
self.start(ext_args, keep_datadir)
|
||||
|
||||
def __del__(self):
|
||||
self.shutdown()
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Dispatches any unrecognised messages to the RPC connection or a CLI instance."""
|
||||
return self.rpc.__getattr__(name)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Dispatches any keys to the underlying config file"""
|
||||
return self.config[key]
|
||||
|
||||
def sync_all(nodes, timeout_sec = 10):
|
||||
totalWait = timeout_sec
|
||||
|
||||
stop_time = time.time() + timeout_sec
|
||||
while time.time() <= stop_time:
|
||||
best_hash = [x.getbestblockhash() for x in nodes]
|
||||
if best_hash.count(best_hash[0]) == len(nodes):
|
||||
break
|
||||
time.sleep(1)
|
||||
while time.time() <= stop_time:
|
||||
pool = [set(x.getrawmempool()) for x in nodes]
|
||||
if pool.count(pool[0]) == len(nodes):
|
||||
return
|
||||
time.sleep(1)
|
||||
raise Exception("Nodes cannot sync blocks or mempool!")
|
||||
|
||||
# Setup daemons
|
||||
bitcoin = Daemon(
|
||||
"Bitcoin",
|
||||
|
|
|
|||
334
contrib/assets_tutorial/pset_swap_tutorial.py
Executable file
334
contrib/assets_tutorial/pset_swap_tutorial.py
Executable file
|
|
@ -0,0 +1,334 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from test_framework.authproxy import JSONRPCException
|
||||
from test_framework.daemon import Daemon, sync_all
|
||||
import argparse
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
|
||||
## PSET Swap Tutorial
|
||||
#
|
||||
# This script demonstrates how to implement an atomic swap, using a single
|
||||
# Elements transaction, between two parties, Alice and Carol. Alice has
|
||||
# ten thousand BTC and Carol has 1000 of a new asset. After the swap, Alice
|
||||
# will have the new asset and Carol will have the BTC.
|
||||
#
|
||||
# This script can be executed in a Python intepreter. The user is encouraged
|
||||
# to add tracing or debugging code throughout to better understand what is
|
||||
# going on.
|
||||
#
|
||||
|
||||
## 0. Boilerplate to make the tutorial executable as a script
|
||||
#
|
||||
# Skip ahead to step 1 if you are reading
|
||||
#
|
||||
|
||||
# Parse arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--elementsd-dir", type=str, default="./src")
|
||||
parser.add_argument("--no-cleanup", default=False, action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Setup daemons
|
||||
alice = Daemon(
|
||||
"Alice",
|
||||
"elements",
|
||||
args.elementsd_dir + "/elementsd",
|
||||
"contrib/assets_tutorial/elements1.conf",
|
||||
not args.no_cleanup,
|
||||
)
|
||||
|
||||
carol = Daemon(
|
||||
"Carol",
|
||||
"elements",
|
||||
args.elementsd_dir + "/elementsd",
|
||||
"contrib/assets_tutorial/elements2.conf",
|
||||
not args.no_cleanup,
|
||||
)
|
||||
|
||||
## 1. Start nodes
|
||||
print ("1. Start nodes and setup scenario")
|
||||
|
||||
# 1a. Turn on both nodes. Disable -validatepegin as we will just swap the
|
||||
# initialcoins asset with a newly issued asset. Using the peg would
|
||||
# be an inessential distraction.
|
||||
#
|
||||
alice.start(["-validatepegin=0"])
|
||||
carol.start(["-validatepegin=0"])
|
||||
alice.connect_to(carol)
|
||||
carol.connect_to(alice)
|
||||
alice.createwallet("wallet")
|
||||
carol.createwallet("wallet")
|
||||
alice.rescanblockchain()
|
||||
|
||||
# 1b. Split the initial coins so that both sides have some (but Alice has more)
|
||||
alice.sendmany("", { alice.getnewaddress(): 10000 for x in range(0, 10) })
|
||||
alice.sendtoaddress(carol.getnewaddress(), 100000)
|
||||
alice.generatetoaddress(1, alice.getnewaddress())
|
||||
|
||||
# 1c. Issue 1000 units of a new asset. No reissuance tokens. Send them
|
||||
# all to the second node.
|
||||
issue = alice.issueasset(1000, 0)
|
||||
alice.sendtoaddress(address=carol.getnewaddress(), amount=1000, assetlabel=issue["asset"])
|
||||
alice.generatetoaddress(1, alice.getnewaddress())
|
||||
sync_all([alice, carol])
|
||||
|
||||
# 1d. Move the coins around on each wallet so that they do not share
|
||||
# any wallet transaction. (Otherwise they may fill in each others'
|
||||
# UTXO data, which is harmless but makes the tutorial harder to
|
||||
# follow.)
|
||||
alice.sendtoaddress(alice.getnewaddress(), alice.getbalance()["bitcoin"], "", "", True)
|
||||
carol.sendtoaddress(carol.getnewaddress(), carol.getbalance()["bitcoin"], "", "", True)
|
||||
carol.sendtoaddress(address=carol.getnewaddress(), amount=1000, assetlabel=issue["asset"])
|
||||
sync_all([alice, carol])
|
||||
alice.generatetoaddress(1, alice.getnewaddress())
|
||||
sync_all([alice, carol])
|
||||
|
||||
# Define some variables and continue..
|
||||
asset_ALT = issue["asset"]
|
||||
asset_BTC = alice.dumpassetlabels()["bitcoin"]
|
||||
|
||||
assert len(alice.listunspent()) > 0
|
||||
assert len(carol.listunspent()) > 0
|
||||
|
||||
## 2. Construct a swap transaction in PSET format
|
||||
#
|
||||
# At this point node `alice` has 10.5MM "bitcoin" and node `carol` has 1000 of
|
||||
# a new asset. We want to do an atomic swap: 2500 BTC for 1000 of
|
||||
# the new asset.
|
||||
#
|
||||
print ("2. Create an unsigned swap transaction in PSET format.")
|
||||
|
||||
#
|
||||
# Although PSBT2 was designed with coinjoining in mind, the RPC interface
|
||||
# currently does not support iteratively creating a transaction. So we must
|
||||
# create the coinjoin transaction using the raw transaction API.
|
||||
#
|
||||
# Our strategy will be for both parties to create single transactions, then
|
||||
# we will manually combine them.
|
||||
#
|
||||
# This step is fairly complicated and will be significantly simplified by
|
||||
# future RPC improvements.
|
||||
#
|
||||
|
||||
# First, each party generates a new address
|
||||
print ("2a. Exchange addresses")
|
||||
alice_addr = alice.getnewaddress()
|
||||
carol_addr = carol.getnewaddress()
|
||||
print (" Alice: ", alice_addr)
|
||||
print (" Carol: ", carol_addr)
|
||||
|
||||
# Then they each create and fund (but don't sign!) partial transactions
|
||||
# which simply send the assets to each other.
|
||||
print ("2b. Exchange partial transaction data")
|
||||
raw_tx_a = alice.createrawtransaction(outputs=[{carol_addr: 2500}])
|
||||
funded_a = alice.fundrawtransaction(raw_tx_a)['hex']
|
||||
|
||||
raw_tx_c = carol.createrawtransaction(outputs=[{alice_addr: 1000, "asset": asset_ALT}])
|
||||
funded_c = carol.fundrawtransaction(raw_tx_c)['hex']
|
||||
|
||||
# Each party cross-checks the other's transaction
|
||||
print ("2c. Check partial transactions")
|
||||
decoded_a = carol.decoderawtransaction(funded_a)
|
||||
|
||||
carol_spk = carol.validateaddress(carol_addr)['scriptPubKey']
|
||||
found_my_output = False
|
||||
feerate = None
|
||||
for output in decoded_a["vout"]:
|
||||
if output["scriptPubKey"]["type"] == "fee" and output["asset"] == asset_BTC:
|
||||
# Feerate multiplier is 10^8 (btc->sat) / 10^3 (bytes->kb) to get a value in sat/kb
|
||||
feerate = 100000.0 * float(output["value"]) / decoded_a["vsize"]
|
||||
if output["scriptPubKey"]["type"] == "witness_v0_keyhash" \
|
||||
and output["scriptPubKey"]["hex"] == carol_spk \
|
||||
and output["asset"] == asset_BTC \
|
||||
and output["value"] == 2500:
|
||||
found_my_output = True
|
||||
|
||||
assert feerate > 0.1 # should probably compare against your own feerate
|
||||
assert found_my_output
|
||||
|
||||
##
|
||||
## Check that none of the inputs in the counterparty's transaction actually
|
||||
## belong to us. This is the second-most important check (after making sure
|
||||
## that your outputs are present :)) and by far the easiest to forget. Do
|
||||
## not forget this!!!
|
||||
##
|
||||
|
||||
# The way we do this check in Python is by making a set of each
|
||||
# output the wallet can spend...
|
||||
my_unspent = { (x["txid"], x["vout"]) for x in carol.listunspent() if x["spendable"] }
|
||||
# ...then making sure that none of the inputs in the counterparty's
|
||||
# transaction appear in this set.
|
||||
for inp in decoded_a["vin"]:
|
||||
assert not (inp["txid"], inp["vout"]) in my_unspent
|
||||
|
||||
# ...and same on Alice's end. We don't bother repeating the checks
|
||||
# in this script but they should be done.
|
||||
decoded_c = alice.decoderawtransaction(funded_c)
|
||||
|
||||
print ("2d. Create combined transaction")
|
||||
|
||||
#
|
||||
# Once each party has cross-checked the counterparty's transaction, they can
|
||||
# just combine the two. There is no need to communicate here since both parties
|
||||
# will construct the same transaction. To eliminate all ambiguity we sort inputs
|
||||
# by txid:vout and sort outputs by scriptPubKey.
|
||||
#
|
||||
|
||||
inputs = sorted(
|
||||
decoded_a["vin"] + decoded_c["vin"],
|
||||
key = lambda vin: vin["txid"] + str(vin["vout"])
|
||||
)
|
||||
outputs = sorted(
|
||||
decoded_a["vout"] + decoded_c["vout"],
|
||||
key = lambda vout: vout["scriptPubKey"]["hex"]
|
||||
)
|
||||
# Sorting by scriptPubKey will put the fee outputs first
|
||||
first_fee = outputs[0]
|
||||
outputs = outputs[1:]
|
||||
outputs[0]["value"] += first_fee["value"]
|
||||
|
||||
# Determine blinder indices from inputs
|
||||
alice_idx = None
|
||||
carol_idx = None
|
||||
for n, inp in enumerate(inputs):
|
||||
if inp in decoded_a["vin"]:
|
||||
alice_idx = n
|
||||
elif inp in decoded_c["vin"]:
|
||||
carol_idx = n
|
||||
|
||||
# Convert inputs and outputs to a format needed by `createpsbt`
|
||||
inputs_createpsbt = [{"txid": x["txid"], "vout": x["vout"], "sequence": 0xffffffff} for x in inputs]
|
||||
outputs_createpsbt = []
|
||||
|
||||
for out in outputs:
|
||||
blinder_idx = None
|
||||
if out["scriptPubKey"]["type"] == "fee":
|
||||
address = "fee"
|
||||
else:
|
||||
if out in decoded_a["vout"]:
|
||||
blinder_idx = alice_idx
|
||||
elif out in decoded_c["vout"]:
|
||||
blinder_idx = carol_idx
|
||||
|
||||
# The crappy rawtransaction API requires that we reconstruct blinded addresses,
|
||||
# which are split between the "addresses" and "nonce" field
|
||||
address = out["scriptPubKey"]["addresses"][0]
|
||||
if "commitmentnonce" in out:
|
||||
address = alice.createblindedaddress(address, out["commitmentnonce"])
|
||||
|
||||
if blinder_idx is None:
|
||||
outputs_createpsbt.append({address: out["value"], "asset": out["asset"]})
|
||||
else:
|
||||
outputs_createpsbt.append({address: out["value"], "asset": out["asset"], "blinder_index": blinder_idx})
|
||||
|
||||
alice_pset = alice.createpsbt(inputs_createpsbt, outputs_createpsbt)
|
||||
carol_pset = alice.createpsbt(inputs_createpsbt, outputs_createpsbt)
|
||||
assert alice_pset == carol_pset
|
||||
print ("Created PSET: ", alice_pset)
|
||||
|
||||
# Use `analyzepsbt` to see what's happening
|
||||
analysis = alice.analyzepsbt(alice_pset)
|
||||
assert analysis["next"] == "updater"
|
||||
assert not any([x["has_utxo"] for x in analysis["inputs"]])
|
||||
assert not any([x["is_final"] for x in analysis["inputs"]])
|
||||
assert all([x["next"] == "updater" for x in analysis["inputs"]])
|
||||
|
||||
## 3. Update the PSET
|
||||
#
|
||||
# Before blinding can take place, both parties need to provide UTXO data
|
||||
# for their inputs. They can share this data by either updating the PSET
|
||||
# in turn, or by both updating their copies and then one party calls
|
||||
# `combinepsbt` to combine the results.
|
||||
#
|
||||
# We will do the latter for symmetry reasons.
|
||||
#
|
||||
|
||||
print ("3. Both parties fill in their UTXO data.")
|
||||
alice_pset = alice.walletprocesspsbt(alice_pset)["psbt"]
|
||||
carol_pset = carol.walletprocesspsbt(carol_pset)["psbt"]
|
||||
filled_pset = alice.combinepsbt([alice_pset, carol_pset])
|
||||
|
||||
# Use `analyzepsbt` to see what's happening
|
||||
analysis = alice.analyzepsbt(filled_pset)
|
||||
assert analysis["next"] == "blinder"
|
||||
assert all([x["has_utxo"] for x in analysis["inputs"]])
|
||||
assert not any([x["is_final"] for x in analysis["inputs"]])
|
||||
assert all([x["next"] == "signer" for x in analysis["inputs"]])
|
||||
|
||||
## 3. Blind the PSET
|
||||
#
|
||||
# Both parties now blind the PSET. Importantly, the final person to blind must
|
||||
# have a combined PSET that has everyone else's blinding data included, so that
|
||||
# they can make the blinding factors add up.
|
||||
#
|
||||
# In the 2-party case this is particularly simple because there is no need to
|
||||
# combine anything.
|
||||
#
|
||||
|
||||
# Just for fun, try to have both parties blind independently and combine
|
||||
alice_blinded = alice.walletprocesspsbt(filled_pset)
|
||||
carol_blinded = carol.walletprocesspsbt(filled_pset)
|
||||
try:
|
||||
alice.combinepsbt([alice_blinded["psbt"], carol_blinded["psbt"]])
|
||||
except JSONRPCException:
|
||||
pass
|
||||
else:
|
||||
raise Exception("combinepsbt should return 'Cannot combine PSETs as the values and blinders would become imbalanced'")
|
||||
|
||||
# Ok, back to the tutorial
|
||||
print ("3. One party blinds the PSET and passes to the other party, who also blinds")
|
||||
# We set sign=False here, because otherwise carol's `walletprocesspsbt`
|
||||
# will sign the transaction (since after she does her blinding, the
|
||||
# transaction will be completely blinded and therefore signable).
|
||||
# But for this tutorial we want that to happen only in the next step.
|
||||
alice_blinded = alice.walletprocesspsbt(filled_pset)
|
||||
carol_blinded = carol.walletprocesspsbt(psbt=alice_blinded["psbt"], sign=False)
|
||||
blinded = carol_blinded
|
||||
|
||||
# We won't print these out because they're very big now
|
||||
assert not alice_blinded["complete"]
|
||||
assert not carol_blinded["complete"]
|
||||
assert len(blinded["psbt"]) > 5000
|
||||
assert len(blinded["psbt"]) > 10000
|
||||
|
||||
# Use `analyzepsbt` to see what's happening
|
||||
analysis = alice.analyzepsbt(blinded["psbt"])
|
||||
assert analysis["next"] == "signer"
|
||||
assert all([x["has_utxo"] for x in analysis["inputs"]])
|
||||
assert not any([x["is_final"] for x in analysis["inputs"]])
|
||||
assert all([x["next"] == "signer" for x in analysis["inputs"]])
|
||||
|
||||
## 4. Sign the PSET
|
||||
#
|
||||
# When signing, there are a couple options for workflows. Each party can sign
|
||||
# in turn, like we did when blinding, or they can each sign independently and
|
||||
# then combine the results with `combinepsbt`. We choose the latter one (a) for
|
||||
# demonstration purposes.
|
||||
#
|
||||
|
||||
print ("4. Both parties sign the PSET")
|
||||
|
||||
alice_signed = alice.walletprocesspsbt(blinded["psbt"])
|
||||
carol_signed = carol.walletprocesspsbt(blinded["psbt"])
|
||||
assert not alice_signed["complete"]
|
||||
assert not carol_signed["complete"]
|
||||
|
||||
complete = alice.combinepsbt([alice_signed["psbt"], carol_signed["psbt"]])
|
||||
|
||||
# Use `analyzepsbt` to see what's happening
|
||||
analysis = alice.analyzepsbt(complete)
|
||||
assert analysis["next"] == "extractor"
|
||||
assert all([x["has_utxo"] for x in analysis["inputs"]])
|
||||
assert all([x["is_final"] for x in analysis["inputs"]])
|
||||
assert all([x["next"] == "extractor" for x in analysis["inputs"]])
|
||||
|
||||
print ("5. Finalize and Extract")
|
||||
x = alice.finalizepsbt(complete, True)
|
||||
assert x["complete"]
|
||||
# The complete transaction is in x["hex"] but again we won't print it because
|
||||
# it's massive.
|
||||
|
||||
|
||||
|
||||
109
contrib/assets_tutorial/test_framework/daemon.py
Normal file
109
contrib/assets_tutorial/test_framework/daemon.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
|
||||
from test_framework.authproxy import AuthServiceProxy
|
||||
|
||||
import pathlib
|
||||
import tempfile
|
||||
import time
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
class Daemon():
|
||||
"""
|
||||
A class for representing a bitcoind or elementsd node.
|
||||
|
||||
Wraps the process management, creation and deletion of datadirs, and
|
||||
RPC connectivity, into a simple object that will clean itself up on
|
||||
exit. The `cleanup_on_exit` parameter can be set to False to prevent
|
||||
the node from deleting its datadir on restart (and can be set by
|
||||
passing --no-cleanup to the main program).
|
||||
"""
|
||||
|
||||
def __init__(self, name, daemon, path, conf_path, cleanup_on_exit = True):
|
||||
self.name = name
|
||||
self.daemon = daemon
|
||||
self.conf_path = path
|
||||
self.path = path
|
||||
self.cleanup_on_exit = cleanup_on_exit
|
||||
self.conf_path = conf_path
|
||||
self.datadir_path = None
|
||||
self.proc = None
|
||||
self.rpc = None
|
||||
|
||||
# Parse config
|
||||
self.config = {}
|
||||
with open(self.conf_path, encoding="utf8") as f:
|
||||
for line in f:
|
||||
if len(line) == 0 or line[0] == "#" or len(line.split("=")) != 2:
|
||||
continue
|
||||
self.config[line.split("=")[0]] = line.split("=")[1].strip()
|
||||
|
||||
def shutdown(self):
|
||||
if self.proc is not None:
|
||||
print ("Shutting down %s" % self.name)
|
||||
self.proc.terminate()
|
||||
## FIXME determine why we need 30+ seconds to shut down with a tiny regtest chain
|
||||
self.proc.wait(120)
|
||||
self.proc = None
|
||||
|
||||
if self.datadir_path is not None:
|
||||
if self.cleanup_on_exit:
|
||||
shutil.rmtree(self.datadir_path)
|
||||
else:
|
||||
print ("Leaving %s datadir at %s." % (self.name, self.datadir_path))
|
||||
|
||||
def start(self, ext_args = None, keep_datadir = False):
|
||||
if keep_datadir and self.datadir_path is not None:
|
||||
temp = self.datadir_path
|
||||
self.datadir_path = None
|
||||
self.shutdown()
|
||||
self.datadir_path = temp
|
||||
else:
|
||||
self.shutdown()
|
||||
# Create datadir and copy config into place
|
||||
self.datadir_path = tempfile.mkdtemp()
|
||||
shutil.copyfile(self.conf_path, self.datadir_path + '/' + self.daemon + '.conf')
|
||||
print("%s datadir: %s" % (self.name, self.datadir_path))
|
||||
|
||||
# Start process
|
||||
print ("Starting %s" % self.name)
|
||||
if ext_args is None:
|
||||
ext_args = []
|
||||
self.proc = subprocess.Popen([self.path, "-datadir=" + self.datadir_path] + ext_args)
|
||||
self.rpc = AuthServiceProxy("http://" + self.config["rpcuser"] + ":" + self.config["rpcpassword"] + "@127.0.0.1:" + self.config["rpcport"])
|
||||
|
||||
# Give daemon a moment to start up
|
||||
time.sleep(1)
|
||||
|
||||
def connect_to(self, other):
|
||||
self.addnode("localhost:%s" % other.config['port'], "onetry")
|
||||
|
||||
def restart(self, ext_args = None, keep_datadir = False):
|
||||
self.start(ext_args, keep_datadir)
|
||||
|
||||
def __del__(self):
|
||||
self.shutdown()
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Dispatches any unrecognised messages to the RPC connection or a CLI instance."""
|
||||
return self.rpc.__getattr__(name)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Dispatches any keys to the underlying config file"""
|
||||
return self.config[key]
|
||||
|
||||
def sync_all(nodes, timeout_sec = 10):
|
||||
totalWait = timeout_sec
|
||||
|
||||
stop_time = time.time() + timeout_sec
|
||||
while time.time() <= stop_time:
|
||||
best_hash = [x.getbestblockhash() for x in nodes]
|
||||
if best_hash.count(best_hash[0]) == len(nodes):
|
||||
break
|
||||
time.sleep(1)
|
||||
while time.time() <= stop_time:
|
||||
pool = [set(x.getrawmempool()) for x in nodes]
|
||||
if pool.count(pool[0]) == len(nodes):
|
||||
return
|
||||
time.sleep(1)
|
||||
raise Exception("Nodes cannot sync blocks or mempool!")
|
||||
|
||||
|
|
@ -22,6 +22,7 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx)
|
|||
bool has_blinded_outputs = false;
|
||||
|
||||
result.inputs.resize(psbtx.inputs.size());
|
||||
result.next = PSBTRole::EXTRACTOR;
|
||||
|
||||
for (unsigned int i = 0; i < psbtx.inputs.size(); ++i) {
|
||||
PSBTInput& input = psbtx.inputs[i];
|
||||
|
|
@ -150,7 +151,6 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx)
|
|||
}
|
||||
|
||||
// Calculate next role for PSBT by grabbing "minimum" PSBTInput next role
|
||||
result.next = PSBTRole::EXTRACTOR;
|
||||
for (unsigned int i = 0; i < psbtx.inputs.size(); ++i) {
|
||||
PSBTInputAnalysis& input_analysis = result.inputs[i];
|
||||
result.next = std::min(result.next, input_analysis.next);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue