You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

680 lines
36 KiB

#!/usr/bin/env python
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import * # noqa: F401
#Proof Of Discrete Logarithm Equivalence
#For algorithm steps, see https://gist.github.com/AdamISZ/9cbba5e9408d23813ca8
import os
import sys
import hashlib
import json
import binascii
import struct
from jmbase import jmprint
from jmbitcoin import multiply, add_pubkeys, getG, podle_PublicKey,\
podle_PrivateKey, encode, decode, N, podle_PublicKey_class
PODLE_COMMIT_FILE = None
def set_commitment_file(file_loc):
global PODLE_COMMIT_FILE
PODLE_COMMIT_FILE = file_loc
def get_commitment_file():
return PODLE_COMMIT_FILE
class PoDLEError(Exception):
pass
class PoDLE(object):
"""See the comment to PoDLE.generate_podle for the
mathematical structure. This class encapsulates the
input data, the commitment and the opening (the "proof").
"""
def __init__(self,
u=None,
priv=None,
P=None,
P2=None,
s=None,
e=None,
used=False):
#This class allows storing of utxo in format "txid:n" only for
#convenience of storage/access; it doesn't check or use the data.
#Arguments must be provided in hex.
self.u = u
if not priv:
if P:
#Construct a pubkey from raw hex
self.P = podle_PublicKey(binascii.unhexlify(P))
else:
self.P = None
else:
if P:
raise PoDLEError("Pubkey should not be provided with privkey")
#any other formatting abnormality will just throw in PrivateKey
if len(priv) == 66 and priv[-2:] == '01':
priv = priv[:-2]
self.priv = podle_PrivateKey(binascii.unhexlify(priv))
self.P = self.priv.public_key
if P2:
self.P2 = podle_PublicKey(binascii.unhexlify(P2))
else:
self.P2 = None
#These sig values should be passed in hex.
self.s = None
self.e = None
if s:
self.s = binascii.unhexlify(s)
if e:
self.e = binascii.unhexlify(e)
#Optionally maintain usage state (boolean)
self.used = used
#the H(P2) value
self.commitment = None
def get_commitment(self):
"""Set the commitment to sha256(serialization of public key P2)
Return in hex to calling function
"""
if not self.P2:
raise PoDLEError("Cannot construct commitment, no P2 available")
if not isinstance(self.P2, podle_PublicKey_class):
raise PoDLEError("Cannot construct commitment, P2 is not a pubkey")
self.commitment = hashlib.sha256(self.P2.format()).digest()
return binascii.hexlify(self.commitment).decode('ascii')
def generate_podle(self, index=0, k=None):
"""Given a raw private key, in hex format,
construct a commitment sha256(P2), which is
the hash of the value x*J, where x is the private
key as a raw scalar, and J is a NUMS alternative
basepoint on the Elliptic Curve; we use J(i) where i
is an index, so as to be able to create multiple
commitments against the same privkey. The procedure
for generating the J(i) value is shown in getNUMS().
Also construct a signature (s,e) of Schnorr type,
which will serve as a zero knowledge proof that the
private key of P2 is the same as the private key of P (=x*G).
Signature is constructed as:
s = k + x*e
where k is a standard 32 byte nonce and:
e = sha256(k*G || k*J || P || P2)
Possibly Joinmarket specific comment:
Users *should* generate with lower indices first,
since verifiers will give preference to lower indices
(each verifier may have their own policy about how high
an index to allow, which really means how many reuses of utxos
to allow in Joinmarket).
Returns a commitment of form H(P2) which, note, will depend
on the index choice. Repeated calls will reset the commitment
and the associated signature data that can be used to open
the commitment.
"""
#TODO nonce could be rfc6979?
if not k:
k = os.urandom(32)
J = getNUMS(index)
KG = podle_PrivateKey(k).public_key
KJ = multiply(k, J.format(), False, return_serialized=False)
self.P2 = getP2(self.priv, J)
self.get_commitment()
self.e = hashlib.sha256(b''.join([x.format(
) for x in [KG, KJ, self.P, self.P2]])).digest()
k_int = decode(k, 256)
priv_int = decode(self.priv.secret, 256)
e_int = decode(self.e, 256)
sig_int = (k_int + priv_int * e_int) % N
self.s = encode(sig_int, 256, minlen=32)
return self.reveal()
def reveal(self):
"""Encapsulate all the data representing the proof
in a dict for client functions. Data output in hex.
"""
if not all([self.u, self.P, self.P2, self.s, self.e]):
raise PoDLEError("Cannot generate proof, data is missing")
if not self.commitment:
self.get_commitment()
Phex, P2hex, shex, ehex, commit = [
binascii.hexlify(x).decode('ascii')
for x in [self.P.format(), self.P2.format(), self.s, self.e,
self.commitment]
]
return {'used': str(self.used),
'utxo': self.u,
'P': Phex,
'P2': P2hex,
'commit': commit,
'sig': shex,
'e': ehex}
def serialize_revelation(self, separator='|'):
state_dict = self.reveal()
ser_list = []
for k in ['utxo', 'P', 'P2', 'sig', 'e']:
ser_list += [state_dict[k]]
ser_string = separator.join(ser_list)
return ser_string
@classmethod
def deserialize_revelation(cls, ser_rev, separator='|'):
ser_list = ser_rev.split(separator)
if len(ser_list) != 5:
raise PoDLEError("Failed to deserialize, wrong format")
utxo, P, P2, s, e = ser_list
return {'utxo': utxo, 'P': P, 'P2': P2, 'sig': s, 'e': e}
def verify(self, commitment, index_range):
"""For an object created without a private key,
check that the opened commitment verifies for at least
one NUMS point as defined by the range in index_range
"""
if not all([self.P, self.P2, self.s, self.e]):
raise PoDLEError("Verify called without sufficient data")
if not self.get_commitment() == commitment:
return False
for J in [getNUMS(i) for i in index_range]:
sig_priv = podle_PrivateKey(self.s)
sG = sig_priv.public_key
sJ = multiply(self.s, J.format(), False)
e_int = decode(self.e, 256)
minus_e = encode(-e_int % N, 256, minlen=32)
minus_e_P = multiply(minus_e, self.P.format(), False)
minus_e_P2 = multiply(minus_e, self.P2.format(), False)
KGser = add_pubkeys([sG.format(), minus_e_P], False)
KJser = add_pubkeys([sJ, minus_e_P2], False)
#check 2: e =?= H(K_G || K_J || P || P2)
e_check = hashlib.sha256(KGser + KJser + self.P.format() +
self.P2.format()).digest()
if e_check == self.e:
return True
#commitment fails for any NUMS in the provided range
return False
def getNUMS(index=0):
"""Taking secp256k1's G as a seed,
either in compressed or uncompressed form,
append "index" as a byte, and append a second byte "counter"
try to create a new NUMS base point from the sha256 of that
bytestring. Loop counter and alternate compressed/uncompressed
until finding a valid curve point. The first such point is
considered as "the" NUMS base point alternative for this index value.
The search process is of course deterministic/repeatable, so
it's fine to just store a list of all the correct values for
each index, but for transparency left in code for initialization
by any user.
The NUMS generator generated is returned as a secp256k1.PublicKey.
"""
assert index in range(256)
nums_point = None
for G in [getG(True), getG(False)]:
seed = G + struct.pack(b'B', index)
for counter in range(256):
seed_c = seed + struct.pack(b'B', counter)
hashed_seed = hashlib.sha256(seed_c).digest()
#Every x-coord on the curve has two y-values, encoded
#in compressed form with 02/03 parity byte. We just
#choose the former.
claimed_point = b"\x02" + hashed_seed
try:
nums_point = podle_PublicKey(claimed_point)
return nums_point
except:
continue
assert False, "It seems inconceivable, doesn't it?" # pragma: no cover
def verify_all_NUMS(write=False):
"""Check that the algorithm produces the expected NUMS
values; more a sanity check than anything since if the file
is modified, all of it could be; this function is mostly
for testing, but runs fast with pre-computed context so can
be run in user code too.
"""
nums_points = {}
for i in range(256):
nums_points[i] = binascii.hexlify(getNUMS(i).format()).decode('ascii')
if write:
with open("nums_basepoints.txt", "wb") as f:
from pprint import pformat
f.write(pformat(nums_points).encode('utf-8'))
assert nums_points == precomp_NUMS, "Precomputed NUMS points are not valid!"
def getP2(priv, nums_pt):
"""Given a secp256k1.PrivateKey priv and a
secp256k1.PublicKey nums_pt, an alternate
generator point (note: it's in no sense a
pubkey, its privkey is unknowable - that's
just the most easy way to manipulate it in the
library), calculate priv*nums_pt
"""
priv_raw = priv.secret
return multiply(priv_raw,
nums_pt.format(),
False,
return_serialized=False)
def get_podle_commitments():
"""Returns set of commitments used as a list:
[H(P2),..] (hex) and a dict of all existing external commitments.
It is presumed that each H(P2) can
be used only once (this may not literally be true, but represents
good joinmarket "citizenship").
This is stored as part of the data in PODLE_COMMIT_FILE
Since takers request transactions serially there should be no
locking requirement here. Multiple simultaneous taker bots
would require extra attention.
"""
if not os.path.isfile(PODLE_COMMIT_FILE):
return ([], {})
with open(PODLE_COMMIT_FILE, "rb") as f:
c = json.loads(f.read().decode('utf-8'))
if 'used' not in c.keys() or 'external' not in c.keys():
raise PoDLEError("Incorrectly formatted file: " + PODLE_COMMIT_FILE)
return (c['used'], c['external'])
def add_external_commitments(ecs):
"""To allow external functions to add
PoDLE commitments that were calculated elsewhere;
the format of each entry in ecs must be:
{txid:N:{'P':pubkey, 'reveal':{1:{'P2':P2,'s':s,'e':e}, 2:{..},..}}}
"""
update_commitments(external_to_add=ecs)
def update_commitments(commitment=None,
external_to_remove=None,
external_to_add=None):
"""Optionally add the commitment commitment to the list of 'used',
and optionally remove the available external commitment
whose key value is the utxo in external_to_remove,
persist updated entries to disk.
"""
c = {}
if os.path.isfile(PODLE_COMMIT_FILE):
with open(PODLE_COMMIT_FILE, "rb") as f:
try:
c = json.loads(f.read().decode('utf-8'))
except ValueError: #pragma: no cover
#Exit conditions cannot be included in tests.
jmprint("the file: " + PODLE_COMMIT_FILE + " is not valid json.",
"error")
sys.exit(0)
if 'used' in c:
commitments = c['used']
else:
commitments = []
if 'external' in c:
external = c['external']
else:
external = {}
if commitment:
commitments.append(commitment)
#remove repeats
commitments = list(set(commitments))
if external_to_remove:
external = {
k: v
for k, v in external.items() if k not in external_to_remove
}
if external_to_add:
external.update(external_to_add)
to_write = {}
to_write['used'] = commitments
to_write['external'] = external
with open(PODLE_COMMIT_FILE, "wb") as f:
f.write(json.dumps(to_write, indent=4).encode('utf-8'))
def get_podle_tries(utxo, priv=None, max_tries=1, external=False):
used_commitments, external_commitments = get_podle_commitments()
if external:
if utxo in external_commitments:
ec = external_commitments[utxo]
#use as many as were provided in the file, up to a max of max_tries
m = min([len(ec['reveal'].keys()), max_tries])
for i in reversed(range(m)):
key = str(i)
p = PoDLE(u=utxo,P=ec['P'],P2=ec['reveal'][key]['P2'],
s=ec['reveal'][key]['s'], e=ec['reveal'][key]['e'])
if p.get_commitment() in used_commitments:
return i+1
else:
for i in reversed(range(max_tries)):
p = PoDLE(u=utxo, priv=priv)
c = p.generate_podle(i)
if c['commit'] in used_commitments:
return i+1
return 0
def generate_podle(priv_utxo_pairs, max_tries=1, allow_external=None, k=None):
"""Given a list of privkeys, try to generate a
PoDLE which is not yet used more than max_tries times.
This effectively means satisfying two criteria:
(1) the generated commitment is not in the list of used
commitments
(2) the index required to generate is not greater than 'max_tries'.
Note that each retry means using a different generator
(see notes in PoDLE.generate_podle)
Once used, add the commitment to the list of used.
If we fail to find an unused commitment with this algorithm,
we fallback to sourcing an unused commitment from the "external"
section of the commitments file; if we succeed in finding an unused
one there, use it and add it to the list of used commitments.
If still nothing available, return None.
"""
used_commitments, external_commitments = get_podle_commitments()
for priv, utxo in priv_utxo_pairs:
tries = get_podle_tries(utxo, priv, max_tries)
if tries >= max_tries:
continue
#Note that we will return the *lowest* index
#which is still available.
index = tries
p = PoDLE(u=utxo, priv=priv)
c = p.generate_podle(index)
#persist for future checks
update_commitments(commitment=c['commit'])
return c
if allow_external:
for u in allow_external:
tries = get_podle_tries(utxo=u, max_tries=max_tries, external=True)
if (tries >= max_tries):
#If none of the entries in the 'reveal' list for this external
#commitment were available, they've all been used up, so
#remove this entry
update_commitments(external_to_remove=u)
continue
index = str(tries)
ec = external_commitments[u]
p = PoDLE(u=u,P=ec['P'],P2=ec['reveal'][index]['P2'],
s=ec['reveal'][index]['s'], e=ec['reveal'][index]['e'])
update_commitments(commitment=p.get_commitment())
return p.reveal()
#Failed to find any non-used valid commitment:
return None
def verify_podle(Pser, P2ser, sig, e, commitment, index_range=range(10)):
verifying_podle = PoDLE(P=Pser, P2=P2ser, s=sig, e=e)
#check 1: Hash(P2ser) =?= commitment
if not verifying_podle.verify(commitment, index_range):
return False
return True
precomp_NUMS = {
0: '0296f47ec8e6d6a9c3379c2ce983a6752bcfa88d46f2a6ffe0dd12c9ae76d01a1f',
1: '023f9976b86d3f1426638da600348d96dc1f1eb0bd5614cc50db9e9a067c0464a2',
2: '023745b000f6db094a794d9ee08637d714393cd009f86087438ac3804e929bfe89',
3: '023346660dcb1f8d56e44d23f93c3ad79761cdd5f4972a638e9e15517832f6a165',
4: '02ec91c86964dcbb077c8193156f3cfa91476d5adfcfcf64913a4b082c75d5bca7',
5: '02bbc5c4393395a38446e2bd4d638b7bfd864afb5ffaf4bed4caf797df0e657434',
6: '02967efd39dc59e6f060bf3bd0080e8ecf4a22b9d1754924572b3e51ce2cde2096',
7: '02cfce8a7f9b8a1735c4d827cd84e3f2a444de1d1f7ed419d23c88d72de341357f',
8: '0206d6d6b1d88936bb6013ae835716f554d864954ea336e3e0141fefb2175b82f9',
9: '021b739f21b981c2dcbaf9af4d89223a282939a92aee079e94a46c273759e5b42e',
10: '025d72106845e03c3747f1416e539c5aa0712d858e7762807fdc4f3757fd980631',
11: '02e7d4defb5d287734a0f96c2b390aa14f5f38e80c5a5e592e4ce10d55a5f5246b',
12: '023c1bf301bcfa0f097f1a3931c68b4fd39b77a28cc7b61b2b1e0b7ca6d332493c',
13: '0283ac2cdd6b362c90665802c264ee8e6342318070943717faee62ef9addeff3e9',
14: '02cb9f6164cd2acdf071caef9deab870fc3d390a09b37ba7af8e91139b817ce807',
15: '02f0a3a3e22c5b04b6fe97430d68f33861c3e9be412220dc2a24485ea5d55d94db',
16: '02860ca3475757d90d999e6553e62c07fce5a6598d060cceeead08c8689b928095',
17: '0246c8eabc38ce6a93868369d5900d84f36b2407eecb81286a25eb22684355b41d',
18: '026aa6379d74e6cd6c721aef82a34341d1d15f0c96600566ad3fa8e9c43cbb5505',
19: '02fdeacb3b4d15e0aae1a1d257b4861bcc9addb5dc3780a13eb982eb656f73d741',
20: '021a83ecfaeb2c057f66a6b0d4a42bff3fe5fda11fe2eea9734f45f255444cddc0',
21: '02d93580f3e0c2ec8ea461492415cc6a4be00c50969e2c32a2135e7d04f112309a',
22: '0292c57be6c3e6ba8b44cf5e619529cf75e9c6b795ddecd383fb78f9059812cb3f',
23: '02480f099771d0034d657f6b00cd17c7315b033b19bed9ca95897bc8189928dd47',
24: '02ac0701cdc6f96c63752c01dc8400eab19431dfa15f85a7314b1e9a3df69a4a66',
25: '026a304ceb69e37d655c1ef100d7ad23192867151983ab0d168af96afe7f1997f6',
26: '023b9ff8e4a853b29ecae1e8312fae53863e86b8f8cb3155f31f7325ffb2baf02c',
27: '021894ce66d61c33e439f38a36d92c0e45bf28dbc7e30bfb4d7135b87fc8e890e1',
28: '02d9e7680e583cf904774d4c19f36cb3d238b6c770e1e7db03f444dc8b15b29687',
29: '024350c7ff5b2bf2c58e3b17a792716d0e76cff7ad537375d1abc6e249466b25a3',
30: '02c6577e1cdcbcfadb0ae037d01fbf6d74786eecdb9d1ee277d9ba69b969728cfe',
31: '029f395b4c7b20bcb6120b57bee6d2f7353cd0aa9fe246176064068c1bd9b714d1',
32: '02d180786087720b827bf04ae800547102470a1e43de314203e90228c586b481a1',
33: '023548173a673965c18d994028bc6d5f5df1f60dccf9368b0eae34f8cff3106943',
34: '02118124c53b86fdade932c4304ad347a19ce0af79a9ab885d7d3a6358a396e360',
35: '02930bcdee5887fa5a258335d6948017e6d7f2665b32dcc76a84d5ca7cd604d89b',
36: '0267e79a47058758a8ee240afd941e0ae8b4f175f29a3cf195ad6ff0e6d02955b1',
37: '027e53d9fb04f1bb69324245306d26aa60172fd13d8fe27809b093222226914de6',
38: '02ef09fbdcd22e1be4f0d4b2d13a141051b18009d7001f6828c6a40b145c9df23e',
39: '028742fd08c60ba13e78913581db19af2f708c7ec53364589f6cbcf9d1c8b5105f',
40: '020ce14308d2f516bf4f9e0944fb104907adef8f4c319bfcc3afab73e874a9ce4a',
41: '027635f125f05a2548201f74c4bbdcbe89561204117bd8b82dfae29c85a576a58e',
42: '02fe878f3ae59747ee8e9c34876b86851d5396124e1411f86fe5c58f08f413a549',
43: '02f2a6af33bd08ab41a010d785694e9682fa1cc65733f30a53c40541d1c1bfb660',
44: '02cbe9d18b6d5fc9993ef862892e5b2b1ea5d2710a4f208672c0f7c36a08bb5686',
45: '023fb079b25c0a8241465fb55802f22ebb354e6da81f7dabfe214ddbd9d3dfcd5a',
46: '021a5b234b9a10fc5f08ed9c1a136a250e92156adc12109a97dd7467276d6848a8',
47: '0240fbe9363d50585da40aef95f311fc2795550e787f62421cd9b6e2f719bb9547',
48: '02a245fbbc00f1d6feb72a9e1d3fd0033522839d33440aea64f52e8bccee616be8',
49: '02fd1e94bb23a4306de64064841165e3db497ae5b246dabff738eb3e6ea51685a7',
50: '0298362705914c839e45505369e54faefbb3aaebb4c486b4d6e59ca03304f3552c',
51: '021b8109a23b858114d287273620dd920029d84b90f63af273c1c78492b1a70105',
52: '028df6ce4fec30229cddb86c62606cff80e95cb8277028277f3dcc8ac9f98eef9d',
53: '02ed02925d806df4ac764769d11743093708808157fb2933eb19af5399dcfd500c',
54: '02ce88da0e81988bd8f5d63ad06898a355f7dc7f46bb08cf5f1e9bc5c3752ad13c',
55: '02f4868cc8285cd8d74d4213d18d53d5f410d50223818f1be6fe8090904e03743d',
56: '02770cecdf18aa2115b6e5c4295468f2e92a53068dc4295d0e5d0890b71d1a2fcc',
57: '02b5d4dce8932de37c6ef13a7f063f164dfd07f7399e8e815e22b5af420608fd2a',
58: '0284ad07924dbac50a72455aec3ddba50b1ed71e678ba935bb5d95c8a8232b1353',
59: '02cb8c916a6f9bc39c8825f5b0378bb1b0a0679e191843aa4db2195b81f14c87e0',
60: '0235aa30ec3df8dd193a132dbaf3b351af879c59504ed8b7b5ad5f1f1ea712854f',
61: '02df91206e955cefe7bcda4555fc6ad761b0e98d464629f098d4483306851704e9',
62: '02ed4f1fccd47e66a8d74e58b4f6e31b5172b628fc0dacdb408128c914eb80f506',
63: '0263991bb62aaca78a128917f5c4e15183f98aefddf04070c5ca537186f1c1a97a',
64: '02ffe2b017882d57db27864446ad7b21d3855ae64bddf74d46e3a611bf903580be',
65: '02d647aba2c01eecd0fac7e82580dd8b92d66db7341d1b65a5e4b01234f1fbb2cd',
66: '023134ff85401dba9aff426d3f3ba292ea59684b8c48ea0b495660797a839246a6',
67: '02827880fe0410c9ea84f75a629f8f8e6eed1f26528af421cf23b8ecf93b6b4b7b',
68: '02859b3f9f1f5ba6aa0787f8d3f3f2f21b4932c40bc36b6669383e3bbd19654a5f',
69: '02a7d204dfc3eed44abd0419202e280f6772efd5acf9fd34331b8f47c81c6dab19',
70: '02e15d11b443a9340ac31a8c5774ce34cd347834470c8d68c959828fae3a7eb0c6',
71: '029931f65e46627d60519bfd08bd8a1bb3d8d2921f7f8c9ef31f4bfcdd8028ead2',
72: '02e5415ba78743d736018f19757ee0e1ca5d4a4fb1d0464cd3eea8d89b34dd37b8',
73: '027ea7860afc3de502d056d9a19ca330f16cd61cfefbeb768df68a882d1f8f15f5',
74: '026c19becac43626582622e2b7e86ebd8056f40aa8ab031e70f4deae8cab34503f',
75: '02098dab044c888ddebe6713fcb8481f178e3ba42d63310b08d8234e20fe1de13f',
76: '02ed6af1a2bebcb381ce92f87638267b1afefe7a1cdce16253f5bf9f99a84ce4b2',
77: '023d8493f9e72cd3212166de50940d980f603ae429309abb29e15cccc1983efe37',
78: '025c07d7513b1bae52a0089a4faee127148e2ba5a651983083aedc1ae8403cf1eb',
79: '0285a93a8c8e6134b3a53c5bd1b5b7d24e7911763ea887847c5d66af172ed17f10',
80: '02fea28fb142aa95fcd44398c9482a3c185ec22fee8f24ad6b2297ac7478423f21',
81: '02f9840a1635ae3fa131405526974d40d2edee17adf58278956373ce6c69757c2a',
82: '023579e441a7dcdbd36a2932c64fa3318023b1f3d04daab148622b7646246a6d7c',
83: '02bcbc2933f90a88996c1363c8d3a7004e0c6b75040041201fb022e45acb0af6a7',
84: '02cd52e0d28f5564fc2bf842fa63dfefbcf2bb5fe0325703c132be5cd14cca7291',
85: '021e648e261b93fedd3439352899c0fa1acedd1f68ab508050a13ed3cbbc93c2ff',
86: '0295f9caea5f57d11b12ddee154a36a14921a8980fa76726e48e1d76443d4e306f',
87: '02396edf4c18283dd3ef68a2c57b642bd87ae9f8b6be5e5fe4a41c5b86c5db8eb2',
88: '0264f323ca3eee79385c9bfd35cd4cf576e51722f38dd98531d531a29913e5170d',
89: '02facd3f63f543e0ab9b13323340113acbe8ed3bafdfabdc80626cdd15386c80f3',
90: '02b6762640f96367fbf65eecfafcee5c6f7d6a42b706113053bb36a882659d3e65',
91: '02ed63f2eca15d9b338fcdb9b3efa3b326e173a1390706258829680f7973fa851c',
92: '026f6d47d0d48ff13d64ec6a1db2dc51173cee86ab8010a17809b3fe01483d9fc5',
93: '02814e7cae580a1ef86d6ee9b2f9f26fe771e8ea47acf11153b04680ada9cd3042',
94: '020e46225fb3ee8f04d08ffbe12d9092ff7f7227f9cb55709890c669e8a1c97963',
95: '028194469e8d6ee660e95d6125ba0152ad5c24bf7e452adf80db7062d6926851c4',
96: '02b3e1f5754562635ebeecfd32edb0d84a79b2f0c270bac153e60dd29334dc2663',
97: '02afff20730724a2d422f330e962362e7831753545ac0a931dd94be011ccf93e9c',
98: '02a9cfdf0471a34babfc2f6201dbc79530f3f319204daedb7ec05effc2bdfc5a74',
99: '02838fe450f2dd0c460b5fae90ec2feb5b7f001f9cd14c01a475c492cf16ea594b',
100: '02aacc3145d04972d0527c4458629d328219feda92bef6ef6025878e3a252e105a',
101: '02720fe09616d4325d3c4c702a0aeafbbbff95ef962af531c5ae9461ec81fdf8c5',
102: '02e6408f24461a6c484f6c4493c992d303211d5e4297d34afede719a2b70c96c14',
103: '02b9ecf2d3fdf2611c6d4be441a0f9a3810dadae39feb3c0d855748cc2dd98a968',
104: '027a32d12a536af038631890a9b90ee20b219c9c8231a95b1cde24c143d8173fec',
105: '02d26c98fb50b57b7defdf1e8062a52b2a859ba42f3d1760ee8ff99c4e9eb3ec03',
106: '02df85556e8d1e97a8093e4d9950905ebced0ea9a1e49728713df1974eeb455774',
107: '021fe1dbada397155a80225b59b4fb9a32450a991b2d9d11d8500e98344927c856',
108: '0211ccd0980a9ab6f4bb82fdc2e2d1ddace063a7bc1914a6ab4d02b0fa1ca746ec',
109: '0264bd41f41aad19f8bfd290fd3af346ebbf80efd33f515854f82bd57e9740f7aa',
110: '0226d5fb607cadb8720e900ce9afb9607386ad7b767e4ab3a4e0966223324b92eb',
111: '02b3bbf2e2ceae25701bd3b78ba13bea3f0dfed7581b8a8a67c66de9fd96ee41e2',
112: '024b8dd765e385d0e04772f3dbf1b1a82abc2de3e5740baac1f6306cd9fd45fe99',
113: '022153f6a884ae893ebb0642a84d624c0b62894d7cb9e2a48a3a0c4696e593f9db',
114: '0245e22b6388cb14c9c8dbcac94853bdf1e81816c07e926a82b96fc958aa874626',
115: '02cba97826b089c695b1acffdcdbf1484beec5eb95853fea1535d6d7bdb4e678b0',
116: '02ed006fbab2d18adbd96d2f1de6b83948e2a47acc8d2f92d7af9ba01ffae58276',
117: '02513592f4434ee62802d3965f847684693000830107c72cd8de4b34e05b532dae',
118: '028adc75647453a247bd44855abb56b60794aaed5ce21c9898e62adac7adcfbe8e',
119: '02a712d5dc572086359f1688e8e7b9a5f7fc3079644aea27cdddb382208fee885b',
120: '029abf8551218c9076f6d344baa099041fe73e5e844aac6e5c20240834105cdf60',
121: '027d480071a2d128c51e84c380467e1ac8435f05b985bbfee0099d35b4121fb0ca',
122: '02a7f2e4253fa0d833beca742e210c0d59a4ffc8559764766dcffb1aa3e4961826',
123: '023521309a6bdfafdf7bdae574a5f6010eb992e4bae46d8f83c478eac137889270',
124: '02b99fe8623aa19ca2bed6fe435ae95c5072a40193913bebe5466f675c92a31db7',
125: '02dc035112a2b4881917ea1db159e7f35ee9d98d31533e1285ca150ce84e538e4f',
126: '0291a07ecce8061561624de7348135b9081c5edd61541b24fa002fb6c074318fec',
127: '020d8a5253d7e0166aa37680a5f64cab0cdad2cdc4c0e8ae61d310df4c4f7386eb',
128: '026285db47fee60b5ad54cbd4c27a4e0cd723b86a920f03b12dc9b8c5f19f06448',
129: '020f94a9df4302f701b4629f74d401484daf84c7aabaf533f8c21c1626009e923c',
130: '027bb78af54b01ddad4e96b51a4e024105b373aab7e1a6ec16279967fcbbb096b4',
131: '02e1b20c0da3b8c991f8909fd0d31874be00e9fcb130d7c28b8ad53326cdf13755',
132: '02bbdd4dfc047f216e2cbff789bcf850423bedf2006d959963f75621810fecf0d9',
133: '024e1fe4b23feda8651a467090e0ce7e8b8db2ccb1c27d52255c76754aa1940d1b',
134: '0241aad8f575556c49c4fefae178c2c38541962bfff2ca84ebecea9f661ccf3536',
135: '02bcf6203d725ca0640bd045389e854e00087c54ba01fd739c6ef685b22f89340c',
136: '0202178e6b3a9b498399aa392b32dc9010f1eea322a6d439ad0c8cacf2008b3e34',
137: '026db3289d470df0fdf04f5f608fae2d7ec4ddbd3de2603f6685789520bdee01fc',
138: '0239bcfc796488129e3b2f01e6fbbda2f1b357b602e94b5091b44c916e9806dc34',
139: '020513bc4a618d32d784083f13d46e6c6d547f01b24942351760f6dc42e2bb7167',
140: '0204d2495e4fc20e0571ab2fcb4c1989fdda4542923aa97fe1a77a11c79ade1964',
141: '021eaa6af99ea4f1143a45a1b5af7b2d3c3e8810f358be6261248c5ba2492a7b4e',
142: '02799849e87e3862170add5b28a3b7e54b04cc60c2cec39de7eca9bfdfaaf930a8',
143: '02639bced287084268136c5b6e9e22f743b6c8f813e6aabe39521715bfa4a46ab8',
144: '0283c8b21fc038c1fbeedfae0b3abc4dbde672b0dcfda540f9fcfcf8c6e6d29fc3',
145: '02b284f4510535ff98e683f25c08b7ae7dd19f7b861e70a202469ddfb2877bc729',
146: '0256af1c82cde40ffd03564368b8256a5e48ef056df2655013f0b1aa15de1de8d2',
147: '02964b55eab2f19518ee735cae2f7f780bfab480bcbd360f7a90a2904301203366',
148: '02f046486f4a473f2226f6bd120aafc55a5c8651f3eb0855aa6a821f69f3016cc6',
149: '02eb8dfb7c59fbf24671e258ca5e8eda3ea74c5f0455eed4987cfda79f4fcf823f',
150: '020fac2c37cc273d982c07b2719a3694348629d5bdaebc22967fb9d0e1d7f01842',
151: '025c0c8ff9a102f99f700081526d2b93b9d51caf81dcf4d02e93cf83b4a7ff5c92',
152: '02a118f5fa9c5ef02707e021f9cb8056e69018ef145bec80ead4e09c06a60050c1',
153: '029ea72333d1908bb082bffec9da8824883df76a89709ab090df86c45be4abf784',
154: '02bacc52256e5221dbfc9a3f22e30fa8e86ddd38e3877e3dc41de91bdcf989b00b',
155: '02bc8b37dc66e2296ae706c896f5b86bd335f724cfa9783e41b9dc5e901b42b1de',
156: '02eca1099cea9bcab80820d6b64aec16dce1efa0e997b589f6dba3a8fd391fb100',
157: '027f1c1bb99bd1a0e486f415f8960d45614a6fcac8cedc260e07197733844827d0',
158: '021fc54df458bcfafc8a83d4759224c49c4b338cf23cd9825d6e9cdeffc276375b',
159: '027d4fff88da831999ba9b21e19baf747dc26ea76651146e463d4f3e51c586ee91',
160: '02e49c0fef0ebc52908cdcea4d913a42e5f24439fffdfaa21cc55a6add0ad9d122',
161: '0208b5e8e5035fdb62517d4ebab0696775dbfbdba8ff80f2031c1156cda195a2ab',
162: '0202e990bab267fff1575d6acc76fe2041f4196f4b17678872f9c160d930e5be35',
163: '02c73fcedd9f6eabc8fe4e1e7211cdb0f28967391d200147d46e4077d2915c262d',
164: '0261490abc5f14387ef585f42d99dbddb0837b166694d4af521086a1ffd46e5640',
165: '02b46a143e4e0af20a12c39f3105aca57ca79f9332df67619ee859b5d9bffb6d6d',
166: '0299f53c064d068f003f8871acae31b84ddda9d8dbe516d02dc170c70314ee2af7',
167: '023305144dccba65c67001474ee1135aa96432f386b5eb27582393b2ed4bfc185d',
168: '02e044b70ff7e9c784b3c40d09bdfadd4a037e692b0b3aa9ab6bb91203f86a0b37',
169: '02ded067a2e44282b0d731a28ffbd03ca6046c5b1a262887ea7cab4810050fbb8c',
170: '02e00e4c9198194d92a93059bce61f8249e1006eee287aa94fe51bb207462e5492',
171: '0241b89d9164f4c07595ca99b7d73cad2b20ac39847cf703dff1d7d6add339ebeb',
172: '02eba24cd4946e149025a9bf7759df5362245bf7c53c5a3205be0c92c59db8d5dc',
173: '026bd40c611246a789521c46d758a80337ff40bb298a964612b2af74039211727a',
174: '02b9095e071e4edfddf8afb0e176536957509d23f90fb7175ad086b4098e731c73',
175: '0214ad0014dfddc5c7eb0801b97268c1b7e03d64215d6b9d5ed80b468089e4a01d',
176: '02c455b8e38103ade8794fb51a1656e1439b42bdf79afd17a9df8542153914a7cf',
177: '02cc89d6437fdcf711a76eb16f4014f2e21b71740afc8b3ec13ccb60a45b12d815',
178: '0208eee5857dda0ae1c721e6ed4c74044add4e1ce66f105413e9ef1cccbdca87ad',
179: '02edc663693827cad44d004ac24753bfc3167f81ff4074bb862453376593229c0f',
180: '0202a4b7fb31e30b6d8f90a5442ef31f800902ea7a9511e24437b7a0ef516f79a9',
181: '02ff05472c2019ac2c9ab8b7fcb0604a94b7379c350306be262144588ea252d0f4',
182: '02b131bb594a1270d231e18459e484c49f3eca3b3b2291c9be81c01dc8a4037fa1',
183: '02f50125277ea19f633e93868cf8e8a4cd76b21eedf8e3ef59de43f40d73a01d01',
184: '027aab228a7d6f87003b01fb9c0b9bcfb2098adbc76f5f9b856aedd28077fc4471',
185: '02925200e4f74bea719a99f4a0b05165b9af475f2187381bd0b79cad4d5f2593b6',
186: '02c311f1750c6d5c364b71c3b0f369f6959d34a3718da695c5b227ecf1a4669bf6',
187: '02cb030c71169d0a1ae30ffba92311bc06bb64b27570598dedabdea0b24631a0ca',
188: '02e64669898eecff7aa887307be696a694f61559e7ca41119677b7e94f37cd2914',
189: '028fe93e32c24df7f8aaf8d777335fd9ce9f9b5c121dec2ab1ff21575c047497e7',
190: '026f08c1c3cb4cff5cdbd7985db4a8ebf0ebc0924530b0fa118d095c4667efeb52',
191: '02afe08dbba6c999efb73aeae1da0ad8b143a1b51759caffd3ed2de4494adc47fb',
192: '02e99aec0b5e869b3885a3b9f527fd3c546dde83d41a5a156703d0da5e10e04743',
193: '02b7e5f4cb9233107bf7a47789dca4eb811af108822f2d4bd03dec13251ec45984',
194: '023b971e135daa0b851797b17e3a1cc5ac8a9a6207a2e784a0fe36732a00407b49',
195: '02b1742739bfbb528b2a2731cb5d5f1bd03f4fa9c94607837e586c7c6f6589be4a',
196: '022cd1b023bb2afc68ee27b40f8deb1d1c6d7b7aa97c32c444f1ceebd449dbeb22',
197: '02704e21f8bf38158d7e8100e297adfc930c14c8791beee9b907407f4ca654d95b',
198: '02caabeb678374ca75bd815c370b2e37fb0470591557219d6289b1b1e655ed80c6',
199: '026aa8d45112aa0da335054194c739e04787526250493f5a0eaaa8a346541d1a0f',
200: '022fb12408355439bbee33066bbeefcffb0bdc9cfd1950510fd2a42bdc4eaa1d53',
201: '02639fe47769f7694ca6dbfd934762472391d70b23868a58e11d2bd46373e1df29',
202: '02f75360f52df674247c5f005b3451ee47becf3204862154d4e7ee97a0e40df3d2',
203: '0230241e27d0d3ad727d26472541fcd48f2bb128db5611237fa9f33f86ede8d5c9',
204: '0255d5a0aa37a226c001f6b7f19e2bddb10aeaa0652430b8defe35c3f03dfb3c0e',
205: '024e6faa398b0acf8a8dfdd9d21e0a46a22d07cd0fcffd89749f74f94f9993f4d9',
206: '020c1a256587306f58f274cc2238f651bbfadfd42436e6eb8f318ac08fae04e7ae',
207: '025858b8188da173e8b01b8713b154ffae8b2d2eb8f9670362877102cf0c0c4f28',
208: '02dc7509c77d7fa61c08c5525fb151bf4fe12deb1989a3be560a63105dae2ecd2e',
209: '02a272df6dab1c22c209b45b601737c0077acb7869bb9fe264c991b4ef199e337d',
210: '025168f2fdd730b4c33b57d3956e6a40dd27a4f32db70d9f9b5898fa2bed3de342',
211: '028133baac70bc2c2ebe8a22af04b5faedd070e276c90e2f910bb9bf89441a80db',
212: '029064628ebd6e97a945c1d52641a27bff3c4f59659e657b88d23c2ce1c4d04644',
213: '023cf20c4e8675bce999a0128602fe21699db651540f3dcbe7a4ef2126243ba17a',
214: '02cc685739a4b20e2d52ddf256e597c06b7eb69e65d009820c6744b739c7215340',
215: '02d061544ce21398af3e0e6c329ce49976a9ecd804ebc543f4c16f6a32798f37c2',
216: '029fe49ff440f23c69360a92d249db429bdc3601fc8a5a3fc1aa894de817c05490',
217: '0222c8c4e90585f9816b5801bad43fb608857269fdaaefbe2b5b85903231685679',
218: '0296b72ed4968860b733fb99846698df2e95c65af281b3ef8b5ab90e2d5de966cb',
219: '02c27565a7fd5d1f4bcbe969bddbace99553fb65cb7750965350ff230b1f09f97d',
220: '02e1254be9833236609bf44c62ef6da7188a44bbe2d53a72cf39a38ef9f99bb783',
221: '0280663ce16afadc77e00ade780da53e7c11b02a66cbf36837ef7d9d2488f23417',
222: '02ad8b11e62c6753917307bdde89a42896e0070d33f6f93c608d82f6d041b814a4',
223: '02ce1d943dfc14654266507def2b7b9940bffceb4f54d709a149f99962083398fc',
224: '023ea7eb26248c05beb4e4d8ba9f9785d5fd1a55d3137c90f40b807b60aa4262df',
225: '0211c802fec9b31710d3849e2c1700cea5374ae422e54551946d96fc240c63fba0',
226: '02204ad97ebe2ec30d6db1bfc1e1d4660331909668634c3cd928b5c369a6013367',
227: '020251bf4271d359a082cdad23d9a5cd48916d78eed010fe1e7d9711cd420b3cdf',
228: '0292b9757195350676e447e49425f887d3df7e27774bb3e0aab5b528da0a1a0340',
229: '022be18362b2a167199a76f6065358063b1167d5bbcfe7652fc55f93a5ebd42e89',
230: '02e6b1e618efe5f468bdb40f5ec167ed4fa7636849c4ff4ddab0199c903b37306c',
231: '02a6676873de91890ecae000c575e46e4a9629865fb1662606da5e9c1fdcd55d5c',
232: '02c088a3c96b13413caa5f32a8f4640e76ec0a37990577d679d2062e859547f058',
233: '023e9703ed6209d5a25e0ecb34e04c22f274f37845aa2a4e2f2343e39928360e25',
234: '02977d845787c4690152827bfd15e801044c84d33430a7ed928499e828cf131d14',
235: '0224ea648555445d1305aaf6bd74fda3041b2a10bf7900a4c067462b01c6dc25f1',
236: '02dfd472c98ece1dc2a18c1bebf98a09990fba673e725c029928937247022b9d24',
237: '02a2a03933d06617adcf0f4ad692e95d463a5fa9938e8d451e5d6271f4a5af8bb4',
238: '02ca24fa8d7aa53f7f5b4e1ca16eb6fd9b9cfb0162a332abb7a88ddf8e964c99bc',
239: '02bbce92d1db3ef0c9c09793b760fd3b929c9168e4dff396c618fa0ed3cf6a5edb',
240: '028af15d26d3b297f4d2aeaf308632b60251accf87aa8470b3d4d1ef2dabb99209',
241: '021b81c0e878389231339fd9d622a736fc9d36de93a58ea6a4bc38fef86672278a',
242: '021adc24309f605c7a5af106e8b930feaec0bec6545fb4c70b83ebe5cf341cab2d',
243: '020462a3ff101ac379f87f43190459b7494f4128ea30035877ce22a35afb995e34',
244: '02f1019851779a6d0db09e8abeba3b9a07b6931b43b0d973cfe261a96b4516cca4',
245: '02d7023276f01ff22a9efeadd5b539d1d9ceb80ebf6813e6042a49c946a82f366f',
246: '021594f45af3a21e0210a2ca4cbc3e95ea95db5aca3561fc1f759cb7f104dd0f62',
247: '021398309b6c293c0dc28cdd7e55ad06306b59cb9c10d947df565e4a90f095a62a',
248: '029f39d84383200e841187c5b0564e3b01a2ba019b86221c0c1dd3eae1b4dabb26',
249: '0252ec719852f71c2d58886dd6ace6461a64677a368b7b8e220da005ac977abdc8',
250: '0237f0d7de84b2cc6d2109b7241c3d49479066a09d1412c7a4734192715b021e06',
251: '021e9e0e4784d15a29721c9a33fbcfb0af305d559c98a38dcf0ce647edd2c50caa',
252: '02e705994a78f7942726209947d62d64edd062acfa8a708c21ac65de71e7ae71df',
253: '0295f1cafd97e026341af3670ef750de4c44c82e6882f65908ec167d93d7056806',
254: '023a0d381598e185bbff88494dc54e0a083d3b9ce9c8c4b86b5a4c9d5f949b1828',
255: '02a0a8694820c794852110e5939a2c03f8482f81ed57396042c6b34557f6eb430a'
}