assets_tutorial: basically rewrite the whole thing

This commit is contained in:
Andrew Poelstra 2021-09-06 14:48:42 +00:00
parent 6a74528a40
commit 1d2dacb1e2
4 changed files with 509 additions and 252 deletions

View file

@ -56,7 +56,10 @@ class Daemon():
def shutdown(self):
if self.proc is not None:
self.proc.kill()
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:
@ -65,21 +68,34 @@ class Daemon():
else:
print ("Leaving %s datadir at %s." % (self.name, self.datadir_path))
def start(self, ext_args = None):
self.shutdown()
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 = []
# 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
self.proc = subprocess.Popen([self.path, "-datadir=" + self.datadir_path] + ext_args, stdout=subprocess.PIPE)
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"])
def restart(self, ext_args = None):
self.start(ext_args)
# 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()
@ -95,11 +111,17 @@ class Daemon():
def sync_all(nodes, timeout_sec = 10):
totalWait = timeout_sec
stop_time = time.time() + timeout
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
@ -128,6 +150,7 @@ e2 = Daemon(
)
## 1. Start nodes
print ("1. Start nodes")
#
# 1a. Confirm that we not start an elements node if validatepegin is set and there
# is no bitcoind. When validatepegin is set, elementsd attempts to connect to
@ -136,11 +159,11 @@ e2 = Daemon(
# Alternatively, you can set validatepegin=0 (it defaults to being on) in the
# elementsd config, and run it without a Bitcoin node, but this means that you
# will not be fully validating the two-way peg.
print ("1a. Attempting to start a validatepegin daemon without a bitcoind (will fail)")
assert e1["validatepegin"] == "1"
e1.start()
time.sleep(1) ## give daemon a moment to start up (or not)
try:
e1.getinfo()
print ("ERROR: was able to start an elementsd without a working bitcoind")
@ -151,106 +174,208 @@ except:
# 1b. Start bitcoind, then elementsd. Initially, the bitcoind may be warming up and
# inaccessible over RPC. elementsd can detect this case and will stall until the
# bitcoind is warmed up.
print ("1b. Attempting to start validatepegin daemons with a bitcoind (will succeed)")
bitcoin.start()
e1.start()
e2.start()
time.sleep(1) ## give daemons a moment to start up
# Connect the nodes. This can also be accomplished with the `connect=` config
# parameter, but when starting two nodes simultaneously, this is unreliable.
e1.connect_to(e2)
e2.connect_to(e1)
# 1c. Create a wallet on the Elements nodes. This is needed since version 0.21
# of the daemon; previously a wallet was created by default if one does not
# already exist.
e1.createwallet("wallet1")
e2.createwallet("wallet2")
print ("1c. Creating wallets on all daemons")
# We have configured this regtest chain to start with 21M bitcoins, which are initally
# in a single OP_TRUE output. All Elements wallets recognize OP_TRUE outputs as their
# own (this differs from Bitcoin), so the 21M bitcoins are immediately available for
# use.
# use. This can be disabled by setting `anyonecanspend_aremine=0` in the daemon config.
#
# This is useful for testing basic functionality and for blockchains that have no peg,
# since every blockchain needs a default "policy asset". This policy asset is used
# for transaction fees (which are required for anti-DoS purposes). Also, asset
# issuances require some pre-existing asset, since they consume inputs for entropy.
#
# To separate the policy asset (used for fees) from the peg asset, use the `-policyasset`
# configuration value.
#
# In Elements there is no block subsidy. In a production sidechain, `initialfreecoins`
# will likely be set to zero, necessitating peg-in functionality to get a policy asset.
e1.createwallet("wallet1")
e2.createwallet("wallet2")
# Because of https://github.com/ElementsProject/elements/issues/956 we need to run
# `rescanblockchain` after creating the wallets to detect the `TRUE` outputs
assert e1["initialfreecoins"] == "2100000000000000"
print (e1.getwalletinfo())
assert e1.getwalletinfo()['balance'] == { 'bitcoin': 0 }
e1.rescanblockchain()
e2.rescanblockchain()
assert e1.getwalletinfo()['balance'] == { 'bitcoin': 21000000 }
assert e2.getwalletinfo()['balance'] == { 'bitcoin': 21000000 }
# All the initial coins coins are in one UTXO
assert len(e1.listunspent()) == 1
# ...and both nodes think they own it. This is a common situation when
# using near-empty test chains, so be aware of it.
assert e1.listunspent() == e2.listunspent()
print ("Waiting 2 mins")
time.sleep(120) ## give daemons a moment to start up
sys.exit(0)
# 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)
# Generate 10 blocks to demonstrate how block generation work. On our test chain,
# each block has a subsidy of zero (this can be changed with `con_blocksubsidy`)
# and sends the coins to an OP_TRUE output (this script can be changed with
# the `-signblockscript` config).
e1.generatetoaddress(10, e1.getnewaddress())
# The wallet does not recognize zero-valued outputs as being owned
assert len(e1.listunspent()) == 1
assert e1.getwalletinfo()['balance'] == { 'bitcoin': 21000000 }
# WALLET
# Synchronize the chains
sync_all([e1, e2])
assert e1.getblockcount() == 10
assert e1.getbestblockhash() == e2.getbestblockhash()
# 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)
## 2. Basic wallet usage
print ("")
print ("2. Basic wallet usage")
# Funds should now be evenly split between the two wallets
e1.getwalletinfo()
e2.getwalletinfo()
# 2a. Send all the coins to e1
# Observe that this address is a confidential address (is much longer
# than an ordinary address). Using the `validateaddress` RPC, you can
# see details of the address, including its unconfidential version.
addr = e1.getnewaddress()
print ("2a. Sending all initial coins to e1 (address %s)", addr)
txid1 = e1.sendtoaddress(addr, 21000000, "", "", True)
e1.generatetoaddress(1, e1.getnewaddress())
sync_all([e1, e2])
assert len(e1.listunspent()) == 1 # change output, but no coinbase
assert len(e2.listunspent()) == 0
# Have e2 send coins to themself using a blinded Elements address
# Blinded addresses start with `CTE`, unblinded `2`
# 2b. Send half of these to e2
addr = e2.getnewaddress()
print ("2b. Sending half to e2 (address %s)." % addr)
txid2 = e1.sendtoaddress(addr, 10500000, "", "", False)
e1.generatetoaddress(1, e1.getnewaddress())
assert len(e1.listunspent()) == 1 # change output, but no coinbase
e1.generatetoaddress(99, e1.getnewaddress())
assert len(e1.listunspent()) == 2 # change output, and coinbase with fees from first transaction
e1.generatetoaddress(1, e1.getnewaddress())
assert len(e1.listunspent()) == 3 # ...and fees from the second transaction
sync_all([e1, e2])
assert len(e2.listunspent()) == 1
# How do we know it's blinded? Check for blinding key, unblinded address.
e2.getaddressinfo(addr)
# Funds should now be evenly split between the two wallets. e2 directly
# received 10500000, while e1 has the remainder, less fees (because it
# created the transactions), plus fees (because it created the blocks).
assert e1.getbalance() == e2.getbalance()
assert e1.listunspent() != e2.listunspent()
# Basic blinded send
txid = e2.sendtoaddress(addr, 1)
# 2c. Self-send this half to e2 again
addr = e2.getnewaddress()
print ("2c. Self-sending this half to e2 (address %s)." % addr)
txid3 = e2.sendtoaddress(addr, 10500000, "", "", True)
sync_all([e1, e2])
e1.generatetoaddress(101, e1.getnewaddress())
sync_all([e1, e2])
e2.generatetoaddress(1, e1.getnewaddress())
sync_all(e1, e2)
# New e1 has slightly more coins than e2, because it received the fees
# from the last transaction
assert len(e1.listunspent()) == 4
assert len(e2.listunspent()) == 1
assert e1.getbalance()['bitcoin'] > e2.getbalance()['bitcoin']
# Now let's examine the transaction, both in wallet and without
# 2d. Analyze transactions
print ("2d. Analyzing transactions.")
# In-wallet, take a look at blinding information
e2.gettransaction(txid)
# The first transaction is a 1-input-2-output transaction starting from an
# unblinded input and where one output (the fee) must be unblinded. Since
# blinding the remaining output would accomplish nothing, it is unblinded
# even though a confidential address was used.
tx1 = e1.getrawtransaction(txid1, True)
assert len(tx1['vin']) == 1
assert len(tx1['vout']) == 2
assert all(['value' in out for out in tx1['vout']])
# The second transaction though has three outputs, including change. Now
# there is value in blinding, so both the destination output and change
# output are blinded
tx2 = e1.getrawtransaction(txid2, True)
assert len(tx2['vin']) == 1
assert len(tx2['vout']) == 3
assert any(['value-minimum' in out for out in tx2['vout']])
# The third transaction, which set subtractfeefromamount (the `True` passed
# to `sendtoaddress`), will again be a 1-input-2-output transaction, where
# one output is the unblinded fee. But since its input is confidential, the
# output will be too.
tx3 = e1.getrawtransaction(txid3, True)
assert len(tx3['vin']) == 1
assert len(tx3['vout']) == 2
assert any(['value' in out for out in tx3['vout']])
assert any(['value-minimum' in out for out in tx3['vout']])
# e1 doesn't have in wallet since it's unrelated
# Check that these transactions are visible in the correct wallets, with the
# expected effects. Check that they are not visible in the opposing wallet
assert e1.gettransaction(txid1)['amount'] == { 'bitcoin': 0 }
assert e2.gettransaction(txid1)['amount']['bitcoin'] < -20999999 # exact value depends on fee
assert e1.gettransaction(txid2)['amount'] == { 'bitcoin': -10500000 }
assert e2.gettransaction(txid2)['amount'] == { 'bitcoin': 10500000 }
assert e2.gettransaction(txid3)['amount'] == { 'bitcoin': 0 }
# txid3 appears only in e2, not e1
try:
e1.gettransaction(txid)
raise Exception("Transaction should not be in wallet")
e1.gettransaction(txid3)
except JSONRPCException:
pass
else:
raise Exception("Transaction 3 should not be in wallet 1")
# Get public info, see blinded ranges, etc
e1.getrawtransaction(txid, 1)
## 3. Confidential assets and keys
print ("")
print ("3. Confidential Keys")
current_e1_balance = e1.getbalance()
# 3a. Import an address's secret key
print ("3a. Import an address's secret key")
# Recall that `addr` was last set to an address owned by e2, and which
# we sent 10.5 million coins to in `tx3`. The public data (mostly hidden)
# is visible with `getrawtransaction`, while the confidential data is
# visible on `e2` (but not `e1`") with `gettransaction`.
# Now let's private import the key to attempt a spend
e1.importprivkey(e2.dumpprivkey(addr))
# Now `gettransaction` no longer triggers an exception, but the confidential
# data is still not available.
assert e1.gettransaction(txid3)['details'] == []
assert e2.gettransaction(txid3)['details'] != []
# Its output won't appear in listunspent, and the wallet balance will be unaffected
assert len(e1.listunspent()) == 4
assert e1.getbalance() == current_e1_balance
# 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)
# 3a. Import an address's blinding key
print ("3b. Import an address's blinding key")
# Solution: Import blinding key
e1.importblindingkey(addr, e2.dumpblindingkey(addr))
# Check again, funds should show
e1.getwalletinfo()
e1.listunspent(1, 1)
e1.gettransaction(txid)
assert len(e1.listunspent()) == 5
assert e1.getbalance()['bitcoin'] > current_e1_balance['bitcoin']
assert e1.gettransaction(txid3)['details'] != []
# Move funds to fresh addresses to avoid any confusion related to shared
# coins down the line (e.g. conflicting transactions).
e1.sendtoaddress(e1.getnewaddress(), e1.getbalance()['bitcoin'], "", "", True)
e1.sendtoaddress(e2.getnewaddress(), e1.getbalance()['bitcoin'] / 2, "", "", True)
e1.generatetoaddress(1, e1.getnewaddress())
sync_all([e1, e2])
## 4. 2-of-2 multisig
#
# Let's build a blinded 2-of-2 multisig p2sh address
print ("")
print ("4. 2-of-2 multisig")
print ("4a. Create a multisig address.")
# 1) Get unblinded addresses from each participant
addr1 = e1.getaddressinfo(e1.getnewaddress())["unconfidential"]
addr2 = e2.getaddressinfo(e2.getnewaddress())["unconfidential"]
@ -264,6 +389,7 @@ blindingpubkey = addrinfo1["confidential_key"]
# 3) Make multisig address like usual
multisig = e1.createmultisig(2, [addrinfo1["pubkey"], addrinfo2["pubkey"]])
print ("4b. Blind the multisig address (using a blinding key from e1).")
# 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
@ -273,105 +399,171 @@ 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)
sync_all([e1, e2])
assert e1.gettransaction(txid, True)['details'] != []
assert e2.gettransaction(txid, True)['details'] == []
# 6) Import the blinding privkey and decode the values
print ("4c. Share the blinding key with e2")
e2.importblindingkey(blinded_addr, blindingkey)
e2.gettransaction(txid, True)
assert e1.gettransaction(txid, True)['details'] != []
assert e2.gettransaction(txid, True)['details'] != []
# 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()
## 5. Multi-asset support
#
# Many of the RPC calls have added asset type or label arguments, and reveal
# alternative asset information. With no argument all are listed. For example,
# try `getwalletinfo` or `getbalance`. (Notice in the above code our assertions
# have taken forms like {"bitcoin": 100} rather than bare numbers.)
#
# Notice we now see "bitcoin" as an asset. This is the asset label for the hex
# for "bitcoin" which can be discovered using the `dumpassetlabels` RPC. We
# can see more details of each issuance that your wallet knows about with the
# `listissuances` RPC. Initially there is only one asset, "bitcoin", and one
# issuance (the initial issuance).
print ("")
print ("5. Multi-asset support")
print ("Existing assets: ", e1.dumpassetlabels())
assert len(e1.dumpassetlabels()) == 1
assert len(e1.listissuances()) == 1
assert e1.listissuances()[0]['assetlabel'] == "bitcoin"
assert e1.listissuances()[0]['assetamount'] == 21000000 # 21M initial free coins
assert e1.listissuances()[0]['tokenamount'] == 0 # no reissuance tokens
assert e1.dumpassetlabels() == e2.dumpassetlabels()
print ("5a. Issue a new asset, with reissuance token")
# 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()
assert len(e1.listissuances()) == 2
assert len(e2.listissuances()) == 1 ## e2 does not recognize this as a wallet issuance
new_issuances = [i for i in e1.listissuances() if 'assetlabel' not in i]
assert len(new_issuances) == 1
assert new_issuances[0]['assetamount'] == 1
assert new_issuances[0]['tokenamount'] == 1
assert len(e2.listissuances()) == 1 ## ANDREW
print ("5b. Reissue the asset using the reissuance token.")
# If you gave `issueasset` a reissuance token argument greater than 0
# you can also reissue the base asset
# you can also reissue the base asset. This will appear as a second issuance
# in `listissuances`.
e1.reissueasset(asset, 1)
new_issuances = [i for i in e1.listissuances() if 'assetlabel' not in i]
assert len(new_issuances) == 2
assert new_issuances[0]['assetamount'] == 1
assert new_issuances[1]['assetamount'] == 1
# The original issuance will show `tokenamount` while the new one will not have
# this field. Python makes it annoying to assert this.
# or make another different unblinded asset issuance, with only reissuance tokens initially
e1.issueasset(0, 1, False)
print ("5c. Issue a new asset with only reissuance tokens, no actual asset.")
new_issue = e1.issueasset(0, 1, False) # `False` tells elementsd not to blind the issuance
assert len(e1.listissuances()) == 4
assert len(e2.listissuances()) == 1
issuance = [i for i in e1.listissuances() if i.get('asset') == new_issue['asset']][0]
assert issuance['assetamount'] == -1 # should be 0, see https://github.com/ElementsProject/elements/issues/1035
assert issuance['tokenamount'] == 1
# Then two issuances for that particular asset will show
e1.listissuances(asset)
sync_all([e1, e2])
e1.generatetoaddress(1, e1.getnewaddress())
sync_all([e1, e2])
print ("5d. Label a new asset.")
# To label any asset add a new argument like this to your elements.conf file
# then restart your daemon:
assetentry = "-assetdir="+asset+":namedasset"
# then restart your daemon. Remember to reload the wallet after restarting.
# Wallet labels have no consensus meaning, only local node/wallet meaning
assetentry = "-assetdir="+asset+":namedasset"
e1.restart([assetentry], keep_datadir=True)
sync_all(e1, e2)
e1.stop()
time.sleep(5)
assert e1.getbestblockhash() == e2.getbestblockhash() ## sanity check that node remembers the blockchain
e1.connect_to(e2)
e2.connect_to(e1)
e1.loadwallet("wallet1")
# Restart with a new asset label
e1 = startelementsd(e1_datadir, e1conf, [assetentry])
time.sleep(5)
e1.getwalletinfo()
# The new label will be reflected in the RPC
assert e1.getwalletinfo()['balance']['namedasset'] == 2
assert e1.getbalance()['namedasset'] == 2
print ("5e. Transfer assets.")
# 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)
sync_all([e1, e2])
# e2 wallet doesn't know about label, just an unnamed asset
e2.getwalletinfo()["unconfirmed_balance"][asset]
assert e2.getwalletinfo()["unconfirmed_balance"][asset] == 1
assert "namedasset" not in e2.getwalletinfo()["unconfirmed_balance"]
e2.generatetoaddress(1, e2.getnewaddress())
sync_all(e1, e2)
sync_all([e1, e2])
# e2 maybe doesn't know about the issuance for the transaction sending him the new asset
e2.listissuances()
# e2, despite receiving an asset, continues not to know about its issuances
assert len(e2.listissuances()) == 1
# ...and therefore, despite receiving a reissuance token, does not understand
# it and cannot use it to issue
try:
e2.reissueasset(issue["asset"], 5)
except JSONRPCException:
pass
else:
raise Exception("Should not be able to reissue a reissuance token")
# let's import an associated address(so the wallet captures issuance transaction) and rescan
print ("5e. Import an address used in an issuance.")
# However, if we import the address used in the issuance transaction and
# rescan, the wallet _will_ learn about the issuance, although it will
# not know about the amounts (which are blinded)
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()
assert len(e2.listissuances()) == 2
new_issuances = [i for i in e2.listissuances() if 'assetlabel' not in i]
assert len(new_issuances) == 1
assert new_issuances[0]['assetamount'] == -1
assert new_issuances[0]['tokenamount'] == -1
# At this point, e2 knows that its reissuance token is actually a reissuance
# token, and can use it to reissue. Even though it does not know about the
# original issuance.
e2.reissueasset(issue["asset"], 1)
# 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()
assert len(e2.listissuances()) == 3
new_issuances = [i for i in e2.listissuances() if 'assetlabel' not in i]
assert len(new_issuances) == 2
assert new_issuances[0]['assetamount'] == 1
assert new_issuances[1]['assetamount'] == 1
# 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
# Reissuing reissuance tokens is not supported
try:
e2.reissueasset(issue["token"], 1)
except JSONRPCException:
pass
else:
raise Exception("Should not be able to reissue a reissuance token")
# For de-issuance, we can send assets or issuance tokens to an OP_RETURN output, provably burning them
e2.destroyamount(issue["asset"], 5)
e2.destroyamount(issue["asset"], 1)
# BLOCKSIGNING
# Recall blocksigning is OP_TRUE
sync_all([e1, e2])
e1.generatetoaddress(1, e1.getnewaddress())
sync_all(e1, e2)
sync_all([e1, e2])
# Let's set it to something more interesting... 2-of-2 multisig
## 6. Blocksigning
#
# Up to now, we have been generating blocks to the default OP_TRUE script. Let's
# make this script more interesting. We'll use a 2-of-2 multisig made from keys
# from our two Elements nodes.
#
print ("")
print ("6. Blocksigning")
print ("6a. Generating 2-of-2 blocksigning script")
# First lets get some keys from both clients to make our block "challenge"
addr1 = e1.getnewaddress()
@ -384,46 +576,51 @@ 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"
dyna_deploy_start="-con_dyna_deploy_start=0"
# We need to define a witness script, which defines the 2-of-2 checkmultisig
witness_script = "5221" + pubkey1 + "21" + pubkey2 + "52ae"
extra_args = [
signblockarg,
blocksign_max_size,
dyna_deploy_start,
# We set the signblockscript to the witness script
"-signblockscript=" + witness_script,
# To prevent malleability attacks, we must set a maximum signature size. Since
# we expect to have two ECDSA signatures (each at most 73 bytes), 150 bytes is
# a sufficient value.
"-con_max_block_sig_size=150",
# We also disable dynamic federations, since we are not going to do any
# dynafed transitions in this tutorial. FIXME we probably should.
"-con_dyna_deploy_start=0",
]
# Wipe out datadirs, start over
shutil.rmtree(e1_datadir)
shutil.rmtree(e2_datadir)
os.makedirs(e1_datadir)
os.makedirs(e2_datadir)
print ("6b. Restart both nodes")
# Restart both nodes with the new consensus rules
e1.restart(extra_args)
e2.restart(extra_args)
e1.connect_to(e2)
e2.connect_to(e1)
# 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)
# We cleared the datadirs, but even if we had not, changing the consensus
# rules would have invalidated the past blockchain and required we reset
# anyway. Now we have only the genesis block.
assert e1.getblockcount() == 0
assert e2.getblockcount() == 0
assert e1.getbestblockhash() == e2.getbestblockhash()
print ("6c. Import signing keys")
# Now import signing keys
e1.createwallet("wallet1")
e2.createwallet("wallet2")
e1.importprivkey(key1)
e2.importprivkey(key2)
# Generate no longer works, even if keys are in wallet
# Generate no longer works, since neither node has sufficiently
# many keys to sign a block. In fact, even if both keys were
# available, `generatetoaddress` would not work because it does
# not attempt to solve the blocksigning script.
try:
e1.generatetoaddress(1, e1.getnewaddress())
raise Exception("Generate shouldn't work")
except JSONRPCException:
pass
else:
raise Exception("Generate shouldn't work")
try:
e1.generatetoaddress(1, e1.getnewaddress())
@ -431,143 +628,192 @@ try:
except JSONRPCException:
pass
# Let's propose and accept some blocks, e1 is master!
print ("6d. Propose a block")
# Have e1 propose a block for both nodes to sign
blockhex = e1.getnewblockhex()
# Unsigned is no good
# Without signing the block, it is not accepted by consensus
# 0 before, 0 after
e1.getblockcount() == 0
assert e1.getblockcount() == 0
e1.submitblock(blockhex)
assert e1.getblockcount() == 0
# Still 0
e1.getblockcount() == 0
print ("6d. Sign the block")
# 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)
sign1 = e1.signblock(blockhex, witness_script)
sign2 = e2.signblock(blockhex, witness_script)
assert len(sign1) == 1 # both nodes produce one signature
assert len(sign2) == 1
# We now can gather signatures any way you want, combine them into a fully signed block
# Obtain signatures from both nodes. Both signatures are required for the block
# to be considered complete.
blockresult = e1.combineblocksigs(blockhex, [sign1[0]])
assert not blockresult["complete"]
blockresult = e1.combineblocksigs(blockhex, [sign2[0]])
assert not blockresult["complete"]
blockresult = e1.combineblocksigs(blockhex, [sign1[0], sign2[0]])
assert blockresult["complete"]
blockresult["complete"] == True
signedblock = blockresult["hex"]
# Now submit the block, doesn't matter who
e2.submitblock(signedblock)
sync_all(e1, e2)
print ("6d. Submit the block")
# Either node may submit the block to the network
e2.submitblock(blockresult["hex"])
sync_all([e1, e2])
# We now have moved forward one block!
e1.getblockcount() == 1
e2.getblockcount() == 1
assert e1.getblockcount() == 1
assert e2.getblockcount() == 1
e1.stop()
e2.stop()
time.sleep(5)
## The peg
#
# Everything peg-related can be done inside the Elements daemon directly,
# except for processing pegouts. This is because processing pegouts involves
# moving coins on the Bitcoin blockchain. In a production system, this is
# the most difficult part to get right, and by far the most important, as
# there is no going back if you lose funds on Bitcoin.
#
print ("")
print ("7. Dealing with the peg")
print ("7a. Restart both nodes")
# Further Exercises:
# - Make a python script that does round-robin consensus
extra_args = [
# We'll lazily reuse our blocksigning script to handle the peg, and
# leave the blocksigning script unset (so it will revert to OP_TRUE)
"-fedpegscript=" + witness_script,
# Set initial free coins to 0, since we will now use the peg
"-initialfreecoins=0",
]
# Restart both nodes with the new consensus rules
e1.restart(extra_args)
e2.restart(extra_args)
e1.connect_to(e2)
e2.connect_to(e1)
# Pegging
e1.createwallet("wallet1")
e2.createwallet("wallet2")
e1.rescanblockchain()
e2.rescanblockchain()
assert e1.getwalletinfo()['balance'] == { 'bitcoin': 0 }
assert e2.getwalletinfo()['balance'] == { 'bitcoin': 0 }
# 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.
# Generate some Elements blocks as we cannot accept pegin claims
# while the blockheight is 0 (this may be unintended behavior).
e1.generatetoaddress(5, e1.getnewaddress())
sync_all([e1, e2])
# 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())
# Create some mature Bitcoin outputs
bitcoin.createwallet("bwallet")
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
print ("7b. Create a peg-in address")
# Once there are mature coins on the Bitcoin side, we can peg them in.
# We create a pegin address, which we create with the Elements wallet
# but which is a Bitcoin address.
#
# Internally, this address is computed by generating an Elements address
# then using the underlying script, called a "claim script", to "tweak"
# the peg witness script.
#
# The claim script is provided by the `getpeginaddress` RPC, but it is
# not necessary to keep, for most usecases, because it is also stored
# in the wallet.
peg_in_addr = e1.getpeginaddress()
assert "mainchain_address" in peg_in_addr
assert "claim_script" in peg_in_addr
# 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()
print ("7c. Send Bitcoin to the pegin address")
txid = bitcoin.sendtoaddress(peg_in_addr["mainchain_address"], 10)
#Send funds to unique watchmen P2SH address
txid = bitcoin.sendtoaddress(addrs["mainchain_address"], 1)
print ("7d. Claim the Bitcoin on the sidechain")
# Once the coins are sent on the Bitcoin side, they will not be recognized
# or accepted by the sidechain until they are buried by 100 confirmations.
# This value may be changed by use of the `-peginconfirmationdepth` config
# setting on the Elements daemon.
#
# Bear in mind that this is a consensus rule and all nodes must agree on it.
# First, try claiming the pegin early. This will fail.
bitcoin.generatetoaddress(1, bitcoin.getnewaddress())
# 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)
try:
claimtxid = e1.claimpegin(raw, proof, peg_in_addr["claim_script"])
except JSONRPCException:
pass
else:
raise Exception("Should not be able to claim a pegin early")
# Attempt claim!
claimtxid = e1.claimpegin(raw, proof, addrs["claim_script"])
sync_all(e1, e2)
# Other node should accept to mempool and mine
# After 100 blocks, the claim will work. Note that the original proof will
# work, as long as no reorgs changed which block the pegin transaction was
# included in.
bitcoin.generatetoaddress(100, bitcoin.getnewaddress())
claimtxid = e1.claimpegin(raw, proof, peg_in_addr["claim_script"])
# Mine this from the other node, to confirm that mempool propagation works
sync_all([e1, e2])
e2.generatetoaddress(1, e1.getnewaddress())
sync_all(e1, e2)
sync_all([e1, e2])
# Should see confirmations
"confirmations" in e1.getrawtransaction(claimtxid, 1)
assert "confirmations" in e1.getrawtransaction(claimtxid, 1)
sys.exit(0)
# Pegging Out
print ("7e. Request pegout")
# This burns coins on Liquid using a specially-structured OP_RETURN output.
# In a production network, doing this would trigger the watchman federation
# to send payment to the specified Bitcoin address on the mainchain.
#
# The Bitcoin-side functionality is not supported directly in Elements;
# the watchmen are expected to notice this transaction and send the funds
# from their collective wallet.
e1.sendtomainchain(bitcoin.getnewaddress(), 5)
# 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
## Exercise(s)
#
# 1. Implement really dumb/unsafe watchmen to allow pegouts for learning purposes.
# To custody the coins that users peg in, you need to extract the tweak from
# the pegin claim, use this tweak to adjust the secret keys, and import the
# resulting keys to a Core wallet.
#
# 2. Create an alternate peg from testnet to Liquid, using asset issuance and
# destruction.
#
# 3. Implement a round-robin blocksigner protocol, where each signer takes a turn
# proposing blocks for the others to sign.
#
print ("")
print ("8. Raw transaction demo")
# 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
# 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,
# which may be set in `createrawtransaction`. These will be added or adjusted
# by `fundrawtransaction` to make the transaction balance.
rawtx2 = e1.createrawtransaction(
[],
[{ e2.getnewaddress(): 100 }, { e1.getnewaddress() :5 }, { "fee": Decimal("0.1") }],
)
# Fee outputs are unblinded, with a scriptPubKey of "". On Elements, empty
# 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.
# In some cases, such as when there is one blinded output but no blinded inputs,
# blinding will fail. The `ignoreblindfails` option to `blindrawtransaction` may
# be set, in which case the transaction will "successfully" not be blinded.
#
# To unconditionally blind a transaction, ensure that it has 2 or more outputs,
# which will ensure that blinding is (a) useful and (b) mathematically possible.
# Sign
signedtx = e1.signrawtransactionwithwallet(blindedtx)
@ -575,18 +821,5 @@ 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)
print ("Finished!")

