#!/usr/bin/env python #Proof Of Discrete Logarithm Equivalence #For algorithm steps, see https://gist.github.com/AdamISZ/9cbba5e9408d23813ca8 import os import sys import hashlib import json import struct from pprint import pformat from jmbase import jmprint from jmbitcoin import multiply, add_pubkeys, getG, podle_PublicKey,\ podle_PrivateKey, N, podle_PublicKey_class from jmbase import (EXIT_FAILURE, utxostr_to_utxo, utxo_to_utxostr, hextobin, bintohex) 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 binary not hex. self.u = u if not priv: if P: self.P = podle_PublicKey(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) == 33 and priv[-1:] == b"\x01": priv = priv[:-1] self.priv = podle_PrivateKey(priv) self.P = self.priv.public_key if P2: self.P2 = podle_PublicKey(P2) else: self.P2 = None self.s = None self.e = None if s: self.s = s if e: self.e = 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) """ 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 self.commitment 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. """ self.i = index #TODO nonce could be rfc6979? if not k: k = os.urandom(32) J = getNUMS(self.i) KG = podle_PrivateKey(k).public_key KJ = multiply(k, J.format(), 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, priv_int, e_int = (int.from_bytes(x, byteorder="big") for x in [k, self.priv.secret, self.e]) sig_int = (k_int + priv_int * e_int) % N self.s = (sig_int).to_bytes(32, byteorder="big") return self.reveal() def reveal(self): """Encapsulate all the data representing the proof in a dict for client functions. """ 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() return {'used': self.used, 'utxo': self.u, 'P': self.P.format(), 'P2': self.P2.format(), 'commit': self.commitment, 'sig': self.s, 'e': self.e} def serialize_revelation(self, separator='|'): """ Outputs the over-the-wire format as used in Joinmarket communication protocol. """ state_dict = self.reveal() success, utxo = utxo_to_utxostr(state_dict["utxo"]) assert success, "invalid utxo in PoDLE" ser_list = [utxo] ser_list += [bintohex(state_dict[x]) for x in ["P", "P2", "sig", "e"]] ser_string = separator.join(ser_list) return ser_string @classmethod def deserialize_revelation(cls, ser_rev, separator='|'): """ Reads the over-the-wire format as used in Joinmarket communication protocol. """ ser_list = ser_rev.split(separator) if len(ser_list) != 5: raise PoDLEError("Failed to deserialize, wrong format") utxostr, P, P2, s, e = ser_list success, utxo = utxostr_to_utxo(utxostr) assert success, "invalid utxo format in PoDLE." return {'utxo': utxo, 'P': hextobin(P), 'P2': hextobin(P2), 'sig': hextobin(s), 'e': hextobin(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()) e_int = int.from_bytes(self.e, byteorder="big") minus_e = (-e_int % N).to_bytes(32, byteorder="big") minus_e_P = multiply(minus_e, self.P.format()) minus_e_P2 = multiply(minus_e, self.P2.format()) KGser = add_pubkeys([sG.format(), minus_e_P]) KJser = add_pubkeys([sJ, minus_e_P2]) #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 __repr__(self): """ Specified here to allow logging. """ # note: will throw if not fully initalised r = self.reveal() success, utxo = utxo_to_utxostr(r["utxo"]) assert success, "invalid utxo in PoDLE." return pformat({'used': r["used"], 'utxo': utxo, 'P': bintohex(r["P"]), 'P2': bintohex(r["P2"]), 'commit': bintohex(r["commit"]), 'sig': bintohex(r["sig"]), 'e': bintohex(r["e"])}) 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] = bintohex(getNUMS(i).format()) 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(), return_serialized=False) # functions which interact with the external persistence of podle data: def switch_external_dict_format(ed, utxo_converter, hexbinconverter): """External dict has structure: {txid:N:{'P':pubkey, 'reveal':{1:{'P2':P2,'s':s,'e':e}, 2:{..},..}}} This function switches between readable/writable in file (strings, hex) and that used internally. """ retval = {} for u in ed: success, u2 = utxo_converter(u) assert success, "invalid utxo format in external dict parsing." retval[u2] = {"P": hexbinconverter(ed[u]["P"])} retval[u2]["reveal"] = {} for i in ed[u]["reveal"]: # hack: python json does not allow int dict keys, they must # be str, so when reading from file, we convert back to int: if not isinstance(i, int): j = int(i) else: j = i retval[u2]["reveal"][j] = { "P2": hexbinconverter(ed[u]["reveal"][i]["P2"]), "s": hexbinconverter(ed[u]["reveal"][i]["s"]), "e": hexbinconverter(ed[u]["reveal"][i]["e"])} return retval def external_dict_to_file(ed): """ Converts internal format of dict to one writable/readable in file. """ return switch_external_dict_format(ed, utxo_to_utxostr, bintohex) def external_dict_from_file(ed): """ Takes the external dict extracted through json deserialization from a file and converts it to internal format: {txid:N:{'P':pubkey, 'reveal':{1:{'P2':P2,'s':s,'e':e}, 2:{..},..}}} """ return switch_external_dict_format(ed, utxostr_to_utxo, hextobin) def write_to_podle_file(used, external): """ Update persisted commitment data in PODLE_COMMIT_FILE. """ to_write = {} to_write['used'] = [bintohex(x) for x in used] externalfmt = external_dict_to_file(external) to_write['external'] = externalfmt with open(PODLE_COMMIT_FILE, "wb") as f: f.write(json.dumps(to_write, indent=4).encode('utf-8')) def read_from_podle_file(): """ Returns used commitment list and external commitments dict struct currently stored in PODLE_COMMIT_FILE. """ 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(EXIT_FAILURE) if 'used' not in c.keys() or 'external' not in c.keys(): raise PoDLEError("Incorrectly formatted file: " + PODLE_COMMIT_FILE) used = [hextobin(x) for x in c["used"]] external = external_dict_from_file(c["external"]) return (used, external) return ([], {}) def get_podle_commitments(): """Returns set of commitments used as a list: [H(P2),..] 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 ([], {}) return read_from_podle_file() def add_external_commitments(ecs): """To allow external functions to add PoDLE commitments that were calculated elsewhere. """ 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. """ commitments, external = read_from_podle_file() 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) write_to_podle_file(commitments, external) 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)): p = PoDLE(u=utxo, P=ec["P"], P2=ec["reveal"][i]["P2"], s=ec["reveal"][i]["s"], e=ec["reveal"][i]["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) p.generate_podle(index) #persist for future checks update_commitments(commitment=p.commitment) return p 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 ec = external_commitments[u] ecri = ec["reveal"][tries] p = PoDLE(u=u, P=ec["P"], P2=ecri["P2"], s=ecri["s"], e=ecri["e"]) update_commitments(commitment=p.get_commitment()) return p #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) 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' }