mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-19 13:27:35 +02:00
Port asset tutorial, update to 0.17+ API
This commit is contained in:
parent
09e20ab819
commit
4c157da8bd
6 changed files with 771 additions and 0 deletions
507
contrib/assets_tutorial/assets_tutorial.py
Executable file
507
contrib/assets_tutorial/assets_tutorial.py
Executable file
|
|
@ -0,0 +1,507 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from test_framework.authproxy import AuthServiceProxy, JSONRPCException
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
from decimal import Decimal
|
||||||
|
ELEMENTSPATH=""
|
||||||
|
BITCOINPATH=""
|
||||||
|
|
||||||
|
if len(sys.argv) == 2:
|
||||||
|
ELEMENTSPATH=sys.argv[0]
|
||||||
|
BITCOINPATH=sys.argv[1]
|
||||||
|
else:
|
||||||
|
ELEMENTSPATH="./src"
|
||||||
|
BITCOINPATH="./../bitcoin/src"
|
||||||
|
|
||||||
|
def startbitcoind(datadir, conf, args=""):
|
||||||
|
subprocess.Popen((BITCOINPATH+"/bitcoind -datadir="+datadir+" "+args).split(), stdout=subprocess.PIPE)
|
||||||
|
return AuthServiceProxy("http://"+conf["rpcuser"]+":"+conf["rpcpassword"]+"@127.0.0.1:"+conf["regtest.rpcport"])
|
||||||
|
|
||||||
|
def startelementsd(datadir, conf, args=""):
|
||||||
|
subprocess.Popen((ELEMENTSPATH+"/elementsd -datadir="+datadir+" "+args).split(), stdout=subprocess.PIPE)
|
||||||
|
return AuthServiceProxy("http://"+conf["rpcuser"]+":"+conf["rpcpassword"]+"@127.0.0.1:"+conf["elementsregtest.rpcport"])
|
||||||
|
|
||||||
|
def loadConfig(filename):
|
||||||
|
conf = {}
|
||||||
|
with open(filename, encoding="utf8") as f:
|
||||||
|
for line in f:
|
||||||
|
if len(line) == 0 or line[0] == "#" or len(line.split("=")) != 2:
|
||||||
|
continue
|
||||||
|
conf[line.split("=")[0]] = line.split("=")[1].strip()
|
||||||
|
conf["filename"] = filename
|
||||||
|
return conf
|
||||||
|
|
||||||
|
def sync_all(e1, e2):
|
||||||
|
totalWait = 10
|
||||||
|
while e1.getblockcount() != e2.getblockcount() or len(e1.getrawmempool()) != len(e2.getrawmempool()):
|
||||||
|
totalWait -= 1
|
||||||
|
if totalWait == 0:
|
||||||
|
raise Exception("Nodes cannot sync blocks or mempool!")
|
||||||
|
time.sleep(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Preparations
|
||||||
|
|
||||||
|
# Make data directories for each daemon
|
||||||
|
b_datadir="/tmp/"+''.join(random.choice('0123456789ABCDEF') for i in range(5))
|
||||||
|
e1_datadir="/tmp/"+''.join(random.choice('0123456789ABCDEF') for i in range(5))
|
||||||
|
e2_datadir="/tmp/"+''.join(random.choice('0123456789ABCDEF') for i in range(5))
|
||||||
|
|
||||||
|
os.makedirs(b_datadir)
|
||||||
|
os.makedirs(e1_datadir)
|
||||||
|
os.makedirs(e2_datadir)
|
||||||
|
|
||||||
|
print("Bitcoin datadir: "+b_datadir)
|
||||||
|
print("Elements1 datadir: "+e1_datadir)
|
||||||
|
print("Elements2 datadir: "+e2_datadir)
|
||||||
|
|
||||||
|
# Also configure the nodes by copying the configuration files from
|
||||||
|
# this directory (and read them back for arguments):
|
||||||
|
shutil.copyfile("contrib/assets_tutorial/bitcoin.conf", b_datadir+"/bitcoin.conf")
|
||||||
|
shutil.copyfile("contrib/assets_tutorial/elements1.conf", e1_datadir+"/elements.conf")
|
||||||
|
shutil.copyfile("contrib/assets_tutorial/elements2.conf", e2_datadir+"/elements.conf")
|
||||||
|
|
||||||
|
bconf = loadConfig("contrib/assets_tutorial/bitcoin.conf")
|
||||||
|
e1conf = loadConfig("contrib/assets_tutorial/elements1.conf")
|
||||||
|
e2conf = loadConfig("contrib/assets_tutorial/elements2.conf")
|
||||||
|
|
||||||
|
# Startup
|
||||||
|
|
||||||
|
# Can not start since bitcoind isn't running and validatepegin is set
|
||||||
|
# elementsd attempts to connect to bitcoind to check if peg-in transactions
|
||||||
|
# are confirmed in the Bitcoin chain.
|
||||||
|
e1 = startelementsd(e1_datadir, e1conf)
|
||||||
|
time.sleep(2)
|
||||||
|
try:
|
||||||
|
e1.getinfo()
|
||||||
|
raise AssertionError("This should fail unless working bitcoind can be reached via JSON RPC")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Start bitcoind, then elementsd. As long as bitcoind is in RPC warmup, elementsd will connect
|
||||||
|
bitcoin = startbitcoind(b_datadir, bconf)
|
||||||
|
e1 = startelementsd(e1_datadir, e1conf)
|
||||||
|
e2 = startelementsd(e2_datadir, e2conf)
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Alternatively, you can set validatepegin=0 in their configs and not
|
||||||
|
# run the bitcoin node, but it is necessary for fully validating the two way peg.
|
||||||
|
|
||||||
|
# Regtest chain starts with 21M bitcoins as OP_TRUE which the wallet
|
||||||
|
# understands. This is useful for testing basic functionality and for
|
||||||
|
# blockchains that have no pegging functionality. A fee currency is required
|
||||||
|
# for anti-DoS purposes as well as asset issuance, which consumes inputs for entropy.
|
||||||
|
# In Elements there is no block subsidy. In a production sidechain it can
|
||||||
|
# be configured to start with no outputs, necessitating peg-in functionality
|
||||||
|
# for asset issuance.
|
||||||
|
e1.getwalletinfo()
|
||||||
|
|
||||||
|
# In regtest mining "target" is OP_TRUE since we have not set `-signblockscript` argument
|
||||||
|
# Generate simply works.
|
||||||
|
e1.generatetoaddress(101, e1.getnewaddress())
|
||||||
|
sync_all(e1, e2)
|
||||||
|
|
||||||
|
# WALLET
|
||||||
|
|
||||||
|
# First, send all anyone-can-spend coins to e1 then split so balances are even
|
||||||
|
e1.sendtoaddress(e1.getnewaddress(), 21000000, "", "", True)
|
||||||
|
e1.generatetoaddress(101, e1.getnewaddress())
|
||||||
|
sync_all(e1, e2)
|
||||||
|
e1.sendtoaddress(e2.getnewaddress(), 10500000, "", "", False)
|
||||||
|
e1.generatetoaddress(101, e1.getnewaddress())
|
||||||
|
sync_all(e1, e2)
|
||||||
|
|
||||||
|
# Funds should now be evenly split between the two wallets
|
||||||
|
e1.getwalletinfo()
|
||||||
|
e2.getwalletinfo()
|
||||||
|
|
||||||
|
# Have e2 send coins to themself using a blinded Elements address
|
||||||
|
# Blinded addresses start with `CTE`, unblinded `2`
|
||||||
|
addr = e2.getnewaddress()
|
||||||
|
|
||||||
|
# How do we know it's blinded? Check for blinding key, unblinded address.
|
||||||
|
e2.getaddressinfo(addr)
|
||||||
|
|
||||||
|
# Basic blinded send
|
||||||
|
txid = e2.sendtoaddress(addr, 1)
|
||||||
|
|
||||||
|
e2.generatetoaddress(1, e1.getnewaddress())
|
||||||
|
sync_all(e1, e2)
|
||||||
|
|
||||||
|
# Now let's examine the transaction, both in wallet and without
|
||||||
|
|
||||||
|
# In-wallet, take a look at blinding information
|
||||||
|
e2.gettransaction(txid)
|
||||||
|
|
||||||
|
# e1 doesn't have in wallet since it's unrelated
|
||||||
|
try:
|
||||||
|
e1.gettransaction(txid)
|
||||||
|
raise Exception("Transaction should not be in wallet")
|
||||||
|
except JSONRPCException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get public info, see blinded ranges, etc
|
||||||
|
e1.getrawtransaction(txid, 1)
|
||||||
|
|
||||||
|
# Now let's private import the key to attempt a spend
|
||||||
|
e1.importprivkey(e2.dumpprivkey(addr))
|
||||||
|
|
||||||
|
# We can't see output value info though
|
||||||
|
# and can not send.
|
||||||
|
e1.gettransaction(txid)
|
||||||
|
|
||||||
|
# And it won't show in balance or known outputs
|
||||||
|
e1.getwalletinfo()
|
||||||
|
# Amount for transaction is unknown, so it is not shown in listunspent.
|
||||||
|
e1.listunspent(1, 1)
|
||||||
|
|
||||||
|
# Solution: Import blinding key
|
||||||
|
e1.importblindingkey(addr, e2.dumpblindingkey(addr))
|
||||||
|
|
||||||
|
# Check again, funds should show
|
||||||
|
e1.getwalletinfo()
|
||||||
|
e1.listunspent(1, 1)
|
||||||
|
e1.gettransaction(txid)
|
||||||
|
|
||||||
|
# Let's build a blinded 2-of-2 multisig p2sh address
|
||||||
|
|
||||||
|
# 1) Get unblinded addresses from each participant
|
||||||
|
addr1 = e1.getaddressinfo(e1.getnewaddress())["unconfidential"]
|
||||||
|
addr2 = e2.getaddressinfo(e2.getnewaddress())["unconfidential"]
|
||||||
|
|
||||||
|
# 2) Get blinding keys, private and public
|
||||||
|
addrinfo1 = e1.getaddressinfo(e1.getnewaddress())
|
||||||
|
addrinfo2 = e2.getaddressinfo(addr2)
|
||||||
|
blindingkey = e1.dumpblindingkey(addrinfo1["address"])
|
||||||
|
blindingpubkey = addrinfo1["confidential_key"]
|
||||||
|
|
||||||
|
# 3) Make multisig address like usual
|
||||||
|
multisig = e1.createmultisig(2, [addrinfo1["pubkey"], addrinfo2["pubkey"]])
|
||||||
|
|
||||||
|
# 4) Blind the address using the blinding pubkey
|
||||||
|
blinded_addr = e1.createblindedaddress(multisig["address"], blindingpubkey)
|
||||||
|
e1.importaddress(multisig["redeemScript"], "", True, True) # Make sure p2sh addr is added
|
||||||
|
e2.importaddress(multisig["redeemScript"], "", True, True)
|
||||||
|
e1.importaddress(blinded_addr)
|
||||||
|
e2.importaddress(blinded_addr)
|
||||||
|
|
||||||
|
# 5) Now the address can be funded, though e2 will not be able to see values
|
||||||
|
txid = e1.sendtoaddress(blinded_addr, 1)
|
||||||
|
sync_all(e1, e2)
|
||||||
|
e2.gettransaction(txid, True)
|
||||||
|
|
||||||
|
# 6) Import the blinding privkey and decode the values
|
||||||
|
e2.importblindingkey(blinded_addr, blindingkey)
|
||||||
|
e2.gettransaction(txid, True)
|
||||||
|
|
||||||
|
# ASSETS
|
||||||
|
|
||||||
|
# Many of the RPC calls have added asset type or label
|
||||||
|
# arguments and reveal alternative asset information. With no argument all are listed:
|
||||||
|
e1.getwalletinfo()
|
||||||
|
|
||||||
|
# Notice we now see "bitcoin" as an asset. This is the asset label for the hex for "bitcoin" which can be discovered:
|
||||||
|
e1.dumpassetlabels()
|
||||||
|
|
||||||
|
# We can also issue our own assets, 1 asset and 1 reissuance token in this case
|
||||||
|
issue = e1.issueasset(1, 1)
|
||||||
|
asset = issue["asset"]
|
||||||
|
|
||||||
|
# From there you can look at the issuances you have in your wallet
|
||||||
|
e1.listissuances()
|
||||||
|
|
||||||
|
# If you gave `issueasset` a reissuance token argument greater than 0
|
||||||
|
# you can also reissue the base asset
|
||||||
|
e1.reissueasset(asset, 1)
|
||||||
|
|
||||||
|
# or make another different unblinded asset issuance, with only reissuance tokens initially
|
||||||
|
e1.issueasset(0, 1, False)
|
||||||
|
|
||||||
|
# Then two issuances for that particular asset will show
|
||||||
|
e1.listissuances(asset)
|
||||||
|
|
||||||
|
# To label any asset add a new argument like this to your elements.conf file
|
||||||
|
# then restart your daemon:
|
||||||
|
assetentry = "-assetdir="+asset+":namedasset"
|
||||||
|
# Wallet labels have no consensus meaning, only local node/wallet meaning
|
||||||
|
|
||||||
|
sync_all(e1, e2)
|
||||||
|
e1.stop()
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# Restart with a new asset label
|
||||||
|
e1 = startelementsd(e1_datadir, e1conf, assetentry)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
e1.getwalletinfo()
|
||||||
|
|
||||||
|
# To send issued assets, add an additional argument to sendtoaddress using the hex or label
|
||||||
|
e1.sendtoaddress(address=e2.getnewaddress(), amount=1, assetlabel="namedasset")
|
||||||
|
# Reissuance tokens can also be sent like any other asset
|
||||||
|
e1.sendtoaddress(address=e2.getnewaddress(), amount=1, assetlabel=issue["token"])
|
||||||
|
sync_all(e1, e2)
|
||||||
|
# e2 wallet doesn't know about label, just an unnamed asset
|
||||||
|
e2.getwalletinfo()["unconfirmed_balance"][asset]
|
||||||
|
e2.generatetoaddress(1, e2.getnewaddress())
|
||||||
|
sync_all(e1, e2)
|
||||||
|
|
||||||
|
# e2 maybe doesn't know about the issuance for the transaction sending him the new asset
|
||||||
|
e2.listissuances()
|
||||||
|
|
||||||
|
# let's import an associated address(so the wallet captures issuance transaction) and rescan
|
||||||
|
txid = issue["txid"]
|
||||||
|
addr = e1.gettransaction(txid)["details"][0]["address"]
|
||||||
|
e2.importaddress(addr)
|
||||||
|
|
||||||
|
# e2 now sees issuance, but doesn't know amounts as they are blinded
|
||||||
|
e2.listissuances()
|
||||||
|
|
||||||
|
# We need to import the issuance blinding key. We refer to issuances by their txid/vin pair
|
||||||
|
# as there is only one per input
|
||||||
|
vin = issue["vin"]
|
||||||
|
issuekey = e1.dumpissuanceblindingkey(txid, vin)
|
||||||
|
|
||||||
|
e2.importissuanceblindingkey(txid, vin, issuekey)
|
||||||
|
|
||||||
|
# Now e2 can see issuance amounts and blinds
|
||||||
|
e2.listissuances()
|
||||||
|
|
||||||
|
# Since it was also sent a reissuance token, it can reissue the base asset
|
||||||
|
e2.reissueasset(issue["asset"], 5)
|
||||||
|
|
||||||
|
|
||||||
|
# Reissuing reissuance tokens is currently not supported
|
||||||
|
try:
|
||||||
|
e2.reissueasset(issue["token"], 1)
|
||||||
|
except JSONRPCException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# For de-issuance, we can send assets or issuance tokens to an OP_RETURN output, provably burning them
|
||||||
|
e2.destroyamount(issue["asset"], 5)
|
||||||
|
|
||||||
|
# BLOCKSIGNING
|
||||||
|
|
||||||
|
# Recall blocksigning is OP_TRUE
|
||||||
|
e1.generatetoaddress(1, e1.getnewaddress())
|
||||||
|
sync_all(e1, e2)
|
||||||
|
|
||||||
|
# Let's set it to something more interesting... 2-of-2 multisig
|
||||||
|
|
||||||
|
# First lets get some keys from both clients to make our block "challenge"
|
||||||
|
addr1 = e1.getnewaddress()
|
||||||
|
addr2 = e2.getnewaddress()
|
||||||
|
valid1 = e1.getaddressinfo(addr1)
|
||||||
|
pubkey1 = valid1["pubkey"]
|
||||||
|
valid2 = e2.getaddressinfo(addr2)
|
||||||
|
pubkey2 = valid2["pubkey"]
|
||||||
|
|
||||||
|
key1 = e1.dumpprivkey(addr1)
|
||||||
|
key2 = e2.dumpprivkey(addr2)
|
||||||
|
|
||||||
|
e1.stop()
|
||||||
|
e2.stop()
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# Now filled with the pubkeys as 2-of-2 checkmultisig
|
||||||
|
signblockarg="-signblockscript=5221"+pubkey1+"21"+pubkey2+"52ae"
|
||||||
|
# Anti-DoS argument, custom chain default is ~1 sig so let's make it at least 2 sigs
|
||||||
|
blocksign_max_size="-con_max_block_sig_size=150"
|
||||||
|
extra_args=signblockarg+" "+blocksign_max_size
|
||||||
|
|
||||||
|
# Wipe out datadirs, start over
|
||||||
|
shutil.rmtree(e1_datadir)
|
||||||
|
shutil.rmtree(e2_datadir)
|
||||||
|
os.makedirs(e1_datadir)
|
||||||
|
os.makedirs(e2_datadir)
|
||||||
|
|
||||||
|
# Copy back config files
|
||||||
|
shutil.copyfile("contrib/assets_tutorial/elements1.conf", e1_datadir+"/elements.conf")
|
||||||
|
shutil.copyfile("contrib/assets_tutorial/elements2.conf", e2_datadir+"/elements.conf")
|
||||||
|
|
||||||
|
e1 = startelementsd(e1_datadir, e1conf, extra_args)
|
||||||
|
e2 = startelementsd(e2_datadir, e2conf, extra_args)
|
||||||
|
time.sleep(5)
|
||||||
|
sync_all(e1, e2)
|
||||||
|
|
||||||
|
# Now import signing keys
|
||||||
|
e1.importprivkey(key1)
|
||||||
|
e2.importprivkey(key2)
|
||||||
|
|
||||||
|
# Generate no longer works, even if keys are in wallet
|
||||||
|
try:
|
||||||
|
e1.generatetoaddress(1, e1.getnewaddress())
|
||||||
|
raise Exception("Generate shouldn't work")
|
||||||
|
except JSONRPCException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
e1.generatetoaddress(1, e1.getnewaddress())
|
||||||
|
raise Exception("Generate shouldn't work")
|
||||||
|
except JSONRPCException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Let's propose and accept some blocks, e1 is master!
|
||||||
|
blockhex = e1.getnewblockhex()
|
||||||
|
|
||||||
|
# Unsigned is no good
|
||||||
|
# 0 before, 0 after
|
||||||
|
e1.getblockcount() == 0
|
||||||
|
|
||||||
|
e1.submitblock(blockhex)
|
||||||
|
|
||||||
|
# Still 0
|
||||||
|
e1.getblockcount() == 0
|
||||||
|
|
||||||
|
|
||||||
|
# Signblock tests validity except block signatures
|
||||||
|
# This signing step can be outsourced to a HSM signing to enforce business logic of any sort
|
||||||
|
# See Strong Federations paper
|
||||||
|
sign1 = e1.signblock(blockhex)
|
||||||
|
sign2 = e2.signblock(blockhex)
|
||||||
|
|
||||||
|
|
||||||
|
# We now can gather signatures any way you want, combine them into a fully signed block
|
||||||
|
blockresult = e1.combineblocksigs(blockhex, [sign1[0], sign2[0]])
|
||||||
|
|
||||||
|
blockresult["complete"] == True
|
||||||
|
|
||||||
|
signedblock = blockresult["hex"]
|
||||||
|
|
||||||
|
# Now submit the block, doesn't matter who
|
||||||
|
e2.submitblock(signedblock)
|
||||||
|
sync_all(e1, e2)
|
||||||
|
|
||||||
|
# We now have moved forward one block!
|
||||||
|
e1.getblockcount() == 1
|
||||||
|
e2.getblockcount() == 1
|
||||||
|
|
||||||
|
e1.stop()
|
||||||
|
e2.stop()
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# Further Exercises:
|
||||||
|
# - Make a python script that does round-robin consensus
|
||||||
|
|
||||||
|
# Pegging
|
||||||
|
|
||||||
|
# Everything pegging related can be done inside the Elements daemon directly, except for
|
||||||
|
# pegging out. This is due to the multisig pool aka Watchmen that controls the bitcoin
|
||||||
|
# on the Bitcoin blockchain. That is the easiest part to get wrong, and by far the most
|
||||||
|
# important as there is no going back if you lose the funds.
|
||||||
|
|
||||||
|
# Wipe out datadirs, start over
|
||||||
|
shutil.rmtree(e1_datadir)
|
||||||
|
shutil.rmtree(e2_datadir)
|
||||||
|
os.makedirs(e1_datadir)
|
||||||
|
os.makedirs(e2_datadir)
|
||||||
|
|
||||||
|
# Copy back config files
|
||||||
|
shutil.copyfile("contrib/assets_tutorial/elements1.conf", e1_datadir+"/elements.conf")
|
||||||
|
shutil.copyfile("contrib/assets_tutorial/elements2.conf", e2_datadir+"/elements.conf")
|
||||||
|
|
||||||
|
fedpegarg="-fedpegscript=5221"+pubkey1+"21"+pubkey2+"52ae"
|
||||||
|
|
||||||
|
# Back to OP_TRUE blocks, re-using pubkeys for pegin pool instead
|
||||||
|
# Keys can be the same or different, doesn't matter
|
||||||
|
e1 = startelementsd(e1_datadir, e1conf, fedpegarg)
|
||||||
|
e2 = startelementsd(e2_datadir, e2conf, fedpegarg)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# Mature some outputs on each side
|
||||||
|
e1.generatetoaddress(101, e1.getnewaddress())
|
||||||
|
bitcoin.generatetoaddress(101, bitcoin.getnewaddress())
|
||||||
|
sync_all(e1, e2)
|
||||||
|
|
||||||
|
# Now we can actually start pegging in. Examine the pegin address fields
|
||||||
|
e1.getpeginaddress()
|
||||||
|
# Changes each time as it's a new sidechain address as well as new "tweak" for the watchmen keys
|
||||||
|
# mainchain_address : where you send your bitcoin from Bitcoin network
|
||||||
|
# sidechain_address : where the bitcoin will end up on the sidechain after pegging in
|
||||||
|
|
||||||
|
# Each call of this takes the pubkeys defined in the config file, adds a random number to them
|
||||||
|
# that is essetially the hash of the sidechain_address and other information,
|
||||||
|
# then creates a new P2SH Bitcoin address from that. We reveal that "tweak" to the functionaries
|
||||||
|
# during `claimpegin`, then they are able to calculate the necessary private key and control
|
||||||
|
# funds.
|
||||||
|
addrs = e1.getpeginaddress()
|
||||||
|
|
||||||
|
#Send funds to unique watchmen P2SH address
|
||||||
|
txid = bitcoin.sendtoaddress(addrs["mainchain_address"], 1)
|
||||||
|
|
||||||
|
# Confirmations in Bitcoin are what protects the
|
||||||
|
# sidechain from becoming fractional reserve during reorgs.
|
||||||
|
bitcoin.generatetoaddress(101, bitcoin.getnewaddress())
|
||||||
|
proof = bitcoin.gettxoutproof([txid])
|
||||||
|
raw = bitcoin.getrawtransaction(txid)
|
||||||
|
|
||||||
|
# Attempt claim!
|
||||||
|
claimtxid = e1.claimpegin(raw, proof, addrs["claim_script"])
|
||||||
|
sync_all(e1, e2)
|
||||||
|
|
||||||
|
# Other node should accept to mempool and mine
|
||||||
|
e2.generatetoaddress(1, e1.getnewaddress())
|
||||||
|
sync_all(e1, e2)
|
||||||
|
|
||||||
|
# Should see confirmations
|
||||||
|
"confirmations" in e1.getrawtransaction(claimtxid, 1)
|
||||||
|
|
||||||
|
# Pegging Out
|
||||||
|
|
||||||
|
# This command would trigger watchmen to send payment to Bitcoin address on mainchain
|
||||||
|
# The Bitcoin-side functionality is not supported directly in Elements.
|
||||||
|
# The watchmen will notice this transaction and send the funds from their collective
|
||||||
|
# wallet.
|
||||||
|
e1.sendtomainchain(bitcoin.getnewaddress(), 10)
|
||||||
|
|
||||||
|
#Exercise(s)
|
||||||
|
#1. Implement really dumb/unsafe watchmen to allow pegouts for learning purposes
|
||||||
|
# Recover tweak from pegin, add to privkey, combined tweaked pubkeys into a redeemscript, add to Core wallet
|
||||||
|
|
||||||
|
# RAW API
|
||||||
|
|
||||||
|
# Let's create a basic transaction using the raw api, blind it, sign, and send
|
||||||
|
|
||||||
|
# Create a transaction with a single destination output to other wallet
|
||||||
|
rawtx = e1.createrawtransaction([], {e2.getnewaddress():100})
|
||||||
|
# Biggest difference compared to Bitcoin is that we have explicit fee outputs
|
||||||
|
rawtx2 = e1.createrawtransaction([], {e2.getnewaddress():100, e1.getnewaddress():5, "fee":Decimal("0.1")})
|
||||||
|
# Fee outputs are unblinded, with a scriptPubKey of "", in other words ""
|
||||||
|
# scriptPubKeys are unspendable
|
||||||
|
|
||||||
|
# Next we can fund the transaction (and replaces fee with something more appropriate)
|
||||||
|
fundedtx = e1.fundrawtransaction(rawtx2)
|
||||||
|
|
||||||
|
# Blind
|
||||||
|
blindedtx = e1.blindrawtransaction(fundedtx["hex"])
|
||||||
|
# *Warning*: Raw blinding logic can be quite complicated, requiring the use of `ignoreblindfails`
|
||||||
|
# to avoid having calls fail without manually inspecting transactions in great detail.
|
||||||
|
# In general any transaction with 2 or more outputs to blind should succeed, so adding additional
|
||||||
|
# is one strategy to resolve this.
|
||||||
|
|
||||||
|
# Sign
|
||||||
|
signedtx = e1.signrawtransactionwithwallet(blindedtx)
|
||||||
|
|
||||||
|
# And send
|
||||||
|
txid = e1.sendrawtransaction(signedtx["hex"])
|
||||||
|
|
||||||
|
sync_all(e1, e2)
|
||||||
|
|
||||||
|
e2.gettransaction(txid)
|
||||||
|
|
||||||
|
# ADVANCED OPTIONS
|
||||||
|
# rawblindrawtransaction : blind a raw transaction with no access to a wallet
|
||||||
|
# -policyasset=<hex> : set network fee asset type to something other than BTC
|
||||||
|
|
||||||
|
bitcoin.stop()
|
||||||
|
e1.stop()
|
||||||
|
e2.stop()
|
||||||
|
time.sleep(2)
|
||||||
|
shutil.rmtree(e1_datadir)
|
||||||
|
shutil.rmtree(e2_datadir)
|
||||||
|
|
||||||
9
contrib/assets_tutorial/bitcoin.conf
Normal file
9
contrib/assets_tutorial/bitcoin.conf
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
regtest=1
|
||||||
|
daemon=1
|
||||||
|
txindex=1
|
||||||
|
|
||||||
|
regtest.rpcport=18888
|
||||||
|
regtest.port=18889
|
||||||
|
|
||||||
|
rpcuser=user3
|
||||||
|
rpcpassword=password3
|
||||||
44
contrib/assets_tutorial/elements1.conf
Normal file
44
contrib/assets_tutorial/elements1.conf
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
chain=elementsregtest
|
||||||
|
|
||||||
|
# Standard bitcoind stuff
|
||||||
|
rpcuser=user1
|
||||||
|
rpcpassword=password1
|
||||||
|
elementsregtest.rpcport=18884
|
||||||
|
elementsregtest.port=18886
|
||||||
|
|
||||||
|
rpcport=18884
|
||||||
|
# Over p2p we will only connect to local other elementsd
|
||||||
|
elementsregtest.connect=localhost:18887
|
||||||
|
|
||||||
|
daemon=1
|
||||||
|
# Make sure you set listen after -connect, otherwise neither
|
||||||
|
# will accept incoming connections!
|
||||||
|
listen=1
|
||||||
|
# Just for looking at random txs
|
||||||
|
txindex=1
|
||||||
|
|
||||||
|
# This is the script that controls pegged in funds in Bitcoin network
|
||||||
|
# Users will be pegging into a P2SH of this, and the "watchmen"
|
||||||
|
# can then recover these funds and send them to users who desire to peg out.
|
||||||
|
# This template is 1-of-1 checkmultisig
|
||||||
|
#fedpegscript=5121<pubkey>51ae
|
||||||
|
|
||||||
|
# This is the script that controls how blocks are made
|
||||||
|
# We have to supply a signature that satisfies this to create
|
||||||
|
# a valid block.
|
||||||
|
#signblockscript=5121<pubkey2>51ae
|
||||||
|
|
||||||
|
# We want to validate pegins by checking with bitcoind if header exists
|
||||||
|
# in best known chain, and how deep. We combine this with pegin
|
||||||
|
# proof included in the pegin to get full security.
|
||||||
|
validatepegin=1
|
||||||
|
|
||||||
|
# If in the same datadir and using standard ports, these are unneeded
|
||||||
|
# thanks to cookie auth. If not, like in our situation, elementsd needs
|
||||||
|
# more info to connect to bitcoind:
|
||||||
|
mainchainrpcport=18888
|
||||||
|
mainchainrpcuser=user3
|
||||||
|
mainchainrpcpassword=password3
|
||||||
|
|
||||||
|
# Free money to make testing easier
|
||||||
|
initialfreecoins=2100000000000000
|
||||||
19
contrib/assets_tutorial/elements2.conf
Normal file
19
contrib/assets_tutorial/elements2.conf
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
rpcuser=user2
|
||||||
|
rpcpassword=password2
|
||||||
|
elementsregtest.rpcport=18885
|
||||||
|
elementsregtest.port=18887
|
||||||
|
elementsregtest.connect=localhost:18886
|
||||||
|
|
||||||
|
daemon=1
|
||||||
|
listen=1
|
||||||
|
txindex=1
|
||||||
|
|
||||||
|
#fedpegscript=51<pubkey>51ae
|
||||||
|
#signblockscript=51<pubkey2>51ae
|
||||||
|
|
||||||
|
mainchainrpcport=18888
|
||||||
|
mainchainrpcuser=user3
|
||||||
|
mainchainrpcpassword=password3
|
||||||
|
validatepegin=1
|
||||||
|
|
||||||
|
initialfreecoins=2100000000000000
|
||||||
1
contrib/assets_tutorial/test_framework/__init__.py
Normal file
1
contrib/assets_tutorial/test_framework/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
191
contrib/assets_tutorial/test_framework/authproxy.py
Normal file
191
contrib/assets_tutorial/test_framework/authproxy.py
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2011 Jeff Garzik
|
||||||
|
|
||||||
|
AuthServiceProxy has the following improvements over python-jsonrpc's
|
||||||
|
ServiceProxy class:
|
||||||
|
|
||||||
|
- HTTP connections persist for the life of the AuthServiceProxy object
|
||||||
|
(if server supports HTTP/1.1)
|
||||||
|
- sends protocol 'version', per JSON-RPC 1.1
|
||||||
|
- sends proper, incrementing 'id'
|
||||||
|
- sends Basic HTTP authentication headers
|
||||||
|
- parses all JSON numbers that look like floats as Decimal
|
||||||
|
- uses standard Python json lib
|
||||||
|
|
||||||
|
Previous copyright, from python-jsonrpc/jsonrpc/proxy.py:
|
||||||
|
|
||||||
|
Copyright (c) 2007 Jan-Klaas Kollhof
|
||||||
|
|
||||||
|
This file is part of jsonrpc.
|
||||||
|
|
||||||
|
jsonrpc is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This software is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with this software; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
import http.client as httplib
|
||||||
|
except ImportError:
|
||||||
|
import httplib
|
||||||
|
import base64
|
||||||
|
import decimal
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
try:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
USER_AGENT = "AuthServiceProxy/0.1"
|
||||||
|
|
||||||
|
HTTP_TIMEOUT = 30
|
||||||
|
|
||||||
|
log = logging.getLogger("BitcoinRPC")
|
||||||
|
|
||||||
|
class JSONRPCException(Exception):
|
||||||
|
def __init__(self, rpc_error):
|
||||||
|
try:
|
||||||
|
errmsg = '%(message)s (%(code)i)' % rpc_error
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
errmsg = ''
|
||||||
|
Exception.__init__(self, errmsg)
|
||||||
|
self.error = rpc_error
|
||||||
|
|
||||||
|
|
||||||
|
def EncodeDecimal(o):
|
||||||
|
if isinstance(o, decimal.Decimal):
|
||||||
|
return str(o)
|
||||||
|
raise TypeError(repr(o) + " is not JSON serializable")
|
||||||
|
|
||||||
|
class AuthServiceProxy(object):
|
||||||
|
__id_count = 0
|
||||||
|
|
||||||
|
# ensure_ascii: escape unicode as \uXXXX, passed to json.dumps
|
||||||
|
def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True):
|
||||||
|
self.__service_url = service_url
|
||||||
|
self._service_name = service_name
|
||||||
|
self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests
|
||||||
|
self.__url = urlparse.urlparse(service_url)
|
||||||
|
if self.__url.port is None:
|
||||||
|
port = 80
|
||||||
|
else:
|
||||||
|
port = self.__url.port
|
||||||
|
(user, passwd) = (self.__url.username, self.__url.password)
|
||||||
|
try:
|
||||||
|
user = user.encode('utf8')
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
passwd = passwd.encode('utf8')
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
authpair = user + b':' + passwd
|
||||||
|
self.__auth_header = b'Basic ' + base64.b64encode(authpair)
|
||||||
|
|
||||||
|
if connection:
|
||||||
|
# Callables re-use the connection of the original proxy
|
||||||
|
self.__conn = connection
|
||||||
|
elif self.__url.scheme == 'https':
|
||||||
|
self.__conn = httplib.HTTPSConnection(self.__url.hostname, port,
|
||||||
|
timeout=timeout)
|
||||||
|
else:
|
||||||
|
self.__conn = httplib.HTTPConnection(self.__url.hostname, port,
|
||||||
|
timeout=timeout)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name.startswith('__') and name.endswith('__'):
|
||||||
|
# Python internal stuff
|
||||||
|
raise AttributeError
|
||||||
|
if self._service_name is not None:
|
||||||
|
name = "%s.%s" % (self._service_name, name)
|
||||||
|
return AuthServiceProxy(self.__service_url, name, connection=self.__conn)
|
||||||
|
|
||||||
|
def _request(self, method, path, postdata):
|
||||||
|
'''
|
||||||
|
Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout).
|
||||||
|
This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5.
|
||||||
|
'''
|
||||||
|
headers = {'Host': self.__url.hostname,
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
'Authorization': self.__auth_header,
|
||||||
|
'Content-type': 'application/json'}
|
||||||
|
try:
|
||||||
|
self.__conn.request(method, path, postdata, headers)
|
||||||
|
return self._get_response()
|
||||||
|
except httplib.BadStatusLine as e:
|
||||||
|
if e.line == "''": # if connection was closed, try again
|
||||||
|
self.__conn.close()
|
||||||
|
self.__conn.request(method, path, postdata, headers)
|
||||||
|
return self._get_response()
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except (BrokenPipeError,ConnectionResetError):
|
||||||
|
# Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset
|
||||||
|
# ConnectionResetError happens on FreeBSD with Python 3.4
|
||||||
|
self.__conn.close()
|
||||||
|
self.__conn.request(method, path, postdata, headers)
|
||||||
|
return self._get_response()
|
||||||
|
|
||||||
|
def __call__(self, *args, **argsn):
|
||||||
|
AuthServiceProxy.__id_count += 1
|
||||||
|
|
||||||
|
log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self._service_name,
|
||||||
|
json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
|
||||||
|
if args and argsn:
|
||||||
|
raise ValueError('Cannot handle both named and positional arguments')
|
||||||
|
postdata = json.dumps({'version': '1.1',
|
||||||
|
'method': self._service_name,
|
||||||
|
'params': args or argsn,
|
||||||
|
'id': AuthServiceProxy.__id_count}, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
|
||||||
|
response = self._request('POST', self.__url.path, postdata.encode('utf-8'))
|
||||||
|
if response['error'] is not None:
|
||||||
|
raise JSONRPCException(response['error'])
|
||||||
|
elif 'result' not in response:
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -343, 'message': 'missing JSON-RPC result'})
|
||||||
|
else:
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
def _batch(self, rpc_call_list):
|
||||||
|
postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
|
||||||
|
log.debug("--> "+postdata)
|
||||||
|
return self._request('POST', self.__url.path, postdata.encode('utf-8'))
|
||||||
|
|
||||||
|
def _get_response(self):
|
||||||
|
try:
|
||||||
|
http_response = self.__conn.getresponse()
|
||||||
|
except socket.timeout as e:
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -344,
|
||||||
|
'message': '%r RPC took longer than %f seconds. Consider '
|
||||||
|
'using larger timeout for calls that take '
|
||||||
|
'longer to return.' % (self._service_name,
|
||||||
|
self.__conn.timeout)})
|
||||||
|
if http_response is None:
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -342, 'message': 'missing HTTP response from server'})
|
||||||
|
|
||||||
|
content_type = http_response.getheader('Content-Type')
|
||||||
|
if content_type != 'application/json':
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)})
|
||||||
|
|
||||||
|
responsedata = http_response.read().decode('utf8')
|
||||||
|
response = json.loads(responsedata, parse_float=decimal.Decimal)
|
||||||
|
if "error" in response and response["error"] is None:
|
||||||
|
log.debug("<-%s- %s"%(response["id"], json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
|
||||||
|
else:
|
||||||
|
log.debug("<-- "+responsedata)
|
||||||
|
return response
|
||||||
Loading…
Add table
Add a link
Reference in a new issue