View file

@ -2,10 +2,19 @@ regtest=1
txindex=1
# We are spawning these inside a Python script which will manage them, so don't daemonize
daemon=0
# Extra debugging output in case things go wrong
debug=1
debugexclude=libevent
debugexclude=leveldb
printtoconsole=0
rpcuser=user3
rpcpassword=password3
# Set a fallback fee, since initially the nodes will have no transaction data
# to do fee estimation from
fallbackfee=0.0002
[regtest]
rpcport=18888
port=18889

View file

@ -12,6 +12,11 @@ daemon=0
listen=1
# Just for looking at random txs
txindex=1
# Extra debugging output in case things go wrong
debug=1
debugexclude=libevent
debugexclude=leveldb
printtoconsole=0
# This is the script that controls pegged in funds in Bitcoin network
# Users will be pegging into a P2SH of this, and the "watchmen"
@ -39,9 +44,11 @@ mainchainrpcpassword=password3
# Free money to make testing easier
initialfreecoins=2100000000000000
# Set a fallback fee, since initially the nodes will have no transaction data
# to do fee estimation from
fallbackfee=0.0002
[elementsregtest]
rpcport=18884
port=18886
# Over p2p we will only connect to local other elementsd
connect=localhost:18887

View file

@ -7,6 +7,11 @@ rpcpassword=password2
daemon=0
listen=1
txindex=1
# Extra debugging output in case things go wrong
debug=1
debugexclude=libevent
debugexclude=leveldb
printtoconsole=0
#fedpegscript=51<pubkey>51ae
#signblockscript=51<pubkey2>51ae
@ -18,8 +23,11 @@ validatepegin=1
initialfreecoins=2100000000000000
# Set a fallback fee, since initially the nodes will have no transaction data
# to do fee estimation from
fallbackfee=0.0002
[elementsregtest]
rpcport=18885
port=18887
connect=localhost:18886