mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-13 12:33:42 +02:00
Add contrib/fedpeg for fedpeg signers
This commit is contained in:
parent
fd7f08ac21
commit
f8977fa5eb
4 changed files with 912 additions and 0 deletions
58
contrib/fedpeg/blocksign.py
Executable file
58
contrib/fedpeg/blocksign.py
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env python2
|
||||
|
||||
import sys, os, json, traceback, decimal
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../python-bitcoinrpc"))
|
||||
from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException
|
||||
from rotating_consensus import RotatingConsensus
|
||||
from threading import Lock, current_thread
|
||||
from time import sleep
|
||||
from constants import FedpegConstants
|
||||
from httplib import CannotSendRequest
|
||||
|
||||
|
||||
settings = FedpegConstants()
|
||||
port = 14252
|
||||
|
||||
sidechain = AuthServiceProxy(settings.sidechain_url)
|
||||
|
||||
class WatchPeerController(RotatingConsensus):
|
||||
round_local_block_hex = ""
|
||||
|
||||
def gen_master_msg(self):
|
||||
global sidechain
|
||||
try:
|
||||
self.round_local_block_hex = sidechain.getnewblockhex()
|
||||
except CannotSendRequest as e:
|
||||
sidechain = AuthServiceProxy(settings.sidechain_url)
|
||||
return None
|
||||
return self.round_local_block_hex
|
||||
|
||||
def recv_master_msg(self, msg):
|
||||
self.round_local_block_hex = msg
|
||||
return sidechain.signblock(msg)
|
||||
|
||||
def round_done(self, peer_messages):
|
||||
mysig = sidechain.signblock(self.round_local_block_hex)
|
||||
peer_messages.append(("self", mysig))
|
||||
sys.stdout.write("Got signatures from %s, now combining..." % str([x[0] for x in peer_messages]))
|
||||
sys.stdout.flush()
|
||||
res = sidechain.combineblocksigs(self.round_local_block_hex, [x[1] for x in peer_messages])
|
||||
if res["complete"]:
|
||||
sys.stdout.write("got completely signed block, submitting to sidechaind...")
|
||||
sys.stdout.flush()
|
||||
sidechain.submitblock(res["hex"])
|
||||
print("done")
|
||||
else:
|
||||
print("got incomplete block")
|
||||
|
||||
def round_failed(self):
|
||||
self.round_local_block_hex = ""
|
||||
return
|
||||
|
||||
sidechain.importprivkey(settings.blocksigning_private_key)
|
||||
|
||||
settings.nodes.remove(settings.my_node)
|
||||
WatchPeerController(settings.nodes, settings.my_node, port, 60, settings.socks_proxy)
|
||||
|
||||
while True:
|
||||
sleep(10)
|
||||
46
contrib/fedpeg/constants.py
Executable file
46
contrib/fedpeg/constants.py
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env python2
|
||||
|
||||
import os
|
||||
|
||||
class FedpegConstants:
|
||||
# VARIOUS SETTINGS...
|
||||
sidechain_url = "http://user:pass@127.0.0.1:4241"
|
||||
bitcoin_url = "http://user:pass@127.0.0.1:18332"
|
||||
|
||||
redeem_script = "53210300ce2bc14b316474085b4037d126e948042d625f15502c7c9fab01a1a219e79b21021df31471281d4478df85bfce08a10aab82601dca949a79950f8ddf7002bd915a2103b426a9d88f3116fb4beeb4ccadaca9a52071cb8745c469c7b06a9141f9d54b3021023f37702bbe29dccba705cbb38fcdc3b1c19671693fdd3d84707d738a4625598d2103f08409b15dc6cc051b8a640d14c90eee98312d56d1fd4032408d12646acb73b255ae"
|
||||
redeem_script_address = "2N3cy1E6jX1penE8QVsV1piyikvooib4ZCA"
|
||||
secondScriptPubKeyHash = "9eac001049d5c38ece8996485418421f4a01e2d7"
|
||||
|
||||
blocksigning_private_key = "FILL_ME_IN"
|
||||
functionary_private_key = "FILL_ME_IN"
|
||||
|
||||
bitcoin_tx_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../bitcoin-tx")
|
||||
contracthashtool_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../contracthashtool/contracthashtool")
|
||||
is_testnet = 1
|
||||
|
||||
#Bitcoin:
|
||||
bitcoin_genesis_hash = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
|
||||
#Testnet:
|
||||
#bitcoin_genesis_hash = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
|
||||
|
||||
nodes = ["127.0.0.1"]
|
||||
my_node = "FILL_ME_IN"
|
||||
|
||||
# Set this to non-None if you're using a proxy (eg for Tor)
|
||||
# Note that this requires ZMQ 4.1
|
||||
socks_proxy = None
|
||||
#socks_proxy = "127.0.0.1:9050"
|
||||
|
||||
def __init__(self):
|
||||
# Derived constants (dont touch)
|
||||
self.testnet_arg = ""
|
||||
if self.is_testnet == 1:
|
||||
self.cht_testnet_arg = "-t"
|
||||
self.btc_testnet_arg = "-testnet"
|
||||
else:
|
||||
self.cht_testnet_arg = ""
|
||||
self.btc_testnet_arg = ""
|
||||
|
||||
self.sigs_required = int(self.redeem_script[:2], 16) - 0x50
|
||||
|
||||
self.inverse_bitcoin_genesis_hash = "".join(reversed([self.bitcoin_genesis_hash[i:i+2] for i in range(0, len(self.bitcoin_genesis_hash), 2)]))
|
||||
181
contrib/fedpeg/rotating_consensus.py
Normal file
181
contrib/fedpeg/rotating_consensus.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
#!/usr/bin/env python2
|
||||
|
||||
from time import sleep, time
|
||||
import socket
|
||||
import threading
|
||||
import zmq
|
||||
import traceback
|
||||
|
||||
|
||||
# For error printing
|
||||
import sys, os
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../python-bitcoinrpc"))
|
||||
from bitcoinrpc.authproxy import JSONRPCException
|
||||
|
||||
zmq_context = zmq.Context()
|
||||
zmq_poller = zmq.Poller()
|
||||
|
||||
if (zmq.zmq_version() < 4):
|
||||
print("It is highly recommended you use a version of ZMQ > 4")
|
||||
|
||||
class ConsensusPublisher:
|
||||
def __init__(self, port):
|
||||
self.socket = zmq_context.socket(zmq.PUB)
|
||||
self.socket.bind("tcp://*:%d" % port)
|
||||
zmq_poller.register(self.socket, zmq.POLLOUT)
|
||||
|
||||
def send_message(self, msg):
|
||||
self.socket.send("42 %s" % msg.encode("ascii", "strict"))
|
||||
|
||||
class ConsensusSocket:
|
||||
def __init__(self, host, port, proxy):
|
||||
self.host = host
|
||||
self.isSelf = False
|
||||
self.sock = zmq_context.socket(zmq.SUB)
|
||||
if proxy != None:
|
||||
self.sock.setsockopt(zmq.SOCKS_PROXY, proxy)
|
||||
self.sock.setsockopt(zmq.RECONNECT_IVL, 500)
|
||||
self.sock.setsockopt(zmq.RECONNECT_IVL_MAX, 10000)
|
||||
self.sock.connect("tcp://%s:%d" % (host, port))
|
||||
self.sock.setsockopt(zmq.SUBSCRIBE, "42")
|
||||
zmq_poller.register(self.sock, zmq.POLLIN)
|
||||
|
||||
def read_message(self):
|
||||
if not dict(zmq_poller.poll()).has_key(self.sock):
|
||||
return None
|
||||
topic, msg = self.sock.recv().split(" ", 1)
|
||||
return msg
|
||||
|
||||
class Self:
|
||||
def __init__(self, host):
|
||||
self.host = host
|
||||
self.isSelf = True
|
||||
def read_message(self):
|
||||
return None
|
||||
|
||||
class RotatingConsensus:
|
||||
def __init__(self, nodes_list, my_host, port, interval, proxy):
|
||||
self.interval = interval
|
||||
self.nodes = [Self(my_host)]
|
||||
for host in nodes_list:
|
||||
self.nodes.append(ConsensusSocket(host, port, proxy))
|
||||
self.nodes.sort(key=lambda node: node.host)
|
||||
self.publisher = ConsensusPublisher(port)
|
||||
thread = threading.Thread(target=self.main_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def main_loop(self):
|
||||
while True:
|
||||
sleep(self.interval - time() % self.interval)
|
||||
start_time = int(time())
|
||||
step = int(time()) % (self.interval * len(self.nodes)) / self.interval
|
||||
|
||||
for node in self.nodes:
|
||||
msg = ""
|
||||
while msg != None:
|
||||
msg = node.read_message()
|
||||
|
||||
if self.nodes[step].isSelf:
|
||||
print("Starting master round (as %s)" % self.nodes[step].host)
|
||||
|
||||
sleep(self.interval / 10)
|
||||
msg = self._gen_master_msg()
|
||||
if msg == None:
|
||||
print("gen_master_msg threw or returned None")
|
||||
self._round_failed()
|
||||
continue
|
||||
if time() - start_time > self.interval / 5:
|
||||
print("gen_master_msg took longer than interval/5: Skipping round!")
|
||||
self._round_failed()
|
||||
continue
|
||||
self.publisher.send_message(msg)
|
||||
sleep(self.interval / 2 - (time() - start_time))
|
||||
|
||||
else:
|
||||
print("Starting round with master %s" % self.nodes[step].host)
|
||||
sleep(self.interval / 4)
|
||||
msg = self.nodes[step].read_message()
|
||||
|
||||
if msg == None:
|
||||
print("Missed message from master")
|
||||
self._round_failed()
|
||||
continue
|
||||
|
||||
broadcast_msg = self._recv_master_msg(msg)
|
||||
if broadcast_msg == None:
|
||||
print("recv_master_msg threw or returned None")
|
||||
self._round_failed()
|
||||
continue
|
||||
if time() - start_time > self.interval / 2:
|
||||
print("recv_master_msg took longer than interval/4: Skipping round!")
|
||||
self._round_failed()
|
||||
continue
|
||||
self.publisher.send_message(broadcast_msg)
|
||||
|
||||
sleep(self.interval / 2 - (time() - start_time))
|
||||
|
||||
msgs = []
|
||||
for node in self.nodes:
|
||||
msg = node.read_message()
|
||||
if msg != None:
|
||||
msgs.append((node.host, msg))
|
||||
|
||||
self._round_done(msgs)
|
||||
if time() > start_time + self.interval:
|
||||
print("round_done took longer than interval/2: We skipped a round!")
|
||||
|
||||
def _gen_master_msg(self):
|
||||
try:
|
||||
return self.gen_master_msg()
|
||||
except Exception as e:
|
||||
if isinstance(e, JSONRPCException):
|
||||
print(e.error)
|
||||
print("gen_master_msg threw!")
|
||||
print(traceback.format_exc())
|
||||
return None
|
||||
|
||||
def _recv_master_msg(self, msg):
|
||||
try:
|
||||
return self.recv_master_msg(msg)
|
||||
except Exception as e:
|
||||
if isinstance(e, JSONRPCException):
|
||||
print(e.error)
|
||||
print("recv_master_msg threw!")
|
||||
print(traceback.format_exc())
|
||||
return None
|
||||
|
||||
def _round_done(self, peer_messages):
|
||||
try:
|
||||
self.round_done(peer_messages)
|
||||
except Exception as e:
|
||||
if isinstance(e, JSONRPCException):
|
||||
print(e.error)
|
||||
print("round_done threw!")
|
||||
print(traceback.format_exc())
|
||||
|
||||
def _round_failed(self):
|
||||
try:
|
||||
self.round_failed()
|
||||
except Exception as e:
|
||||
if isinstance(e, JSONRPCException):
|
||||
print(e.error)
|
||||
print("round_failed threw!")
|
||||
print(traceback.format_exc())
|
||||
|
||||
#OVERRIDE THESE:
|
||||
|
||||
def gen_master_msg(self):
|
||||
return "MASTER INITIAL BROADCAST"
|
||||
|
||||
def recv_master_msg(self, msg):
|
||||
print("GOT '%s' from master" % msg)
|
||||
return "PEER RESPONSE BROADCAST"
|
||||
|
||||
def round_done(self, peer_messages):
|
||||
print("Finished round...")
|
||||
for msg in peer_messages:
|
||||
print("Got %s from %s" % (msg[1], msg[0]))
|
||||
|
||||
def round_failed(self):
|
||||
return
|
||||
627
contrib/fedpeg/withdrawwatch.py
Executable file
627
contrib/fedpeg/withdrawwatch.py
Executable file
|
|
@ -0,0 +1,627 @@
|
|||
#!/usr/bin/env python2
|
||||
|
||||
import sys, os, json, traceback, decimal
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../python-bitcoinrpc"))
|
||||
from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException
|
||||
from rotating_consensus import RotatingConsensus
|
||||
from threading import Lock, current_thread
|
||||
from time import sleep
|
||||
from constants import FedpegConstants
|
||||
|
||||
from httplib import CannotSendRequest
|
||||
import socket
|
||||
|
||||
settings = FedpegConstants()
|
||||
port = 14242
|
||||
|
||||
sidechain = [AuthServiceProxy(settings.sidechain_url), AuthServiceProxy(settings.sidechain_url)]
|
||||
# We need to do a rescan on bitcoin, so we set a huge timeout
|
||||
bitcoin = [AuthServiceProxy(settings.bitcoin_url, timeout=60*10), AuthServiceProxy(settings.bitcoin_url)]
|
||||
|
||||
spent_from_history = {}
|
||||
|
||||
open('spent_from.log', 'a').close() # Touch file (create if not already present)
|
||||
with open('spent_from.log') as f:
|
||||
for line in f.readlines():
|
||||
l = eval(line)
|
||||
if l[0] not in spent_from_history:
|
||||
spent_from_history[l[0]] = set()
|
||||
spent_from_history[l[0]].add(l[1])
|
||||
|
||||
spent_from_log = os.open("spent_from.log", os.O_CREAT | os.O_WRONLY | os.O_SYNC | os.O_DSYNC | os.O_APPEND)
|
||||
|
||||
def check_reset_connections():
|
||||
global sidechain, bitcoin
|
||||
connections_good = True
|
||||
|
||||
try:
|
||||
sidechain[thread_id()].getblockcount()
|
||||
except CannotSendRequest as e:
|
||||
sidechain[thread_id()] = AuthServiceProxy(settings.sidechain_url)
|
||||
connections_good = False
|
||||
except socket.timeout as e:
|
||||
sidechain[thread_id()] = AuthServiceProxy(settings.sidechain_url)
|
||||
connections_good = False
|
||||
|
||||
try:
|
||||
bitcoin[thread_id()].getblockcount()
|
||||
except CannotSendRequest as e:
|
||||
bitcoin[thread_id()] = AuthServiceProxy(settings.bitcoin_url)
|
||||
connections_good = False
|
||||
except socket.timeout as e:
|
||||
bitcoin[thread_id()] = AuthServiceProxy(settings.bitcoin_url)
|
||||
connections_good = False
|
||||
|
||||
return connections_good
|
||||
|
||||
# If there are two outputs to the same destination, the first output must fully
|
||||
# confirm before we allow the second to process.
|
||||
# This is really for ease of developer headache, though we could change some
|
||||
# indexes and allow this
|
||||
|
||||
map_lock = Lock()
|
||||
# withdraw metadata map: {"txid_concat": sidechain_txid_concat, "sidechain_height": height,
|
||||
# "script_gen": p2sh_script_in_asm_for_bitcoin-tx, "script_match": p2sh_script_in_hex,
|
||||
# "value": value, "spent_from": set({frozenset({(bitcoin_txid, bitcoin_vout), ...}), ...})}
|
||||
# (spent_from is a set of sets of inputs used in every tx which was signed signed and had output)
|
||||
# sidechain txid_concat -> withdraw metadata map
|
||||
outputs_pending = {}
|
||||
# withdraw_target_p2sh_script_hex -> txid_concat (for withdraw_claims_pending use)
|
||||
outputs_pending_by_p2sh_hex = {}
|
||||
# withdraw_target_p2sh_script_hex -> [withdraw metadata map, ...]
|
||||
outputs_waiting = {}
|
||||
|
||||
# utxo metadata map: {"redeem_info": redeem_info_for_bitcoin_signrawtransaction, "privateKey": gen_private_key, "value": Decimal(value),
|
||||
# "spent_by": set(), "donated_map": {frozenset({(bitcoin_txid, bitcoin_vout), ...}): value} }
|
||||
# spent_by is a set of sidechain txid_concats which can be used to look up in outputs_pending
|
||||
# donated_map is a map from input sets to the value taken from donated_funds as a fee
|
||||
# (bitcoin_txid, bitcoin_vout) -> utxo metadata map
|
||||
utxos = {}
|
||||
|
||||
#set of sets of txos we need to ensure are spent by fraud proofs
|
||||
fraud_check_map = {}
|
||||
|
||||
donated_funds = 0
|
||||
|
||||
manual_check_lock = Lock()
|
||||
manual_check_set = set()
|
||||
|
||||
main_thread = current_thread()
|
||||
def thread_id():
|
||||
if current_thread() == main_thread:
|
||||
return 0
|
||||
return 1
|
||||
|
||||
def check_raise(cond):
|
||||
if not cond:
|
||||
raise Exception("assertion failed")
|
||||
|
||||
def trigger_bitcoin_rescan():
|
||||
# TODO: Replace with a really random one, instead
|
||||
cht = os.popen("%s %s -c -p %s -a SALT -n %s" % (settings.contracthashtool_path, settings.cht_testnet_arg, settings.functionary_private_key, os.urandom(16).encode("hex")))
|
||||
useless_private_key = cht.read().split("\n")[0 + settings.is_testnet][16:]
|
||||
check_raise(cht.close() == None)
|
||||
# Trigger a rescan by importing something useless and new
|
||||
sys.stdout.write("Now triggering a full wallet rescan of the bitcoin chain...")
|
||||
sys.stdout.flush()
|
||||
bitcoin[thread_id()].importprivkey(useless_private_key, "", True)
|
||||
print("done")
|
||||
|
||||
|
||||
def process_bitcoin_tx_for_utxos(tx, is_donation=False, manual_check=False):
|
||||
global donated_funds
|
||||
|
||||
manual_check_lock.acquire()
|
||||
if not manual_check and tx["txid"] in manual_check_set:
|
||||
manual_check_set.remove(tx["txid"])
|
||||
return
|
||||
elif manual_check:
|
||||
manual_check_set.add(tx["txid"])
|
||||
manual_check_lock.release()
|
||||
|
||||
# Go through the outputs, adding any coins sent to the raw functionary address to utxos
|
||||
for nout, outp in enumerate(tx["vout"]):
|
||||
if outp["scriptPubKey"]["type"] == "scripthash" and outp["scriptPubKey"]["addresses"][0] == settings.redeem_script_address:
|
||||
txo = tx["vout"][nout]
|
||||
map_lock.acquire()
|
||||
|
||||
print("Got %s UTXO sent to raw functioanry address (change or donation): %s:%d" % ("new" if (tx["txid"], nout) not in utxos else "existing", tx["txid"], nout))
|
||||
utxos[(tx["txid"], nout)] = {"redeem_info": {"txid": tx["txid"], "vout": nout, "scriptPubKey": outp["scriptPubKey"]["hex"], "redeemScript": settings.redeem_script}, "privateKey": settings.functionary_private_key, "value": decimal.Decimal(outp["value"]), "spent_by": set(), "donated_map": {}}
|
||||
|
||||
if is_donation:
|
||||
print("Got donation of %s, now possibly paying fees" % str(outp["value"]))
|
||||
donated_funds = donated_funds + outp["value"]
|
||||
|
||||
map_lock.release()
|
||||
|
||||
|
||||
def sign_withdraw_tx(tx_hex, txid_concat_list):
|
||||
global donated_funds
|
||||
|
||||
tx_raw = bitcoin[thread_id()].decoderawtransaction(tx_hex)
|
||||
max_sidechain_height = sidechain[thread_id()].getblockcount() - 6
|
||||
|
||||
check_raise(len(tx_raw["vout"]) == len(txid_concat_list) + 1)
|
||||
check_raise(tx_raw["vout"][-1]["scriptPubKey"]["type"] == "scripthash")
|
||||
check_raise(tx_raw["vout"][-1]["scriptPubKey"]["addresses"][0] == settings.redeem_script_address)
|
||||
|
||||
tx_value = decimal.Decimal(0)
|
||||
privKeys = []
|
||||
redeemScripts = []
|
||||
inputs_set = set()
|
||||
input_size = 0
|
||||
for inp in tx_raw["vin"]:
|
||||
if (inp["txid"], inp["vout"]) not in utxos:
|
||||
# To-functionary UTXOs are only added after sufficient confirmations,
|
||||
# so we may need to find them here.
|
||||
spent_tx = bitcoin[thread_id()].getrawtransaction(inp["txid"], 1)
|
||||
process_bitcoin_tx_for_utxos(spent_tx, manual_check=True)
|
||||
|
||||
check_raise((inp["txid"], inp["vout"]) in utxos)
|
||||
utxo = utxos[(inp["txid"], inp["vout"])]
|
||||
redeemScripts.append(utxo["redeem_info"])
|
||||
privKeys.append(utxo["privateKey"])
|
||||
tx_value = tx_value + decimal.Decimal(utxo["value"])
|
||||
|
||||
inputs_set.add((inp["txid"], inp["vout"]))
|
||||
input_size = input_size + len(inp["scriptSig"]["hex"])/2
|
||||
if len(inp["scriptSig"]["hex"])/2 >= 0xfd:
|
||||
input_size += 2
|
||||
|
||||
txid_concat_set = set()
|
||||
for i, txid_concat in enumerate(txid_concat_list):
|
||||
check_raise(txid_concat in outputs_pending)
|
||||
output = outputs_pending[txid_concat]
|
||||
check_raise(output["sidechain_height"] <= max_sidechain_height)
|
||||
|
||||
tx_vout = tx_raw["vout"][i]
|
||||
check_raise(tx_vout["scriptPubKey"]["hex"] == output["script_match"])
|
||||
check_raise(decimal.Decimal(tx_vout["value"]) == output["value"])
|
||||
tx_value = tx_value - decimal.Decimal(tx_vout["value"])
|
||||
for input_set in output["spent_from"]:
|
||||
check_raise(not inputs_set.isdisjoint(input_set))
|
||||
|
||||
txid_concat_set.add(txid_concat)
|
||||
|
||||
# scriptSig is OP_0 x*(1-byte pushlen + 73-byte max-sized signature) + redeemScript push
|
||||
# if it triggers a long var-int for the scriptlen we have to include that, too
|
||||
RS_push_size = len(settings.redeem_script) / 2
|
||||
RS_push_size += 1 if RS_push_size <= 0x4b else (2 if RS_push_size <= 0xff else 3)
|
||||
scriptSig_size = 1 + 74 * settings.sigs_required + RS_push_size
|
||||
if scriptSig_size >= 0xfd:
|
||||
scriptSig_size += 2
|
||||
|
||||
fee_allowed = len(tx_hex)/2 - input_size + scriptSig_size * len(tx_raw["vin"])
|
||||
fee_allowed = min(fee_allowed, donated_funds * 100000000)
|
||||
fee_paid = tx_value - decimal.Decimal(tx_raw["vout"][-1]["value"])
|
||||
check_raise(fee_paid * 100000000 <= fee_allowed)
|
||||
|
||||
donated_funds = donated_funds - fee_paid
|
||||
|
||||
inputs_set = frozenset(inputs_set)
|
||||
|
||||
for txid_concat in txid_concat_list:
|
||||
output = outputs_pending[txid_concat]
|
||||
if inputs_set not in output["spent_from"]:
|
||||
output["spent_from"].add(inputs_set)
|
||||
os.write(spent_from_log, "[%s, %s]\n" % (txid_concat, repr(inputs_set)))
|
||||
|
||||
old_paid_memory = -1
|
||||
for inp in tx_raw["vin"]:
|
||||
utxo = utxos[(inp["txid"], inp["vout"])]
|
||||
utxo["spent_by"] = utxo["spent_by"] | txid_concat_set
|
||||
old_paid = 0
|
||||
if inputs_set in utxo["donated_map"]:
|
||||
old_paid = utxo["donated_map"][inputs_set]
|
||||
if old_paid_memory == -1:
|
||||
old_paid_memory = old_paid
|
||||
elif old_paid != old_paid_memory:
|
||||
print("Internal data structure inconsistency!")
|
||||
sys.exit(1)
|
||||
utxo["donated_map"][inputs_set] = fee_paid + old_paid
|
||||
|
||||
return bitcoin[thread_id()].signrawtransaction(tx_hex, redeemScripts, privKeys)["hex"]
|
||||
|
||||
|
||||
class WatchPeerController(RotatingConsensus):
|
||||
round_local_tx_hex = ""
|
||||
|
||||
def gen_master_msg(self):
|
||||
if not check_reset_connections():
|
||||
return None
|
||||
|
||||
map_lock.acquire()
|
||||
try:
|
||||
|
||||
max_sidechain_height = sidechain[thread_id()].getblockcount() - 8
|
||||
txid_concat_list_untried = []
|
||||
txid_concat_list_retries = []
|
||||
command_untried = '%s %s -create' % (settings.bitcoin_tx_path, settings.btc_testnet_arg)
|
||||
command_retries = command_untried
|
||||
input_sets_retries = set()
|
||||
input_pairs_retries = set()
|
||||
for txid_concat in outputs_pending:
|
||||
output = outputs_pending[txid_concat]
|
||||
if output["sidechain_height"] > max_sidechain_height:
|
||||
continue
|
||||
if len(output["spent_from"]) == 0:
|
||||
command_untried = command_untried + ' outscript=%.16g:"%s"' % (output["value"], output["script_gen"])
|
||||
txid_concat_list_untried.append(txid_concat)
|
||||
elif len(txid_concat_list_untried) == 0:
|
||||
all_still_spendable = True
|
||||
for input_set in output["spent_from"]:
|
||||
for input_pair in input_set:
|
||||
if bitcoin[thread_id()].gettxout(input_pair[0], input_pair[1], True) == None:
|
||||
all_still_spendable = False
|
||||
break
|
||||
if not all_still_spendable:
|
||||
break
|
||||
if all_still_spendable:
|
||||
command_retries = command_retries + ' outscript=%.16g:"%s"' % (output["value"], output["script_gen"])
|
||||
txid_concat_list_retries.append(txid_concat)
|
||||
input_sets_retries = input_sets_retries | output["spent_from"]
|
||||
for input_set in output["spent_from"]:
|
||||
input_pairs_retries = input_pairs_retries | input_set
|
||||
|
||||
if len(txid_concat_list_untried) != 0:
|
||||
txid_concat_list = txid_concat_list_untried
|
||||
command = command_untried
|
||||
elif len(txid_concat_list_retries) != 0:
|
||||
inputs_required = []
|
||||
while len(input_sets_retries) != 0:
|
||||
e = max(input_pairs_retries, key=lambda x: len([i for i in input_sets_retries if x in i]))
|
||||
inputs_required.append(e)
|
||||
input_sets_retries = set([x for x in input_sets_retries if e not in x])
|
||||
for input_pair in inputs_required:
|
||||
command_retries = command_retries + ' in="%s":%d' % (input_pair[0], input_pair[1])
|
||||
|
||||
txid_concat_list = txid_concat_list_retries
|
||||
command = command_retries
|
||||
else:
|
||||
return None
|
||||
|
||||
cht = os.popen(command)
|
||||
tx_hex = cht.read().split("\n")[0]
|
||||
check_raise(cht.close() == None)
|
||||
|
||||
funded_tx = bitcoin[thread_id()].fundrawtransaction(tx_hex, True)
|
||||
tx_raw = bitcoin[thread_id()].decoderawtransaction(funded_tx["hex"])
|
||||
change_value = decimal.Decimal(funded_tx["fee"]) + decimal.Decimal(tx_raw["vout"][funded_tx["changepos"]]["value"])
|
||||
|
||||
cht = os.popen('%s %s %s delout=%d outaddr=%s:%s' % (settings.bitcoin_tx_path, settings.btc_testnet_arg, funded_tx["hex"], funded_tx["changepos"], "0", settings.redeem_script_address))
|
||||
tx_hex = cht.read().split("\n")[0]
|
||||
check_raise(cht.close() == None)
|
||||
|
||||
redeem_script_push_size = len(settings.redeem_script)/2
|
||||
if redeem_script_push_size <= 0x4b:
|
||||
redeem_script_push_size += 1
|
||||
elif redeem_script_push_size <= 0xff:
|
||||
redeem_script_push_size += 2
|
||||
else:
|
||||
redeem_script_push_size += 3
|
||||
|
||||
input_size = 1 + 74 * settings.sigs_required + redeem_script_push_size
|
||||
if input_size >= 0xfd:
|
||||
input_size += 2
|
||||
|
||||
pay_fee = decimal.Decimal(len(tx_hex)/2 + input_size * len(tx_raw["vin"])) / decimal.Decimal(100000000)
|
||||
pay_fee = min(pay_fee, funded_tx["fee"])
|
||||
if pay_fee > donated_funds:
|
||||
pay_fee = 0
|
||||
print("Paying fee of %s" % str(pay_fee))
|
||||
change_value = change_value - pay_fee
|
||||
|
||||
cht = os.popen('%s %s %s delout=%d outaddr=%s:%s' % (settings.bitcoin_tx_path, settings.btc_testnet_arg, tx_hex, len(tx_raw["vout"]) - 1, change_value, settings.redeem_script_address))
|
||||
tx_hex = cht.read().split("\n")[0]
|
||||
check_raise(cht.close() == None)
|
||||
|
||||
self.round_local_tx_hex = sign_withdraw_tx(tx_hex, txid_concat_list)
|
||||
|
||||
return json.dumps([self.round_local_tx_hex, txid_concat_list])
|
||||
finally:
|
||||
map_lock.release()
|
||||
|
||||
def recv_master_msg(self, msg):
|
||||
msg_decoded = json.loads(msg)
|
||||
map_lock.acquire()
|
||||
try:
|
||||
self.round_local_tx_hex = sign_withdraw_tx(msg_decoded[0], msg_decoded[1])
|
||||
return self.round_local_tx_hex
|
||||
finally:
|
||||
map_lock.release()
|
||||
|
||||
def round_done(self, peer_messages):
|
||||
txn_concat = self.round_local_tx_hex
|
||||
check_raise(txn_concat != "")
|
||||
|
||||
input_list = []
|
||||
for inp in bitcoin[thread_id()].decoderawtransaction(txn_concat)["vin"]:
|
||||
input_list.append((inp["txid"], inp["vout"]))
|
||||
|
||||
for msg in peer_messages:
|
||||
try:
|
||||
for i, inp in enumerate(bitcoin[thread_id()].decoderawtransaction(msg[1])["vin"]):
|
||||
check_raise(input_list[i] == (inp["txid"], inp["vout"]))
|
||||
txn_concat = txn_concat + msg[1]
|
||||
except:
|
||||
print("Peer %s sent invalid transaction" % msg[0])
|
||||
|
||||
res = bitcoin[thread_id()].signrawtransaction(txn_concat)
|
||||
print("Final round result:")
|
||||
print(res)
|
||||
|
||||
if res["complete"]:
|
||||
bitcoin[thread_id()].sendrawtransaction(res["hex"])
|
||||
return
|
||||
|
||||
def round_failed(self):
|
||||
self.round_local_tx_hex = ""
|
||||
return
|
||||
|
||||
|
||||
def process_sidechain_tx_for_utxos(tx, height):
|
||||
for vout, output in enumerate(tx["vout"]):
|
||||
if output["scriptPubKey"]["type"] == "withdrawout":
|
||||
outp = output["scriptPubKey"]["asm"].split(" ")
|
||||
check_raise(len(outp) == 16)
|
||||
|
||||
bitcoin_tx = outp[2]
|
||||
bitcoin_raw_tx = bitcoin[thread_id()].getrawtransaction(bitcoin_tx, 1)
|
||||
txo = bitcoin_raw_tx["vout"][int(outp[3])]
|
||||
|
||||
inp = tx["vin"][vout]["scriptSig"]["asm"].split(" ")
|
||||
contract = inp[2]
|
||||
|
||||
cht = os.popen("%s %s -g -r %s -f %s" % (settings.contracthashtool_path, settings.cht_testnet_arg, settings.redeem_script, contract))
|
||||
cht_out = cht.read()
|
||||
check_raise(cht.close() == None)
|
||||
modified_redeem_script = cht_out.split("\n")[2 + settings.is_testnet][24:]
|
||||
modified_address = cht_out.split("\n")[3 + settings.is_testnet][40:]
|
||||
bitcoin[thread_id()].importaddress(modified_redeem_script, "", False, True)
|
||||
|
||||
cht = os.popen("%s %s -c -p %s -f %s" % (settings.contracthashtool_path, settings.cht_testnet_arg, settings.functionary_private_key, contract))
|
||||
gen_private_key = cht.read().split("\n")[0 + settings.is_testnet][16:]
|
||||
check_raise(cht.close() == None)
|
||||
|
||||
outp[3] = int(outp[3])
|
||||
|
||||
map_lock.acquire()
|
||||
already_had = (bitcoin_tx, outp[3]) in utxos
|
||||
utxos[(bitcoin_tx, outp[3])] = {"redeem_info": {"txid": bitcoin_tx, "vout": outp[3], "scriptPubKey": txo["scriptPubKey"]["hex"], "redeemScript": modified_redeem_script}, "privateKey": gen_private_key, "value": decimal.Decimal(txo["value"]), "spent_by": set(), "donated_map": {}}
|
||||
if already_had:
|
||||
if height not in fraud_check_map:
|
||||
fraud_check_map[height] = []
|
||||
fraud_check_map[height].append((tx["txid"], vout))
|
||||
map_lock.release()
|
||||
|
||||
print("Got %s UTXO (%s:%d) from sidechain tx %s:%d" % ("new" if not already_had else "existing", bitcoin_tx, outp[3], tx["txid"], vout))
|
||||
|
||||
def process_sidechain_tx_for_withdraw(tx, height):
|
||||
for vout, output in enumerate(tx["vout"]):
|
||||
if output["scriptPubKey"]["type"] == "withdraw":
|
||||
outp = output["scriptPubKey"]["asm"].split(" ")
|
||||
if len(outp) == 5 and outp[2] == settings.inverse_bitcoin_genesis_hash and outp[3] == settings.secondScriptPubKeyHash:
|
||||
check_raise(outp[1] == "OP_DROP" and outp[4] == "OP_WITHDRAWPROOFVERIFY")
|
||||
if outp[0][0:8] != "50325348":
|
||||
continue
|
||||
contract = outp[0][8:]
|
||||
check_raise(len(contract) == 40)
|
||||
|
||||
p2sh_script = "OP_HASH160 0x14%s OP_EQUAL" % outp[0][8:]
|
||||
p2sh_hex = "a914%s87" % outp[0][8:]
|
||||
txid_concat = tx["txid"] + ":" + str(vout)
|
||||
value = decimal.Decimal(output["value"])
|
||||
if txid_concat in spent_from_history:
|
||||
output = {"txid_concat": txid_concat, "sidechain_height": height, "script_gen": p2sh_script, "script_match": p2sh_hex, "value": value, "spent_from": spent_from_history[txid_concat]}
|
||||
else:
|
||||
output = {"txid_concat": txid_concat, "sidechain_height": height, "script_gen": p2sh_script, "script_match": p2sh_hex, "value": value, "spent_from": set()}
|
||||
|
||||
# We track the set of inputs (from the utxos map) from which we've sent the withdraw,
|
||||
# freely signing double-spends, but never allowing two non-conflicting withdraws
|
||||
map_lock.acquire()
|
||||
if txid_concat in outputs_pending:
|
||||
print("Re-ran process_sidechain_tx_for_withdraw with existing withdraw: %s???" % txid_concat)
|
||||
sys.exit(1)
|
||||
if p2sh_hex in outputs_pending_by_p2sh_hex:
|
||||
if p2sh_hex in outputs_waiting:
|
||||
outputs_waiting[p2sh_hex].append(output)
|
||||
else:
|
||||
outputs_waiting[p2sh_hex] = [output]
|
||||
|
||||
print("Got new txo for withdraw (waiting on previous tx %s): %s" % (txid_concat, outputs_pending_by_p2sh_hex[p2sh_hex]))
|
||||
map_lock.release()
|
||||
continue
|
||||
|
||||
outputs_pending[txid_concat] = output
|
||||
outputs_pending_by_p2sh_hex[p2sh_hex] = txid_concat
|
||||
print("Got new txo for withdraw: %s (to %s with value %s)" % (txid_concat, p2sh_hex, str(value)))
|
||||
map_lock.release()
|
||||
|
||||
def process_sidechain_blockchain(min_height, max_height):
|
||||
for height in range(min_height, max_height):
|
||||
block = sidechain[thread_id()].getblock(sidechain[thread_id()].getblockhash(height))
|
||||
for tx in sidechain[thread_id()].batch_([["getrawtransaction", txhash, 1] for txhash in block["tx"]]):
|
||||
process_sidechain_tx_for_utxos(tx, height)
|
||||
process_sidechain_tx_for_withdraw(tx, height)
|
||||
|
||||
def process_confirmed_sidechain_blockchain(min_height, max_height):
|
||||
global donated_funds
|
||||
|
||||
for height in range(min_height, max_height):
|
||||
map_lock.acquire()
|
||||
fraud_check_list = None
|
||||
if height in fraud_check_map:
|
||||
fraud_check_list = fraud_check_map[height]
|
||||
del fraud_check_map[height]
|
||||
map_lock.release()
|
||||
if fraud_check_list != None:
|
||||
for txo in fraud_check_list:
|
||||
if sidechain[thread_id()].gettxout(txo[0], txo[1], False) != None:
|
||||
print("NO FRAUD PROOF GENERATED WITHIN CONFIRMATION PERIOD FOR TXO %s" % str(txo))
|
||||
sys.exit(1)
|
||||
|
||||
block = sidechain[thread_id()].getblock(sidechain[thread_id()].getblockhash(height))
|
||||
for tx in sidechain[thread_id()].batch_([["getrawtransaction", txhash, 1] for txhash in block["tx"]]):
|
||||
for outp in tx["vout"]:
|
||||
if outp["scriptPubKey"]["type"] == "nulldata":
|
||||
map_lock.acquire()
|
||||
donated_funds += outp["value"]
|
||||
map_lock.release()
|
||||
|
||||
|
||||
def process_confirmed_bitcoin_blockchain(min_height, max_height):
|
||||
global donated_funds
|
||||
|
||||
for height in range(min_height, max_height):
|
||||
block = bitcoin[thread_id()].getblock(bitcoin[thread_id()].getblockhash(height))
|
||||
for tx in bitcoin[thread_id()].batch_([["getrawtransaction", txhash, 1] for txhash in block["tx"]]):
|
||||
map_lock.acquire()
|
||||
is_withdraw = False
|
||||
is_not_withdraw = False
|
||||
tx_value = 0
|
||||
|
||||
# First process the inputs, checking if its a withdraw transaction (ie spends from utxos)
|
||||
# then remove that utxo, including from ensure-double-spend-sets in outputs_pending
|
||||
for inp in tx["vin"]:
|
||||
if "coinbase" in inp:
|
||||
continue
|
||||
|
||||
txid_pair = (inp["txid"], inp["vout"])
|
||||
if txid_pair not in utxos:
|
||||
if is_withdraw:
|
||||
print("Got transaction that spent both functionary utxos and non-functionary utxos...very confused")
|
||||
sys.exit(1)
|
||||
is_not_withdraw = True
|
||||
else:
|
||||
if is_not_withdraw:
|
||||
print("Got transaction that spent both functionary utxos and non-functionary utxos...very confused")
|
||||
sys.exit(1)
|
||||
is_withdraw = True
|
||||
|
||||
utxo = utxos[txid_pair]
|
||||
|
||||
for txid_concat in utxo["spent_by"]:
|
||||
if txid_concat in outputs_pending:
|
||||
new_spent_from = set()
|
||||
output = outputs_pending[txid_concat]
|
||||
for inputs_set in output["spent_from"]:
|
||||
if txid_pair not in inputs_set:
|
||||
new_spent_from.add(inputs_set)
|
||||
output["spent_from"] = new_spent_from
|
||||
|
||||
# Calculate donated_funds by re-adding all temporary removals that this invalidated
|
||||
total_donated_value = 0
|
||||
for txid_set in utxo["donated_map"]:
|
||||
donated_value = utxo["donated_map"][txid_set]
|
||||
for txid_pair_it in txid_set:
|
||||
if txid_pair_it == txid_pair:
|
||||
continue
|
||||
|
||||
if utxos[txid_pair_it]["donated_map"][txid_set] != donated_value:
|
||||
print("Internal data structure inconsistency")
|
||||
sys.exit(1)
|
||||
del utxos[txid_pair_it]["donated_map"][txid_set]
|
||||
|
||||
total_donated_value = total_donated_value + donated_value
|
||||
donated_funds = donated_funds + total_donated_value
|
||||
|
||||
tx_value = tx_value + utxo["value"]
|
||||
del utxos[txid_pair]
|
||||
|
||||
# Then go through outputs, removing them from outputs_pending and warning if
|
||||
# we dont know where the money went
|
||||
if is_withdraw:
|
||||
for outp in tx["vout"]:
|
||||
script_asm = outp["scriptPubKey"]["hex"]
|
||||
if script_asm in outputs_pending_by_p2sh_hex:
|
||||
sys.stdout.write("Successfully completed withdraw for sidechain tx %s in bitcoin tx %s:%d" % (outputs_pending_by_p2sh_hex[script_asm], tx["txid"], outp["n"]))
|
||||
|
||||
del outputs_pending[outputs_pending_by_p2sh_hex[script_asm]]
|
||||
del outputs_pending_by_p2sh_hex[script_asm]
|
||||
|
||||
if script_asm in outputs_waiting:
|
||||
output = outputs_waiting[script_asm].pop(0)
|
||||
outputs_pending[output["txid_concat"]] = output
|
||||
outputs_pending_by_p2sh_hex[script_asm] = output["txid_concat"]
|
||||
if len(outputs_waiting[script_asm]) == 0:
|
||||
del outputs_waiting[script_asm]
|
||||
sys.stdout.write("...next output to same address is %s" % output["txid_concat"])
|
||||
|
||||
sys.stdout.write("\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
elif outp["scriptPubKey"]["type"] != "scripthash" or outp["scriptPubKey"]["addresses"][0] != settings.redeem_script_address:
|
||||
print("MONEY MOVED FROM FUNCTIONARY OUTPUT TO UNKNOWN DESTINATION!!!!")
|
||||
print("In transaction %s in output %d" % (tx["txid"], outp["n"]))
|
||||
sys.exit(1)
|
||||
|
||||
tx_value = tx_value - outp["value"]
|
||||
|
||||
# Remove fee from donated_funds
|
||||
if tx_value > 0:
|
||||
donated_funds = donated_funds - tx_value
|
||||
|
||||
map_lock.release()
|
||||
|
||||
# Finally, without map_lock held (we'll grab it again if needed in process_bitcoin_tx_for_utxos),
|
||||
# we add any outputs which are to the functionary address to the utxos set.
|
||||
process_bitcoin_tx_for_utxos(tx, not is_withdraw)
|
||||
|
||||
try:
|
||||
print("Doing chain-scan init...")
|
||||
|
||||
print("Step 1. Sidechain blockchain scan for coins in and withdraws...")
|
||||
# First do a pass over all existing blocks to collect all utxos
|
||||
sidechain_block_count = sidechain[thread_id()].getblockcount()
|
||||
process_sidechain_blockchain(1, sidechain_block_count)
|
||||
process_confirmed_sidechain_blockchain(1, sidechain_block_count - 5)
|
||||
print("done")
|
||||
|
||||
print("Step 2. Bitcoin blockchain scan for withdraws completed and coins to functionaries...")
|
||||
bitcoin_block_count = bitcoin[thread_id()].getblockcount()
|
||||
process_confirmed_bitcoin_blockchain(1, bitcoin_block_count - 5)
|
||||
print("done")
|
||||
|
||||
sys.stdout.write("Step 3. Bitcoin blockchain rescan to load functionary outputs in wallet...")
|
||||
sys.stdout.flush()
|
||||
bitcoin[thread_id()].importaddress(settings.redeem_script, "", False, True)
|
||||
trigger_bitcoin_rescan()
|
||||
print("done")
|
||||
|
||||
print("Init done. Joining rotating consensus and watching chain for withdraws...")
|
||||
#TODO: Change interval to ~60
|
||||
settings.nodes.remove(settings.my_node)
|
||||
WatchPeerController(settings.nodes, settings.my_node, port, 10, settings.socks_proxy)
|
||||
|
||||
print("Outputs to be created:")
|
||||
for txid_concat in outputs_pending:
|
||||
sys.stdout.write(" " + txid_concat)
|
||||
print("\nOutputs waiting:")
|
||||
for p2sh_hex in outputs_waiting:
|
||||
for output in outputs_waiting[p2sh_hex]:
|
||||
sys.stdout.write(" " + output["txid_concat"])
|
||||
print("")
|
||||
|
||||
while True:
|
||||
if not check_reset_connections():
|
||||
sleep(1)
|
||||
continue
|
||||
|
||||
new_block_count = sidechain[thread_id()].getblockcount()
|
||||
process_sidechain_blockchain(sidechain_block_count, new_block_count)
|
||||
process_confirmed_sidechain_blockchain(sidechain_block_count - 5, new_block_count - 5)
|
||||
sidechain_block_count = new_block_count
|
||||
|
||||
if not check_reset_connections():
|
||||
sleep(1)
|
||||
continue
|
||||
|
||||
new_block_count = bitcoin[thread_id()].getblockcount()
|
||||
process_confirmed_bitcoin_blockchain(bitcoin_block_count - 5, new_block_count - 5)
|
||||
bitcoin_block_count = new_block_count
|
||||
|
||||
sleep(1)
|
||||
|
||||
except JSONRPCException as e:
|
||||
print(e.error)
|
||||
print(traceback.format_exc())
|
||||
Loading…
Add table
Add a link
Reference in a new issue