-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathscenario.py
526 lines (458 loc) · 18.4 KB
/
scenario.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
#!/usr/bin/env python3
# Copyright (c) 2015-2020 Clearmatics Technologies Ltd
#
# SPDX-License-Identifier: LGPL-3.0+
import os
from python_web3.codegen import ABICodegen
from zeth.mixer_client import MixerClient, OwnershipKeyPair, joinsplit_sign, \
encrypt_notes, get_dummy_input_and_address, compute_h_sig, \
JoinsplitSigVerificationKey
import zeth.contracts as contracts
from zeth.constants import ZETH_PRIME, FIELD_CAPACITY, DEFAULT_MIX_GAS_WEI
import zeth.signing as signing
from zeth.merkle_tree import MerkleTree, compute_merkle_path
from zeth.utils import EtherValue, to_zeth_units
import test_commands.mock as mock
from api.zeth_messages_pb2 import ZethNote
from zeth.contracts import Interface
from zeth.utils import get_zeth_dir
from zeth.constants import SOL_COMPILER_VERSION
from click import command, argument
from os.path import join
from solcx import compile_files, set_solc_version
from typing import Any
from os import urandom
from web3 import Web3 # type: ignore
from typing import List, Tuple, Optional, Any
ZERO_UNITS_HEX = "0000000000000000"
BOB_DEPOSIT_ETH = 200
BOB_SPLIT_1_ETH = 100
BOB_SPLIT_2_ETH = 100
BOB_TO_CHARLIE_ETH = 50
BOB_TO_CHARLIE_CHANGE_ETH = BOB_SPLIT_1_ETH - BOB_TO_CHARLIE_ETH
CHARLIE_WITHDRAW_ETH = 10.5
CHARLIE_WITHDRAW_CHANGE_ETH = 39.5
def dump_merkle_tree(mk_tree: List[bytes]) -> None:
print("[DEBUG] Displaying the Merkle tree of commitments: ")
for node in mk_tree:
print("Node: " + Web3.toHex(node)[2:])
def _event_args_to_mix_result(event_args: Any) -> contracts.MixResult:
mix_out_args = zip(event_args.commitments, event_args.ciphertexts)
out_events = [contracts.MixOutputEvents(c, ciph) for (c, ciph) in mix_out_args]
return contracts.MixResult(
new_merkle_root=event_args.root,
nullifiers=event_args.nullifiers,
output_events=out_events)
class LogMixEvent(object):
def __init__(
self,
root: bytes,
nullifiers: bytes(2),
commitments: bytes(2),
ciphertexts: bytes(2)):
self.root = root
self.nullifiers = nullifiers
self.commitments = commitments
self.ciphertexts = ciphertexts
def wait_for_tx_update_mk_tree(
zeth_client: MixerClient,
mk_tree: MerkleTree,
receipt: Any) -> contracts.MixResult:
#tx_receipt = zeth_client.web3.eth.waitForTransactionReceipt(tx_hash, 10000)
#result = contracts.parse_mix_call(zeth_client.mixer_instance, tx_receipt)
logresult = zeth_client.mixer_instance.data_parser.parse_event_logs(receipt["logs"])
print("logresult: ", logresult)
logMix = logresult[0]['eventdata']
logMixEvent = LogMixEvent(logMix[0],logMix[1], logMix[2], logMix[3])
result = _event_args_to_mix_result(logMixEvent)
for out_ev in result.output_events:
mk_tree.insert(out_ev.commitment)
if mk_tree.recompute_root() != result.new_merkle_root:
raise Exception("Merkle root mismatch between log and local tree")
return result
def bob_deposit(
zeth_client: MixerClient,
mk_tree: MerkleTree,
bob_eth_address: str,
keystore: mock.KeyStore,
tx_value: Optional[EtherValue] = None) -> contracts.MixResult:
print(
f"=== Bob deposits {BOB_DEPOSIT_ETH} ETH for himself and splits into " +
f"note1: {BOB_SPLIT_1_ETH}ETH, note2: {BOB_SPLIT_2_ETH}ETH ===")
bob_js_keypair = keystore["Bob"]
bob_addr = keystore["Bob"].addr_pk
outputs = [
(bob_addr, EtherValue(BOB_SPLIT_1_ETH)),
(bob_addr, EtherValue(BOB_SPLIT_2_ETH)),
]
(outputresult, receipt) = zeth_client.deposit(
mk_tree,
bob_js_keypair,
bob_eth_address,
EtherValue(BOB_DEPOSIT_ETH),
outputs,
tx_value)
# print(outputresult)
print("receipt status: ", receipt['status'])
return wait_for_tx_update_mk_tree(zeth_client, mk_tree, receipt)
def bob_to_charlie(
zeth_client: MixerClient,
mk_tree: MerkleTree,
input1: Tuple[int, ZethNote],
bob_eth_address: str,
keystore: mock.KeyStore) -> contracts.MixResult:
print(
f"=== Bob transfers {BOB_TO_CHARLIE_ETH}ETH to Charlie from his funds " +
"on the mixer ===")
bob_ask = keystore["Bob"].addr_sk.a_sk
charlie_addr = keystore["Charlie"].addr_pk
bob_addr = keystore["Bob"].addr_pk
# Coin for Bob (change)
output0 = (bob_addr, EtherValue(BOB_TO_CHARLIE_ETH))
# Coin for Charlie
output1 = (charlie_addr, EtherValue(BOB_TO_CHARLIE_CHANGE_ETH))
# Send the tx
(outputresult, receipt) = zeth_client.joinsplit(
mk_tree,
OwnershipKeyPair(bob_ask, bob_addr.a_pk),
bob_eth_address,
[input1],
[output0, output1],
EtherValue(0),
EtherValue(0),
EtherValue(1, 'wei'))
return wait_for_tx_update_mk_tree(zeth_client, mk_tree, receipt)
def charlie_withdraw(
zeth_client: MixerClient,
mk_tree: MerkleTree,
input1: Tuple[int, ZethNote],
charlie_eth_address: str,
keystore: mock.KeyStore) -> contracts.MixResult:
print(
f" === Charlie withdraws {CHARLIE_WITHDRAW_ETH}ETH from his funds " +
"on the Mixer ===")
charlie_pk = keystore["Charlie"].addr_pk
charlie_apk = charlie_pk.a_pk
charlie_ask = keystore["Charlie"].addr_sk.a_sk
charlie_ownership_key = \
OwnershipKeyPair(charlie_ask, charlie_apk)
(outputresult, receipt) = zeth_client.joinsplit(
mk_tree,
charlie_ownership_key,
charlie_eth_address,
[input1],
[(charlie_pk, EtherValue(CHARLIE_WITHDRAW_CHANGE_ETH))],
EtherValue(0),
EtherValue(CHARLIE_WITHDRAW_ETH),
EtherValue(1, 'wei'))
return wait_for_tx_update_mk_tree(zeth_client, mk_tree, receipt)
def charlie_double_withdraw(
zeth_client: MixerClient,
mk_tree: MerkleTree,
input1: Tuple[int, ZethNote],
charlie_eth_address: str,
keystore: mock.KeyStore) -> contracts.MixResult:
"""
Charlie tries to carry out a double spending by modifying the value of the
nullifier of the previous payment
"""
print(
f" === Charlie attempts to withdraw {CHARLIE_WITHDRAW_ETH}ETH once " +
"more (double spend) one of his note on the Mixer ===")
charlie_apk = keystore["Charlie"].addr_pk.a_pk
charlie_ask = keystore["Charlie"].addr_sk.a_sk
tree_depth = mk_tree.depth
mk_path1 = compute_merkle_path(input1[0], mk_tree)
mk_root = mk_tree.get_root()
# Create the an additional dummy input for the MixerClient
input2 = get_dummy_input_and_address(charlie_apk)
dummy_mk_path = mock.get_dummy_merkle_path(tree_depth)
note1_value = to_zeth_units(EtherValue(CHARLIE_WITHDRAW_CHANGE_ETH))
v_out = EtherValue(CHARLIE_WITHDRAW_ETH)
# ### ATTACK BLOCK
# Add malicious nullifiers: we reuse old nullifiers to double spend by
# adding $r$ to them so that they have the same value as before in Z_r,
# and so the zksnark verification passes, but have different values in
# {0;1}^256 so that they appear different to the contract.
# See: https://github.com/clearmatics/zeth/issues/38
attack_primary_input3: int = 0
attack_primary_input4: int = 0
def compute_h_sig_attack_nf(
nf0: bytes,
nf1: bytes,
sign_vk: JoinsplitSigVerificationKey) -> bytes:
# We disassemble the nfs to get the formatting of the primary inputs
input_nullifier0 = nf0.hex()
input_nullifier1 = nf1.hex()
nf0_rev = "{0:0256b}".format(int(input_nullifier0, 16))
primary_input3_bits = nf0_rev[:FIELD_CAPACITY]
primary_input3_res_bits = nf0_rev[FIELD_CAPACITY:]
nf1_rev = "{0:0256b}".format(int(input_nullifier1, 16))
primary_input4_bits = nf1_rev[:FIELD_CAPACITY]
primary_input4_res_bits = nf1_rev[FIELD_CAPACITY:]
# We perform the attack, recoding the modified public input values
nonlocal attack_primary_input3
nonlocal attack_primary_input4
attack_primary_input3 = int(primary_input3_bits, 2) + ZETH_PRIME
attack_primary_input4 = int(primary_input4_bits, 2) + ZETH_PRIME
# We reassemble the nfs
attack_primary_input3_bits = "{0:0256b}".format(attack_primary_input3)
attack_nf0_bits = attack_primary_input3_bits[
len(attack_primary_input3_bits) - FIELD_CAPACITY:] +\
primary_input3_res_bits
attack_nf0 = "{0:064x}".format(int(attack_nf0_bits, 2))
attack_primary_input4_bits = "{0:0256b}".format(attack_primary_input4)
attack_nf1_bits = attack_primary_input4_bits[
len(attack_primary_input4_bits) - FIELD_CAPACITY:] +\
primary_input4_res_bits
attack_nf1 = "{0:064x}".format(int(attack_nf1_bits, 2))
return compute_h_sig(
bytes.fromhex(attack_nf0), bytes.fromhex(attack_nf1), sign_vk)
(output_note1, output_note2, proof_json, signing_keypair) = \
zeth_client.get_proof_joinsplit_2_by_2(
mk_root,
input1,
mk_path1,
input2,
dummy_mk_path,
charlie_ask, # sender
(charlie_apk, note1_value), # recipient1
(charlie_apk, 0), # recipient2
to_zeth_units(EtherValue(0)), # v_in
to_zeth_units(v_out), # v_out
compute_h_sig_attack_nf)
# Update the primary inputs to the modified nullifiers, since libsnark
# overwrites them with values in Z_p
assert attack_primary_input3 != 0
assert attack_primary_input4 != 0
print("proof_json => ", proof_json)
print("proof_json[inputs][3] => ", proof_json["inputs"][3])
print("proof_json[inputs][4] => ", proof_json["inputs"][4])
proof_json["inputs"][3] = hex(attack_primary_input3)
proof_json["inputs"][4] = hex(attack_primary_input4)
# ### ATTACK BLOCK
# construct pk object from bytes
pk_charlie = keystore["Charlie"].addr_pk.k_pk
# encrypt the coins
ciphertexts = encrypt_notes([
(output_note1, pk_charlie),
(output_note2, pk_charlie)])
# Compute the joinSplit signature
joinsplit_sig_charlie = joinsplit_sign(
signing_keypair,
charlie_eth_address,
ciphertexts,
proof_json)
mix_params = contracts.MixParameters(
proof_json,
signing_keypair.vk,
joinsplit_sig_charlie,
ciphertexts)
tx_hash = zeth_client.mix(
mix_params,
charlie_eth_address,
# Pay an arbitrary amount (1 wei here) that will be refunded since the
# `mix` function is payable
Web3.toWei(1, 'wei'),
DEFAULT_MIX_GAS_WEI)
return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash)
def charlie_corrupt_bob_deposit(
zeth_client: MixerClient,
mk_tree: MerkleTree,
bob_eth_address: str,
charlie_eth_address: str,
keystore: mock.KeyStore) -> contracts.MixResult:
"""
Charlie tries to break transaction malleability and corrupt the coins
bob is sending in a transaction
She does so by intercepting bob's transaction and either:
- case 1: replacing the ciphertexts (or sender_eph_pk) by garbage/arbitrary
data
- case 2: replacing the ciphertexts by garbage/arbitrary data and using a
new OT-signature
- case 3: Charlie replays the mix call of Bob, to try to receive the vout
Both attacks should fail,
- case 1: the signature check should fail, else Charlie broke UF-CMA of the
OT signature
- case 2: the h_sig/vk verification should fail, as h_sig is not a function
of vk any longer
- case 3: the signature check should fail, because `msg.sender` will no match
the value used in the mix parameters (Bob's Ethereum Address).
NB. If the adversary were to corrupt the ciphertexts (or the encryption key),
replace the OT-signature by a new one and modify the h_sig accordingly so that
the check on the signature verification (key h_sig/vk) passes, the proof would
not verify, which is why we do not test this case.
"""
print(
f"=== Bob deposits {BOB_DEPOSIT_ETH} ETH for himself and split into " +
f"note1: {BOB_SPLIT_1_ETH}ETH, note2: {BOB_SPLIT_2_ETH}ETH" +
f"but Charlie attempts to corrupt the transaction ===")
bob_apk = keystore["Bob"].addr_pk.a_pk
bob_ask = keystore["Bob"].addr_sk.a_sk
tree_depth = mk_tree.depth
mk_root = mk_tree.get_root()
# mk_tree_depth = zeth_client.mk_tree_depth
# mk_root = zeth_client.merkle_root
# Create the JoinSplit dummy inputs for the deposit
input1 = get_dummy_input_and_address(bob_apk)
input2 = get_dummy_input_and_address(bob_apk)
dummy_mk_path = mock.get_dummy_merkle_path(tree_depth)
note1_value = to_zeth_units(EtherValue(BOB_SPLIT_1_ETH))
note2_value = to_zeth_units(EtherValue(BOB_SPLIT_2_ETH))
v_in = to_zeth_units(EtherValue(BOB_DEPOSIT_ETH))
(output_note1, output_note2, proof_json, joinsplit_keypair) = \
zeth_client.get_proof_joinsplit_2_by_2(
mk_root,
input1,
dummy_mk_path,
input2,
dummy_mk_path,
bob_ask, # sender
(bob_apk, note1_value), # recipient1
(bob_apk, note2_value), # recipient2
v_in, # v_in
to_zeth_units(EtherValue(0)) # v_out
)
# Encrypt the coins to bob
pk_bob = keystore["Bob"].addr_pk.k_pk
ciphertexts = encrypt_notes([
(output_note1, pk_bob),
(output_note2, pk_bob)])
# ### ATTACK BLOCK
# Charlie intercepts Bob's deposit, corrupts it and
# sends her transaction before Bob's transaction is accepted
# Case 1: replacing the ciphertexts by garbage/arbitrary data
# Corrupt the ciphertexts
# (another way would have been to overwrite sender_eph_pk)
fake_ciphertext0 = urandom(32)
fake_ciphertext1 = urandom(32)
result_corrupt1 = None
try:
joinsplit_sig_charlie = joinsplit_sign(
joinsplit_keypair,
charlie_eth_address,
ciphertexts,
proof_json)
mix_params = contracts.MixParameters(
proof_json,
joinsplit_keypair.vk,
joinsplit_sig_charlie,
[fake_ciphertext0, fake_ciphertext1])
tx_hash = zeth_client.mix(
mix_params,
charlie_eth_address,
Web3.toWei(BOB_DEPOSIT_ETH, 'ether'),
DEFAULT_MIX_GAS_WEI)
result_corrupt1 = \
wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash)
except Exception as e:
print(
f"Charlie's first corruption attempt" +
f" successfully rejected! (msg: {e})"
)
assert(result_corrupt1 is None), \
"Charlie managed to corrupt Bob's deposit the first time!"
print("")
# Case 2: replacing the ciphertexts by garbage/arbitrary data and
# using a new OT-signature
# Corrupt the ciphertexts
fake_ciphertext0 = urandom(32)
fake_ciphertext1 = urandom(32)
new_joinsplit_keypair = signing.gen_signing_keypair()
# Sign the primary inputs, sender_eph_pk and the ciphertexts
result_corrupt2 = None
try:
joinsplit_sig_charlie = joinsplit_sign(
new_joinsplit_keypair,
charlie_eth_address,
[fake_ciphertext0, fake_ciphertext1],
proof_json)
mix_params = contracts.MixParameters(
proof_json,
new_joinsplit_keypair.vk,
joinsplit_sig_charlie,
[fake_ciphertext0, fake_ciphertext1])
tx_hash = zeth_client.mix(
mix_params,
charlie_eth_address,
Web3.toWei(BOB_DEPOSIT_ETH, 'ether'),
DEFAULT_MIX_GAS_WEI)
result_corrupt2 = \
wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash)
except Exception as e:
print(
f"Charlie's second corruption attempt" +
f" successfully rejected! (msg: {e})"
)
assert(result_corrupt2 is None), \
"Charlie managed to corrupt Bob's deposit the second time!"
# Case3: Charlie uses the correct mix data, but attempts to send the mix
# call from his own address (thereby receiving the output).
result_corrupt3 = None
try:
joinsplit_sig_bob = joinsplit_sign(
joinsplit_keypair,
bob_eth_address,
ciphertexts,
proof_json)
mix_params = contracts.MixParameters(
proof_json,
joinsplit_keypair.vk,
joinsplit_sig_bob,
ciphertexts)
tx_hash = zeth_client.mix(
mix_params,
charlie_eth_address,
Web3.toWei(BOB_DEPOSIT_ETH, 'ether'),
4000000)
result_corrupt3 = \
wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash)
except Exception as e:
print(
f"Charlie's third corruption attempt" +
f" successfully rejected! (msg: {e})"
)
assert(result_corrupt3 is None), \
"Charlie managed to corrupt Bob's deposit the third time!"
# ### ATTACK BLOCK
# Bob transaction is finally mined
joinsplit_sig_bob = joinsplit_sign(
joinsplit_keypair,
bob_eth_address,
ciphertexts,
proof_json)
mix_params = contracts.MixParameters(
proof_json,
joinsplit_keypair.vk,
joinsplit_sig_bob,
ciphertexts)
tx_hash = zeth_client.mix(
mix_params,
bob_eth_address,
Web3.toWei(BOB_DEPOSIT_ETH, 'ether'),
DEFAULT_MIX_GAS_WEI)
return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash)
def compile_mixer() -> Interface:
allowed_path ="./contract/mixer"
path_to_token = "./contract/mixer/Groth16Mixer.sol"
# Compilation
set_solc_version(SOL_COMPILER_VERSION)
compiled_sol = compile_files([path_to_token], allow_paths=allowed_path)
mixer_interface = compiled_sol[path_to_token + ":Groth16Mixer"]
fo = open("./contract/mixer/abi/Groth16Mixer.abi", "w")
fo1 = open("./contract/mixer/abi/Groth16Mixer.bin", "w")
fo.write(str(mixer_interface["abi"]))
fo.close()
fo1.write(str(mixer_interface["bin"]))
fo1.close()
return mixer_interface
def code_gen(abi_file):
codegen = ABICodegen("./contract/mixer/abi/"+abi_file)
template = codegen.gen_all()
name = codegen.name + '.py'
# outputfile = os.path.join("./contract/mixer/abi/", )
fo = open("./contract/mixer/abi/Groth16Mixer.py", "w")
fo.write(template)
fo.close()
if __name__ == '__main__':
compile_mixer()