forked from ElementsProject/elements
-
Notifications
You must be signed in to change notification settings - Fork 0
/
assets_tutorial.py
executable file
·508 lines (387 loc) · 16.6 KB
/
assets_tutorial.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
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"
dyna_deploy_start="-con_dyna_deploy_start=0"
extra_args=signblockarg+" "+blocksign_max_size+" "+dyna_deploy_start
# 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)