forked from ElementsProject/elements
-
Notifications
You must be signed in to change notification settings - Fork 1
/
assets_tutorial.py
executable file
·719 lines (616 loc) · 27.3 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
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
#!/usr/bin/env python3
from test_framework.authproxy import JSONRPCException
from test_framework.daemon import Daemon, sync_all
import argparse
import sys
from decimal import Decimal
# 0. Boilerplate to make the tutorial executable as a script
#
# Skip ahead to step 1 if you are reading
#
# Parse arguments
parser = argparse.ArgumentParser()
parser.add_argument('--elementsd-dir', type=str, default='./src')
parser.add_argument('--bitcoind-dir', type=str, default='../bitcoin/src')
parser.add_argument('--no-cleanup', default=False, action="store_true")
args = parser.parse_args()
# Setup daemons
bitcoin = Daemon(
"Bitcoin",
"bitcoin",
args.bitcoind_dir + "/bitcoind",
"contrib/assets_tutorial/bitcoin.conf",
not args.no_cleanup,
)
e1 = Daemon(
"Elements1",
"elements",
args.elementsd_dir + "/elementsd",
"contrib/assets_tutorial/elements1.conf",
not args.no_cleanup,
)
e2 = Daemon(
"Elements2",
"elements",
args.elementsd_dir + "/elementsd",
"contrib/assets_tutorial/elements2.conf",
not args.no_cleanup,
)
# 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
# bitcoind to check if peg-in transactions are confirmed in the Bitcoin chain.
#
# 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()
try:
e1.getinfo()
print ("ERROR: was able to start an elementsd without a working bitcoind")
sys.exit(1)
except:
pass
# 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()
# 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.
print ("1c. Creating wallets on all daemons")
# We have configured this regtest chain to start with 21M bitcoins, which are initially
# 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. 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"
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()
# 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 }
# Synchronize the chains
sync_all([e1, e2])
assert e1.getblockcount() == 10
assert e1.getbestblockhash() == e2.getbestblockhash()
# 2. Basic wallet usage
print ("")
print ("2. Basic wallet usage")
# 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
# 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
# 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()
# 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])
# 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']
# 2d. Analyze transactions
print ("2d. Analyzing transactions.")
# 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']])
# 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(txid3)
except JSONRPCException:
pass
else:
raise Exception("Transaction 3 should not be in wallet 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
# 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
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"]
# 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"]])
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
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])
assert e1.gettransaction(txid, True)['details'] != []
assert e2.gettransaction(txid, True)['details'] == []
print ("4c. Share the blinding key with e2")
e2.importblindingkey(blinded_addr, blindingkey)
assert e1.gettransaction(txid, True)['details'] != []
assert e2.gettransaction(txid, True)['details'] != []
# 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
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
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. 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.
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
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. 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)
assert e1.getbestblockhash() == e2.getbestblockhash() ## sanity check that node remembers the blockchain
e1.connect_to(e2)
e2.connect_to(e1)
e1.loadwallet("wallet1")
# 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])
# e2 wallet doesn't know about label, just an unnamed 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])
# 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")
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)
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
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
# 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"], 1)
sync_all([e1, e2])
e1.generatetoaddress(1, e1.getnewaddress())
sync_all([e1, e2])
# 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()
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)
# We need to define a witness script, which defines the 2-of-2 checkmultisig
witness_script = "5221" + pubkey1 + "21" + pubkey2 + "52ae"
extra_args = [
# 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.
"-evbparams=dynafed:0:::",
]
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)
# 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, 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())
except JSONRPCException:
pass
else:
raise Exception("Generate shouldn't work")
try:
e1.generatetoaddress(1, e1.getnewaddress())
raise Exception("Generate shouldn't work")
except JSONRPCException:
pass
print ("6d. Propose a block")
# Have e1 propose a block for both nodes to sign
blockhex = e1.getnewblockhex()
# Without signing the block, it is not accepted by consensus
# 0 before, 0 after
assert e1.getblockcount() == 0
e1.submitblock(blockhex)
assert 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, witness_script)
sign2 = e2.signblock(blockhex, witness_script)
assert len(sign1) == 1 # both nodes produce one signature
assert len(sign2) == 1
# 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"]
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!
assert e1.getblockcount() == 1
assert e2.getblockcount() == 1
# 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")
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)
e1.createwallet("wallet1")
e2.createwallet("wallet2")
e1.rescanblockchain()
e2.rescanblockchain()
assert e1.getwalletinfo()['balance'] == { 'bitcoin': 0 }
assert e2.getwalletinfo()['balance'] == { 'bitcoin': 0 }
# 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])
# Create some mature Bitcoin outputs
bitcoin.createwallet("bwallet")
bitcoin.generatetoaddress(101, bitcoin.getnewaddress())
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
print ("7c. Send Bitcoin to the pegin address")
txid = bitcoin.sendtoaddress(peg_in_addr["mainchain_address"], 10)
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())
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")
# 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])
assert "confirmations" in e1.getrawtransaction(claimtxid, 1)
sys.exit(0)
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)
# 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,
# 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"])
# 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)
# And send
txid = e1.sendrawtransaction(signedtx["hex"])
print ("Finished!")