diff --git a/.github/workflows/fluffy.yml b/.github/workflows/fluffy.yml index ea0e552c23..1fe44e4cd8 100644 --- a/.github/workflows/fluffy.yml +++ b/.github/workflows/fluffy.yml @@ -315,6 +315,18 @@ jobs: with: fetch-depth: 2 # In PR, has extra merge commit: ^1 = PR, ^2 = base + - name: Check nph formatting + # Pin nph to a specific version to avoid sudden style differences. + # Updating nph version should be accompanied with running the new + # version on the fluffy directory. + run: | + VERSION="v0.5.1" + ARCHIVE="nph-linux_x64.tar.gz" + curl -L "https://github.com/arnetheduck/nph/releases/download/${VERSION}/${ARCHIVE}" -o ${ARCHIVE} + tar -xzf ${ARCHIVE} + ./nph fluffy/ + git diff --exit-code + - name: Check copyright year if: ${{ !cancelled() }} && github.event_name == 'pull_request' run: | diff --git a/fluffy/common/common_types.nim b/fluffy/common/common_types.nim index bc8cd77d6f..3b165e6bae 100644 --- a/fluffy/common/common_types.nim +++ b/fluffy/common/common_types.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2021-2023 Status Research & Development GmbH +# Copyright (c) 2021-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -7,10 +7,7 @@ {.push raises: [].} -import - ssz_serialization, - eth/rlp, - stew/[byteutils, results], nimcrypto/hash +import ssz_serialization, eth/rlp, stew/[byteutils, results], nimcrypto/hash export hash @@ -41,4 +38,4 @@ func decodeSszOrRaise*(input: openArray[byte], T: type): T = try: SSZ.decode(input, T) except SerializationError as e: - raiseAssert(e.msg) \ No newline at end of file + raiseAssert(e.msg) diff --git a/fluffy/common/common_utils.nim b/fluffy/common/common_utils.nim index f7443c44ac..61e358ac29 100644 --- a/fluffy/common/common_utils.nim +++ b/fluffy/common/common_utils.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2021-2023 Status Research & Development GmbH +# Copyright (c) 2021-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -7,10 +7,7 @@ {.push raises: [].} -import - std/[os, strutils], - chronicles, stew/io2, - eth/p2p/discoveryv5/enr +import std/[os, strutils], chronicles, stew/io2, eth/p2p/discoveryv5/enr iterator strippedLines(filename: string): string {.raises: [ref IOError].} = for line in lines(filename): @@ -21,19 +18,18 @@ iterator strippedLines(filename: string): string {.raises: [ref IOError].} = if stripped.len > 0: yield stripped -proc addBootstrapNode(bootstrapAddr: string, - bootstrapEnrs: var seq[Record]) = +proc addBootstrapNode(bootstrapAddr: string, bootstrapEnrs: var seq[Record]) = var enrRec: enr.Record if enrRec.fromURI(bootstrapAddr): bootstrapEnrs.add enrRec else: warn "Ignoring invalid bootstrap ENR", bootstrapAddr -proc loadBootstrapFile*(bootstrapFile: string, - bootstrapEnrs: var seq[Record]) = - if bootstrapFile.len == 0: return +proc loadBootstrapFile*(bootstrapFile: string, bootstrapEnrs: var seq[Record]) = + if bootstrapFile.len == 0: + return let ext = splitFile(bootstrapFile).ext - if cmpIgnoreCase(ext, ".txt") == 0 or cmpIgnoreCase(ext, ".enr") == 0 : + if cmpIgnoreCase(ext, ".txt") == 0 or cmpIgnoreCase(ext, ".enr") == 0: try: for ln in strippedLines(bootstrapFile): addBootstrapNode(ln, bootstrapEnrs) @@ -50,8 +46,8 @@ proc loadBootstrapFile*(bootstrapFile: string, # However that would require the pull the keystore.nim and parts of # keystore_management.nim out of nimbus-eth2. proc getPersistentNetKey*( - rng: var HmacDrbgContext, keyFilePath: string): - tuple[key: PrivateKey, newNetKey: bool] = + rng: var HmacDrbgContext, keyFilePath: string +): tuple[key: PrivateKey, newNetKey: bool] = logScope: key_file = keyFilePath @@ -60,8 +56,7 @@ proc getPersistentNetKey*( let readResult = readAllChars(keyFilePath) if readResult.isErr(): - fatal "Could not load network key file", - error = ioErrorMsg(readResult.error) + fatal "Could not load network key file", error = ioErrorMsg(readResult.error) quit QuitFailure let netKeyInHex = readResult.get() @@ -76,14 +71,12 @@ proc getPersistentNetKey*( else: fatal "Invalid length of private in file" quit QuitFailure - else: info "Network key file is missing, creating a new one" let key = PrivateKey.random(rng) if (let res = io2.writeFile(keyFilePath, $key); res.isErr): - fatal "Failed to write the network key file", - error = ioErrorMsg(res.error) + fatal "Failed to write the network key file", error = ioErrorMsg(res.error) quit 1 info "New network key file was created" @@ -99,8 +92,7 @@ proc getPersistentEnr*(enrFilePath: string): Opt[enr.Record] = let readResult = readAllChars(enrFilePath) if readResult.isErr(): - warn "Could not load ENR file", - error = ioErrorMsg(readResult.error) + warn "Could not load ENR file", error = ioErrorMsg(readResult.error) return Opt.none(enr.Record) let enrUri = readResult.get() @@ -113,7 +105,6 @@ proc getPersistentEnr*(enrFilePath: string): Opt[enr.Record] = return Opt.none(enr.Record) else: return Opt.some(record) - else: warn "Could not find ENR file. Was it manually deleted?" return Opt.none(enr.Record) diff --git a/fluffy/conf.nim b/fluffy/conf.nim index 8359bdf41f..1db1704cf7 100644 --- a/fluffy/conf.nim +++ b/fluffy/conf.nim @@ -9,8 +9,12 @@ import std/os, - uri, confutils, confutils/std/net, chronicles, - eth/keys, eth/p2p/discoveryv5/[enr, node, routing_table], + uri, + confutils, + confutils/std/net, + chronicles, + eth/keys, + eth/p2p/discoveryv5/[enr, node, routing_table], json_rpc/rpcproxy, nimcrypto/hash, stew/byteutils, @@ -19,12 +23,13 @@ import ./network/wire/portal_protocol_config proc defaultDataDir*(): string = - let dataDir = when defined(windows): - "AppData" / "Roaming" / "Fluffy" - elif defined(macosx): - "Library" / "Application Support" / "Fluffy" - else: - ".cache" / "fluffy" + let dataDir = + when defined(windows): + "AppData" / "Roaming" / "Fluffy" + elif defined(macosx): + "Library" / "Application Support" / "Fluffy" + else: + ".cache" / "fluffy" getHomeDir() / dataDir @@ -41,12 +46,9 @@ const defaultStorageCapacity* = 2000'u32 # 2 GB default defaultStorageCapacityDesc* = $defaultStorageCapacity - defaultTableIpLimitDesc* = - $defaultPortalProtocolConfig.tableIpLimits.tableIpLimit - defaultBucketIpLimitDesc* = - $defaultPortalProtocolConfig.tableIpLimits.bucketIpLimit - defaultBitsPerHopDesc* = - $defaultPortalProtocolConfig.bitsPerHop + defaultTableIpLimitDesc* = $defaultPortalProtocolConfig.tableIpLimits.tableIpLimit + defaultBucketIpLimitDesc* = $defaultPortalProtocolConfig.tableIpLimits.bucketIpLimit + defaultBitsPerHopDesc* = $defaultPortalProtocolConfig.bitsPerHop type TrustedDigest* = MDigest[32 * 8] @@ -60,221 +62,244 @@ type PortalConf* = object logLevel* {. - desc: "Sets the log level for process and topics (e.g. \"DEBUG; TRACE:discv5,portal_wire; REQUIRED:none; DISABLED:none\")" - defaultValue: "INFO" - name: "log-level" .}: string + desc: + "Sets the log level for process and topics (e.g. \"DEBUG; TRACE:discv5,portal_wire; REQUIRED:none; DISABLED:none\")", + defaultValue: "INFO", + name: "log-level" + .}: string logStdout* {. - hidden - desc: "Specifies what kind of logs should be written to stdout (auto, colors, nocolors, json)" - defaultValueDesc: "auto" - defaultValue: StdoutLogKind.Auto - name: "log-format" .}: StdoutLogKind + hidden, + desc: + "Specifies what kind of logs should be written to stdout (auto, colors, nocolors, json)", + defaultValueDesc: "auto", + defaultValue: StdoutLogKind.Auto, + name: "log-format" + .}: StdoutLogKind - udpPort* {. - defaultValue: 9009 - desc: "UDP listening port" - name: "udp-port" .}: uint16 + udpPort* {.defaultValue: 9009, desc: "UDP listening port", name: "udp-port".}: + uint16 listenAddress* {. - defaultValue: defaultListenAddress - defaultValueDesc: $defaultListenAddressDesc - desc: "Listening address for the Discovery v5 traffic" - name: "listen-address" .}: IpAddress + defaultValue: defaultListenAddress, + defaultValueDesc: $defaultListenAddressDesc, + desc: "Listening address for the Discovery v5 traffic", + name: "listen-address" + .}: IpAddress portalNetwork* {. desc: "Select which Portal network to join. This will set the " & - "network specific bootstrap nodes automatically" - defaultValue: PortalNetwork.testnet0 - name: "network" }: PortalNetwork + "network specific bootstrap nodes automatically", + defaultValue: PortalNetwork.testnet0, + name: "network" + .}: PortalNetwork # Note: This will add bootstrap nodes for both Discovery v5 network and each # enabled Portal network. No distinction is made on bootstrap nodes per # specific network. bootstrapNodes* {. - desc: "ENR URI of node to bootstrap Discovery v5 and the Portal networks from. Argument may be repeated" - name: "bootstrap-node" .}: seq[Record] + desc: + "ENR URI of node to bootstrap Discovery v5 and the Portal networks from. Argument may be repeated", + name: "bootstrap-node" + .}: seq[Record] bootstrapNodesFile* {. - desc: "Specifies a line-delimited file of ENR URIs to bootstrap Discovery v5 and Portal networks from" - defaultValue: "" - name: "bootstrap-file" .}: InputFile + desc: + "Specifies a line-delimited file of ENR URIs to bootstrap Discovery v5 and Portal networks from", + defaultValue: "", + name: "bootstrap-file" + .}: InputFile nat* {. - desc: "Specify method to use for determining public address. " & - "Must be one of: any, none, upnp, pmp, extip:" - defaultValue: NatConfig(hasExtIp: false, nat: NatAny) - defaultValueDesc: "any" - name: "nat" .}: NatConfig + desc: + "Specify method to use for determining public address. " & + "Must be one of: any, none, upnp, pmp, extip:", + defaultValue: NatConfig(hasExtIp: false, nat: NatAny), + defaultValueDesc: "any", + name: "nat" + .}: NatConfig enrAutoUpdate* {. - defaultValue: false - desc: "Discovery can automatically update its ENR with the IP address " & - "and UDP port as seen by other nodes it communicates with. " & - "This option allows to enable/disable this functionality" - name: "enr-auto-update" .}: bool + defaultValue: false, + desc: + "Discovery can automatically update its ENR with the IP address " & + "and UDP port as seen by other nodes it communicates with. " & + "This option allows to enable/disable this functionality", + name: "enr-auto-update" + .}: bool dataDir* {. - desc: "The directory where fluffy will store the content data" - defaultValue: defaultDataDir() - defaultValueDesc: $defaultDataDirDesc - name: "data-dir" .}: OutDir + desc: "The directory where fluffy will store the content data", + defaultValue: defaultDataDir(), + defaultValueDesc: $defaultDataDirDesc, + name: "data-dir" + .}: OutDir networkKeyFile* {. - desc: "Source of network (secp256k1) private key file" + desc: "Source of network (secp256k1) private key file", defaultValue: config.dataDir / "netkey", - name: "netkey-file" }: string + name: "netkey-file" + .}: string networkKey* {. - hidden + hidden, desc: "Private key (secp256k1) for the p2p network, hex encoded.", - defaultValue: none(PrivateKey) - defaultValueDesc: "none" - name: "netkey-unsafe" .}: Option[PrivateKey] + defaultValue: none(PrivateKey), + defaultValueDesc: "none", + name: "netkey-unsafe" + .}: Option[PrivateKey] accumulatorFile* {. desc: "Get the master accumulator snapshot from a file containing an " & - "pre-build SSZ encoded master accumulator." - defaultValue: none(InputFile) - defaultValueDesc: "none" - name: "accumulator-file" .}: Option[InputFile] + "pre-build SSZ encoded master accumulator.", + defaultValue: none(InputFile), + defaultValueDesc: "none", + name: "accumulator-file" + .}: Option[InputFile] metricsEnabled* {. - defaultValue: false - desc: "Enable the metrics server" - name: "metrics" .}: bool + defaultValue: false, desc: "Enable the metrics server", name: "metrics" + .}: bool metricsAddress* {. - defaultValue: defaultAdminListenAddress - defaultValueDesc: $defaultAdminListenAddressDesc - desc: "Listening address of the metrics server" - name: "metrics-address" .}: IpAddress + defaultValue: defaultAdminListenAddress, + defaultValueDesc: $defaultAdminListenAddressDesc, + desc: "Listening address of the metrics server", + name: "metrics-address" + .}: IpAddress metricsPort* {. - defaultValue: 8008 - desc: "Listening HTTP port of the metrics server" - name: "metrics-port" .}: Port + defaultValue: 8008, + desc: "Listening HTTP port of the metrics server", + name: "metrics-port" + .}: Port - rpcEnabled* {. - desc: "Enable the JSON-RPC server" - defaultValue: false - name: "rpc" .}: bool + rpcEnabled* {.desc: "Enable the JSON-RPC server", defaultValue: false, name: "rpc".}: + bool rpcPort* {. - desc: "HTTP port for the JSON-RPC server" - defaultValue: 8545 - name: "rpc-port" .}: Port + desc: "HTTP port for the JSON-RPC server", defaultValue: 8545, name: "rpc-port" + .}: Port rpcAddress* {. - desc: "Listening address of the RPC server" - defaultValue: defaultAdminListenAddress - defaultValueDesc: $defaultAdminListenAddressDesc - name: "rpc-address" .}: IpAddress + desc: "Listening address of the RPC server", + defaultValue: defaultAdminListenAddress, + defaultValueDesc: $defaultAdminListenAddressDesc, + name: "rpc-address" + .}: IpAddress # it makes little sense to have default value here in final release, but until then # it would be troublesome to add some fake uri param every time proxyUri* {. - defaultValue: defaultClientConfig - defaultValueDesc: $defaultClientConfigDesc - desc: "URI of eth client where to proxy unimplemented JSON-RPC methods to" - name: "proxy-uri" .}: ClientConfig + defaultValue: defaultClientConfig, + defaultValueDesc: $defaultClientConfigDesc, + desc: "URI of eth client where to proxy unimplemented JSON-RPC methods to", + name: "proxy-uri" + .}: ClientConfig tableIpLimit* {. - hidden - desc: "Maximum amount of nodes with the same IP in the routing table. " & - "This option is currently required as many nodes are running from " & - "the same machines. The option will be removed/adjusted in the future" - defaultValue: defaultPortalProtocolConfig.tableIpLimits.tableIpLimit - defaultValueDesc: $defaultTableIpLimitDesc - name: "table-ip-limit" .}: uint + hidden, + desc: + "Maximum amount of nodes with the same IP in the routing table. " & + "This option is currently required as many nodes are running from " & + "the same machines. The option will be removed/adjusted in the future", + defaultValue: defaultPortalProtocolConfig.tableIpLimits.tableIpLimit, + defaultValueDesc: $defaultTableIpLimitDesc, + name: "table-ip-limit" + .}: uint bucketIpLimit* {. - hidden - desc: "Maximum amount of nodes with the same IP in the routing table's buckets. " & - "This option is currently required as many nodes are running from " & - "the same machines. The option will be removed/adjusted in the future" - defaultValue: defaultPortalProtocolConfig.tableIpLimits.bucketIpLimit - defaultValueDesc: $defaultBucketIpLimitDesc - name: "bucket-ip-limit" .}: uint + hidden, + desc: + "Maximum amount of nodes with the same IP in the routing table's buckets. " & + "This option is currently required as many nodes are running from " & + "the same machines. The option will be removed/adjusted in the future", + defaultValue: defaultPortalProtocolConfig.tableIpLimits.bucketIpLimit, + defaultValueDesc: $defaultBucketIpLimitDesc, + name: "bucket-ip-limit" + .}: uint bitsPerHop* {. - hidden - desc: "Kademlia's b variable, increase for less hops per lookup" - defaultValue: defaultPortalProtocolConfig.bitsPerHop - defaultValueDesc: $defaultBitsPerHopDesc - name: "bits-per-hop" .}: int + hidden, + desc: "Kademlia's b variable, increase for less hops per lookup", + defaultValue: defaultPortalProtocolConfig.bitsPerHop, + defaultValueDesc: $defaultBitsPerHopDesc, + name: "bits-per-hop" + .}: int radiusConfig* {. - desc: "Radius configuration for a fluffy node. Radius can be either `dynamic` " & - "where the node adjusts the radius based on `storage-size` option, " & - "or `static:` where the node has a hardcoded logarithmic radius value. " & - "Warning: `static:` disables `storage-size` limits and " & - "makes the node store a fraction of the network based on set radius." - defaultValue: defaultRadiusConfig - defaultValueDesc: $defaultRadiusConfigDesc - name: "radius" .}: RadiusConfig + desc: + "Radius configuration for a fluffy node. Radius can be either `dynamic` " & + "where the node adjusts the radius based on `storage-size` option, " & + "or `static:` where the node has a hardcoded logarithmic radius value. " & + "Warning: `static:` disables `storage-size` limits and " & + "makes the node store a fraction of the network based on set radius.", + defaultValue: defaultRadiusConfig, + defaultValueDesc: $defaultRadiusConfigDesc, + name: "radius" + .}: RadiusConfig # TODO maybe it is worth defining minimal storage size and throw error if # value provided is smaller than minimum storageCapacityMB* {. - desc: "Maximum amount (in megabytes) of content which will be stored " & - "in the local database." - defaultValue: defaultStorageCapacity - defaultValueDesc: $defaultStorageCapacityDesc - name: "storage-capacity" .}: uint64 + desc: + "Maximum amount (in megabytes) of content which will be stored " & + "in the local database.", + defaultValue: defaultStorageCapacity, + defaultValueDesc: $defaultStorageCapacityDesc, + name: "storage-capacity" + .}: uint64 trustedBlockRoot* {. - desc: "Recent trusted finalized block root to initialize the consensus light client from. " & - "If not provided by the user, portal light client will be disabled." - defaultValue: none(TrustedDigest) - name: "trusted-block-root" .}: Option[TrustedDigest] + desc: + "Recent trusted finalized block root to initialize the consensus light client from. " & + "If not provided by the user, portal light client will be disabled.", + defaultValue: none(TrustedDigest), + name: "trusted-block-root" + .}: Option[TrustedDigest] forcePrune* {. - hidden - desc: "Force the pruning of the database. This should be used when the " & - "database is decreased in size, e.g. when a lower static radius " & - "or a lower storage capacity is set." - defaultValue: false - name: "force-prune" .}: bool + hidden, + desc: + "Force the pruning of the database. This should be used when the " & + "database is decreased in size, e.g. when a lower static radius " & + "or a lower storage capacity is set.", + defaultValue: false, + name: "force-prune" + .}: bool disablePoke* {. - hidden - desc: "Disable POKE functionality for gossip mechanisms testing" - defaultValue: defaultDisablePoke - defaultValueDesc: $defaultDisablePoke - name: "disable-poke" .}: bool + hidden, + desc: "Disable POKE functionality for gossip mechanisms testing", + defaultValue: defaultDisablePoke, + defaultValueDesc: $defaultDisablePoke, + name: "disable-poke" + .}: bool stateNetworkEnabled* {. - hidden - desc: "Enable State Network" - defaultValue: false - name: "state" .}: bool - - case cmd* {. - command - defaultValue: noCommand .}: PortalCmd + hidden, desc: "Enable State Network", defaultValue: false, name: "state" + .}: bool + + case cmd* {.command, defaultValue: noCommand.}: PortalCmd of noCommand: discard -func parseCmdArg*(T: type TrustedDigest, input: string): T - {.raises: [ValueError].} = +func parseCmdArg*(T: type TrustedDigest, input: string): T {.raises: [ValueError].} = TrustedDigest.fromHex(input) func completeCmdArg*(T: type TrustedDigest, input: string): seq[string] = return @[] -proc parseCmdArg*(T: type enr.Record, p: string): T - {.raises: [ValueError].} = +proc parseCmdArg*(T: type enr.Record, p: string): T {.raises: [ValueError].} = if not fromURI(result, p): raise newException(ValueError, "Invalid ENR") proc completeCmdArg*(T: type enr.Record, val: string): seq[string] = return @[] -proc parseCmdArg*(T: type Node, p: string): T - {.raises: [ValueError].} = +proc parseCmdArg*(T: type Node, p: string): T {.raises: [ValueError].} = var record: enr.Record if not fromURI(record, p): raise newException(ValueError, "Invalid ENR") @@ -291,8 +316,7 @@ proc parseCmdArg*(T: type Node, p: string): T proc completeCmdArg*(T: type Node, val: string): seq[string] = return @[] -proc parseCmdArg*(T: type PrivateKey, p: string): T - {.raises: [ValueError].} = +proc parseCmdArg*(T: type PrivateKey, p: string): T {.raises: [ValueError].} = try: result = PrivateKey.fromHex(p).tryGet() except CatchableError: @@ -301,8 +325,7 @@ proc parseCmdArg*(T: type PrivateKey, p: string): T proc completeCmdArg*(T: type PrivateKey, val: string): seq[string] = return @[] -proc parseCmdArg*(T: type ClientConfig, p: string): T - {.raises: [ValueError].} = +proc parseCmdArg*(T: type ClientConfig, p: string): T {.raises: [ValueError].} = let uri = parseUri(p) if (uri.scheme == "http" or uri.scheme == "https"): getHttpClientConfig(p) @@ -316,6 +339,9 @@ proc parseCmdArg*(T: type ClientConfig, p: string): T proc completeCmdArg*(T: type ClientConfig, val: string): seq[string] = return @[] -chronicles.formatIt(InputDir): $it -chronicles.formatIt(OutDir): $it -chronicles.formatIt(InputFile): $it +chronicles.formatIt(InputDir): + $it +chronicles.formatIt(OutDir): + $it +chronicles.formatIt(InputFile): + $it diff --git a/fluffy/database/content_db.nim b/fluffy/database/content_db.nim index b1b0d232c0..7c49d6137f 100644 --- a/fluffy/database/content_db.nim +++ b/fluffy/database/content_db.nim @@ -1,5 +1,5 @@ # Fluffy -# Copyright (c) 2021-2023 Status Research & Development GmbH +# Copyright (c) 2021-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -38,18 +38,15 @@ declareCounter portal_pruning_counter, labels = ["protocol_id"] declareGauge portal_pruning_deleted_elements, - "Number of elements deleted in the last pruning", - labels = ["protocol_id"] + "Number of elements deleted in the last pruning", labels = ["protocol_id"] const contentDeletionFraction = 0.05 ## 5% of the content will be deleted when the ## storage capacity is hit and radius gets adjusted. type - RowInfo = tuple - contentId: array[32, byte] - payloadLength: int64 - distance: array[32, byte] + RowInfo = + tuple[contentId: array[32, byte], payloadLength: int64, distance: array[32, byte]] ContentDB* = ref object backend: SqStoreRef @@ -66,7 +63,8 @@ type largestDistanceStmt: SqliteStmt[array[32, byte], array[32, byte]] PutResultType* = enum - ContentStored, DbPruned + ContentStored + DbPruned PutResult* = object case kind*: PutResultType @@ -83,56 +81,65 @@ template expectDb(x: auto): untyped = x.expect("working database (disk broken/full?)") proc new*( - T: type ContentDB, path: string, storageCapacity: uint64, - inMemory = false, manualCheckpoint = false): ContentDB = + T: type ContentDB, + path: string, + storageCapacity: uint64, + inMemory = false, + manualCheckpoint = false, +): ContentDB = doAssert(storageCapacity <= uint64(int64.high)) let db = if inMemory: SqStoreRef.init("", "fluffy-test", inMemory = true).expect( - "working database (out of memory?)") + "working database (out of memory?)" + ) else: SqStoreRef.init(path, "fluffy", manualCheckpoint = false).expectDb() db.createCustomFunction("xorDistance", 2, xorDistance).expect( - "Custom function xorDistance creation OK") + "Custom function xorDistance creation OK" + ) db.createCustomFunction("isInRadius", 3, isInRadius).expect( - "Custom function isInRadius creation OK") + "Custom function isInRadius creation OK" + ) let sizeStmt = db.prepareStmt( "SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size();", - NoParams, int64).get() + NoParams, int64, + )[] let unusedSizeStmt = db.prepareStmt( "SELECT freelist_count * page_size as size FROM pragma_freelist_count(), pragma_page_size();", - NoParams, int64).get() + NoParams, int64, + )[] - let vacuumStmt = db.prepareStmt( - "VACUUM;", - NoParams, void).get() + let vacuumStmt = db.prepareStmt("VACUUM;", NoParams, void)[] let kvStore = kvStore db.openKvStore().expectDb() - let contentSizeStmt = db.prepareStmt( - "SELECT SUM(length(value)) FROM kvstore", - NoParams, int64).get() + let contentSizeStmt = + db.prepareStmt("SELECT SUM(length(value)) FROM kvstore", NoParams, int64)[] - let contentCountStmt = db.prepareStmt( - "SELECT COUNT(key) FROM kvstore;", - NoParams, int64).get() + let contentCountStmt = + db.prepareStmt("SELECT COUNT(key) FROM kvstore;", NoParams, int64)[] let getAllOrderedByDistanceStmt = db.prepareStmt( "SELECT key, length(value), xorDistance(?, key) as distance FROM kvstore ORDER BY distance DESC", - array[32, byte], RowInfo).get() + array[32, byte], + RowInfo, + )[] let deleteOutOfRadiusStmt = db.prepareStmt( "DELETE FROM kvstore WHERE isInRadius(?, key, ?) == 0", - (array[32, byte], array[32, byte]), void).get() + (array[32, byte], array[32, byte]), + void, + )[] let largestDistanceStmt = db.prepareStmt( - "SELECT max(xorDistance(?, key)) FROM kvstore", - array[32, byte], array[32, byte]).get() + "SELECT max(xorDistance(?, key)) FROM kvstore", array[32, byte], array[32, byte] + )[] ContentDB( kv: kvStore, @@ -146,7 +153,7 @@ proc new*( contentCountStmt: contentCountStmt, getAllOrderedByDistanceStmt: getAllOrderedByDistanceStmt, deleteOutOfRadiusStmt: deleteOutOfRadiusStmt, - largestDistanceStmt: largestDistanceStmt + largestDistanceStmt: largestDistanceStmt, ) template disposeSafe(s: untyped): untyped = @@ -169,7 +176,8 @@ proc close*(db: ContentDB) = proc get(kv: KvStoreRef, key: openArray[byte]): Opt[seq[byte]] = var res: Opt[seq[byte]] - proc onData(data: openArray[byte]) = res = Opt.some(@data) + proc onData(data: openArray[byte]) = + res = Opt.some(@data) discard kv.get(key, onData).expectDb() @@ -200,8 +208,7 @@ proc del(db: ContentDB, key: openArray[byte]) = # TODO: Do we want to return the bool here too? discard db.kv.del(key).expectDb() -proc getSszDecoded( - db: ContentDB, key: openArray[byte], T: type auto): Opt[T] = +proc getSszDecoded(db: ContentDB, key: openArray[byte], T: type auto): Opt[T] = db.kv.getSszDecoded(key, T) ## Public ContentId based ContentDB calls @@ -242,16 +249,20 @@ proc size*(db: ContentDB): int64 = ## to the way how deleting works in sqlite. ## Good description can be found in: https://www.sqlite.org/lang_vacuum.html var size: int64 = 0 - discard (db.sizeStmt.exec do(res: int64): - size = res).expectDb() + discard ( + db.sizeStmt.exec do(res: int64): + size = res + ).expectDb() return size proc unusedSize(db: ContentDB): int64 = ## Returns the total size of the pages which are unused by the database, ## i.e they can be re-used for new content. var size: int64 = 0 - discard (db.unusedSizeStmt.exec do(res: int64): - size = res).expectDb() + discard ( + db.unusedSizeStmt.exec do(res: int64): + size = res + ).expectDb() return size proc usedSize*(db: ContentDB): int64 = @@ -262,30 +273,38 @@ proc usedSize*(db: ContentDB): int64 = proc contentSize*(db: ContentDB): int64 = ## Returns total size of the content stored in DB. var size: int64 = 0 - discard (db.contentSizeStmt.exec do(res: int64): - size = res).expectDb() + discard ( + db.contentSizeStmt.exec do(res: int64): + size = res + ).expectDb() return size proc contentCount*(db: ContentDB): int64 = var count: int64 = 0 - discard (db.contentCountStmt.exec do(res: int64): - count = res).expectDb() + discard ( + db.contentCountStmt.exec do(res: int64): + count = res + ).expectDb() return count ## Pruning related calls proc getLargestDistance*(db: ContentDB, localId: UInt256): UInt256 = var distanceBytes: array[32, byte] - discard (db.largestDistanceStmt.exec(localId.toBytesBE(), + discard ( + db.largestDistanceStmt.exec( + localId.toBytesBE(), proc(res: array[32, byte]) = distanceBytes = res - )).expectDb() + , + ) + ).expectDb() return UInt256.fromBytesBE(distanceBytes) func estimateNewRadius( - currentSize: uint64, storageCapacity: uint64, - currentRadius: UInt256): UInt256 = + currentSize: uint64, storageCapacity: uint64, currentRadius: UInt256 +): UInt256 = let sizeRatio = currentSize div storageCapacity if sizeRatio > 0: currentRadius div sizeRatio.stuint(256) @@ -296,18 +315,14 @@ func estimateNewRadius*(db: ContentDB, currentRadius: UInt256): UInt256 = estimateNewRadius(uint64(db.usedSize()), db.storageCapacity, currentRadius) proc deleteContentFraction*( - db: ContentDB, - target: UInt256, - fraction: float64): (UInt256, int64, int64, int64) = + db: ContentDB, target: UInt256, fraction: float64 +): (UInt256, int64, int64, int64) = ## Deletes at most `fraction` percent of content from the database. ## The content furthest from the provided `target` is deleted first. # TODO: The usage of `db.contentSize()` for the deletion calculation versus # `db.usedSize()` for the pruning threshold leads sometimes to some unexpected # results of how much content gets up deleted. - doAssert( - fraction > 0 and fraction < 1, - "Deleted fraction should be > 0 and < 1" - ) + doAssert(fraction > 0 and fraction < 1, "Deleted fraction should be > 0 and < 1") let totalContentSize = db.contentSize() let bytesToDelete = int64(fraction * float64(totalContentSize)) @@ -326,7 +341,7 @@ proc deleteContentFraction*( UInt256.fromBytesBE(ri.distance), deletedBytes, totalContentSize, - deletedElements + deletedElements, ) proc reclaimSpace*(db: ContentDB): void = @@ -337,11 +352,12 @@ proc reclaimSpace*(db: ContentDB): void = ## the start of db to leave it up to sqlite to clean up. db.vacuumStmt.exec().expectDb() -proc deleteContentOutOfRadius*( - db: ContentDB, localId: UInt256, radius: UInt256) = +proc deleteContentOutOfRadius*(db: ContentDB, localId: UInt256, radius: UInt256) = ## Deletes all content that falls outside of the given radius range. - db.deleteOutOfRadiusStmt.exec( - (localId.toBytesBE(), radius.toBytesBE())).expect("SQL query OK") + + db.deleteOutOfRadiusStmt.exec((localId.toBytesBE(), radius.toBytesBE())).expect( + "SQL query OK" + ) proc forcePrune*(db: ContentDB, localId: UInt256, radius: UInt256) = ## Force prune the database to a statically set radius. This will also run @@ -361,10 +377,8 @@ proc forcePrune*(db: ContentDB, localId: UInt256, radius: UInt256) = notice "Finished database pruning" proc put*( - db: ContentDB, - key: ContentId, - value: openArray[byte], - target: UInt256): PutResult = + db: ContentDB, key: ContentId, value: openArray[byte], target: UInt256 +): PutResult = db.put(key, value) # The used size is used as pruning threshold. This means that the database @@ -393,12 +407,7 @@ proc put*( # in the trend of: # "SELECT key FROM kvstore ORDER BY xorDistance(?, key) DESC LIMIT 1" # Potential adjusting the LIMIT for how many items require deletion. - let ( - distanceOfFurthestElement, - deletedBytes, - totalContentSize, - deletedElements - ) = + let (distanceOfFurthestElement, deletedBytes, totalContentSize, deletedElements) = db.deleteContentFraction(target, contentDeletionFraction) let deletedFraction = float64(deletedBytes) / float64(totalContentSize) @@ -408,12 +417,12 @@ proc put*( kind: DbPruned, distanceOfFurthestElement: distanceOfFurthestElement, deletedFraction: deletedFraction, - deletedElements: deletedElements) + deletedElements: deletedElements, + ) proc adjustRadius( - p: PortalProtocol, - deletedFraction: float64, - distanceOfFurthestElement: UInt256) = + p: PortalProtocol, deletedFraction: float64, distanceOfFurthestElement: UInt256 +) = # Invert fraction as the UInt256 implementation does not support # multiplication by float let invertedFractionAsInt = int64(1.0 / deletedFraction) @@ -426,9 +435,7 @@ proc adjustRadius( let newRadius = max(scaledRadius, distanceOfFurthestElement) info "Database radius adjusted", - oldRadius = p.dataRadius, - newRadius = newRadius, - distanceOfFurthestElement + oldRadius = p.dataRadius, newRadius = newRadius, distanceOfFurthestElement # Both scaledRadius and distanceOfFurthestElement are smaller than current # dataRadius, so the radius will constantly decrease through the node its @@ -445,42 +452,41 @@ proc createGetHandler*(db: ContentDB): DbGetHandler = ) proc createStoreHandler*( - db: ContentDB, cfg: RadiusConfig, p: PortalProtocol): DbStoreHandler = - return (proc( - contentKey: ByteList, - contentId: ContentId, - content: seq[byte]) {.raises: [], gcsafe.} = - # always re-check that the key is in the node range to make sure only - # content in range is stored. - # TODO: current silent assumption is that both ContentDB and PortalProtocol - # are using the same xor distance function - if p.inRange(contentId): - case cfg.kind: - of Dynamic: - # In case of dynamic radius setting we obey storage limits and adjust - # radius to store network fraction corresponding to those storage limits. - let res = db.put(contentId, content, p.localNode.id) - if res.kind == DbPruned: - portal_pruning_counter.inc(labelValues = [$p.protocolId]) - portal_pruning_deleted_elements.set( - res.deletedElements.int64, - labelValues = [$p.protocolId] - ) - - if res.deletedFraction > 0.0: - p.adjustRadius(res.deletedFraction, res.distanceOfFurthestElement) - else: - # Note: - # This can occur when the furthest content is bigger than the fraction - # size. This is unlikely to happen as it would require either very - # small storage capacity or a very small `contentDeletionFraction` - # combined with some big content. - info "Database pruning attempt resulted in no content deleted" - return - - of Static: - # If the config is set statically, radius is not adjusted, and is kept - # constant thorugh node life time, also database max size is disabled - # so we will effectivly store fraction of the network - db.put(contentId, content) + db: ContentDB, cfg: RadiusConfig, p: PortalProtocol +): DbStoreHandler = + return ( + proc( + contentKey: ByteList, contentId: ContentId, content: seq[byte] + ) {.raises: [], gcsafe.} = + # always re-check that the key is in the node range to make sure only + # content in range is stored. + # TODO: current silent assumption is that both ContentDB and PortalProtocol + # are using the same xor distance function + if p.inRange(contentId): + case cfg.kind + of Dynamic: + # In case of dynamic radius setting we obey storage limits and adjust + # radius to store network fraction corresponding to those storage limits. + let res = db.put(contentId, content, p.localNode.id) + if res.kind == DbPruned: + portal_pruning_counter.inc(labelValues = [$p.protocolId]) + portal_pruning_deleted_elements.set( + res.deletedElements.int64, labelValues = [$p.protocolId] + ) + + if res.deletedFraction > 0.0: + p.adjustRadius(res.deletedFraction, res.distanceOfFurthestElement) + else: + # Note: + # This can occur when the furthest content is bigger than the fraction + # size. This is unlikely to happen as it would require either very + # small storage capacity or a very small `contentDeletionFraction` + # combined with some big content. + info "Database pruning attempt resulted in no content deleted" + return + of Static: + # If the config is set statically, radius is not adjusted, and is kept + # constant thorugh node life time, also database max size is disabled + # so we will effectivly store fraction of the network + db.put(contentId, content) ) diff --git a/fluffy/database/content_db_custom_sql_functions.nim b/fluffy/database/content_db_custom_sql_functions.nim index 60674d6037..21fb2eefd4 100644 --- a/fluffy/database/content_db_custom_sql_functions.nim +++ b/fluffy/database/content_db_custom_sql_functions.nim @@ -1,5 +1,5 @@ # Fluffy -# Copyright (c) 2023 Status Research & Development GmbH +# Copyright (c) 2023-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -7,25 +7,21 @@ {.push raises: [].} -import - stew/ptrops, - stint, - sqlite3_abi, - eth/db/kvstore_sqlite3 +import stew/ptrops, stint, sqlite3_abi, eth/db/kvstore_sqlite3 func xorDistance(a: openArray[byte], b: openArray[byte]): seq[byte] = doAssert(a.len == b.len) let length = a.len var distance: seq[byte] = newSeq[byte](length) - for i in 0.. distance func isInRadius*( - ctx: SqliteContext, n: cint, v: SqliteValue) - {.cdecl, gcsafe, raises: [].} = + ctx: SqliteContext, n: cint, v: SqliteValue +) {.cdecl, gcsafe, raises: [].} = doAssert(n == 3) let @@ -59,12 +55,12 @@ func isInRadius*( doAssert(blob1Len == 32 and blob2Len == 32 and blob3Len == 32) let - localId = UInt256.fromBytesBE( - makeOpenArray(sqlite3_value_blob(ptrs[][0]), byte, blob1Len)) - contentId = UInt256.fromBytesBE( - makeOpenArray(sqlite3_value_blob(ptrs[][1]), byte, blob2Len)) - radius = UInt256.fromBytesBE( - makeOpenArray(sqlite3_value_blob(ptrs[][2]), byte, blob3Len)) + localId = + UInt256.fromBytesBE(makeOpenArray(sqlite3_value_blob(ptrs[][0]), byte, blob1Len)) + contentId = + UInt256.fromBytesBE(makeOpenArray(sqlite3_value_blob(ptrs[][1]), byte, blob2Len)) + radius = + UInt256.fromBytesBE(makeOpenArray(sqlite3_value_blob(ptrs[][2]), byte, blob3Len)) if isInRadius(contentId, localId, radius): ctx.sqlite3_result_int(cint 1) diff --git a/fluffy/database/seed_db.nim b/fluffy/database/seed_db.nim index e0dd3623e5..17589b3486 100644 --- a/fluffy/database/seed_db.nim +++ b/fluffy/database/seed_db.nim @@ -1,5 +1,5 @@ # Fluffy -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -18,22 +18,23 @@ import export kvstore_sqlite3 type - ContentData = tuple - contentId: array[32, byte] - contentKey: seq[byte] - content: seq[byte] + ContentData = + tuple[contentId: array[32, byte], contentKey: seq[byte], content: seq[byte]] - ContentDataDist* = tuple - contentId: array[32, byte] - contentKey: seq[byte] - content: seq[byte] - distance: array[32, byte] + ContentDataDist* = + tuple[ + contentId: array[32, byte], + contentKey: seq[byte], + content: seq[byte], + distance: array[32, byte], + ] SeedDb* = ref object store: SqStoreRef putStmt: SqliteStmt[(array[32, byte], seq[byte], seq[byte]), void] getStmt: SqliteStmt[array[32, byte], ContentData] - getInRangeStmt: SqliteStmt[(array[32, byte], array[32, byte], int64, int64), ContentDataDist] + getInRangeStmt: + SqliteStmt[(array[32, byte], array[32, byte], int64, int64), ContentDataDist] template expectDb(x: auto): untyped = # There's no meaningful error handling implemented for a corrupt database or @@ -56,12 +57,14 @@ proc new*(T: type SeedDb, path: string, name: string, inMemory = false): SeedDb let db = if inMemory: SqStoreRef.init("", "seed-db-test", inMemory = true).expect( - "working database (out of memory?)") + "working database (out of memory?)" + ) else: SqStoreRef.init(path, name).expectDb() if not db.readOnly: - let createSql = """ + let createSql = + """ CREATE TABLE IF NOT EXISTS seed_data ( contentid BLOB PRIMARY KEY, contentkey BLOB, @@ -70,77 +73,79 @@ proc new*(T: type SeedDb, path: string, name: string, inMemory = false): SeedDb db.exec(createSql).expectDb() - let putStmt = - db.prepareStmt( - "INSERT OR REPLACE INTO seed_data (contentid, contentkey, content) VALUES (?, ?, ?);", - (array[32, byte], seq[byte], seq[byte]), - void).get() + let putStmt = db.prepareStmt( + "INSERT OR REPLACE INTO seed_data (contentid, contentkey, content) VALUES (?, ?, ?);", + (array[32, byte], seq[byte], seq[byte]), + void, + )[] - let getStmt = - db.prepareStmt( - "SELECT contentid, contentkey, content FROM seed_data WHERE contentid = ?;", - array[32, byte], - ContentData - ).get() + let getStmt = db.prepareStmt( + "SELECT contentid, contentkey, content FROM seed_data WHERE contentid = ?;", + array[32, byte], + ContentData, + )[] db.createCustomFunction("xorDistance", 2, xorDistance).expect( - "Custom function xorDistance creation OK") + "Custom function xorDistance creation OK" + ) - let getInRangeStmt = - db.prepareStmt( - """ + let getInRangeStmt = db.prepareStmt( + """ SELECT contentid, contentkey, content, xorDistance(?, contentid) as distance FROM seed_data WHERE distance <= ? LIMIT ? OFFSET ?; """, - (array[32, byte], array[32, byte], int64, int64), - ContentDataDist - ).get() - - SeedDb( - store: db, - putStmt: putStmt, - getStmt: getStmt, - getInRangeStmt: getInRangeStmt - ) + (array[32, byte], array[32, byte], int64, int64), + ContentDataDist, + )[] + + SeedDb(store: db, putStmt: putStmt, getStmt: getStmt, getInRangeStmt: getInRangeStmt) -proc put*(db: SeedDb, contentId: array[32, byte], contentKey: seq[byte], content: seq[byte]): void = +proc put*( + db: SeedDb, contentId: array[32, byte], contentKey: seq[byte], content: seq[byte] +): void = db.putStmt.exec((contentId, contentKey, content)).expectDb() -proc put*(db: SeedDb, contentId: UInt256, contentKey: seq[byte], content: seq[byte]): void = +proc put*( + db: SeedDb, contentId: UInt256, contentKey: seq[byte], content: seq[byte] +): void = db.put(contentId.toBytesBE(), contentKey, content) proc get*(db: SeedDb, contentId: array[32, byte]): Option[ContentData] = var res = none[ContentData]() - discard db.getStmt.exec(contentId, proc (v: ContentData) = res = some(v)).expectDb() + discard db.getStmt + .exec( + contentId, + proc(v: ContentData) = + res = some(v) + , + ) + .expectDb() return res proc get*(db: SeedDb, contentId: UInt256): Option[ContentData] = db.get(contentId.toBytesBE()) proc getContentInRange*( - db: SeedDb, - nodeId: UInt256, - nodeRadius: UInt256, - max: int64, - offset: int64): seq[ContentDataDist] = + db: SeedDb, nodeId: UInt256, nodeRadius: UInt256, max: int64, offset: int64 +): seq[ContentDataDist] = ## Return `max` amount of content in `nodeId` range, starting from `offset` position ## i.e using `offset` 0 will return `max` closest items, using `offset` `10` will ## will retrun `max` closest items except first 10 var res: seq[ContentDataDist] = @[] var cd: ContentDataDist - for e in db.getInRangeStmt.exec((nodeId.toBytesBE(), nodeRadius.toBytesBE(), max, offset), cd): + for e in db.getInRangeStmt.exec( + (nodeId.toBytesBE(), nodeRadius.toBytesBE(), max, offset), cd + ): res.add(cd) return res proc getContentInRange*( - db: SeedDb, - nodeId: UInt256, - nodeRadius: UInt256, - max: int64): seq[ContentDataDist] = + db: SeedDb, nodeId: UInt256, nodeRadius: UInt256, max: int64 +): seq[ContentDataDist] = ## Return `max` amount of content in `nodeId` range, starting from closest content return db.getContentInRange(nodeId, nodeRadius, max, 0) diff --git a/fluffy/docs/the_fluffy_book/docs/basics-for-developers.md b/fluffy/docs/the_fluffy_book/docs/basics-for-developers.md index c26ee5d84f..49f16805f7 100644 --- a/fluffy/docs/the_fluffy_book/docs/basics-for-developers.md +++ b/fluffy/docs/the_fluffy_book/docs/basics-for-developers.md @@ -19,3 +19,17 @@ can be found on the general nimbus-eth1 readme. The code follows the [Status Nim Style Guide](https://status-im.github.io/nim-style-guide/). + +## Nim code formatting + +The fluffy codebase is formatted with [nph](https://github.com/arnetheduck/nph). +Check out the [this page](https://arnetheduck.github.io/nph/installation.html) +on how to install nph. + +The fluffy CI tests check the code formatting according to the style rules of nph. +Developers will need to make sure the code changes in PRs are formatted as such. + +!!! note + In the future the nph formatting might be added within the build environment + make targets or similar, but currently it is a manual step that developers + will need to perform. diff --git a/fluffy/eth_data/era1.nim b/fluffy/eth_data/era1.nim index a7f3ea0a36..6ef5611642 100644 --- a/fluffy/eth_data/era1.nim +++ b/fluffy/eth_data/era1.nim @@ -9,8 +9,10 @@ import std/[strformat, typetraits], - results, stew/[endians2, io2, byteutils, arrayops], - stint, snappy, + results, + stew/[endians2, io2, byteutils, arrayops], + stint, + snappy, eth/common/eth_types_rlp, beacon_chain/spec/beacon_time, ssz_serialization, @@ -47,12 +49,12 @@ export e2store.readRecord const # Note: When specification is more official, these could go with the other # E2S types. - CompressedHeader* = [byte 0x03, 0x00] - CompressedBody* = [byte 0x04, 0x00] + CompressedHeader* = [byte 0x03, 0x00] + CompressedBody* = [byte 0x04, 0x00] CompressedReceipts* = [byte 0x05, 0x00] - TotalDifficulty* = [byte 0x06, 0x00] - AccumulatorRoot* = [byte 0x07, 0x00] - E2BlockIndex* = [byte 0x66, 0x32] + TotalDifficulty* = [byte 0x06, 0x00] + AccumulatorRoot* = [byte 0x07, 0x00] + E2BlockIndex* = [byte 0x66, 0x32] MaxEra1Size* = 8192 @@ -78,18 +80,18 @@ template lenu64(x: untyped): untyped = # (first slot) and the last era (era1 ends at merge block). proc appendIndex*( - f: IoHandle, startNumber: uint64, offsets: openArray[int64]): - Result[int64, string] = + f: IoHandle, startNumber: uint64, offsets: openArray[int64] +): Result[int64, string] = let len = offsets.len() * sizeof(int64) + 16 - pos = ? f.appendHeader(E2BlockIndex, len) + pos = ?f.appendHeader(E2BlockIndex, len) - ? f.append(startNumber.uint64.toBytesLE()) + ?f.append(startNumber.uint64.toBytesLE()) for v in offsets: - ? f.append(cast[uint64](v - pos).toBytesLE()) + ?f.append(cast[uint64](v - pos).toBytesLE()) - ? f.append(offsets.lenu64().toBytesLE()) + ?f.append(offsets.lenu64().toBytesLE()) ok(pos) @@ -98,47 +100,54 @@ proc appendRecord(f: IoHandle, index: BlockIndex): Result[int64, string] = proc readBlockIndex*(f: IoHandle): Result[BlockIndex, string] = let - startPos = ? f.getFilePos().mapErr(toString) - fileSize = ? f.getFileSize().mapErr(toString) - header = ? f.readHeader() + startPos = ?f.getFilePos().mapErr(toString) + fileSize = ?f.getFileSize().mapErr(toString) + header = ?f.readHeader() - if header.typ != E2BlockIndex: return err("not an index") - if header.len < 16: return err("index entry too small") - if header.len mod 8 != 0: return err("index length invalid") + if header.typ != E2BlockIndex: + return err("not an index") + if header.len < 16: + return err("index entry too small") + if header.len mod 8 != 0: + return err("index length invalid") var buf: array[8, byte] - ? f.readFileExact(buf) + ?f.readFileExact(buf) let blockNumber = uint64.fromBytesLE(buf) count = header.len div 8 - 2 var offsets = newSeqUninitialized[int64](count) - for i in 0.. fileSize: return err("Invalid offset") + if absolute < 0 or absolute > fileSize: + return err("Invalid offset") offsets[i] = absolute - ? f.readFileExact(buf) - if uint64(count) != uint64.fromBytesLE(buf): return err("invalid count") + ?f.readFileExact(buf) + if uint64(count) != uint64.fromBytesLE(buf): + return err("invalid count") # technically not an error, but we'll throw this sanity check in here.. - if blockNumber > int32.high().uint64: return err("fishy block number") + if blockNumber > int32.high().uint64: + return err("fishy block number") ok(BlockIndex(startNumber: blockNumber, offsets: offsets)) proc skipRecord*(f: IoHandle): Result[void, string] = - let header = ? readHeader(f) + let header = ?readHeader(f) if header.len > 0: - ? f.setFilePos(header.len, SeekPosition.SeekCurrent).mapErr(ioErrorMsg) + ?f.setFilePos(header.len, SeekPosition.SeekCurrent).mapErr(ioErrorMsg) ok() @@ -175,51 +184,58 @@ proc fromCompressedRlpBytes(bytes: openArray[byte], T: type): Result[T, string] except RlpError as e: err("Invalid Compressed RLP data" & e.msg) -proc init*( - T: type Era1Group, f: IoHandle, startNumber: uint64 - ): Result[T, string] = - discard ? f.appendHeader(E2Version, 0) +proc init*(T: type Era1Group, f: IoHandle, startNumber: uint64): Result[T, string] = + discard ?f.appendHeader(E2Version, 0) - ok(Era1Group( - blockIndex: BlockIndex( - startNumber: startNumber, - offsets: newSeq[int64](startNumber.offsetsLen()) - ))) + ok( + Era1Group( + blockIndex: BlockIndex( + startNumber: startNumber, offsets: newSeq[int64](startNumber.offsetsLen()) + ) + ) + ) proc update*( - g: var Era1Group, f: IoHandle, blockNumber: uint64, - header, body, receipts, totalDifficulty: openArray[byte] - ): Result[void, string] = + g: var Era1Group, + f: IoHandle, + blockNumber: uint64, + header, body, receipts, totalDifficulty: openArray[byte], +): Result[void, string] = doAssert blockNumber >= g.blockIndex.startNumber g.blockIndex.offsets[int(blockNumber - g.blockIndex.startNumber)] = - ? f.appendRecord(CompressedHeader, header) - discard ? f.appendRecord(CompressedBody, body) - discard ? f.appendRecord(CompressedReceipts, receipts) - discard ? f.appendRecord(TotalDifficulty, totalDifficulty) + ?f.appendRecord(CompressedHeader, header) + discard ?f.appendRecord(CompressedBody, body) + discard ?f.appendRecord(CompressedReceipts, receipts) + discard ?f.appendRecord(TotalDifficulty, totalDifficulty) ok() proc update*( - g: var Era1Group, f: IoHandle, blockNumber: uint64, - header: BlockHeader, body: BlockBody, receipts: seq[Receipt], - totalDifficulty: UInt256 - ): Result[void, string] = + g: var Era1Group, + f: IoHandle, + blockNumber: uint64, + header: BlockHeader, + body: BlockBody, + receipts: seq[Receipt], + totalDifficulty: UInt256, +): Result[void, string] = g.update( - f, blockNumber, + f, + blockNumber, toCompressedRlpBytes(header), toCompressedRlpBytes(body), toCompressedRlpBytes(receipts), - totalDifficulty.toBytesLE() + totalDifficulty.toBytesLE(), ) proc finish*( g: var Era1Group, f: IoHandle, accumulatorRoot: Digest, lastBlockNumber: uint64 - ):Result[void, string] = - let accumulatorRootPos = ? f.appendRecord(AccumulatorRoot, accumulatorRoot.data) +): Result[void, string] = + let accumulatorRootPos = ?f.appendRecord(AccumulatorRoot, accumulatorRoot.data) if lastBlockNumber > 0: - discard ? f.appendRecord(g.blockIndex) + discard ?f.appendRecord(g.blockIndex) # TODO: # This is not something added in current specification of era1. @@ -250,22 +266,21 @@ type tuple[header: BlockHeader, body: BlockBody, receipts: seq[Receipt], td: UInt256] proc open*(_: type Era1File, name: string): Result[Era1File, string] = - var - f = Opt[IoHandle].ok(? openFile(name, {OpenFlags.Read}).mapErr(ioErrorMsg)) + var f = Opt[IoHandle].ok(?openFile(name, {OpenFlags.Read}).mapErr(ioErrorMsg)) defer: - if f.isSome(): discard closeFile(f[]) + if f.isSome(): + discard closeFile(f[]) # Indices can be found at the end of each era file - we only support # single-era files for now - ? f[].setFilePos(0, SeekPosition.SeekEnd).mapErr(ioErrorMsg) + ?f[].setFilePos(0, SeekPosition.SeekEnd).mapErr(ioErrorMsg) # Last in the file is the block index - let - blockIdxPos = ? f[].findIndexStartOffset() - ? f[].setFilePos(blockIdxPos, SeekPosition.SeekCurrent).mapErr(ioErrorMsg) + let blockIdxPos = ?f[].findIndexStartOffset() + ?f[].setFilePos(blockIdxPos, SeekPosition.SeekCurrent).mapErr(ioErrorMsg) - let blockIdx = ? f[].readBlockIndex() + let blockIdx = ?f[].readBlockIndex() if blockIdx.offsets.len() != blockIdx.startNumber.offsetsLen(): return err("Block index length invalid") @@ -286,7 +301,7 @@ proc skipRecord*(f: Era1File): Result[void, string] = proc getBlockHeader(f: Era1File): Result[BlockHeader, string] = var bytes: seq[byte] - let header = ? f[].handle.get().readRecord(bytes) + let header = ?f[].handle.get().readRecord(bytes) if header.typ != CompressedHeader: return err("Invalid era file: didn't find block header at index position") @@ -295,7 +310,7 @@ proc getBlockHeader(f: Era1File): Result[BlockHeader, string] = proc getBlockBody(f: Era1File): Result[BlockBody, string] = var bytes: seq[byte] - let header = ? f[].handle.get().readRecord(bytes) + let header = ?f[].handle.get().readRecord(bytes) if header.typ != CompressedBody: return err("Invalid era file: didn't find block body at index position") @@ -304,7 +319,7 @@ proc getBlockBody(f: Era1File): Result[BlockBody, string] = proc getReceipts(f: Era1File): Result[seq[Receipt], string] = var bytes: seq[byte] - let header = ? f[].handle.get().readRecord(bytes) + let header = ?f[].handle.get().readRecord(bytes) if header.typ != CompressedReceipts: return err("Invalid era file: didn't find receipts at index position") @@ -313,7 +328,7 @@ proc getReceipts(f: Era1File): Result[seq[Receipt], string] = proc getTotalDifficulty(f: Era1File): Result[UInt256, string] = var bytes: seq[byte] - let header = ? f[].handle.get().readRecord(bytes) + let header = ?f[].handle.get().readRecord(bytes) if header.typ != TotalDifficulty: return err("Invalid era file: didn't find total difficulty at index position") @@ -322,61 +337,53 @@ proc getTotalDifficulty(f: Era1File): Result[UInt256, string] = ok(UInt256.fromBytesLE(bytes)) -proc getNextBlockTuple*( - f: Era1File - ): Result[BlockTuple, string] = +proc getNextBlockTuple*(f: Era1File): Result[BlockTuple, string] = doAssert not isNil(f) and f[].handle.isSome let - blockHeader = ? getBlockHeader(f) - blockBody = ? getBlockBody(f) - receipts = ? getReceipts(f) - totalDifficulty = ? getTotalDifficulty(f) + blockHeader = ?getBlockHeader(f) + blockBody = ?getBlockBody(f) + receipts = ?getReceipts(f) + totalDifficulty = ?getTotalDifficulty(f) ok((blockHeader, blockBody, receipts, totalDifficulty)) -proc getBlockTuple*( - f: Era1File, blockNumber: uint64 - ): Result[BlockTuple, string] = +proc getBlockTuple*(f: Era1File, blockNumber: uint64): Result[BlockTuple, string] = doAssert not isNil(f) and f[].handle.isSome doAssert( - blockNumber >= f[].blockIdx.startNumber and - blockNumber <= f[].blockIdx.endNumber, - "Wrong era1 file for selected block number") + blockNumber >= f[].blockIdx.startNumber and blockNumber <= f[].blockIdx.endNumber, + "Wrong era1 file for selected block number", + ) let pos = f[].blockIdx.offsets[blockNumber - f[].blockIdx.startNumber] - ? f[].handle.get().setFilePos(pos, SeekPosition.SeekBegin).mapErr(ioErrorMsg) + ?f[].handle.get().setFilePos(pos, SeekPosition.SeekBegin).mapErr(ioErrorMsg) getNextBlockTuple(f) -proc getBlockHeader*( - f: Era1File, blockNumber: uint64 - ): Result[BlockHeader, string] = +proc getBlockHeader*(f: Era1File, blockNumber: uint64): Result[BlockHeader, string] = doAssert not isNil(f) and f[].handle.isSome doAssert( - blockNumber >= f[].blockIdx.startNumber and - blockNumber <= f[].blockIdx.endNumber, - "Wrong era1 file for selected block number") + blockNumber >= f[].blockIdx.startNumber and blockNumber <= f[].blockIdx.endNumber, + "Wrong era1 file for selected block number", + ) let pos = f[].blockIdx.offsets[blockNumber - f[].blockIdx.startNumber] - ? f[].handle.get().setFilePos(pos, SeekPosition.SeekBegin).mapErr(ioErrorMsg) + ?f[].handle.get().setFilePos(pos, SeekPosition.SeekBegin).mapErr(ioErrorMsg) getBlockHeader(f) -proc getTotalDifficulty*( - f: Era1File, blockNumber: uint64 - ): Result[UInt256, string] = +proc getTotalDifficulty*(f: Era1File, blockNumber: uint64): Result[UInt256, string] = doAssert not isNil(f) and f[].handle.isSome doAssert( - blockNumber >= f[].blockIdx.startNumber and - blockNumber <= f[].blockIdx.endNumber, - "Wrong era1 file for selected block number") + blockNumber >= f[].blockIdx.startNumber and blockNumber <= f[].blockIdx.endNumber, + "Wrong era1 file for selected block number", + ) let pos = f[].blockIdx.offsets[blockNumber - f[].blockIdx.startNumber] - ? f[].handle.get().setFilePos(pos, SeekPosition.SeekBegin).mapErr(ioErrorMsg) + ?f[].handle.get().setFilePos(pos, SeekPosition.SeekBegin).mapErr(ioErrorMsg) ?skipRecord(f) # BlockHeader ?skipRecord(f) # BlockBody @@ -386,15 +393,17 @@ proc getTotalDifficulty*( # TODO: Should we add this perhaps in the Era1File object and grab it in open()? proc getAccumulatorRoot*(f: Era1File): Result[Digest, string] = # Get position of BlockIndex - ? f[].handle.get().setFilePos(0, SeekPosition.SeekEnd).mapErr(ioErrorMsg) - let blockIdxPos = ? f[].handle.get().findIndexStartOffset() + ?f[].handle.get().setFilePos(0, SeekPosition.SeekEnd).mapErr(ioErrorMsg) + let blockIdxPos = ?f[].handle.get().findIndexStartOffset() # Accumulator root is 40 bytes before the BlockIndex let accumulatorRootPos = blockIdxPos - 40 # 8 + 32 - ? f[].handle.get().setFilePos(accumulatorRootPos, SeekPosition.SeekCurrent).mapErr(ioErrorMsg) + ?f[].handle.get().setFilePos(accumulatorRootPos, SeekPosition.SeekCurrent).mapErr( + ioErrorMsg + ) var bytes: seq[byte] - let header = ? f[].handle.get().readRecord(bytes) + let header = ?f[].handle.get().readRecord(bytes) if header.typ != AccumulatorRoot: return err("Invalid era file: didn't find accumulator root at index position") @@ -410,14 +419,14 @@ proc buildAccumulator*(f: Era1File): Result[EpochAccumulatorCached, string] = endNumber = f.blockIdx.endNumber() var headerRecords: seq[HeaderRecord] - for blockNumber in startNumber..endNumber: + for blockNumber in startNumber .. endNumber: let - blockHeader = ? f.getBlockHeader(blockNumber) - totalDifficulty = ? f.getTotalDifficulty(blockNumber) + blockHeader = ?f.getBlockHeader(blockNumber) + totalDifficulty = ?f.getTotalDifficulty(blockNumber) - headerRecords.add(HeaderRecord( - blockHash: blockHeader.blockHash(), - totalDifficulty: totalDifficulty)) + headerRecords.add( + HeaderRecord(blockHash: blockHeader.blockHash(), totalDifficulty: totalDifficulty) + ) ok(EpochAccumulatorCached.init(@headerRecords)) @@ -427,10 +436,10 @@ proc verify*(f: Era1File): Result[Digest, string] = endNumber = f.blockIdx.endNumber() var headerRecords: seq[HeaderRecord] - for blockNumber in startNumber..endNumber: + for blockNumber in startNumber .. endNumber: let (blockHeader, blockBody, receipts, totalDifficulty) = - ? f.getBlockTuple(blockNumber) + ?f.getBlockTuple(blockNumber) txRoot = calcTxRoot(blockBody.transactions) ommershHash = keccakHash(rlp.encode(blockBody.uncles)) @@ -444,11 +453,11 @@ proc verify*(f: Era1File): Result[Digest, string] = if blockHeader.receiptRoot != calcReceiptRoot(receipts): return err("Invalid receipts root") - headerRecords.add(HeaderRecord( - blockHash: blockHeader.blockHash(), - totalDifficulty: totalDifficulty)) + headerRecords.add( + HeaderRecord(blockHash: blockHeader.blockHash(), totalDifficulty: totalDifficulty) + ) - let expectedRoot = ? f.getAccumulatorRoot() + let expectedRoot = ?f.getAccumulatorRoot() let accumulatorRoot = getEpochAccumulatorRoot(headerRecords) if accumulatorRoot != expectedRoot: @@ -461,7 +470,7 @@ iterator era1BlockHeaders*(f: Era1File): BlockHeader = startNumber = f.blockIdx.startNumber endNumber = f.blockIdx.endNumber() - for blockNumber in startNumber..endNumber: + for blockNumber in startNumber .. endNumber: let header = f.getBlockHeader(blockNumber).valueOr: raiseAssert("Failed to read block header") yield header @@ -471,7 +480,7 @@ iterator era1BlockTuples*(f: Era1File): BlockTuple = startNumber = f.blockIdx.startNumber endNumber = f.blockIdx.endNumber() - for blockNumber in startNumber..endNumber: + for blockNumber in startNumber .. endNumber: let blockTuple = f.getBlockTuple(blockNumber).valueOr: raiseAssert("Failed to read block header") yield blockTuple diff --git a/fluffy/eth_data/history_data_json_store.nim b/fluffy/eth_data/history_data_json_store.nim index 3c88f7304f..c6130088a5 100644 --- a/fluffy/eth_data/history_data_json_store.nim +++ b/fluffy/eth_data/history_data_json_store.nim @@ -1,5 +1,5 @@ # Nimbus - Portal Network -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -8,8 +8,10 @@ {.push raises: [].} import - json_serialization, json_serialization/std/tables, - stew/[byteutils, io2, results], chronicles, + json_serialization, + json_serialization/std/tables, + stew/[byteutils, io2, results], + chronicles, eth/[rlp, common/eth_types], ../../nimbus/common/[chain_config, genesis], ../network/history/[history_content, accumulator] @@ -45,19 +47,17 @@ iterator blockHashes*(blockData: BlockDataTable): BlockHash = yield blockHash func readBlockData*( - hash: string, blockData: BlockData, verify = false): - Result[seq[(ContentKey, seq[byte])], string] = + hash: string, blockData: BlockData, verify = false +): Result[seq[(ContentKey, seq[byte])], string] = var res: seq[(ContentKey, seq[byte])] var blockHash: BlockHash try: blockHash.data = hexToByteArray[sizeof(BlockHash)](hash) except ValueError as e: - return err("Invalid hex for blockhash, number " & - $blockData.number & ": " & e.msg) + return err("Invalid hex for blockhash, number " & $blockData.number & ": " & e.msg) - let contentKeyType = - BlockKey(blockHash: blockHash) + let contentKeyType = BlockKey(blockHash: blockHash) try: # If wanted the hash for the corresponding header can be verified @@ -66,33 +66,28 @@ func readBlockData*( return err("Data is not matching hash, number " & $blockData.number) block: - let contentKey = ContentKey( - contentType: blockHeader, - blockHeaderKey: contentKeyType) + let contentKey = + ContentKey(contentType: blockHeader, blockHeaderKey: contentKeyType) res.add((contentKey, blockData.header.hexToSeqByte())) block: - let contentKey = ContentKey( - contentType: blockBody, - blockBodyKey: contentKeyType) + let contentKey = ContentKey(contentType: blockBody, blockBodyKey: contentKeyType) res.add((contentKey, blockData.body.hexToSeqByte())) block: - let contentKey = ContentKey( - contentType: receipts, - receiptsKey: contentKeyType) + let contentKey = ContentKey(contentType: receipts, receiptsKey: contentKeyType) res.add((contentKey, blockData.receipts.hexToSeqByte())) - except ValueError as e: return err("Invalid hex data, number " & $blockData.number & ": " & e.msg) ok(res) iterator blocks*( - blockData: BlockDataTable, verify = false): seq[(ContentKey, seq[byte])] = + blockData: BlockDataTable, verify = false +): seq[(ContentKey, seq[byte])] = for k, v in blockData: let res = readBlockData(k, v, verify) @@ -102,7 +97,8 @@ iterator blocks*( error "Failed reading block from block data", error = res.error iterator blocksContent*( - blockData: BlockDataTable, verify = false): (ContentId, seq[byte], seq[byte]) = + blockData: BlockDataTable, verify = false +): (ContentId, seq[byte], seq[byte]) = for b in blocks(blockData, verify): for value in b: if len(value[1]) > 0: @@ -115,8 +111,9 @@ func readBlockHeader*(blockData: BlockData): Result[BlockHeader, string] = try: rlpFromHex(blockData.header) except ValueError as e: - return err("Invalid hex for rlp block data, number " & - $blockData.number & ": " & e.msg) + return err( + "Invalid hex for rlp block data, number " & $blockData.number & ": " & e.msg + ) try: return ok(rlp.read(BlockHeader)) @@ -124,17 +121,15 @@ func readBlockHeader*(blockData: BlockData): Result[BlockHeader, string] = return err("Invalid header, number " & $blockData.number & ": " & e.msg) func readHeaderData*( - hash: string, blockData: BlockData, verify = false): - Result[(ContentKey, seq[byte]), string] = + hash: string, blockData: BlockData, verify = false +): Result[(ContentKey, seq[byte]), string] = var blockHash: BlockHash try: blockHash.data = hexToByteArray[sizeof(BlockHash)](hash) except ValueError as e: - return err("Invalid hex for blockhash, number " & - $blockData.number & ": " & e.msg) + return err("Invalid hex for blockhash, number " & $blockData.number & ": " & e.msg) - let contentKeyType = - BlockKey(blockHash: blockHash) + let contentKeyType = BlockKey(blockHash: blockHash) try: # If wanted the hash for the corresponding header can be verified @@ -142,18 +137,15 @@ func readHeaderData*( if keccakHash(blockData.header.hexToSeqByte()) != blockHash: return err("Data is not matching hash, number " & $blockData.number) - let contentKey = ContentKey( - contentType: blockHeader, - blockHeaderKey: contentKeyType) + let contentKey = + ContentKey(contentType: blockHeader, blockHeaderKey: contentKeyType) let res = (contentKey, blockData.header.hexToSeqByte()) return ok(res) - except ValueError as e: return err("Invalid hex data, number " & $blockData.number & ": " & e.msg) -iterator headers*( - blockData: BlockDataTable, verify = false): (ContentKey, seq[byte]) = +iterator headers*(blockData: BlockDataTable, verify = false): (ContentKey, seq[byte]) = for k, v in blockData: let res = readHeaderData(k, v, verify) @@ -184,11 +176,13 @@ type JsonPortalContentTable* = OrderedTable[string, JsonPortalContent] proc toString(v: IoErrorCode): string = - try: ioErrorMsg(v) - except Exception as e: raiseAssert e.msg + try: + ioErrorMsg(v) + except Exception as e: + raiseAssert e.msg proc readJsonType*(dataFile: string, T: type): Result[T, string] = - let data = ? readAllFile(dataFile).mapErr(toString) + let data = ?readAllFile(dataFile).mapErr(toString) let decoded = try: @@ -212,27 +206,27 @@ type number: uint64 proc writeHeaderRecord*( - writer: var JsonWriter, header: BlockHeader) - {.raises: [IOError].} = + writer: var JsonWriter, header: BlockHeader +) {.raises: [IOError].} = let dataRecord = HeaderRecord( - header: rlp.encode(header).to0xHex(), - number: header.blockNumber.truncate(uint64)) + header: rlp.encode(header).to0xHex(), number: header.blockNumber.truncate(uint64) + ) headerHash = to0xHex(rlpHash(header).data) writer.writeField(headerHash, dataRecord) proc writeBlockRecord*( - writer: var JsonWriter, - header: BlockHeader, body: BlockBody, receipts: seq[Receipt]) - {.raises: [IOError].} = + writer: var JsonWriter, header: BlockHeader, body: BlockBody, receipts: seq[Receipt] +) {.raises: [IOError].} = let dataRecord = BlockRecord( header: rlp.encode(header).to0xHex(), body: encode(body).to0xHex(), receipts: encode(receipts).to0xHex(), - number: header.blockNumber.truncate(uint64)) + number: header.blockNumber.truncate(uint64), + ) headerHash = to0xHex(rlpHash(header).data) diff --git a/fluffy/eth_data/history_data_seeding.nim b/fluffy/eth_data/history_data_seeding.nim index 1012b77fee..6c6f1b29d4 100644 --- a/fluffy/eth_data/history_data_seeding.nim +++ b/fluffy/eth_data/history_data_seeding.nim @@ -9,8 +9,11 @@ import std/[strformat, os], - results, chronos, chronicles, - eth/common/eth_types, eth/rlp, + results, + chronos, + chronicles, + eth/common/eth_types, + eth/rlp, ../network/wire/portal_protocol, ../network/history/[history_content, history_network, accumulator], "."/[era1, history_data_json_store, history_data_ssz_e2s] @@ -20,9 +23,9 @@ export results ### Helper calls to seed the local database and/or the network proc historyStore*( - p: PortalProtocol, dataFile: string, verify = false): - Result[void, string] = - let blockData = ? readJsonType(dataFile, BlockDataTable) + p: PortalProtocol, dataFile: string, verify = false +): Result[void, string] = + let blockData = ?readJsonType(dataFile, BlockDataTable) for b in blocks(blockData, verify): for value in b: @@ -33,8 +36,8 @@ proc historyStore*( ok() proc propagateEpochAccumulator*( - p: PortalProtocol, file: string): - Future[Result[void, string]] {.async.} = + p: PortalProtocol, file: string +): Future[Result[void, string]] {.async.} = ## Propagate a specific epoch accumulator into the network. ## file holds the SSZ serialized epoch accumulator. let epochAccumulatorRes = readEpochAccumulator(file) @@ -46,34 +49,33 @@ proc propagateEpochAccumulator*( rootHash = accumulator.hash_tree_root() key = ContentKey( contentType: epochAccumulator, - epochAccumulatorKey: EpochAccumulatorKey( - epochHash: rootHash)) + epochAccumulatorKey: EpochAccumulatorKey(epochHash: rootHash), + ) encKey = history_content.encode(key) # Note: The file actually holds the SSZ encoded accumulator, but we need # to decode as we need the root for the content key. encodedAccumulator = SSZ.encode(accumulator) info "Gossiping epoch accumulator", rootHash, contentKey = encKey - p.storeContent( - encKey, - history_content.toContentId(encKey), - encodedAccumulator - ) + p.storeContent(encKey, history_content.toContentId(encKey), encodedAccumulator) discard await p.neighborhoodGossip( - Opt.none(NodeId), ContentKeysList(@[encKey]), @[encodedAccumulator]) + Opt.none(NodeId), ContentKeysList(@[encKey]), @[encodedAccumulator] + ) return ok() proc propagateEpochAccumulators*( - p: PortalProtocol, path: string): - Future[Result[void, string]] {.async.} = + p: PortalProtocol, path: string +): Future[Result[void, string]] {.async.} = ## Propagate all epoch accumulators created when building the accumulator ## from the block headers. ## path is a directory that holds all SSZ encoded epoch accumulator files. - for i in 0.. LightClientDataFork.None: - info "New LC finalized header", - finalized_header = shortLog(forkyHeader) + info "New LC finalized header", finalized_header = shortLog(forkyHeader) proc onOptimisticHeader( - lightClient: LightClient, optimisticHeader: ForkedLightClientHeader) = + lightClient: LightClient, optimisticHeader: ForkedLightClientHeader +) = withForkyHeader(optimisticHeader): when lcDataFork > LightClientDataFork.None: - info "New LC optimistic header", - optimistic_header = shortLog(forkyHeader) + info "New LC optimistic header", optimistic_header = shortLog(forkyHeader) proc run(config: PortalConf) {.raises: [CatchableError].} = setupLogging(config.logLevel, config.logStdout) - notice "Launching Fluffy", - version = fullVersionStr, cmdParams = commandLineParams() + notice "Launching Fluffy", version = fullVersionStr, cmdParams = commandLineParams() # Make sure dataDir exists let pathExists = createPath(config.dataDir.string) if pathExists.isErr(): - fatal "Failed to create data directory", dataDir = config.dataDir, - error = pathExists.error + fatal "Failed to create data directory", + dataDir = config.dataDir, error = pathExists.error quit 1 let @@ -69,11 +77,12 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = udpPort = Port(config.udpPort) # TODO: allow for no TCP port mapping! (extIp, _, extUdpPort) = - try: setupAddress(config.nat, - config.listenAddress, udpPort, udpPort, "fluffy") - except CatchableError as exc: raise exc - # TODO: Ideally we don't have the Exception here - except Exception as exc: raiseAssert exc.msg + try: + setupAddress(config.nat, config.listenAddress, udpPort, udpPort, "fluffy") + except CatchableError as exc: + raise exc # TODO: Ideally we don't have the Exception here + except Exception as exc: + raiseAssert exc.msg (netkey, newNetKey) = if config.networkKey.isSome(): (config.networkKey.get(), true) @@ -101,34 +110,42 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = discard let - discoveryConfig = DiscoveryConfig.init( - config.tableIpLimit, config.bucketIpLimit, config.bitsPerHop) + discoveryConfig = + DiscoveryConfig.init(config.tableIpLimit, config.bucketIpLimit, config.bitsPerHop) d = newProtocol( netkey, - extIp, none(Port), extUdpPort, + extIp, + none(Port), + extUdpPort, # Note: The addition of default clientInfo to the ENR is a temporary # measure to easily identify & debug the clients used in the testnet. # Might make this into a, default off, cli option. localEnrFields = {"c": enrClientInfoShort}, bootstrapRecords = bootstrapRecords, - previousRecord = # TODO: discv5/enr code still uses Option, to be changed. + previousRecord = + # TODO: discv5/enr code still uses Option, to be changed. if previousEnr.isSome(): some(previousEnr.get()) else: - none(enr.Record), - bindIp = bindIp, bindPort = udpPort, + none(enr.Record) + , + bindIp = bindIp, + bindPort = udpPort, enrAutoUpdate = config.enrAutoUpdate, config = discoveryConfig, - rng = rng) + rng = rng, + ) d.open() # Force pruning if config.forcePrune: - let db = ContentDB.new(config.dataDir / "db" / "contentdb_" & - d.localNode.id.toBytesBE().toOpenArray(0, 8).toHex(), + let db = ContentDB.new( + config.dataDir / "db" / "contentdb_" & + d.localNode.id.toBytesBE().toOpenArray(0, 8).toHex(), storageCapacity = config.storageCapacityMB * 1_000_000, - manualCheckpoint = true) + manualCheckpoint = true, + ) let radius = if config.radiusConfig.kind == Static: @@ -153,25 +170,29 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = # This is done because the content in the db is dependant on the `NodeId` and # the selected `Radius`. let - db = ContentDB.new(config.dataDir / "db" / "contentdb_" & - d.localNode.id.toBytesBE().toOpenArray(0, 8).toHex(), - storageCapacity = config.storageCapacityMB * 1_000_000) + db = ContentDB.new( + config.dataDir / "db" / "contentdb_" & + d.localNode.id.toBytesBE().toOpenArray(0, 8).toHex(), + storageCapacity = config.storageCapacityMB * 1_000_000, + ) portalConfig = PortalProtocolConfig.init( - config.tableIpLimit, - config.bucketIpLimit, - config.bitsPerHop, - config.radiusConfig, - config.disablePoke + config.tableIpLimit, config.bucketIpLimit, config.bitsPerHop, config.radiusConfig, + config.disablePoke, ) streamManager = StreamManager.new(d) stateNetwork = if config.stateNetworkEnabled: - Opt.some(StateNetwork.new( - d, db, streamManager, + Opt.some( + StateNetwork.new( + d, + db, + streamManager, bootstrapRecords = bootstrapRecords, - portalConfig = portalConfig)) + portalConfig = portalConfig, + ) + ) else: Opt.none(StateNetwork) @@ -183,7 +204,8 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = # - Start with file containing SSZ encoded accumulator if config.accumulatorFile.isSome(): readAccumulator(string config.accumulatorFile.get()).expect( - "Need a file with a valid SSZ encoded accumulator") + "Need a file with a valid SSZ encoded accumulator" + ) else: # Get it from binary file containing SSZ encoded accumulator try: @@ -191,10 +213,16 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = except SszError as err: raiseAssert "Invalid baked-in accumulator: " & err.msg - historyNetwork = Opt.some(HistoryNetwork.new( - d, db, streamManager, accumulator, - bootstrapRecords = bootstrapRecords, - portalConfig = portalConfig)) + historyNetwork = Opt.some( + HistoryNetwork.new( + d, + db, + streamManager, + accumulator, + bootstrapRecords = bootstrapRecords, + portalConfig = portalConfig, + ) + ) beaconLightClient = # TODO: Currently disabled by default as it is not sufficiently polished. @@ -203,19 +231,19 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = let # Portal works only over mainnet data currently networkData = loadNetworkData("mainnet") - beaconDb = BeaconDb.new( - networkData, config.dataDir / "db" / "beacon_db") + beaconDb = BeaconDb.new(networkData, config.dataDir / "db" / "beacon_db") beaconNetwork = BeaconNetwork.new( d, beaconDb, streamManager, networkData.forks, bootstrapRecords = bootstrapRecords, - portalConfig = portalConfig) + portalConfig = portalConfig, + ) let beaconLightClient = LightClient.new( - beaconNetwork, rng, networkData, - LightClientFinalizationMode.Optimistic) + beaconNetwork, rng, networkData, LightClientFinalizationMode.Optimistic + ) beaconLightClient.onFinalizedHeader = onFinalizedHeader beaconLightClient.onOptimisticHeader = onOptimisticHeader @@ -238,7 +266,6 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = fatal "Failed to write the enr file", file = enrFile quit 1 - ## Start metrics HTTP server if config.metricsEnabled: let @@ -248,9 +275,11 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = url = "http://" & $address & ":" & $port & "/metrics" try: chronos_httpserver.startMetricsHttpServer($address, port) - except CatchableError as exc: raise exc + except CatchableError as exc: + raise exc # TODO: Ideally we don't have the Exception here - except Exception as exc: raiseAssert exc.msg + except Exception as exc: + raiseAssert exc.msg ## Starting the different networks. d.start() @@ -294,17 +323,22 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = rpcHttpServerWithProxy.installWeb3ApiHandlers() if stateNetwork.isSome(): rpcHttpServerWithProxy.installPortalApiHandlers( - stateNetwork.get().portalProtocol, "state") + stateNetwork.get().portalProtocol, "state" + ) if historyNetwork.isSome(): rpcHttpServerWithProxy.installEthApiHandlers( - historyNetwork.get(), beaconLightClient) + historyNetwork.get(), beaconLightClient + ) rpcHttpServerWithProxy.installPortalApiHandlers( - historyNetwork.get().portalProtocol, "history") + historyNetwork.get().portalProtocol, "history" + ) rpcHttpServerWithProxy.installPortalDebugApiHandlers( - historyNetwork.get().portalProtocol, "history") + historyNetwork.get().portalProtocol, "history" + ) if beaconLightClient.isSome(): rpcHttpServerWithProxy.installPortalApiHandlers( - beaconLightClient.get().network.portalProtocol, "beacon") + beaconLightClient.get().network.portalProtocol, "beacon" + ) # TODO: Test proxy with remote node over HTTPS waitFor rpcHttpServerWithProxy.start() @@ -314,7 +348,7 @@ when isMainModule: {.pop.} let config = PortalConf.load( version = clientName & " " & fullVersionStr & "\p\p" & nimBanner, - copyrightBanner = copyrightBanner + copyrightBanner = copyrightBanner, ) {.push raises: [].} diff --git a/fluffy/logging.nim b/fluffy/logging.nim index 2fd45eeb2f..822b0a197f 100644 --- a/fluffy/logging.nim +++ b/fluffy/logging.nim @@ -1,5 +1,5 @@ # Nimbus Fluffy -# Copyright (c) 2023 Status Research & Development GmbH +# Copyright (c) 2023-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -14,18 +14,19 @@ import std/[strutils, tables, terminal, typetraits], - pkg/chronicles, pkg/chronicles/helpers, chronicles/topics_registry, + pkg/chronicles, + pkg/chronicles/helpers, + chronicles/topics_registry, pkg/stew/results export results -type - StdoutLogKind* {.pure.} = enum - Auto = "auto" - Colors = "colors" - NoColors = "nocolors" - Json = "json" - None = "none" +type StdoutLogKind* {.pure.} = enum + Auto = "auto" + Colors = "colors" + NoColors = "nocolors" + Json = "json" + None = "none" # silly chronicles, colors is a compile-time property proc stripAnsi(v: string): string = @@ -46,7 +47,7 @@ proc stripAnsi(v: string): string = if c2 != '[': break else: - if c2 in {'0'..'9'} + {';'}: + if c2 in {'0' .. '9'} + {';'}: discard # keep looking elif c2 == 'm': i = x + 1 @@ -69,10 +70,12 @@ proc updateLogLevel(logLevel: string) {.raises: [ValueError].} = try: setLogLevel(parseEnum[LogLevel](directives[0].capitalizeAscii())) except ValueError: - raise (ref ValueError)(msg: "Please specify one of TRACE, DEBUG, INFO, NOTICE, WARN, ERROR or FATAL") + raise (ref ValueError)( + msg: "Please specify one of TRACE, DEBUG, INFO, NOTICE, WARN, ERROR or FATAL" + ) if directives.len > 1: - for topicName, settings in parseTopicDirectives(directives[1..^1]): + for topicName, settings in parseTopicDirectives(directives[1 ..^ 1]): if not setTopicState(topicName, settings.state, settings.logLevel): warn "Unrecognized logging topic", topic = topicName @@ -89,8 +92,7 @@ proc detectTTY(stdoutKind: StdoutLogKind): StdoutLogKind = else: stdoutKind -proc setupLogging*( - logLevel: string, stdoutKind: StdoutLogKind) = +proc setupLogging*(logLevel: string, stdoutKind: StdoutLogKind) = # In the cfg file for fluffy, we create two formats: textlines and json. # Here, we either write those logs to an output, or not, depending on the # given configuration. @@ -101,7 +103,9 @@ proc setupLogging*( else: # Naive approach where chronicles will form a string and we will discard # it, even if it could have skipped the formatting phase - proc noOutput(logLevel: LogLevel, msg: LogOutputStr) = discard + proc noOutput(logLevel: LogLevel, msg: LogOutputStr) = + discard + proc writeAndFlush(f: File, msg: LogOutputStr) = try: f.write(msg) @@ -120,7 +124,8 @@ proc setupLogging*( let tmp = detectTTY(stdoutKind) case tmp - of StdoutLogKind.Auto: raiseAssert "checked in detectTTY" + of StdoutLogKind.Auto: + raiseAssert "checked in detectTTY" of StdoutLogKind.Colors: defaultChroniclesStream.outputs[0].writer = stdoutFlush of StdoutLogKind.NoColors: @@ -129,12 +134,13 @@ proc setupLogging*( defaultChroniclesStream.outputs[0].writer = noOutput let prevWriter = defaultChroniclesStream.outputs[1].writer - defaultChroniclesStream.outputs[1].writer = - proc(logLevel: LogLevel, msg: LogOutputStr) = - stdoutFlush(logLevel, msg) - prevWriter(logLevel, msg) + defaultChroniclesStream.outputs[1].writer = proc( + logLevel: LogLevel, msg: LogOutputStr + ) = + stdoutFlush(logLevel, msg) + prevWriter(logLevel, msg) of StdoutLogKind.None: - defaultChroniclesStream.outputs[0].writer = noOutput + defaultChroniclesStream.outputs[0].writer = noOutput try: updateLogLevel(logLevel) diff --git a/fluffy/network/beacon/beacon_chain_historical_summaries.nim b/fluffy/network/beacon/beacon_chain_historical_summaries.nim index bd5b305dcc..e81def180f 100644 --- a/fluffy/network/beacon/beacon_chain_historical_summaries.nim +++ b/fluffy/network/beacon/beacon_chain_historical_summaries.nim @@ -14,10 +14,7 @@ {.push raises: [].} -import - stew/results, - beacon_chain/spec/forks, - beacon_chain/spec/datatypes/capella +import stew/results, beacon_chain/spec/forks, beacon_chain/spec/datatypes/capella export results @@ -30,19 +27,21 @@ type proof*: HistoricalSummariesProof func buildProof*( - state: ForkedHashedBeaconState): Result[HistoricalSummariesProof, string] = + state: ForkedHashedBeaconState +): Result[HistoricalSummariesProof, string] = let gIndex = GeneralizedIndex(59) # 31 + 28 = 59 var proof: HistoricalSummariesProof withState(state): - ? forkyState.data.build_proof(gIndex, proof) + ?forkyState.data.build_proof(gIndex, proof) ok(proof) func verifyProof*( historical_summaries: HistoricalSummaries, proof: HistoricalSummariesProof, - stateRoot: Digest): bool = + stateRoot: Digest, +): bool = let gIndex = GeneralizedIndex(59) leave = hash_tree_root(historical_summaries) @@ -50,7 +49,8 @@ func verifyProof*( verify_merkle_multiproof(@[leave], proof, @[gIndex], stateRoot) func verifyProof*( - summariesWithProof: HistoricalSummariesWithProof, - stateRoot: Digest): bool = + summariesWithProof: HistoricalSummariesWithProof, stateRoot: Digest +): bool = verifyProof( - summariesWithProof.historical_summaries, summariesWithProof.proof, stateRoot) + summariesWithProof.historical_summaries, summariesWithProof.proof, stateRoot + ) diff --git a/fluffy/network/beacon/beacon_content.nim b/fluffy/network/beacon/beacon_content.nim index 5402fcdf2f..b9a830fcea 100644 --- a/fluffy/network/beacon/beacon_content.nim +++ b/fluffy/network/beacon/beacon_content.nim @@ -89,15 +89,15 @@ type List[ForkedLightClientUpdate, MAX_REQUEST_LIGHT_CLIENT_UPDATES] func forkDigestAtEpoch*( - forkDigests: ForkDigests, epoch: Epoch, cfg: RuntimeConfig): ForkDigest = + forkDigests: ForkDigests, epoch: Epoch, cfg: RuntimeConfig +): ForkDigest = forkDigests.atEpoch(epoch, cfg) func encode*(contentKey: ContentKey): ByteList = doAssert(contentKey.contentType != unused) ByteList.init(SSZ.encode(contentKey)) -proc readSszBytes*( - data: openArray[byte], val: var ContentKey) {.raises: [SszError].} = +proc readSszBytes*(data: openArray[byte], val: var ContentKey) {.raises: [SszError].} = mixin readSszValue if data.len() > 0 and data[0] == ord(unused): raise newException(MalformedSszError, "SSZ selector unused value") @@ -128,8 +128,8 @@ func toContentId*(contentKey: ContentKey): ContentId = # Not something we would like to include as a parameter here, so we stick with # just passing the forkDigest and doing the work outside of this encode call. func encodeForkedLightClientObject*( - obj: SomeForkedLightClientObject, - forkDigest: ForkDigest): seq[byte] = + obj: SomeForkedLightClientObject, forkDigest: ForkDigest +): seq[byte] = withForkyObject(obj): when lcDataFork > LightClientDataFork.None: var res: seq[byte] @@ -141,35 +141,36 @@ func encodeForkedLightClientObject*( raiseAssert("No light client objects before Altair") func encodeBootstrapForked*( - forkDigest: ForkDigest, - bootstrap: ForkedLightClientBootstrap): seq[byte] = + forkDigest: ForkDigest, bootstrap: ForkedLightClientBootstrap +): seq[byte] = encodeForkedLightClientObject(bootstrap, forkDigest) func encodeFinalityUpdateForked*( - forkDigest: ForkDigest, - finalityUpdate: ForkedLightClientFinalityUpdate): seq[byte] = + forkDigest: ForkDigest, finalityUpdate: ForkedLightClientFinalityUpdate +): seq[byte] = encodeForkedLightClientObject(finalityUpdate, forkDigest) func encodeOptimisticUpdateForked*( - forkDigest: ForkDigest, - optimisticUpdate: ForkedLightClientOptimisticUpdate): seq[byte] = + forkDigest: ForkDigest, optimisticUpdate: ForkedLightClientOptimisticUpdate +): seq[byte] = encodeForkedLightClientObject(optimisticUpdate, forkDigest) func encodeLightClientUpdatesForked*( - forkDigest: ForkDigest, - updates: openArray[ForkedLightClientUpdate]): seq[byte] = + forkDigest: ForkDigest, updates: openArray[ForkedLightClientUpdate] +): seq[byte] = var list: ForkedLightClientUpdateBytesList for update in updates: discard list.add( - ForkedLightClientUpdateBytes( - encodeForkedLightClientObject(update, forkDigest))) + ForkedLightClientUpdateBytes(encodeForkedLightClientObject(update, forkDigest)) + ) SSZ.encode(list) func decodeForkedLightClientObject( ObjType: type SomeForkedLightClientObject, forkDigests: ForkDigests, - data: openArray[byte]): Result[ObjType, string] = + data: openArray[byte], +): Result[ObjType, string] = if len(data) < 4: return Result[ObjType, string].err("Not enough data for forkDigest") @@ -180,8 +181,7 @@ func decodeForkedLightClientObject( withLcDataFork(lcDataForkAtConsensusFork(contextFork)): when lcDataFork > LightClientDataFork.None: - let res = decodeSsz( - data.toOpenArray(4, len(data) - 1), ObjType.Forky(lcDataFork)) + let res = decodeSsz(data.toOpenArray(4, len(data) - 1), ObjType.Forky(lcDataFork)) if res.isOk: # TODO: # How can we verify the Epoch vs fork, e.g. with `consensusForkAtEpoch`? @@ -195,51 +195,33 @@ func decodeForkedLightClientObject( Result[ObjType, string].err("Invalid Fork") func decodeLightClientBootstrapForked*( - forkDigests: ForkDigests, - data: openArray[byte]): Result[ForkedLightClientBootstrap, string] = - decodeForkedLightClientObject( - ForkedLightClientBootstrap, - forkDigests, - data - ) + forkDigests: ForkDigests, data: openArray[byte] +): Result[ForkedLightClientBootstrap, string] = + decodeForkedLightClientObject(ForkedLightClientBootstrap, forkDigests, data) func decodeLightClientUpdateForked*( - forkDigests: ForkDigests, - data: openArray[byte]): Result[ForkedLightClientUpdate, string] = - decodeForkedLightClientObject( - ForkedLightClientUpdate, - forkDigests, - data - ) + forkDigests: ForkDigests, data: openArray[byte] +): Result[ForkedLightClientUpdate, string] = + decodeForkedLightClientObject(ForkedLightClientUpdate, forkDigests, data) func decodeLightClientFinalityUpdateForked*( - forkDigests: ForkDigests, - data: openArray[byte]): Result[ForkedLightClientFinalityUpdate, string] = - decodeForkedLightClientObject( - ForkedLightClientFinalityUpdate, - forkDigests, - data - ) + forkDigests: ForkDigests, data: openArray[byte] +): Result[ForkedLightClientFinalityUpdate, string] = + decodeForkedLightClientObject(ForkedLightClientFinalityUpdate, forkDigests, data) func decodeLightClientOptimisticUpdateForked*( - forkDigests: ForkDigests, - data: openArray[byte]): Result[ForkedLightClientOptimisticUpdate, string] = - decodeForkedLightClientObject( - ForkedLightClientOptimisticUpdate, - forkDigests, - data - ) + forkDigests: ForkDigests, data: openArray[byte] +): Result[ForkedLightClientOptimisticUpdate, string] = + decodeForkedLightClientObject(ForkedLightClientOptimisticUpdate, forkDigests, data) func decodeLightClientUpdatesByRange*( - forkDigests: ForkDigests, - data: openArray[byte]): - Result[ForkedLightClientUpdateList, string] = - let list = ? decodeSsz(data, ForkedLightClientUpdateBytesList) + forkDigests: ForkDigests, data: openArray[byte] +): Result[ForkedLightClientUpdateList, string] = + let list = ?decodeSsz(data, ForkedLightClientUpdateBytesList) var res: ForkedLightClientUpdateList for encodedUpdate in list: - let update = ? decodeLightClientUpdateForked( - forkDigests, encodedUpdate.asSeq()) + let update = ?decodeLightClientUpdateForked(forkDigests, encodedUpdate.asSeq()) discard res.add(update) ok(res) @@ -247,34 +229,28 @@ func decodeLightClientUpdatesByRange*( func bootstrapContentKey*(blockHash: Digest): ContentKey = ContentKey( contentType: lightClientBootstrap, - lightClientBootstrapKey: LightClientBootstrapKey(blockHash: blockHash) + lightClientBootstrapKey: LightClientBootstrapKey(blockHash: blockHash), ) func updateContentKey*(startPeriod: uint64, count: uint64): ContentKey = ContentKey( contentType: lightClientUpdate, - lightClientUpdateKey: LightClientUpdateKey( - startPeriod: startPeriod, count: count) + lightClientUpdateKey: LightClientUpdateKey(startPeriod: startPeriod, count: count), ) func finalityUpdateContentKey*(finalizedSlot: uint64): ContentKey = ContentKey( contentType: lightClientFinalityUpdate, - lightClientFinalityUpdateKey: LightClientFinalityUpdateKey( - finalizedSlot: finalizedSlot - ) + lightClientFinalityUpdateKey: + LightClientFinalityUpdateKey(finalizedSlot: finalizedSlot), ) func optimisticUpdateContentKey*(optimisticSlot: uint64): ContentKey = ContentKey( contentType: lightClientOptimisticUpdate, - lightClientOptimisticUpdateKey: LightClientOptimisticUpdateKey( - optimisticSlot: optimisticSlot - ) + lightClientOptimisticUpdateKey: + LightClientOptimisticUpdateKey(optimisticSlot: optimisticSlot), ) func historicalSummariesContentKey*(): ContentKey = - ContentKey( - contentType: historicalSummaries, - historicalSummariesKey: 0 - ) + ContentKey(contentType: historicalSummaries, historicalSummariesKey: 0) diff --git a/fluffy/network/beacon/beacon_db.nim b/fluffy/network/beacon/beacon_db.nim index 71526d0cc6..3eed43bd94 100644 --- a/fluffy/network/beacon/beacon_db.nim +++ b/fluffy/network/beacon/beacon_db.nim @@ -50,6 +50,7 @@ type LightClientFinalityUpdateCache = object lastFinalityUpdate: seq[byte] lastFinalityUpdateSlot: uint64 + LightClientOptimisticUpdateCache = object lastOptimisticUpdate: seq[byte] lastOptimisticUpdateSlot: uint64 @@ -65,41 +66,73 @@ template disposeSafe(s: untyped): untyped = s = typeof(s)(nil) proc initBestUpdatesStore( - backend: SqStoreRef, - name: string): KvResult[BestLightClientUpdateStore] = - ? backend.exec(""" - CREATE TABLE IF NOT EXISTS `""" & name & """` ( + backend: SqStoreRef, name: string +): KvResult[BestLightClientUpdateStore] = + ?backend.exec( + """ + CREATE TABLE IF NOT EXISTS `""" & name & + """` ( `period` INTEGER PRIMARY KEY, -- `SyncCommitteePeriod` `update` BLOB -- `altair.LightClientUpdate` (SSZ) ); - """) + """ + ) let - getStmt = backend.prepareStmt(""" + getStmt = backend + .prepareStmt( + """ SELECT `update` - FROM `""" & name & """` + FROM `""" & name & + """` WHERE `period` = ?; - """, int64, seq[byte], managed = false).expect("SQL query OK") - getBulkStmt = backend.prepareStmt(""" + """, + int64, + seq[byte], + managed = false, + ) + .expect("SQL query OK") + getBulkStmt = backend + .prepareStmt( + """ SELECT `update` - FROM `""" & name & """` + FROM `""" & name & + """` WHERE `period` >= ? AND `period` < ?; - """, (int64, int64), seq[byte], managed = false).expect("SQL query OK") - putStmt = backend.prepareStmt(""" - REPLACE INTO `""" & name & """` ( + """, + (int64, int64), + seq[byte], + managed = false, + ) + .expect("SQL query OK") + putStmt = backend + .prepareStmt( + """ + REPLACE INTO `""" & name & + """` ( `period`, `update` ) VALUES (?, ?); - """, (int64, seq[byte]), void, managed = false).expect("SQL query OK") - delStmt = backend.prepareStmt(""" - DELETE FROM `""" & name & """` + """, + (int64, seq[byte]), + void, + managed = false, + ) + .expect("SQL query OK") + delStmt = backend + .prepareStmt( + """ + DELETE FROM `""" & name & + """` WHERE `period` = ?; - """, int64, void, managed = false).expect("SQL query OK") + """, + int64, + void, + managed = false, + ) + .expect("SQL query OK") ok BestLightClientUpdateStore( - getStmt: getStmt, - getBulkStmt: getBulkStmt, - putStmt: putStmt, - delStmt: delStmt + getStmt: getStmt, getBulkStmt: getBulkStmt, putStmt: putStmt, delStmt: delStmt ) func close*(store: var BestLightClientUpdateStore) = @@ -109,14 +142,14 @@ func close*(store: var BestLightClientUpdateStore) = store.delStmt.disposeSafe() proc new*( - T: type BeaconDb, networkData: NetworkInitData, - path: string, inMemory = false): - BeaconDb = + T: type BeaconDb, networkData: NetworkInitData, path: string, inMemory = false +): BeaconDb = let db = if inMemory: SqStoreRef.init("", "lc-test", inMemory = true).expect( - "working database (out of memory?)") + "working database (out of memory?)" + ) else: SqStoreRef.init(path, "lc").expectDb() @@ -128,13 +161,14 @@ proc new*( kv: kvStore, bestUpdates: bestUpdates, cfg: networkData.metadata.cfg, - forkDigests: (newClone networkData.forks)[] + forkDigests: (newClone networkData.forks)[], ) ## Private KvStoreRef Calls proc get(kv: KvStoreRef, key: openArray[byte]): results.Opt[seq[byte]] = var res: results.Opt[seq[byte]] = Opt.none(seq[byte]) - proc onData(data: openArray[byte]) = res = ok(@data) + proc onData(data: openArray[byte]) = + res = ok(@data) discard kv.get(key, onData).expectDb() @@ -148,7 +182,7 @@ proc put(db: BeaconDb, key, value: openArray[byte]) = db.kv.put(key, value).expectDb() ## Public ContentId based ContentDB calls -proc get*(db: BeaconDb, key: ContentId): results.Opt[seq[byte]] = +proc get*(db: BeaconDb, key: ContentId): results.Opt[seq[byte]] = # TODO: Here it is unfortunate that ContentId is a uint256 instead of Digest256. db.get(key.toBytesBE()) @@ -157,8 +191,8 @@ proc put*(db: BeaconDb, key: ContentId, value: openArray[byte]) = # TODO Add checks that uint64 can be safely casted to int64 proc getLightClientUpdates( - db: BeaconDb, start: uint64, to: uint64): - ForkedLightClientUpdateBytesList = + db: BeaconDb, start: uint64, to: uint64 +): ForkedLightClientUpdateBytesList = ## Get multiple consecutive LightClientUpdates for given periods var updates: ForkedLightClientUpdateBytesList var update: seq[byte] @@ -169,8 +203,8 @@ proc getLightClientUpdates( return updates proc getBestUpdate*( - db: BeaconDb, period: SyncCommitteePeriod): - Result[ForkedLightClientUpdate, string] = + db: BeaconDb, period: SyncCommitteePeriod +): Result[ForkedLightClientUpdate, string] = ## Get the best ForkedLightClientUpdate for given period ## Note: Only the best one for a given period is being stored. doAssert period.isSupportedBySQLite @@ -182,8 +216,8 @@ proc getBestUpdate*( return decodeLightClientUpdateForked(db.forkDigests, update) proc putBootstrap*( - db: BeaconDb, - blockRoot: Digest, bootstrap: ForkedLightClientBootstrap) = + db: BeaconDb, blockRoot: Digest, bootstrap: ForkedLightClientBootstrap +) = # Put a ForkedLightClientBootstrap in the db. withForkyBootstrap(bootstrap): when lcDataFork > LightClientDataFork.None: @@ -191,43 +225,43 @@ proc putBootstrap*( contentKey = bootstrapContentKey(blockRoot) contentId = toContentId(contentKey) forkDigest = forkDigestAtEpoch( - db.forkDigests, epoch(forkyBootstrap.header.beacon.slot), db.cfg) + db.forkDigests, epoch(forkyBootstrap.header.beacon.slot), db.cfg + ) encodedBootstrap = encodeBootstrapForked(forkDigest, bootstrap) db.put(contentId, encodedBootstrap) -func putLightClientUpdate*( - db: BeaconDb, period: uint64, update: seq[byte]) = +func putLightClientUpdate*(db: BeaconDb, period: uint64, update: seq[byte]) = # Put an encoded ForkedLightClientUpdate in the db. let res = db.bestUpdates.putStmt.exec((period.int64, update)) res.expect("SQL query OK") func putBestUpdate*( - db: BeaconDb, period: SyncCommitteePeriod, - update: ForkedLightClientUpdate) = + db: BeaconDb, period: SyncCommitteePeriod, update: ForkedLightClientUpdate +) = # Put a ForkedLightClientUpdate in the db. - doAssert not db.backend.readOnly # All `stmt` are non-nil + doAssert not db.backend.readOnly # All `stmt` are non-nil doAssert period.isSupportedBySQLite withForkyUpdate(update): when lcDataFork > LightClientDataFork.None: let numParticipants = forkyUpdate.sync_aggregate.num_active_participants if numParticipants < MIN_SYNC_COMMITTEE_PARTICIPANTS: - let res = db.bestUpdates.delStmt.exec(period.int64) - res.expect("SQL query OK") + let res = db.bestUpdates.delStmt.exec(period.int64) + res.expect("SQL query OK") else: - let - forkDigest = forkDigestAtEpoch( - db.forkDigests, epoch(forkyUpdate.attested_header.beacon.slot), - db.cfg) - encodedUpdate = encodeForkedLightClientObject(update, forkDigest) - res = db.bestUpdates.putStmt.exec((period.int64, encodedUpdate)) - res.expect("SQL query OK") + let + forkDigest = forkDigestAtEpoch( + db.forkDigests, epoch(forkyUpdate.attested_header.beacon.slot), db.cfg + ) + encodedUpdate = encodeForkedLightClientObject(update, forkDigest) + res = db.bestUpdates.putStmt.exec((period.int64, encodedUpdate)) + res.expect("SQL query OK") else: db.bestUpdates.delStmt.exec(period.int64).expect("SQL query OK") proc putUpdateIfBetter*( - db: BeaconDb, - period: SyncCommitteePeriod, update: ForkedLightClientUpdate) = + db: BeaconDb, period: SyncCommitteePeriod, update: ForkedLightClientUpdate +) = let currentUpdate = db.getBestUpdate(period).valueOr: # No current update for that period so we can just put this one db.putBestUpdate(period, update) @@ -236,8 +270,7 @@ proc putUpdateIfBetter*( if is_better_update(update, currentUpdate): db.putBestUpdate(period, update) -proc putUpdateIfBetter*( - db: BeaconDb, period: SyncCommitteePeriod, update: seq[byte]) = +proc putUpdateIfBetter*(db: BeaconDb, period: SyncCommitteePeriod, update: seq[byte]) = let newUpdate = decodeLightClientUpdateForked(db.forkDigests, update).valueOr: # TODO: # Need to go over the usage in offer/accept vs findcontent/content @@ -250,16 +283,17 @@ proc getLastFinalityUpdate*(db: BeaconDb): Opt[ForkedLightClientFinalityUpdate] db.finalityUpdateCache.map( proc(x: LightClientFinalityUpdateCache): ForkedLightClientFinalityUpdate = decodeLightClientFinalityUpdateForked(db.forkDigests, x.lastFinalityUpdate).valueOr: - raiseAssert "Stored finality update must be valid") + raiseAssert "Stored finality update must be valid" + ) proc createGetHandler*(db: BeaconDb): DbGetHandler = return ( proc(contentKey: ByteList, contentId: ContentId): results.Opt[seq[byte]] = let contentKey = contentKey.decode().valueOr: - # TODO: as this should not fail, maybe it is better to raiseAssert ? + # TODO: as this should not fail, maybe it is better to raiseAssert ? return Opt.none(seq[byte]) - case contentKey.contentType: + case contentKey.contentType of unused: raiseAssert "Should not be used and fail at decoding" of lightClientBootstrap: @@ -272,7 +306,7 @@ proc createGetHandler*(db: BeaconDb): DbGetHandler = # get max 128 updates numOfUpdates = min( uint64(MAX_REQUEST_LIGHT_CLIENT_UPDATES), - contentKey.lightClientUpdateKey.count + contentKey.lightClientUpdateKey.count, ) toPeriod = startPeriod + numOfUpdates # Not inclusive updates = db.getLightClientUpdates(startPeriod, toPeriod) @@ -311,59 +345,59 @@ proc createGetHandler*(db: BeaconDb): DbGetHandler = ) proc createStoreHandler*(db: BeaconDb): DbStoreHandler = - return (proc( - contentKey: ByteList, - contentId: ContentId, - content: seq[byte]) {.raises: [], gcsafe.} = - let contentKey = decode(contentKey).valueOr: - # TODO: as this should not fail, maybe it is better to raiseAssert ? - return - - case contentKey.contentType: - of unused: - raiseAssert "Should not be used and fail at decoding" - of lightClientBootstrap: - db.put(contentId, content) - of lightClientUpdate: - let updates = - decodeSsz(content, ForkedLightClientUpdateBytesList).valueOr: + return ( + proc( + contentKey: ByteList, contentId: ContentId, content: seq[byte] + ) {.raises: [], gcsafe.} = + let contentKey = decode(contentKey).valueOr: + # TODO: as this should not fail, maybe it is better to raiseAssert ? + return + + case contentKey.contentType + of unused: + raiseAssert "Should not be used and fail at decoding" + of lightClientBootstrap: + db.put(contentId, content) + of lightClientUpdate: + let updates = decodeSsz(content, ForkedLightClientUpdateBytesList).valueOr: return - # Lot of assumptions here: - # - that updates are continious i.e there is no period gaps - # - that updates start from startPeriod of content key - var period = contentKey.lightClientUpdateKey.startPeriod - for update in updates.asSeq(): - # Only put the update if it is better, although in currently a new offer - # should not be accepted as it is based on only the period. - db.putUpdateIfBetter(SyncCommitteePeriod(period), update.asSeq()) - inc period - of lightClientFinalityUpdate: - db.finalityUpdateCache = - Opt.some(LightClientFinalityUpdateCache( - lastFinalityUpdateSlot: - contentKey.lightClientFinalityUpdateKey.finalizedSlot, - lastFinalityUpdate: content - )) - of lightClientOptimisticUpdate: - db.optimisticUpdateCache = - Opt.some(LightClientOptimisticUpdateCache( - lastOptimisticUpdateSlot: - contentKey.lightClientOptimisticUpdateKey.optimisticSlot, - lastOptimisticUpdate: content - )) - of beacon_content.ContentType.historicalSummaries: - # TODO: Its probably better to use the kvstore here and instead use a sql - # table with slot as index and move the slot logic to the db store handler. - let current = db.get(contentId) - if current.isSome(): - let summariesWithProof = - decodeSszOrRaise(current.get(), HistoricalSummariesWithProof) - let newSummariesWithProof = - decodeSsz(content, HistoricalSummariesWithProof).valueOr: + # Lot of assumptions here: + # - that updates are continious i.e there is no period gaps + # - that updates start from startPeriod of content key + var period = contentKey.lightClientUpdateKey.startPeriod + for update in updates.asSeq(): + # Only put the update if it is better, although in currently a new offer + # should not be accepted as it is based on only the period. + db.putUpdateIfBetter(SyncCommitteePeriod(period), update.asSeq()) + inc period + of lightClientFinalityUpdate: + db.finalityUpdateCache = Opt.some( + LightClientFinalityUpdateCache( + lastFinalityUpdateSlot: + contentKey.lightClientFinalityUpdateKey.finalizedSlot, + lastFinalityUpdate: content, + ) + ) + of lightClientOptimisticUpdate: + db.optimisticUpdateCache = Opt.some( + LightClientOptimisticUpdateCache( + lastOptimisticUpdateSlot: + contentKey.lightClientOptimisticUpdateKey.optimisticSlot, + lastOptimisticUpdate: content, + ) + ) + of beacon_content.ContentType.historicalSummaries: + # TODO: Its probably better to use the kvstore here and instead use a sql + # table with slot as index and move the slot logic to the db store handler. + let current = db.get(contentId) + if current.isSome(): + let summariesWithProof = + decodeSszOrRaise(current.get(), HistoricalSummariesWithProof) + let newSummariesWithProof = decodeSsz(content, HistoricalSummariesWithProof).valueOr: return - if newSummariesWithProof.finalized_slot > summariesWithProof.finalized_slot: + if newSummariesWithProof.finalized_slot > summariesWithProof.finalized_slot: + db.put(contentId, content) + else: db.put(contentId, content) - else: - db.put(contentId, content) ) diff --git a/fluffy/network/beacon/beacon_init_loader.nim b/fluffy/network/beacon/beacon_init_loader.nim index 16eb556105..b4efac5c78 100644 --- a/fluffy/network/beacon/beacon_init_loader.nim +++ b/fluffy/network/beacon/beacon_init_loader.nim @@ -15,21 +15,25 @@ import beacon_chain/beacon_clock, beacon_chain/conf -type - NetworkInitData* = object - clock*: BeaconClock - metadata*: Eth2NetworkMetadata - forks*: ForkDigests - genesis_validators_root*: Eth2Digest +type NetworkInitData* = object + clock*: BeaconClock + metadata*: Eth2NetworkMetadata + forks*: ForkDigests + genesis_validators_root*: Eth2Digest proc loadNetworkData*(networkName: string): NetworkInitData = let metadata = loadEth2Network(some("mainnet")) genesisState = try: - template genesisData(): auto = metadata.genesis.bakedBytes - newClone(readSszForkedHashedBeaconState( - metadata.cfg, genesisData.toOpenArray(genesisData.low, genesisData.high))) + template genesisData(): auto = + metadata.genesis.bakedBytes + + newClone( + readSszForkedHashedBeaconState( + metadata.cfg, genesisData.toOpenArray(genesisData.low, genesisData.high) + ) + ) except SerializationError as err: raiseAssert "Invalid baked-in state: " & err.msg @@ -38,8 +42,7 @@ proc loadNetworkData*(networkName: string): NetworkInitData = error "Invalid genesis time in state", genesisTime quit QuitFailure - genesis_validators_root = - getStateField(genesisState[], genesis_validators_root) + genesis_validators_root = getStateField(genesisState[], genesis_validators_root) forks = newClone ForkDigests.init(metadata.cfg, genesis_validators_root) @@ -47,5 +50,5 @@ proc loadNetworkData*(networkName: string): NetworkInitData = clock: beaconClock, metadata: metadata, forks: forks[], - genesis_validators_root: genesis_validators_root + genesis_validators_root: genesis_validators_root, ) diff --git a/fluffy/network/beacon/beacon_light_client.nim b/fluffy/network/beacon/beacon_light_client.nim index 0cbcbd9d85..dbd1abac8f 100644 --- a/fluffy/network/beacon/beacon_light_client.nim +++ b/fluffy/network/beacon/beacon_light_client.nim @@ -15,16 +15,15 @@ import beacon_chain/beacon_clock, "."/[beacon_init_loader, beacon_network, beacon_light_client_manager] -export - LightClientFinalizationMode, - beacon_network, beacon_light_client_manager +export LightClientFinalizationMode, beacon_network, beacon_light_client_manager -logScope: topics = "beacon_lc" +logScope: + topics = "beacon_lc" type - LightClientHeaderCallback* = - proc(lightClient: LightClient, header: ForkedLightClientHeader) {. - gcsafe, raises: [].} + LightClientHeaderCallback* = proc( + lightClient: LightClient, header: ForkedLightClientHeader + ) {.gcsafe, raises: [].} LightClient* = ref object network*: BeaconNetwork @@ -37,8 +36,7 @@ type onFinalizedHeader*, onOptimisticHeader*: LightClientHeaderCallback trustedBlockRoot*: Option[Eth2Digest] -func finalizedHeader*( - lightClient: LightClient): ForkedLightClientHeader = +func finalizedHeader*(lightClient: LightClient): ForkedLightClientHeader = withForkyStore(lightClient.store[]): when lcDataFork > LightClientDataFork.None: var header = ForkedLightClientHeader(kind: lcDataFork) @@ -47,8 +45,7 @@ func finalizedHeader*( else: default(ForkedLightClientHeader) -func optimisticHeader*( - lightClient: LightClient): ForkedLightClientHeader = +func optimisticHeader*(lightClient: LightClient): ForkedLightClientHeader = withForkyStore(lightClient.store[]): when lcDataFork > LightClientDataFork.None: var header = ForkedLightClientHeader(kind: lcDataFork) @@ -67,13 +64,15 @@ proc new*( forkDigests: ref ForkDigests, getBeaconTime: GetBeaconTimeFn, genesis_validators_root: Eth2Digest, - finalizationMode: LightClientFinalizationMode): T = + finalizationMode: LightClientFinalizationMode, +): T = let lightClient = LightClient( network: network, cfg: cfg, forkDigests: forkDigests, getBeaconTime: getBeaconTime, - store: (ref ForkedLightClientStore)()) + store: (ref ForkedLightClientStore)(), + ) func getTrustedBlockRoot(): Option[Eth2Digest] = lightClient.trustedBlockRoot @@ -83,32 +82,36 @@ proc new*( proc onFinalizedHeader() = if lightClient.onFinalizedHeader != nil: - lightClient.onFinalizedHeader( - lightClient, lightClient.finalizedHeader) + lightClient.onFinalizedHeader(lightClient, lightClient.finalizedHeader) proc onOptimisticHeader() = if lightClient.onOptimisticHeader != nil: - lightClient.onOptimisticHeader( - lightClient, lightClient.optimisticHeader) + lightClient.onOptimisticHeader(lightClient, lightClient.optimisticHeader) lightClient.processor = LightClientProcessor.new( - dumpEnabled, dumpDirInvalid, dumpDirIncoming, - cfg, genesis_validators_root, finalizationMode, - lightClient.store, getBeaconTime, getTrustedBlockRoot, - onStoreInitialized, onFinalizedHeader, onOptimisticHeader) - - proc lightClientVerifier(obj: SomeForkedLightClientObject): - Future[Result[void, VerifierError]] = - let resfut = Future[Result[void, VerifierError]].Raising([CancelledError]).init("lightClientVerifier") + dumpEnabled, dumpDirInvalid, dumpDirIncoming, cfg, genesis_validators_root, + finalizationMode, lightClient.store, getBeaconTime, getTrustedBlockRoot, + onStoreInitialized, onFinalizedHeader, onOptimisticHeader, + ) + + proc lightClientVerifier( + obj: SomeForkedLightClientObject + ): Future[Result[void, VerifierError]] = + let resfut = Future[Result[void, VerifierError]].Raising([CancelledError]).init( + "lightClientVerifier" + ) lightClient.processor[].addObject(MsgSource.gossip, obj, resfut) resfut proc bootstrapVerifier(obj: ForkedLightClientBootstrap): auto = lightClientVerifier(obj) + proc updateVerifier(obj: ForkedLightClientUpdate): auto = lightClientVerifier(obj) + proc finalityVerifier(obj: ForkedLightClientFinalityUpdate): auto = lightClientVerifier(obj) + proc optimisticVerifier(obj: ForkedLightClientOptimisticUpdate): auto = lightClientVerifier(obj) @@ -137,10 +140,10 @@ proc new*( GENESIS_SLOT lightClient.manager = LightClientManager.init( - lightClient.network, rng, getTrustedBlockRoot, - bootstrapVerifier, updateVerifier, finalityVerifier, optimisticVerifier, - isLightClientStoreInitialized, isNextSyncCommitteeKnown, - getFinalizedSlot, getOptimisticSlot, getBeaconTime) + lightClient.network, rng, getTrustedBlockRoot, bootstrapVerifier, updateVerifier, + finalityVerifier, optimisticVerifier, isLightClientStoreInitialized, + isNextSyncCommitteeKnown, getFinalizedSlot, getOptimisticSlot, getBeaconTime, + ) lightClient @@ -149,16 +152,24 @@ proc new*( network: BeaconNetwork, rng: ref HmacDrbgContext, networkData: NetworkInitData, - finalizationMode: LightClientFinalizationMode): T = + finalizationMode: LightClientFinalizationMode, +): T = let getBeaconTime = networkData.clock.getBeaconTimeFn() forkDigests = newClone networkData.forks LightClient.new( - network, rng, - dumpEnabled = false, dumpDirInvalid = ".", dumpDirIncoming = ".", - networkData.metadata.cfg, forkDigests, getBeaconTime, - networkData.genesis_validators_root, finalizationMode) + network, + rng, + dumpEnabled = false, + dumpDirInvalid = ".", + dumpDirIncoming = ".", + networkData.metadata.cfg, + forkDigests, + getBeaconTime, + networkData.genesis_validators_root, + finalizationMode, + ) proc start*(lightClient: LightClient) = notice "Starting beacon light client", @@ -168,6 +179,6 @@ proc start*(lightClient: LightClient) = proc resetToFinalizedHeader*( lightClient: LightClient, header: ForkedLightClientHeader, - current_sync_committee: altair.SyncCommittee) = + current_sync_committee: altair.SyncCommittee, +) = lightClient.processor[].resetToFinalizedHeader(header, current_sync_committee) - diff --git a/fluffy/network/beacon/beacon_light_client_manager.nim b/fluffy/network/beacon/beacon_light_client_manager.nim index 5f5babedc1..43116e3dbc 100644 --- a/fluffy/network/beacon/beacon_light_client_manager.nim +++ b/fluffy/network/beacon/beacon_light_client_manager.nim @@ -9,7 +9,9 @@ import std/typetraits, - chronos, chronicles, stew/[base10, results], + chronos, + chronicles, + stew/[base10, results], eth/p2p/discoveryv5/random2, beacon_chain/spec/datatypes/[phase0, altair, bellatrix, capella, deneb], beacon_chain/spec/[forks_light_client, digest], @@ -27,36 +29,24 @@ type ResponseError = object of CatchableError NetRes*[T] = Result[T, void] - Endpoint[K, V] = - (K, V) # https://github.com/nim-lang/Nim/issues/19531 - Bootstrap = - Endpoint[Eth2Digest, ForkedLightClientBootstrap] - UpdatesByRange = - Endpoint[ - tuple[startPeriod: SyncCommitteePeriod, count: uint64], - ForkedLightClientUpdate] - FinalityUpdate = - Endpoint[Slot, ForkedLightClientFinalityUpdate] - OptimisticUpdate = - Endpoint[Slot, ForkedLightClientOptimisticUpdate] + Endpoint[K, V] = (K, V) # https://github.com/nim-lang/Nim/issues/19531 + Bootstrap = Endpoint[Eth2Digest, ForkedLightClientBootstrap] + UpdatesByRange = Endpoint[ + tuple[startPeriod: SyncCommitteePeriod, count: uint64], ForkedLightClientUpdate + ] + FinalityUpdate = Endpoint[Slot, ForkedLightClientFinalityUpdate] + OptimisticUpdate = Endpoint[Slot, ForkedLightClientOptimisticUpdate] ValueVerifier[V] = proc(v: V): Future[Result[void, VerifierError]] {.gcsafe, raises: [].} - BootstrapVerifier* = - ValueVerifier[ForkedLightClientBootstrap] - UpdateVerifier* = - ValueVerifier[ForkedLightClientUpdate] - FinalityUpdateVerifier* = - ValueVerifier[ForkedLightClientFinalityUpdate] - OptimisticUpdateVerifier* = - ValueVerifier[ForkedLightClientOptimisticUpdate] + BootstrapVerifier* = ValueVerifier[ForkedLightClientBootstrap] + UpdateVerifier* = ValueVerifier[ForkedLightClientUpdate] + FinalityUpdateVerifier* = ValueVerifier[ForkedLightClientFinalityUpdate] + OptimisticUpdateVerifier* = ValueVerifier[ForkedLightClientOptimisticUpdate] - GetTrustedBlockRootCallback* = - proc(): Option[Eth2Digest] {.gcsafe, raises: [].} - GetBoolCallback* = - proc(): bool {.gcsafe, raises: [].} - GetSlotCallback* = - proc(): Slot {.gcsafe, raises: [].} + GetTrustedBlockRootCallback* = proc(): Option[Eth2Digest] {.gcsafe, raises: [].} + GetBoolCallback* = proc(): bool {.gcsafe, raises: [].} + GetSlotCallback* = proc(): Slot {.gcsafe, raises: [].} LightClientManager* = object network: BeaconNetwork @@ -86,7 +76,7 @@ func init*( isNextSyncCommitteeKnown: GetBoolCallback, getFinalizedSlot: GetSlotCallback, getOptimisticSlot: GetSlotCallback, - getBeaconTime: GetBeaconTimeFn + getBeaconTime: GetBeaconTimeFn, ): LightClientManager = ## Initialize light client manager. LightClientManager( @@ -101,7 +91,7 @@ func init*( isNextSyncCommitteeKnown: isNextSyncCommitteeKnown, getFinalizedSlot: getFinalizedSlot, getOptimisticSlot: getOptimisticSlot, - getBeaconTime: getBeaconTime + getBeaconTime: getBeaconTime, ) proc getFinalizedPeriod(self: LightClientManager): SyncCommitteePeriod = @@ -112,9 +102,7 @@ proc getOptimisticPeriod(self: LightClientManager): SyncCommitteePeriod = # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/altair/light-client/p2p-interface.md#getlightclientbootstrap proc doRequest( - e: typedesc[Bootstrap], - n: BeaconNetwork, - blockRoot: Eth2Digest + e: typedesc[Bootstrap], n: BeaconNetwork, blockRoot: Eth2Digest ): Future[NetRes[ForkedLightClientBootstrap]] = n.getLightClientBootstrap(blockRoot) @@ -123,39 +111,31 @@ type LightClientUpdatesByRangeResponse = NetRes[ForkedLightClientUpdateList] proc doRequest( e: typedesc[UpdatesByRange], n: BeaconNetwork, - key: tuple[startPeriod: SyncCommitteePeriod, count: uint64] + key: tuple[startPeriod: SyncCommitteePeriod, count: uint64], ): Future[LightClientUpdatesByRangeResponse] {.async.} = let (startPeriod, count) = key doAssert count > 0 and count <= MAX_REQUEST_LIGHT_CLIENT_UPDATES let response = await n.getLightClientUpdatesByRange(startPeriod, count) if response.isOk: - let e = distinctBase(response.get) - .checkLightClientUpdates(startPeriod, count) + let e = distinctBase(response.get).checkLightClientUpdates(startPeriod, count) if e.isErr: raise newException(ResponseError, e.error) return response # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/altair/light-client/p2p-interface.md#getlightclientfinalityupdate proc doRequest( - e: typedesc[FinalityUpdate], - n: BeaconNetwork, - finalizedSlot: Slot + e: typedesc[FinalityUpdate], n: BeaconNetwork, finalizedSlot: Slot ): Future[NetRes[ForkedLightClientFinalityUpdate]] = - n.getLightClientFinalityUpdate( - distinctBase(finalizedSlot) - ) + n.getLightClientFinalityUpdate(distinctBase(finalizedSlot)) # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/altair/light-client/p2p-interface.md#getlightclientoptimisticupdate proc doRequest( - e: typedesc[OptimisticUpdate], - n: BeaconNetwork, - optimisticSlot: Slot + e: typedesc[OptimisticUpdate], n: BeaconNetwork, optimisticSlot: Slot ): Future[NetRes[ForkedLightClientOptimisticUpdate]] = n.getLightClientOptimisticUpdate(distinctBase(optimisticSlot)) template valueVerifier[E]( - self: LightClientManager, - e: typedesc[E] + self: LightClientManager, e: typedesc[E] ): ValueVerifier[E.V] = when E.V is ForkedLightClientBootstrap: self.bootstrapVerifier @@ -165,7 +145,9 @@ template valueVerifier[E]( self.finalityUpdateVerifier elif E.V is ForkedLightClientOptimisticUpdate: self.optimisticUpdateVerifier - else: static: doAssert false + else: + static: + doAssert false iterator values(v: auto): auto = ## Local helper for `workerTask` to share the same implementation for both @@ -177,12 +159,9 @@ iterator values(v: auto): auto = yield v proc workerTask[E]( - self: LightClientManager, - e: typedesc[E], - key: E.K + self: LightClientManager, e: typedesc[E], key: E.K ): Future[bool] {.async.} = - var - didProgress = false + var didProgress = false try: let value = when E.K is Nothing: @@ -209,14 +188,13 @@ proc workerTask[E]( notice "Received value from an unviable fork", value = forkyObject, endpoint = E.name else: - notice "Received value from an unviable fork", - endpoint = E.name + notice "Received value from an unviable fork", endpoint = E.name return didProgress of VerifierError.Invalid: withForkyObject(val): when lcDataFork > LightClientDataFork.None: - warn "Received invalid value", value = forkyObject.shortLog, - endpoint = E.name + warn "Received invalid value", + value = forkyObject.shortLog, endpoint = E.name else: warn "Received invalid value", endpoint = E.name return didProgress @@ -234,8 +212,7 @@ proc workerTask[E]( when lcDataFork > LightClientDataFork.None: self.network.beaconDb.putBootstrap(key, val) else: - notice "Received value from an unviable fork", - endpoint = E.name + notice "Received value from an unviable fork", endpoint = E.name elif E.V is ForkedLightClientUpdate: withForkyObject(val): when lcDataFork > LightClientDataFork.None: @@ -243,8 +220,7 @@ proc workerTask[E]( forkyObject.attested_header.beacon.slot.sync_committee_period self.network.beaconDb.putUpdateIfBetter(period, val) else: - notice "Received value from an unviable fork", - endpoint = E.name + notice "Received value from an unviable fork", endpoint = E.name didProgress = true else: @@ -259,11 +235,7 @@ proc workerTask[E]( return didProgress -proc query[E]( - self: LightClientManager, - e: typedesc[E], - key: E.K -): Future[bool] = +proc query[E](self: LightClientManager, e: typedesc[E], key: E.K): Future[bool] = # Note: # The libp2p version does concurrent requests here. But it seems to be done # for the same key and thus as redundant request to avoid waiting on a not @@ -308,13 +280,15 @@ proc loop(self: LightClientManager) {.async.} = current = current, finalized = self.getFinalizedPeriod(), optimistic = self.getOptimisticPeriod(), - isNextSyncCommitteeKnown = self.isNextSyncCommitteeKnown()) + isNextSyncCommitteeKnown = self.isNextSyncCommitteeKnown(), + ) didProgress = case syncTask.kind of LcSyncKind.UpdatesByRange: - await self.query(UpdatesByRange, - (startPeriod: syncTask.startPeriod, count: syncTask.count)) + await self.query( + UpdatesByRange, (startPeriod: syncTask.startPeriod, count: syncTask.count) + ) of LcSyncKind.FinalityUpdate: let finalizedSlot = start_slot(epoch(wallTime.slotOrZero()) - 2) await self.query(FinalityUpdate, finalizedSlot) @@ -322,12 +296,15 @@ proc loop(self: LightClientManager) {.async.} = let optimisticSlot = wallTime.slotOrZero() await self.query(OptimisticUpdate, optimisticSlot) - nextSyncTaskTime = wallTime + self.rng.nextLcSyncTaskDelay( - wallTime, - finalized = self.getFinalizedPeriod(), - optimistic = self.getOptimisticPeriod(), - isNextSyncCommitteeKnown = self.isNextSyncCommitteeKnown(), - didLatestSyncTaskProgress = didProgress) + nextSyncTaskTime = + wallTime + + self.rng.nextLcSyncTaskDelay( + wallTime, + finalized = self.getFinalizedPeriod(), + optimistic = self.getOptimisticPeriod(), + isNextSyncCommitteeKnown = self.isNextSyncCommitteeKnown(), + didLatestSyncTaskProgress = didProgress, + ) proc start*(self: var LightClientManager) = ## Start light client manager's loop. diff --git a/fluffy/network/beacon/beacon_network.nim b/fluffy/network/beacon/beacon_network.nim index 5f207fe25f..207f414728 100644 --- a/fluffy/network/beacon/beacon_network.nim +++ b/fluffy/network/beacon/beacon_network.nim @@ -8,7 +8,9 @@ {.push raises: [].} import - stew/results, chronos, chronicles, + stew/results, + chronos, + chronicles, eth/p2p/discoveryv5/[protocol, enr], beacon_chain/spec/forks, beacon_chain/spec/datatypes/[phase0, altair, bellatrix], @@ -22,38 +24,34 @@ export beacon_content, beacon_db logScope: topics = "beacon_network" -const - lightClientProtocolId* = [byte 0x50, 0x1A] +const lightClientProtocolId* = [byte 0x50, 0x1A] -type - BeaconNetwork* = ref object - portalProtocol*: PortalProtocol - beaconDb*: BeaconDb - processor*: ref LightClientProcessor - contentQueue*: AsyncQueue[(Opt[NodeId], ContentKeysList, seq[seq[byte]])] - forkDigests*: ForkDigests - processContentLoop: Future[void] +type BeaconNetwork* = ref object + portalProtocol*: PortalProtocol + beaconDb*: BeaconDb + processor*: ref LightClientProcessor + contentQueue*: AsyncQueue[(Opt[NodeId], ContentKeysList, seq[seq[byte]])] + forkDigests*: ForkDigests + processContentLoop: Future[void] func toContentIdHandler(contentKey: ByteList): results.Opt[ContentId] = ok(toContentId(contentKey)) proc validateHistoricalSummaries( - n: BeaconNetwork, - summariesWithProof: HistoricalSummariesWithProof - ): Result[void, string] = + n: BeaconNetwork, summariesWithProof: HistoricalSummariesWithProof +): Result[void, string] = let finalityUpdate = getLastFinalityUpdate(n.beaconDb).valueOr: return err("Require finality update for verification") # TODO: compare slots first - stateRoot = - withForkyFinalityUpdate(finalityUpdate): - when lcDataFork > LightClientDataFork.None: - forkyFinalityUpdate.finalized_header.beacon.state_root - else: - # Note: this should always be the case as historical_summaries was - # introduced in Capella. - return err("Require Altair or > for verification") + stateRoot = withForkyFinalityUpdate(finalityUpdate): + when lcDataFork > LightClientDataFork.None: + forkyFinalityUpdate.finalized_header.beacon.state_root + else: + # Note: this should always be the case as historical_summaries was + # introduced in Capella. + return err("Require Altair or > for verification") if summariesWithProof.verifyProof(stateRoot): ok() @@ -61,8 +59,8 @@ proc validateHistoricalSummaries( err("Failed verifying historical_summaries proof") proc getContent( - n: BeaconNetwork, contentKey: ContentKey): - Future[results.Opt[seq[byte]]] {.async.} = + n: BeaconNetwork, contentKey: ContentKey +): Future[results.Opt[seq[byte]]] {.async.} = let contentKeyEncoded = encode(contentKey) contentId = toContentId(contentKeyEncoded) @@ -71,8 +69,7 @@ proc getContent( if localContent.isSome(): return localContent - let contentRes = await n.portalProtocol.contentLookup( - contentKeyEncoded, contentId) + let contentRes = await n.portalProtocol.contentLookup(contentKeyEncoded, contentId) if contentRes.isNone(): warn "Failed fetching content from the beacon chain network", @@ -82,9 +79,8 @@ proc getContent( return Opt.some(contentRes.value().content) proc getLightClientBootstrap*( - n: BeaconNetwork, - trustedRoot: Digest): - Future[results.Opt[ForkedLightClientBootstrap]] {.async.} = + n: BeaconNetwork, trustedRoot: Digest +): Future[results.Opt[ForkedLightClientBootstrap]] {.async.} = let contentKey = bootstrapContentKey(trustedRoot) contentResult = await n.getContent(contentKey) @@ -94,8 +90,7 @@ proc getLightClientBootstrap*( let bootstrap = contentResult.value() - decodingResult = decodeLightClientBootstrapForked( - n.forkDigests, bootstrap) + decodingResult = decodeLightClientBootstrapForked(n.forkDigests, bootstrap) if decodingResult.isErr(): return Opt.none(ForkedLightClientBootstrap) @@ -105,10 +100,8 @@ proc getLightClientBootstrap*( return Opt.some(decodingResult.value()) proc getLightClientUpdatesByRange*( - n: BeaconNetwork, - startPeriod: SyncCommitteePeriod, - count: uint64): - Future[results.Opt[ForkedLightClientUpdateList]] {.async.} = + n: BeaconNetwork, startPeriod: SyncCommitteePeriod, count: uint64 +): Future[results.Opt[ForkedLightClientUpdateList]] {.async.} = let contentKey = updateContentKey(distinctBase(startPeriod), count) contentResult = await n.getContent(contentKey) @@ -118,8 +111,7 @@ proc getLightClientUpdatesByRange*( let updates = contentResult.value() - decodingResult = decodeLightClientUpdatesByRange( - n.forkDigests, updates) + decodingResult = decodeLightClientUpdatesByRange(n.forkDigests, updates) if decodingResult.isErr(): return Opt.none(ForkedLightClientUpdateList) @@ -129,9 +121,8 @@ proc getLightClientUpdatesByRange*( return Opt.some(decodingResult.value()) proc getLightClientFinalityUpdate*( - n: BeaconNetwork, - finalizedSlot: uint64 - ): Future[results.Opt[ForkedLightClientFinalityUpdate]] {.async.} = + n: BeaconNetwork, finalizedSlot: uint64 +): Future[results.Opt[ForkedLightClientFinalityUpdate]] {.async.} = let contentKey = finalityUpdateContentKey(finalizedSlot) contentResult = await n.getContent(contentKey) @@ -141,8 +132,8 @@ proc getLightClientFinalityUpdate*( let finalityUpdate = contentResult.value() - decodingResult = decodeLightClientFinalityUpdateForked( - n.forkDigests, finalityUpdate) + decodingResult = + decodeLightClientFinalityUpdateForked(n.forkDigests, finalityUpdate) if decodingResult.isErr(): return Opt.none(ForkedLightClientFinalityUpdate) @@ -150,10 +141,8 @@ proc getLightClientFinalityUpdate*( return Opt.some(decodingResult.value()) proc getLightClientOptimisticUpdate*( - n: BeaconNetwork, - optimisticSlot: uint64 - ): Future[results.Opt[ForkedLightClientOptimisticUpdate]] {.async.} = - + n: BeaconNetwork, optimisticSlot: uint64 +): Future[results.Opt[ForkedLightClientOptimisticUpdate]] {.async.} = let contentKey = optimisticUpdateContentKey(optimisticSlot) contentResult = await n.getContent(contentKey) @@ -163,8 +152,8 @@ proc getLightClientOptimisticUpdate*( let optimisticUpdate = contentResult.value() - decodingResult = decodeLightClientOptimisticUpdateForked( - n.forkDigests, optimisticUpdate) + decodingResult = + decodeLightClientOptimisticUpdateForked(n.forkDigests, optimisticUpdate) if decodingResult.isErr(): return Opt.none(ForkedLightClientOptimisticUpdate) @@ -173,11 +162,11 @@ proc getLightClientOptimisticUpdate*( proc getHistoricalSummaries*( n: BeaconNetwork - ): Future[results.Opt[HistoricalSummaries]] {.async.} = +): Future[results.Opt[HistoricalSummaries]] {.async.} = # Note: when taken from the db, it does not need to verify the proof. let contentKey = historicalSummariesContentKey() - content = ? await n.getContent(contentKey) + content = ?await n.getContent(contentKey) summariesWithProof = decodeSsz(content, HistoricalSummariesWithProof).valueOr: return Opt.none(HistoricalSummaries) @@ -187,7 +176,6 @@ proc getHistoricalSummaries*( else: return Opt.none(HistoricalSummaries) - proc new*( T: type BeaconNetwork, baseProtocol: protocol.Protocol, @@ -195,10 +183,10 @@ proc new*( streamManager: StreamManager, forkDigests: ForkDigests, bootstrapRecords: openArray[Record] = [], - portalConfig: PortalProtocolConfig = defaultPortalProtocolConfig): T = + portalConfig: PortalProtocolConfig = defaultPortalProtocolConfig, +): T = let - contentQueue = newAsyncQueue[( - Opt[NodeId], ContentKeysList, seq[seq[byte]])](50) + contentQueue = newAsyncQueue[(Opt[NodeId], ContentKeysList, seq[seq[byte]])](50) stream = streamManager.registerNewStream(contentQueue) @@ -208,13 +196,18 @@ proc new*( tableIpLimits: portalConfig.tableIpLimits, bitsPerHop: portalConfig.bitsPerHop, radiusConfig: RadiusConfig(kind: Static, logRadius: 256), - disablePoke: portalConfig.disablePoke) + disablePoke: portalConfig.disablePoke, + ) portalProtocol = PortalProtocol.new( - baseProtocol, lightClientProtocolId, + baseProtocol, + lightClientProtocolId, toContentIdHandler, - createGetHandler(beaconDb), stream, bootstrapRecords, - config = portalConfigAdjusted) + createGetHandler(beaconDb), + stream, + bootstrapRecords, + config = portalConfigAdjusted, + ) portalProtocol.dbPut = createStoreHandler(beaconDb) @@ -222,21 +215,20 @@ proc new*( portalProtocol: portalProtocol, beaconDb: beaconDb, contentQueue: contentQueue, - forkDigests: forkDigests + forkDigests: forkDigests, ) proc validateContent( - n: BeaconNetwork, content: seq[byte], contentKey: ByteList): - Result[void, string] = + n: BeaconNetwork, content: seq[byte], contentKey: ByteList +): Result[void, string] = let key = contentKey.decode().valueOr: return err("Error decoding content key") - case key.contentType: + case key.contentType of unused: raiseAssert "Should not be used and fail at decoding" of lightClientBootstrap: - let decodingResult = decodeLightClientBootstrapForked( - n.forkDigests, content) + let decodingResult = decodeLightClientBootstrapForked(n.forkDigests, content) if decodingResult.isOk: # TODO: # Currently only verifying if the content can be decoded. @@ -252,10 +244,8 @@ proc validateContent( ok() else: err("Error decoding content: " & decodingResult.error) - of lightClientUpdate: - let decodingResult = decodeLightClientUpdatesByRange( - n.forkDigests, content) + let decodingResult = decodeLightClientUpdatesByRange(n.forkDigests, content) if decodingResult.isOk: # TODO: # Currently only verifying if the content can be decoded. @@ -264,39 +254,32 @@ proc validateContent( ok() else: err("Error decoding content: " & decodingResult.error) - of lightClientFinalityUpdate: - let update = decodeLightClientFinalityUpdateForked( - n.forkDigests, content).valueOr: - return err("Error decoding content: " & error) + let update = decodeLightClientFinalityUpdateForked(n.forkDigests, content).valueOr: + return err("Error decoding content: " & error) - let res = n.processor[].processLightClientFinalityUpdate( - MsgSource.gossip, update) + let res = n.processor[].processLightClientFinalityUpdate(MsgSource.gossip, update) if res.isErr(): err("Error processing update: " & $res.error[1]) else: ok() - of lightClientOptimisticUpdate: - let update = decodeLightClientOptimisticUpdateForked( - n.forkDigests, content).valueOr: - return err("Error decoding content: " & error) + let update = decodeLightClientOptimisticUpdateForked(n.forkDigests, content).valueOr: + return err("Error decoding content: " & error) - let res = n.processor[].processLightClientOptimisticUpdate( - MsgSource.gossip, update) + let res = n.processor[].processLightClientOptimisticUpdate(MsgSource.gossip, update) if res.isErr(): err("Error processing update: " & $res.error[1]) else: ok() of beacon_content.ContentType.historicalSummaries: - let summariesWithProof = ? decodeSsz(content, HistoricalSummariesWithProof) + let summariesWithProof = ?decodeSsz(content, HistoricalSummariesWithProof) n.validateHistoricalSummaries(summariesWithProof) proc validateContent( - n: BeaconNetwork, - contentKeys: ContentKeysList, - contentItems: seq[seq[byte]]): Future[bool] {.async.} = + n: BeaconNetwork, contentKeys: ContentKeysList, contentItems: seq[seq[byte]] +): Future[bool] {.async.} = # content passed here can have less items then contentKeys, but not more. for i, contentItem in contentItems: let @@ -312,7 +295,6 @@ proc validateContent( n.portalProtocol.storeContent(contentKey, contentId, contentItem) info "Received offered content validated successfully", contentKey - else: error "Received offered content failed validation", contentKey, error = validation.error @@ -323,8 +305,7 @@ proc validateContent( proc processContentLoop(n: BeaconNetwork) {.async.} = try: while true: - let (srcNodeId, contentKeys, contentItems) = - await n.contentQueue.popFirst() + let (srcNodeId, contentKeys, contentItems) = await n.contentQueue.popFirst() # When there is one invalid content item, all other content items are # dropped and not gossiped around. @@ -334,7 +315,6 @@ proc processContentLoop(n: BeaconNetwork) {.async.} = asyncSpawn n.portalProtocol.randomGossipDiscardPeers( srcNodeId, contentKeys, contentItems ) - except CancelledError: trace "processContentLoop canceled" diff --git a/fluffy/network/header/header_content.nim b/fluffy/network/header/header_content.nim index 9cb63cb37d..0577d76bf8 100644 --- a/fluffy/network/header/header_content.nim +++ b/fluffy/network/header/header_content.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -11,7 +11,8 @@ import std/options, - nimcrypto/[sha2, hash], stint, + nimcrypto/[sha2, hash], + stint, ssz_serialization, ../../common/common_types @@ -22,14 +23,13 @@ type # https://github.com/ethereum/portal-network-specs/blob/master/header-gossip-network.md#content-keys # But with Accumulator removed as per # https://github.com/ethereum/portal-network-specs/issues/153 - ContentType* = enum newBlockHeader = 0x00 # TODO: remove or fix this temporary # dummySelector per latest spec. # This is temporary workaround # to fool SSZ.isUnion - dummySelector = 0x01 + dummySelector = 0x01 NewBlockHeaderKey* = object blockHash*: BlockHash diff --git a/fluffy/network/history/accumulator.nim b/fluffy/network/history/accumulator.nim index 005794a3cb..9244b8d770 100644 --- a/fluffy/network/history/accumulator.nim +++ b/fluffy/network/history/accumulator.nim @@ -8,8 +8,10 @@ {.push raises: [].} import - eth/rlp, eth/common/eth_types_rlp, - ssz_serialization, ssz_serialization/[proofs, merkleization], + eth/rlp, + eth/common/eth_types_rlp, + ssz_serialization, + ssz_serialization/[proofs, merkleization], ../../common/common_types, ./history_content @@ -70,20 +72,19 @@ type func init*(T: type Accumulator): T = Accumulator( historicalEpochs: List[Bytes32, int(preMergeEpochs)].init(@[]), - currentEpoch: EpochAccumulator.init(@[]) + currentEpoch: EpochAccumulator.init(@[]), ) -func getEpochAccumulatorRoot*( - headerRecords: openArray[HeaderRecord] - ): Digest = +func getEpochAccumulatorRoot*(headerRecords: openArray[HeaderRecord]): Digest = let epochAccumulator = EpochAccumulator.init(@headerRecords) hash_tree_root(epochAccumulator) -func updateAccumulator*( - a: var Accumulator, header: BlockHeader) = - doAssert(header.blockNumber.truncate(uint64) < mergeBlockNumber, - "No post merge blocks for header accumulator") +func updateAccumulator*(a: var Accumulator, header: BlockHeader) = + doAssert( + header.blockNumber.truncate(uint64) < mergeBlockNumber, + "No post merge blocks for header accumulator", + ) let lastTotalDifficulty = if a.currentEpoch.len() == 0: @@ -101,10 +102,10 @@ func updateAccumulator*( doAssert(a.historicalEpochs.add(epochHash.data)) a.currentEpoch = EpochAccumulator.init(@[]) - let headerRecord = - HeaderRecord( - blockHash: header.blockHash(), - totalDifficulty: lastTotalDifficulty + header.difficulty) + let headerRecord = HeaderRecord( + blockHash: header.blockHash(), + totalDifficulty: lastTotalDifficulty + header.difficulty, + ) let res = a.currentEpoch.add(headerRecord) doAssert(res, "Can't fail because of currentEpoch length check") @@ -144,7 +145,8 @@ func isPreMerge*(header: BlockHeader): bool = isPreMerge(header.blockNumber.truncate(uint64)) func verifyProof( - a: FinishedAccumulator, header: BlockHeader, proof: openArray[Digest]): bool = + a: FinishedAccumulator, header: BlockHeader, proof: openArray[Digest] +): bool = let epochIndex = getEpochIndex(header) epochAccumulatorHash = Digest(data: a.historicalEpochs[epochIndex]) @@ -153,13 +155,13 @@ func verifyProof( headerRecordIndex = getHeaderRecordIndex(header, epochIndex) # TODO: Implement more generalized `get_generalized_index` - gIndex = GeneralizedIndex(epochSize*2*2 + (headerRecordIndex*2)) + gIndex = GeneralizedIndex(epochSize * 2 * 2 + (headerRecordIndex * 2)) verify_merkle_multiproof(@[leave], proof, @[gIndex], epochAccumulatorHash) func verifyAccumulatorProof*( - a: FinishedAccumulator, header: BlockHeader, proof: AccumulatorProof): - Result[void, string] = + a: FinishedAccumulator, header: BlockHeader, proof: AccumulatorProof +): Result[void, string] = if header.isPreMerge(): # Note: The proof is typed with correct depth, so no check on this is # required here. @@ -171,9 +173,9 @@ func verifyAccumulatorProof*( err("Cannot verify post merge header with accumulator proof") func verifyHeader*( - a: FinishedAccumulator, header: BlockHeader, proof: BlockHeaderProof): - Result[void, string] = - case proof.proofType: + a: FinishedAccumulator, header: BlockHeader, proof: BlockHeaderProof +): Result[void, string] = + case proof.proofType of BlockHeaderProofType.accumulatorProof: a.verifyAccumulatorProof(header, proof.accumulatorProof) of BlockHeaderProofType.none: @@ -189,9 +191,8 @@ func verifyHeader*( ok() func buildProof*( - header: BlockHeader, - epochAccumulator: EpochAccumulator | EpochAccumulatorCached): - Result[AccumulatorProof, string] = + header: BlockHeader, epochAccumulator: EpochAccumulator | EpochAccumulatorCached +): Result[AccumulatorProof, string] = doAssert(header.isPreMerge(), "Must be pre merge header") let @@ -199,33 +200,37 @@ func buildProof*( headerRecordIndex = getHeaderRecordIndex(header, epochIndex) # TODO: Implement more generalized `get_generalized_index` - gIndex = GeneralizedIndex(epochSize*2*2 + (headerRecordIndex*2)) + gIndex = GeneralizedIndex(epochSize * 2 * 2 + (headerRecordIndex * 2)) var proof: AccumulatorProof - ? epochAccumulator.build_proof(gIndex, proof) + ?epochAccumulator.build_proof(gIndex, proof) ok(proof) func buildHeaderWithProof*( - header: BlockHeader, - epochAccumulator: EpochAccumulator | EpochAccumulatorCached): - Result[BlockHeaderWithProof, string] = - let proof = ? buildProof(header, epochAccumulator) - - ok(BlockHeaderWithProof( - header: ByteList.init(rlp.encode(header)), - proof: BlockHeaderProof.init(proof))) + header: BlockHeader, epochAccumulator: EpochAccumulator | EpochAccumulatorCached +): Result[BlockHeaderWithProof, string] = + let proof = ?buildProof(header, epochAccumulator) + + ok( + BlockHeaderWithProof( + header: ByteList.init(rlp.encode(header)), proof: BlockHeaderProof.init(proof) + ) + ) func getBlockEpochDataForBlockNumber*( - a: FinishedAccumulator, bn: UInt256): Result[BlockEpochData, string] = + a: FinishedAccumulator, bn: UInt256 +): Result[BlockEpochData, string] = let blockNumber = bn.truncate(uint64) if blockNumber.isPreMerge: let epochIndex = getEpochIndex(blockNumber) - ok(BlockEpochData( - epochHash: a.historicalEpochs[epochIndex], - blockRelativeIndex: getHeaderRecordIndex(blockNumber, epochIndex)) + ok( + BlockEpochData( + epochHash: a.historicalEpochs[epochIndex], + blockRelativeIndex: getHeaderRecordIndex(blockNumber, epochIndex), ) + ) else: err("Block number is post merge: " & $blockNumber) diff --git a/fluffy/network/history/experimental/beacon_chain_block_proof.nim b/fluffy/network/history/experimental/beacon_chain_block_proof.nim index afd97063da..7d4c6c1dac 100644 --- a/fluffy/network/history/experimental/beacon_chain_block_proof.nim +++ b/fluffy/network/history/experimental/beacon_chain_block_proof.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -75,7 +75,8 @@ import stew/results, - ssz_serialization, ssz_serialization/[proofs, merkleization], + ssz_serialization, + ssz_serialization/[proofs, merkleization], beacon_chain/spec/eth2_ssz_serialization, beacon_chain/spec/datatypes/bellatrix @@ -108,7 +109,8 @@ func getBlockRootsIndex*(blockHeader: BeaconBlockHeader): uint64 = # Builds proof to be able to verify that the EL block hash is part of # BeaconBlockBody for given root. func buildProof*( - blockBody: bellatrix.BeaconBlockBody): Result[BeaconBlockBodyProof, string] = + blockBody: bellatrix.BeaconBlockBody +): Result[BeaconBlockBodyProof, string] = # 16 as there are 10 fields # 9 as index (pos) of field = 9 let gIndexTopLevel = (1 * 1 * 16 + 9) @@ -117,60 +119,62 @@ func buildProof*( let gIndex = GeneralizedIndex(gIndexTopLevel * 1 * 16 + 12) var proof: BeaconBlockBodyProof - ? blockBody.build_proof(gIndex, proof) + ?blockBody.build_proof(gIndex, proof) ok(proof) # Builds proof to be able to verify that the CL BlockBody root is part of # BeaconBlockHeader for given root. func buildProof*( - blockHeader: BeaconBlockHeader): Result[BeaconBlockHeaderProof, string] = + blockHeader: BeaconBlockHeader +): Result[BeaconBlockHeaderProof, string] = # 5th field of container with 5 fields -> 7 + 5 let gIndex = GeneralizedIndex(12) var proof: BeaconBlockHeaderProof - ? blockHeader.build_proof(gIndex, proof) + ?blockHeader.build_proof(gIndex, proof) ok(proof) # Builds proof to be able to verify that a BeaconBlock root is part of the # HistoricalBatch for given root. func buildProof*( - batch: HistoricalBatch, blockRootIndex: uint64): - Result[HistoricalRootsProof, string] = + batch: HistoricalBatch, blockRootIndex: uint64 +): Result[HistoricalRootsProof, string] = # max list size * 2 is start point of leaves let gIndex = GeneralizedIndex(2 * SLOTS_PER_HISTORICAL_ROOT + blockRootIndex) var proof: HistoricalRootsProof - ? batch.build_proof(gIndex, proof) + ?batch.build_proof(gIndex, proof) ok(proof) func buildProof*( batch: HistoricalBatch, blockHeader: BeaconBlockHeader, - blockBody: bellatrix.BeaconBlockBody): - Result[BeaconChainBlockProof, string] = + blockBody: bellatrix.BeaconBlockBody, +): Result[BeaconChainBlockProof, string] = let blockRootIndex = getBlockRootsIndex(blockHeader) - beaconBlockBodyProof = ? blockBody.buildProof() - beaconBlockHeaderProof = ? blockHeader.buildProof() - historicalRootsProof = ? batch.buildProof(blockRootIndex) - - ok(BeaconChainBlockProof( - beaconBlockBodyProof: beaconBlockBodyProof, - beaconBlockBodyRoot: hash_tree_root(blockBody), - beaconBlockHeaderProof: beaconBlockHeaderProof, - beaconBlockHeaderRoot: hash_tree_root(blockHeader), - historicalRootsProof: historicalRootsProof, - slot: blockHeader.slot - )) + beaconBlockBodyProof = ?blockBody.buildProof() + beaconBlockHeaderProof = ?blockHeader.buildProof() + historicalRootsProof = ?batch.buildProof(blockRootIndex) + + ok( + BeaconChainBlockProof( + beaconBlockBodyProof: beaconBlockBodyProof, + beaconBlockBodyRoot: hash_tree_root(blockBody), + beaconBlockHeaderProof: beaconBlockHeaderProof, + beaconBlockHeaderRoot: hash_tree_root(blockHeader), + historicalRootsProof: historicalRootsProof, + slot: blockHeader.slot, + ) + ) func verifyProof*( - blockHash: Digest, - proof: BeaconBlockBodyProof, - blockBodyRoot: Digest): bool = + blockHash: Digest, proof: BeaconBlockBodyProof, blockBodyRoot: Digest +): bool = let gIndexTopLevel = (1 * 1 * 16 + 9) gIndex = GeneralizedIndex(gIndexTopLevel * 1 * 16 + 12) @@ -178,9 +182,8 @@ func verifyProof*( verify_merkle_multiproof(@[blockHash], proof, @[gIndex], blockBodyRoot) func verifyProof*( - blockBodyRoot: Digest, - proof: BeaconBlockHeaderProof, - blockHeaderRoot: Digest): bool = + blockBodyRoot: Digest, proof: BeaconBlockHeaderProof, blockHeaderRoot: Digest +): bool = let gIndex = GeneralizedIndex(12) verify_merkle_multiproof(@[blockBodyRoot], proof, @[gIndex], blockHeaderRoot) @@ -189,7 +192,8 @@ func verifyProof*( blockHeaderRoot: Digest, proof: HistoricalRootsProof, historicalRoot: Digest, - blockRootIndex: uint64): bool = + blockRootIndex: uint64, +): bool = let gIndex = GeneralizedIndex(2 * SLOTS_PER_HISTORICAL_ROOT + blockRootIndex) verify_merkle_multiproof(@[blockHeaderRoot], proof, @[gIndex], historicalRoot) @@ -197,15 +201,16 @@ func verifyProof*( func verifyProof*( historical_roots: HashList[Eth2Digest, Limit HISTORICAL_ROOTS_LIMIT], proof: BeaconChainBlockProof, - blockHash: Digest): bool = + blockHash: Digest, +): bool = let historicalRootsIndex = getHistoricalRootsIndex(proof.slot) blockRootIndex = getBlockRootsIndex(proof.slot) - blockHash.verifyProof( - proof.beaconBlockBodyProof, proof.beaconBlockBodyRoot) and + blockHash.verifyProof(proof.beaconBlockBodyProof, proof.beaconBlockBodyRoot) and proof.beaconBlockBodyRoot.verifyProof( - proof.beaconBlockHeaderProof, proof.beaconBlockHeaderRoot) and + proof.beaconBlockHeaderProof, proof.beaconBlockHeaderRoot + ) and proof.beaconBlockHeaderRoot.verifyProof( - proof.historicalRootsProof, historical_roots[historicalRootsIndex], - blockRootIndex) + proof.historicalRootsProof, historical_roots[historicalRootsIndex], blockRootIndex + ) diff --git a/fluffy/network/history/experimental/beacon_chain_block_proof_capella.nim b/fluffy/network/history/experimental/beacon_chain_block_proof_capella.nim index f0734d06cf..5f00140784 100644 --- a/fluffy/network/history/experimental/beacon_chain_block_proof_capella.nim +++ b/fluffy/network/history/experimental/beacon_chain_block_proof_capella.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2023 Status Research & Development GmbH +# Copyright (c) 2023-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -25,7 +25,8 @@ import stew/results, - ssz_serialization, ssz_serialization/[proofs, merkleization], + ssz_serialization, + ssz_serialization/[proofs, merkleization], beacon_chain/spec/eth2_ssz_serialization, beacon_chain/spec/presets, beacon_chain/spec/datatypes/capella @@ -48,7 +49,8 @@ func getHistoricalRootsIndex*(slot: Slot, cfg: RuntimeConfig): uint64 = (slot - cfg.CAPELLA_FORK_EPOCH * SLOTS_PER_EPOCH) div SLOTS_PER_HISTORICAL_ROOT func getHistoricalRootsIndex*( - blockHeader: BeaconBlockHeader, cfg: RuntimeConfig): uint64 = + blockHeader: BeaconBlockHeader, cfg: RuntimeConfig +): uint64 = getHistoricalRootsIndex(blockHeader.slot, cfg) func getBlockRootsIndex*(slot: Slot): uint64 = @@ -60,7 +62,8 @@ func getBlockRootsIndex*(blockHeader: BeaconBlockHeader): uint64 = # Builds proof to be able to verify that the EL block hash is part of # BeaconBlockBody for given root. func buildProof*( - blockBody: capella.BeaconBlockBody): Result[BeaconBlockBodyProof, string] = + blockBody: capella.BeaconBlockBody +): Result[BeaconBlockBodyProof, string] = # 16 as there are 10 fields # 9 as index (pos) of field = 9 let gIndexTopLevel = (1 * 1 * 16 + 9) @@ -69,33 +72,33 @@ func buildProof*( let gIndex = GeneralizedIndex(gIndexTopLevel * 1 * 16 + 12) var proof: BeaconBlockBodyProof - ? blockBody.build_proof(gIndex, proof) + ?blockBody.build_proof(gIndex, proof) ok(proof) # Builds proof to be able to verify that the CL BlockBody root is part of # BeaconBlockHeader for given root. func buildProof*( - blockHeader: BeaconBlockHeader): Result[BeaconBlockHeaderProof, string] = + blockHeader: BeaconBlockHeader +): Result[BeaconBlockHeaderProof, string] = # 5th field of container with 5 fields -> 7 + 5 let gIndex = GeneralizedIndex(12) var proof: BeaconBlockHeaderProof - ? blockHeader.build_proof(gIndex, proof) + ?blockHeader.build_proof(gIndex, proof) ok(proof) # Builds proof to be able to verify that a BeaconBlock root is part of the # block_roots for given root. func buildProof*( - blockRoots: array[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest], - blockRootIndex: uint64): - Result[HistoricalSummariesProof, string] = + blockRoots: array[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest], blockRootIndex: uint64 +): Result[HistoricalSummariesProof, string] = # max list size * 1 is start point of leaves let gIndex = GeneralizedIndex(SLOTS_PER_HISTORICAL_ROOT + blockRootIndex) var proof: HistoricalSummariesProof - ? blockRoots.build_proof(gIndex, proof) + ?blockRoots.build_proof(gIndex, proof) ok(proof) @@ -105,28 +108,29 @@ func buildProof*( blockRoots: array[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest], blockHeader: BeaconBlockHeader, blockBody: capella.BeaconBlockBody, - cfg: RuntimeConfig): - Result[BeaconChainBlockProof, string] = + cfg: RuntimeConfig, +): Result[BeaconChainBlockProof, string] = let blockRootIndex = getBlockRootsIndex(blockHeader) - beaconBlockBodyProof = ? blockBody.buildProof() - beaconBlockHeaderProof = ? blockHeader.buildProof() - historicalSummariesProof = ? blockRoots.buildProof(blockRootIndex) - - ok(BeaconChainBlockProof( - beaconBlockBodyProof: beaconBlockBodyProof, - beaconBlockBodyRoot: hash_tree_root(blockBody), - beaconBlockHeaderProof: beaconBlockHeaderProof, - beaconBlockHeaderRoot: hash_tree_root(blockHeader), - historicalSummariesProof: historicalSummariesProof, - slot: blockHeader.slot - )) + beaconBlockBodyProof = ?blockBody.buildProof() + beaconBlockHeaderProof = ?blockHeader.buildProof() + historicalSummariesProof = ?blockRoots.buildProof(blockRootIndex) + + ok( + BeaconChainBlockProof( + beaconBlockBodyProof: beaconBlockBodyProof, + beaconBlockBodyRoot: hash_tree_root(blockBody), + beaconBlockHeaderProof: beaconBlockHeaderProof, + beaconBlockHeaderRoot: hash_tree_root(blockHeader), + historicalSummariesProof: historicalSummariesProof, + slot: blockHeader.slot, + ) + ) func verifyProof*( - blockHash: Digest, - proof: BeaconBlockBodyProof, - blockBodyRoot: Digest): bool = + blockHash: Digest, proof: BeaconBlockBodyProof, blockBodyRoot: Digest +): bool = let gIndexTopLevel = (1 * 1 * 16 + 9) gIndex = GeneralizedIndex(gIndexTopLevel * 1 * 16 + 12) @@ -134,9 +138,8 @@ func verifyProof*( verify_merkle_multiproof(@[blockHash], proof, @[gIndex], blockBodyRoot) func verifyProof*( - blockBodyRoot: Digest, - proof: BeaconBlockHeaderProof, - blockHeaderRoot: Digest): bool = + blockBodyRoot: Digest, proof: BeaconBlockHeaderProof, blockHeaderRoot: Digest +): bool = let gIndex = GeneralizedIndex(12) verify_merkle_multiproof(@[blockBodyRoot], proof, @[gIndex], blockHeaderRoot) @@ -145,7 +148,8 @@ func verifyProof*( blockHeaderRoot: Digest, proof: HistoricalSummariesProof, historicalRoot: Digest, - blockRootIndex: uint64): bool = + blockRootIndex: uint64, +): bool = let gIndex = GeneralizedIndex(SLOTS_PER_HISTORICAL_ROOT + blockRootIndex) verify_merkle_multiproof(@[blockHeaderRoot], proof, @[gIndex], historicalRoot) @@ -154,16 +158,18 @@ func verifyProof*( historical_summaries: HashList[HistoricalSummary, Limit HISTORICAL_ROOTS_LIMIT], proof: BeaconChainBlockProof, blockHash: Digest, - cfg: RuntimeConfig): bool = + cfg: RuntimeConfig, +): bool = let historicalRootsIndex = getHistoricalRootsIndex(proof.slot, cfg) blockRootIndex = getBlockRootsIndex(proof.slot) - blockHash.verifyProof( - proof.beaconBlockBodyProof, proof.beaconBlockBodyRoot) and + blockHash.verifyProof(proof.beaconBlockBodyProof, proof.beaconBlockBodyRoot) and proof.beaconBlockBodyRoot.verifyProof( - proof.beaconBlockHeaderProof, proof.beaconBlockHeaderRoot) and + proof.beaconBlockHeaderProof, proof.beaconBlockHeaderRoot + ) and proof.beaconBlockHeaderRoot.verifyProof( proof.historicalSummariesProof, historical_summaries[historicalRootsIndex].block_summary_root, - blockRootIndex) + blockRootIndex, + ) diff --git a/fluffy/network/history/experimental/beacon_chain_historical_roots.nim b/fluffy/network/history/experimental/beacon_chain_historical_roots.nim index 902d2f0d63..670950374f 100644 --- a/fluffy/network/history/experimental/beacon_chain_historical_roots.nim +++ b/fluffy/network/history/experimental/beacon_chain_historical_roots.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2023 Status Research & Development GmbH +# Copyright (c) 2023-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -20,10 +20,7 @@ {.push raises: [].} -import - stew/results, - beacon_chain/spec/forks, - beacon_chain/spec/datatypes/bellatrix +import stew/results, beacon_chain/spec/forks, beacon_chain/spec/datatypes/bellatrix export results @@ -34,20 +31,18 @@ type historical_roots: HistoricalRoots proof: HistoricalRootsProof -func buildProof*( - state: ForkedHashedBeaconState): Result[HistoricalRootsProof, string] = +func buildProof*(state: ForkedHashedBeaconState): Result[HistoricalRootsProof, string] = let gIndex = GeneralizedIndex(39) # 31 + 8 = 39 var proof: HistoricalRootsProof withState(state): - ? forkyState.data.build_proof(gIndex, proof) + ?forkyState.data.build_proof(gIndex, proof) ok(proof) func verifyProof*( - historical_roots: HistoricalRoots, - proof: HistoricalRootsProof, - stateRoot: Digest): bool = + historical_roots: HistoricalRoots, proof: HistoricalRootsProof, stateRoot: Digest +): bool = let gIndex = GeneralizedIndex(39) leave = hash_tree_root(historical_roots) diff --git a/fluffy/network/history/history_content.nim b/fluffy/network/history/history_content.nim index 462e1f8061..5c6129d595 100644 --- a/fluffy/network/history/history_content.nim +++ b/fluffy/network/history/history_content.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2021-2023 Status Research & Development GmbH +# Copyright (c) 2021-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -11,7 +11,9 @@ import std/math, - nimcrypto/[sha2, hash], stew/[byteutils, results], stint, + nimcrypto/[sha2, hash], + stew/[byteutils, results], + stint, ssz_serialization, ../../common/common_types @@ -40,7 +42,8 @@ type blockHash*: BlockHash EpochAccumulatorKey* = object - epochHash*: Digest # TODO: Perhaps this should be called epochRoot in the spec instead + epochHash*: Digest + # TODO: Perhaps this should be called epochRoot in the spec instead ContentKey* = object case contentType*: ContentType @@ -53,23 +56,19 @@ type of epochAccumulator: epochAccumulatorKey*: EpochAccumulatorKey -func init*( - T: type ContentKey, contentType: ContentType, - hash: BlockHash | Digest): T = +func init*(T: type ContentKey, contentType: ContentType, hash: BlockHash | Digest): T = case contentType of blockHeader: - ContentKey( - contentType: contentType, blockHeaderKey: BlockKey(blockHash: hash)) + ContentKey(contentType: contentType, blockHeaderKey: BlockKey(blockHash: hash)) of blockBody: - ContentKey( - contentType: contentType, blockBodyKey: BlockKey(blockHash: hash)) + ContentKey(contentType: contentType, blockBodyKey: BlockKey(blockHash: hash)) of receipts: - ContentKey( - contentType: contentType, receiptsKey: BlockKey(blockHash: hash)) + ContentKey(contentType: contentType, receiptsKey: BlockKey(blockHash: hash)) of epochAccumulator: ContentKey( contentType: contentType, - epochAccumulatorKey: EpochAccumulatorKey(epochHash: hash)) + epochAccumulatorKey: EpochAccumulatorKey(epochHash: hash), + ) func encode*(contentKey: ContentKey): ByteList = ByteList.init(SSZ.encode(contentKey)) @@ -97,7 +96,7 @@ func `$`*(x: BlockKey): string = func `$`*(x: ContentKey): string = var res = "(type: " & $x.contentType & ", " - case x.contentType: + case x.contentType of blockHeader: res.add($x.blockHeaderKey) of blockBody: @@ -115,11 +114,11 @@ func `$`*(x: ContentKey): string = ## Types for history network content const - MAX_TRANSACTION_LENGTH* = 2^24 # ~= 16 million - MAX_TRANSACTION_COUNT* = 2^14 # ~= 16k - MAX_RECEIPT_LENGTH* = 2^27 # ~= 134 million - MAX_HEADER_LENGTH = 2^13 # = 8192 - MAX_ENCODED_UNCLES_LENGTH* = MAX_HEADER_LENGTH * 2^4 # = 2**17 ~= 131k + MAX_TRANSACTION_LENGTH* = 2 ^ 24 # ~= 16 million + MAX_TRANSACTION_COUNT* = 2 ^ 14 # ~= 16k + MAX_RECEIPT_LENGTH* = 2 ^ 27 # ~= 134 million + MAX_HEADER_LENGTH = 2 ^ 13 # = 8192 + MAX_ENCODED_UNCLES_LENGTH* = MAX_HEADER_LENGTH * 2 ^ 4 # = 2**17 ~= 131k MAX_WITHDRAWAL_LENGTH = 64 MAX_WITHDRAWALS_COUNT = MAX_WITHDRAWALS_PER_PAYLOAD diff --git a/fluffy/network/history/history_network.nim b/fluffy/network/history/history_network.nim index 49b3243f7c..a02e97d2aa 100644 --- a/fluffy/network/history/history_network.nim +++ b/fluffy/network/history/history_network.nim @@ -1,5 +1,5 @@ # Fluffy -# Copyright (c) 2021-2023 Status Research & Development GmbH +# Copyright (c) 2021-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -8,7 +8,9 @@ {.push raises: [].} import - stew/results, chronos, chronicles, + stew/results, + chronos, + chronicles, eth/[common/eth_types_rlp, rlp], eth/p2p/discoveryv5/[protocol, enr], ../../common/common_types, @@ -46,8 +48,7 @@ export accumulator proc `$`(x: BlockHeader): string = $x -const - historyProtocolId* = [byte 0x50, 0x0B] +const historyProtocolId* = [byte 0x50, 0x0B] type HistoryNetwork* = ref object @@ -66,8 +67,8 @@ func toContentIdHandler(contentKey: ByteList): results.Opt[ContentId] = ## Calls to go from SSZ decoded Portal types to RLP fully decoded EL types func fromPortalBlockBody*( - T: type BlockBody, body: PortalBlockBodyLegacy): - Result[T, string] = + T: type BlockBody, body: PortalBlockBodyLegacy +): Result[T, string] = ## Get the EL BlockBody from the SSZ-decoded `PortalBlockBodyLegacy`. try: var transactions: seq[Transaction] @@ -81,7 +82,8 @@ func fromPortalBlockBody*( err("RLP decoding failed: " & e.msg) func fromPortalBlockBody*( - T: type BlockBody, body: PortalBlockBodyShanghai): Result[T, string] = + T: type BlockBody, body: PortalBlockBodyShanghai +): Result[T, string] = ## Get the EL BlockBody from the SSZ-decoded `PortalBlockBodyShanghai`. try: var transactions: seq[Transaction] @@ -92,17 +94,19 @@ func fromPortalBlockBody*( for w in body.withdrawals: withdrawals.add(rlp.decode(w.asSeq(), Withdrawal)) - ok(BlockBody( - transactions: transactions, - uncles: @[], # Uncles must be empty: TODO where validation? - withdrawals: some(withdrawals))) + ok( + BlockBody( + transactions: transactions, + uncles: @[], # Uncles must be empty: TODO where validation? + withdrawals: some(withdrawals), + ) + ) except RlpError as e: err("RLP decoding failed: " & e.msg) func fromPortalBlockBodyOrRaise*( - T: type BlockBody, - body: PortalBlockBodyLegacy | PortalBlockBodyShanghai): - T = + T: type BlockBody, body: PortalBlockBodyLegacy | PortalBlockBodyShanghai +): T = ## Get the EL BlockBody from one of the SSZ-decoded Portal BlockBody types. ## Will raise Assertion in case of invalid RLP encodings. Only use of data ## has been validated before! @@ -114,7 +118,8 @@ func fromPortalBlockBodyOrRaise*( raiseAssert(res.error) func fromPortalReceipts*( - T: type seq[Receipt], receipts: PortalReceipts): Result[T, string] = + T: type seq[Receipt], receipts: PortalReceipts +): Result[T, string] = ## Get the full decoded EL seq[Receipt] from the SSZ-decoded `PortalReceipts`. try: var res: seq[Receipt] @@ -152,7 +157,9 @@ func fromBlockBody(T: type PortalBlockBodyShanghai, body: BlockBody): T = var withdrawals: Withdrawals for w in body.withdrawals.get(): discard withdrawals.add(WithdrawalByteList(rlp.encode(w))) - PortalBlockBodyShanghai(transactions: transactions, uncles: uncles, withdrawals: withdrawals) + PortalBlockBodyShanghai( + transactions: transactions, uncles: uncles, withdrawals: withdrawals + ) func fromReceipts*(T: type PortalReceipts, receipts: seq[Receipt]): T = var portalReceipts: PortalReceipts @@ -203,9 +210,9 @@ template calcWithdrawalsRoot*(receipts: Withdrawals): Hash256 = calcRootHash(receipts) func validateBlockHeaderBytes*( - bytes: openArray[byte], hash: BlockHash): Result[BlockHeader, string] = - - let header = ? decodeRlp(bytes, BlockHeader) + bytes: openArray[byte], hash: BlockHash +): Result[BlockHeader, string] = + let header = ?decodeRlp(bytes, BlockHeader) # Note: # One could do additional quick-checks here such as timestamp vs the optional @@ -222,8 +229,8 @@ func validateBlockHeaderBytes*( ok(header) proc validateBlockBody( - body: PortalBlockBodyLegacy, header: BlockHeader): - Result[void, string] = + body: PortalBlockBodyLegacy, header: BlockHeader +): Result[void, string] = ## Validate the block body against the txRoot and ommersHash from the header. let calculatedOmmersHash = keccakHash(body.uncles.asSeq()) if calculatedOmmersHash != header.ommersHash: @@ -231,14 +238,16 @@ proc validateBlockBody( let calculatedTxsRoot = calcTxsRoot(body.transactions) if calculatedTxsRoot != header.txRoot: - return err("Invalid transactions root: expected " & - $header.txRoot & " - got " & $calculatedTxsRoot) + return err( + "Invalid transactions root: expected " & $header.txRoot & " - got " & + $calculatedTxsRoot + ) ok() proc validateBlockBody( - body: PortalBlockBodyShanghai, header: BlockHeader): - Result[void, string] = + body: PortalBlockBodyShanghai, header: BlockHeader +): Result[void, string] = ## Validate the block body against the txRoot, ommersHash and withdrawalsRoot ## from the header. # Shortcut the ommersHash calculation as uncles must be an RLP encoded @@ -248,8 +257,10 @@ proc validateBlockBody( let calculatedTxsRoot = calcTxsRoot(body.transactions) if calculatedTxsRoot != header.txRoot: - return err("Invalid transactions root: expected " & - $header.txRoot & " - got " & $calculatedTxsRoot) + return err( + "Invalid transactions root: expected " & $header.txRoot & " - got " & + $calculatedTxsRoot + ) # TODO: This check is done higher up but perhaps this can become cleaner with # some refactor. @@ -259,8 +270,10 @@ proc validateBlockBody( calculatedWithdrawalsRoot = calcWithdrawalsRoot(body.withdrawals) headerWithdrawalsRoot = header.withdrawalsRoot.get() if calculatedWithdrawalsRoot != headerWithdrawalsRoot: - return err("Invalid withdrawals root: expected " & - $headerWithdrawalsRoot & " - got " & $calculatedWithdrawalsRoot) + return err( + "Invalid withdrawals root: expected " & $headerWithdrawalsRoot & " - got " & + $calculatedWithdrawalsRoot + ) ok() @@ -273,8 +286,8 @@ proc decodeBlockBodyBytes*(bytes: openArray[byte]): Result[BlockBody, string] = err("All Portal block body decodings failed") proc validateBlockBodyBytes*( - bytes: openArray[byte], header: BlockHeader): - Result[BlockBody, string] = + bytes: openArray[byte], header: BlockHeader +): Result[BlockBody, string] = ## Fully decode the SSZ encoded Portal Block Body and validate it against the ## header. ## TODO: improve this decoding in combination with the block body validation @@ -289,8 +302,8 @@ proc validateBlockBodyBytes*( elif header.ommersHash != EMPTY_UNCLE_HASH: return err("Expected empty uncles for a Shanghai block") else: - let body = ? decodeSsz(bytes, PortalBlockBodyShanghai) - ? validateBlockBody(body, header) + let body = ?decodeSsz(bytes, PortalBlockBodyShanghai) + ?validateBlockBody(body, header) BlockBody.fromPortalBlockBody(body) elif isPoSBlock(chainConfig, header.blockNumber.truncate(uint64)): if header.withdrawalsRoot.isSome(): @@ -298,19 +311,20 @@ proc validateBlockBodyBytes*( elif header.ommersHash != EMPTY_UNCLE_HASH: return err("Expected empty uncles for a PoS block") else: - let body = ? decodeSsz(bytes, PortalBlockBodyLegacy) - ? validateBlockBody(body, header) + let body = ?decodeSsz(bytes, PortalBlockBodyLegacy) + ?validateBlockBody(body, header) BlockBody.fromPortalBlockBody(body) else: if header.withdrawalsRoot.isSome(): return err("Expected no withdrawalsRoot for pre Shanghai block") else: - let body = ? decodeSsz(bytes, PortalBlockBodyLegacy) - ? validateBlockBody(body, header) + let body = ?decodeSsz(bytes, PortalBlockBodyLegacy) + ?validateBlockBody(body, header) BlockBody.fromPortalBlockBody(body) proc validateReceipts*( - receipts: PortalReceipts, receiptsRoot: KeccakHash): Result[void, string] = + receipts: PortalReceipts, receiptsRoot: KeccakHash +): Result[void, string] = let calculatedReceiptsRoot = calcReceiptsRoot(receipts) if calculatedReceiptsRoot != receiptsRoot: @@ -319,12 +333,12 @@ proc validateReceipts*( return ok() proc validateReceiptsBytes*( - bytes: openArray[byte], - receiptsRoot: KeccakHash): Result[seq[Receipt], string] = + bytes: openArray[byte], receiptsRoot: KeccakHash +): Result[seq[Receipt], string] = ## Fully decode the SSZ Block Body and validate it against the header. - let receipts = ? decodeSsz(bytes, PortalReceipts) + let receipts = ?decodeSsz(bytes, PortalReceipts) - ? validateReceipts(receipts, receiptsRoot) + ?validateReceipts(receipts, receiptsRoot) seq[Receipt].fromPortalReceipts(receipts) @@ -347,8 +361,9 @@ proc get(db: ContentDB, T: type BlockHeader, contentId: ContentId): Opt[T] = else: Opt.none(T) -proc get(db: ContentDB, T: type BlockBody, contentId: ContentId, - header: BlockHeader): Opt[T] = +proc get( + db: ContentDB, T: type BlockBody, contentId: ContentId, header: BlockHeader +): Opt[T] = let encoded = db.get(contentId).valueOr: return Opt.none(T) @@ -357,13 +372,16 @@ proc get(db: ContentDB, T: type BlockBody, contentId: ContentId, body = if isShanghai(chainConfig, timestamp): BlockBody.fromPortalBlockBodyOrRaise( - decodeSszOrRaise(encoded, PortalBlockBodyShanghai)) + decodeSszOrRaise(encoded, PortalBlockBodyShanghai) + ) elif isPoSBlock(chainConfig, header.blockNumber.truncate(uint64)): BlockBody.fromPortalBlockBodyOrRaise( - decodeSszOrRaise(encoded, PortalBlockBodyLegacy)) + decodeSszOrRaise(encoded, PortalBlockBodyLegacy) + ) else: BlockBody.fromPortalBlockBodyOrRaise( - decodeSszOrRaise(encoded, PortalBlockBodyLegacy)) + decodeSszOrRaise(encoded, PortalBlockBodyLegacy) + ) Opt.some(body) @@ -378,18 +396,15 @@ proc get(db: ContentDB, T: type seq[Receipt], contentId: ContentId): Opt[T] = else: Opt.none(T) -proc get( - db: ContentDB, T: type EpochAccumulator, contentId: ContentId): Opt[T] = +proc get(db: ContentDB, T: type EpochAccumulator, contentId: ContentId): Opt[T] = db.getSszDecoded(contentId, T) -proc getContentFromDb( - n: HistoryNetwork, T: type, contentId: ContentId): Opt[T] = +proc getContentFromDb(n: HistoryNetwork, T: type, contentId: ContentId): Opt[T] = if n.portalProtocol.inRange(contentId): n.contentDB.get(T, contentId) else: Opt.none(T) - ## Public API to get the history network specific types, either from database ## or through a lookup on the Portal Network @@ -403,13 +418,13 @@ const requestRetries = 4 # however that response is not yet validated at that moment. func verifyHeader( - n: HistoryNetwork, header: BlockHeader, proof: BlockHeaderProof): - Result[void, string] = + n: HistoryNetwork, header: BlockHeader, proof: BlockHeaderProof +): Result[void, string] = verifyHeader(n.accumulator, header, proof) proc getVerifiedBlockHeader*( - n: HistoryNetwork, hash: BlockHash): - Future[Opt[BlockHeader]] {.async.} = + n: HistoryNetwork, hash: BlockHash +): Future[Opt[BlockHeader]] {.async.} = let contentKey = ContentKey.init(blockHeader, hash).encode() contentId = contentKey.toContentId() @@ -426,20 +441,17 @@ proc getVerifiedBlockHeader*( info "Fetched block header from database" return headerFromDb - for i in 0.. 0 and data[0] == ord(unused): raise newException(MalformedSszError, "SSZ selector is unused value") @@ -152,43 +152,47 @@ func toContentId*(contentKey: ByteList): ContentId = func toContentId*(contentKey: ContentKey): ContentId = toContentId(encode(contentKey)) -func offerContentToRetrievalContent*(offerContent: OfferContentValue): RetrievalContentValue = - case offerContent.contentType: - of unused: - raiseAssert "Converting content with unused content type" - of accountTrieNode: - RetrievalContentValue( - contentType: accountTrieNode, - accountTrieNode: AccountTrieNodeRetrieval(node: offerContent.accountTrieNode.proof[^1]) - ) # TODO implement properly - of contractTrieNode: - RetrievalContentValue( - contentType: contractTrieNode, - contractTrieNode: ContractTrieNodeRetrieval(node: offerContent.contractTrieNode.storageProof[^1]) - ) # TODO implement properly - of contractCode: - RetrievalContentValue( - contentType: contractCode, - contractCode: ContractCodeRetrieval(code: offerContent.contractCode.code) - ) +func offerContentToRetrievalContent*( + offerContent: OfferContentValue +): RetrievalContentValue = + case offerContent.contentType + of unused: + raiseAssert "Converting content with unused content type" + of accountTrieNode: + RetrievalContentValue( + contentType: accountTrieNode, + accountTrieNode: + AccountTrieNodeRetrieval(node: offerContent.accountTrieNode.proof[^1]), + ) # TODO implement properly + of contractTrieNode: + RetrievalContentValue( + contentType: contractTrieNode, + contractTrieNode: + ContractTrieNodeRetrieval(node: offerContent.contractTrieNode.storageProof[^1]), + ) # TODO implement properly + of contractCode: + RetrievalContentValue( + contentType: contractCode, + contractCode: ContractCodeRetrieval(code: offerContent.contractCode.code), + ) func encode*(content: RetrievalContentValue): seq[byte] = - case content.contentType: - of unused: - raiseAssert "Encoding content with unused content type" - of accountTrieNode: - SSZ.encode(content.accountTrieNode) - of contractTrieNode: - SSZ.encode(content.contractTrieNode) - of contractCode: - SSZ.encode(content.contractCode) + case content.contentType + of unused: + raiseAssert "Encoding content with unused content type" + of accountTrieNode: + SSZ.encode(content.accountTrieNode) + of contractTrieNode: + SSZ.encode(content.contractTrieNode) + of contractCode: + SSZ.encode(content.contractCode) func packNibbles*(nibbles: seq[byte]): Nibbles = doAssert(nibbles.len() <= MAX_UNPACKED_NIBBLES_LEN, "Can't pack more than 64 nibbles") if nibbles.len() == 0: return Nibbles(@[byte(0x00)]) - + let isOddLength = (nibbles.len() %% 2 == 1) outputLength = (nibbles.len() + 1) div 2 @@ -234,4 +238,3 @@ func unpackNibbles*(nibbles: Nibbles): seq[byte] = output.add(second) output - diff --git a/fluffy/network/state/state_network.nim b/fluffy/network/state/state_network.nim index 8631094f23..a22fcf5e87 100644 --- a/fluffy/network/state/state_network.nim +++ b/fluffy/network/state/state_network.nim @@ -6,7 +6,9 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - stew/results, chronos, chronicles, + stew/results, + chronos, + chronicles, eth/common/eth_hash, eth/common, eth/p2p/discoveryv5/[protocol, enr], @@ -17,8 +19,7 @@ import logScope: topics = "portal_state" -const - stateProtocolId* = [byte 0x50, 0x0A] +const stateProtocolId* = [byte 0x50, 0x0A] type StateNetwork* = ref object portalProtocol*: PortalProtocol @@ -29,12 +30,15 @@ type StateNetwork* = ref object func toContentIdHandler(contentKey: ByteList): results.Opt[ContentId] = ok(toContentId(contentKey)) -func decodeKV*(contentKey: ByteList, contentValue: seq[byte]): Opt[(ContentKey, OfferContentValue)] = +func decodeKV*( + contentKey: ByteList, contentValue: seq[byte] +): Opt[(ContentKey, OfferContentValue)] = const empty = Opt.none((ContentKey, OfferContentValue)) let key = contentKey.decode().valueOr: return empty - value = case key.contentType: + value = + case key.contentType of unused: return empty of accountTrieNode: @@ -52,62 +56,59 @@ func decodeKV*(contentKey: ByteList, contentValue: seq[byte]): Opt[(ContentKey, Opt.some((key, value)) -func decodeValue*(contentKey: ContentKey, contentValue: seq[byte]): Opt[RetrievalContentValue] = +func decodeValue*( + contentKey: ContentKey, contentValue: seq[byte] +): Opt[RetrievalContentValue] = const empty = Opt.none(RetrievalContentValue) - let - value = case contentKey.contentType: - of unused: + let value = + case contentKey.contentType + of unused: + return empty + of accountTrieNode: + let val = decodeSsz(contentValue, AccountTrieNodeRetrieval).valueOr: return empty - of accountTrieNode: - let val = decodeSsz(contentValue, AccountTrieNodeRetrieval).valueOr: - return empty - RetrievalContentValue(contentType: accountTrieNode, accountTrieNode: val) - of contractTrieNode: - let val = decodeSsz(contentValue, ContractTrieNodeRetrieval).valueOr: - return empty - RetrievalContentValue(contentType: contractTrieNode, contractTrieNode: val) - of contractCode: - let val = decodeSsz(contentValue, ContractCodeRetrieval).valueOr: - return empty - RetrievalContentValue(contentType: contractCode, contractCode: val) + RetrievalContentValue(contentType: accountTrieNode, accountTrieNode: val) + of contractTrieNode: + let val = decodeSsz(contentValue, ContractTrieNodeRetrieval).valueOr: + return empty + RetrievalContentValue(contentType: contractTrieNode, contractTrieNode: val) + of contractCode: + let val = decodeSsz(contentValue, ContractCodeRetrieval).valueOr: + return empty + RetrievalContentValue(contentType: contractCode, contractCode: val) Opt.some(value) proc validateAccountTrieNode( - n: StateNetwork, - key: ContentKey, - contentValue: RetrievalContentValue): bool = - true + n: StateNetwork, key: ContentKey, contentValue: RetrievalContentValue +): bool = + true proc validateContractTrieNode( - n: StateNetwork, - key: ContentKey, - contentValue: RetrievalContentValue): bool = - true + n: StateNetwork, key: ContentKey, contentValue: RetrievalContentValue +): bool = + true proc validateContractCode( - n: StateNetwork, - key: ContentKey, - contentValue: RetrievalContentValue): bool = - true + n: StateNetwork, key: ContentKey, contentValue: RetrievalContentValue +): bool = + true proc validateContent*( - n: StateNetwork, - contentKey: ContentKey, - contentValue: RetrievalContentValue): bool = - case contentKey.contentType: - of unused: - warn "Received content with unused content type" - false - of accountTrieNode: - validateAccountTrieNode(n, contentKey, contentValue) - of contractTrieNode: - validateContractTrieNode(n, contentKey, contentValue) - of contractCode: - validateContractCode(n, contentKey, contentValue) - -proc getContent*(n: StateNetwork, key: ContentKey): - Future[Opt[seq[byte]]] {.async.} = + n: StateNetwork, contentKey: ContentKey, contentValue: RetrievalContentValue +): bool = + case contentKey.contentType + of unused: + warn "Received content with unused content type" + false + of accountTrieNode: + validateAccountTrieNode(n, contentKey, contentValue) + of contractTrieNode: + validateContractTrieNode(n, contentKey, contentValue) + of contractCode: + validateContractCode(n, contentKey, contentValue) + +proc getContent*(n: StateNetwork, key: ContentKey): Future[Opt[seq[byte]]] {.async.} = let keyEncoded = encode(key) contentId = toContentId(key) @@ -145,76 +146,71 @@ proc getContent*(n: StateNetwork, key: ContentKey): return Opt.some(contentResult.content) proc validateAccountTrieNode( - n: StateNetwork, - key: ContentKey, - contentValue: OfferContentValue): bool = - true + n: StateNetwork, key: ContentKey, contentValue: OfferContentValue +): bool = + true proc validateContractTrieNode( - n: StateNetwork, - key: ContentKey, - contentValue: OfferContentValue): bool = - true + n: StateNetwork, key: ContentKey, contentValue: OfferContentValue +): bool = + true proc validateContractCode( - n: StateNetwork, - key: ContentKey, - contentValue: OfferContentValue): bool = - true + n: StateNetwork, key: ContentKey, contentValue: OfferContentValue +): bool = + true proc validateContent*( - n: StateNetwork, - contentKey: ContentKey, - contentValue: OfferContentValue): bool = - case contentKey.contentType: - of unused: - warn "Received content with unused content type" - false - of accountTrieNode: - validateAccountTrieNode(n, contentKey, contentValue) - of contractTrieNode: - validateContractTrieNode(n, contentKey, contentValue) - of contractCode: - validateContractCode(n, contentKey, contentValue) + n: StateNetwork, contentKey: ContentKey, contentValue: OfferContentValue +): bool = + case contentKey.contentType + of unused: + warn "Received content with unused content type" + false + of accountTrieNode: + validateAccountTrieNode(n, contentKey, contentValue) + of contractTrieNode: + validateContractTrieNode(n, contentKey, contentValue) + of contractCode: + validateContractCode(n, contentKey, contentValue) proc recursiveGossipAccountTrieNode( p: PortalProtocol, maybeSrcNodeId: Opt[NodeId], decodedKey: ContentKey, - decodedValue: AccountTrieNodeOffer - ): Future[void] {.async.} = - var - nibbles = decodedKey.accountTrieNodeKey.path.unpackNibbles() - proof = decodedValue.proof - - # When nibbles is empty this means the root node was received. Recursive - # gossiping is finished. - if nibbles.len() == 0: - return - - discard nibbles.pop() - discard (distinctBase proof).pop() - let - updatedValue = AccountTrieNodeOffer( - proof: proof, - blockHash: decodedValue.blockHash, - ) - updatedNodeHash = keccakHash(distinctBase proof[^1]) - encodedValue = SSZ.encode(updatedValue) - updatedKey = AccountTrieNodeKey(path: nibbles.packNibbles(), nodeHash: updatedNodeHash) - encodedKey = ContentKey(accountTrieNodeKey: updatedKey, contentType: accountTrieNode).encode() - - await neighborhoodGossipDiscardPeers( - p, maybeSrcNodeId, ContentKeysList.init(@[encodedKey]), @[encodedValue] - ) + decodedValue: AccountTrieNodeOffer, +): Future[void] {.async.} = + var + nibbles = decodedKey.accountTrieNodeKey.path.unpackNibbles() + proof = decodedValue.proof + + # When nibbles is empty this means the root node was received. Recursive + # gossiping is finished. + if nibbles.len() == 0: + return + + discard nibbles.pop() + discard (distinctBase proof).pop() + let + updatedValue = AccountTrieNodeOffer(proof: proof, blockHash: decodedValue.blockHash) + updatedNodeHash = keccakHash(distinctBase proof[^1]) + encodedValue = SSZ.encode(updatedValue) + updatedKey = + AccountTrieNodeKey(path: nibbles.packNibbles(), nodeHash: updatedNodeHash) + encodedKey = + ContentKey(accountTrieNodeKey: updatedKey, contentType: accountTrieNode).encode() + + await neighborhoodGossipDiscardPeers( + p, maybeSrcNodeId, ContentKeysList.init(@[encodedKey]), @[encodedValue] + ) proc recursiveGossipContractTrieNode( p: PortalProtocol, maybeSrcNodeId: Opt[NodeId], decodedKey: ContentKey, - decodedValue: ContractTrieNodeOffer - ): Future[void] {.async.} = - return + decodedValue: ContractTrieNodeOffer, +): Future[void] {.async.} = + return proc gossipContent*( p: PortalProtocol, @@ -222,19 +218,23 @@ proc gossipContent*( contentKey: ByteList, decodedKey: ContentKey, contentValue: seq[byte], - decodedValue: OfferContentValue - ): Future[void] {.async.} = - case decodedKey.contentType: - of unused: - raiseAssert "Gossiping content with unused content type" - of accountTrieNode: - await recursiveGossipAccountTrieNode(p, maybeSrcNodeId, decodedKey, decodedValue.accountTrieNode) - of contractTrieNode: - await recursiveGossipContractTrieNode(p, maybeSrcNodeId, decodedKey, decodedValue.contractTrieNode) - of contractCode: - await p.neighborhoodGossipDiscardPeers( - maybeSrcNodeId, ContentKeysList.init(@[contentKey]), @[contentValue] - ) + decodedValue: OfferContentValue, +): Future[void] {.async.} = + case decodedKey.contentType + of unused: + raiseAssert "Gossiping content with unused content type" + of accountTrieNode: + await recursiveGossipAccountTrieNode( + p, maybeSrcNodeId, decodedKey, decodedValue.accountTrieNode + ) + of contractTrieNode: + await recursiveGossipContractTrieNode( + p, maybeSrcNodeId, decodedKey, decodedValue.contractTrieNode + ) + of contractCode: + await p.neighborhoodGossipDiscardPeers( + maybeSrcNodeId, ContentKeysList.init(@[contentKey]), @[contentValue] + ) proc new*( T: type StateNetwork, @@ -242,24 +242,27 @@ proc new*( contentDB: ContentDB, streamManager: StreamManager, bootstrapRecords: openArray[Record] = [], - portalConfig: PortalProtocolConfig = defaultPortalProtocolConfig): T = - + portalConfig: PortalProtocolConfig = defaultPortalProtocolConfig, +): T = let cq = newAsyncQueue[(Opt[NodeId], ContentKeysList, seq[seq[byte]])](50) let s = streamManager.registerNewStream(cq) let portalProtocol = PortalProtocol.new( - baseProtocol, stateProtocolId, - toContentIdHandler, createGetHandler(contentDB), s, - bootstrapRecords, config = portalConfig) + baseProtocol, + stateProtocolId, + toContentIdHandler, + createGetHandler(contentDB), + s, + bootstrapRecords, + config = portalConfig, + ) - portalProtocol.dbPut = createStoreHandler(contentDB, portalConfig.radiusConfig, portalProtocol) + portalProtocol.dbPut = + createStoreHandler(contentDB, portalConfig.radiusConfig, portalProtocol) - return StateNetwork( - portalProtocol: portalProtocol, - contentDB: contentDB, - contentQueue: cq - ) + return + StateNetwork(portalProtocol: portalProtocol, contentDB: contentDB, contentQueue: cq) proc processContentLoop(n: StateNetwork) {.async.} = try: @@ -282,12 +285,8 @@ proc processContentLoop(n: StateNetwork) {.async.} = info "Received offered content validated successfully", contentKey await gossipContent( - n.portalProtocol, - maybeSrcNodeId, - contentKey, - decodedKey, - contentValue, - decodedValue + n.portalProtocol, maybeSrcNodeId, contentKey, decodedKey, contentValue, + decodedValue, ) else: error "Received offered content failed validation", contentKey diff --git a/fluffy/network/wire/messages.nim b/fluffy/network/wire/messages.nim index 702737dab5..924b5c2cac 100644 --- a/fluffy/network/wire/messages.nim +++ b/fluffy/network/wire/messages.nim @@ -1,5 +1,5 @@ # Nimbus - Portal Network- Message types -# Copyright (c) 2021-2023 Status Research & Development GmbH +# Copyright (c) 2021-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -11,9 +11,7 @@ {.push raises: [].} import - stint, stew/[results, objects, endians2], - ssz_serialization, - ../../common/common_types + stint, stew/[results, objects, endians2], ssz_serialization, ../../common/common_types export ssz_serialization, stint, common_types @@ -105,26 +103,33 @@ type accept*: AcceptMessage SomeMessage* = - PingMessage or PongMessage or - FindNodesMessage or NodesMessage or - FindContentMessage or ContentMessage or - OfferMessage or AcceptMessage + PingMessage or PongMessage or FindNodesMessage or NodesMessage or FindContentMessage or + ContentMessage or OfferMessage or AcceptMessage template messageKind*(T: typedesc[SomeMessage]): MessageKind = - when T is PingMessage: ping - elif T is PongMessage: pong - elif T is FindNodesMessage: findNodes - elif T is NodesMessage: nodes - elif T is FindContentMessage: findContent - elif T is ContentMessage: content - elif T is OfferMessage: offer - elif T is AcceptMessage: accept + when T is PingMessage: + ping + elif T is PongMessage: + pong + elif T is FindNodesMessage: + findNodes + elif T is NodesMessage: + nodes + elif T is FindContentMessage: + findContent + elif T is ContentMessage: + content + elif T is OfferMessage: + offer + elif T is AcceptMessage: + accept template toSszType*(x: UInt256): array[32, byte] = toBytesLE(x) -func fromSszBytes*(T: type UInt256, data: openArray[byte]): - T {.raises: [MalformedSszError].} = +func fromSszBytes*( + T: type UInt256, data: openArray[byte] +): T {.raises: [MalformedSszError].} = if data.len != sizeof(result): raiseIncorrectSize T @@ -133,14 +138,22 @@ func fromSszBytes*(T: type UInt256, data: openArray[byte]): func encodeMessage*[T: SomeMessage](m: T): seq[byte] = # TODO: Could/should be macro'd away, # or we just use SSZ.encode(Message) directly - when T is PingMessage: SSZ.encode(Message(kind: ping, ping: m)) - elif T is PongMessage: SSZ.encode(Message(kind: pong, pong: m)) - elif T is FindNodesMessage: SSZ.encode(Message(kind: findNodes, findNodes: m)) - elif T is NodesMessage: SSZ.encode(Message(kind: nodes, nodes: m)) - elif T is FindContentMessage: SSZ.encode(Message(kind: findContent, findContent: m)) - elif T is ContentMessage: SSZ.encode(Message(kind: content, content: m)) - elif T is OfferMessage: SSZ.encode(Message(kind: offer, offer: m)) - elif T is AcceptMessage: SSZ.encode(Message(kind: accept, accept: m)) + when T is PingMessage: + SSZ.encode(Message(kind: ping, ping: m)) + elif T is PongMessage: + SSZ.encode(Message(kind: pong, pong: m)) + elif T is FindNodesMessage: + SSZ.encode(Message(kind: findNodes, findNodes: m)) + elif T is NodesMessage: + SSZ.encode(Message(kind: nodes, nodes: m)) + elif T is FindContentMessage: + SSZ.encode(Message(kind: findContent, findContent: m)) + elif T is ContentMessage: + SSZ.encode(Message(kind: content, content: m)) + elif T is OfferMessage: + SSZ.encode(Message(kind: offer, offer: m)) + elif T is AcceptMessage: + SSZ.encode(Message(kind: accept, accept: m)) func decodeMessage*(body: openArray[byte]): Result[Message, string] = try: @@ -151,7 +164,8 @@ func decodeMessage*(body: openArray[byte]): Result[Message, string] = err("Invalid message encoding: " & e.msg) template innerMessage[T: SomeMessage]( - message: Message, expected: MessageKind): Result[T, string] = + message: Message, expected: MessageKind +): Result[T, string] = if (message.kind == expected): ok(message.expected) else: diff --git a/fluffy/network/wire/portal_protocol.nim b/fluffy/network/wire/portal_protocol.nim index 225e43b96f..e5569fa138 100644 --- a/fluffy/network/wire/portal_protocol.nim +++ b/fluffy/network/wire/portal_protocol.nim @@ -12,10 +12,17 @@ import std/[sequtils, sets, algorithm, tables], - stew/[results, byteutils, leb128, endians2], chronicles, chronos, - nimcrypto/hash, bearssl, ssz_serialization, metrics, faststreams, - eth/rlp, eth/p2p/discoveryv5/[protocol, node, enr, routing_table, random2, - nodes_verification, lru], + stew/[results, byteutils, leb128, endians2], + chronicles, + chronos, + nimcrypto/hash, + bearssl, + ssz_serialization, + metrics, + faststreams, + eth/rlp, + eth/p2p/discoveryv5/ + [protocol, node, enr, routing_table, random2, nodes_verification, lru], "."/[portal_stream, portal_protocol_config], ./messages @@ -25,8 +32,7 @@ declareCounter portal_message_requests_incoming, "Portal wire protocol incoming message requests", labels = ["protocol_id", "message_type"] declareCounter portal_message_decoding_failures, - "Portal wire protocol message decoding failures", - labels = ["protocol_id"] + "Portal wire protocol message decoding failures", labels = ["protocol_id"] declareCounter portal_message_requests_outgoing, "Portal wire protocol outgoing message requests", labels = ["protocol_id", "message_type"] @@ -37,21 +43,24 @@ declareCounter portal_message_response_incoming, const requestBuckets = [1.0, 3.0, 5.0, 7.0, 9.0, Inf] declareHistogram portal_lookup_node_requests, "Portal wire protocol amount of requests per node lookup", - labels = ["protocol_id"], buckets = requestBuckets + labels = ["protocol_id"], + buckets = requestBuckets declareHistogram portal_lookup_content_requests, "Portal wire protocol amount of requests per node lookup", - labels = ["protocol_id"], buckets = requestBuckets + labels = ["protocol_id"], + buckets = requestBuckets declareCounter portal_lookup_content_failures, - "Portal wire protocol content lookup failures", - labels = ["protocol_id"] + "Portal wire protocol content lookup failures", labels = ["protocol_id"] const contentKeysBuckets = [0.0, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, Inf] declareHistogram portal_content_keys_offered, "Portal wire protocol amount of content keys per offer message send", - labels = ["protocol_id"], buckets = contentKeysBuckets + labels = ["protocol_id"], + buckets = contentKeysBuckets declareHistogram portal_content_keys_accepted, "Portal wire protocol amount of content keys per accept message received", - labels = ["protocol_id"], buckets = contentKeysBuckets + labels = ["protocol_id"], + buckets = contentKeysBuckets declareCounter portal_gossip_offers_successful, "Portal wire protocol successful content offers from neighborhood gossip", labels = ["protocol_id"] @@ -70,23 +79,28 @@ declareCounter portal_gossip_without_lookup, const enrsBuckets = [0.0, 1.0, 3.0, 5.0, 8.0, 9.0, Inf] declareHistogram portal_nodes_enrs_packed, "Portal wire protocol amount of enrs packed in a nodes message", - labels = ["protocol_id"], buckets = enrsBuckets + labels = ["protocol_id"], + buckets = enrsBuckets # This one will currently hit the max numbers because all neighbours are send, # not only the ones closer to the content. declareHistogram portal_content_enrs_packed, "Portal wire protocol amount of enrs packed in a content message", - labels = ["protocol_id"], buckets = enrsBuckets + labels = ["protocol_id"], + buckets = enrsBuckets -const distanceBuckets = - [float64 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, - 253, 254, 255, 256] +const distanceBuckets = [ + float64 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, + 254, 255, 256, +] declareHistogram portal_find_content_log_distance, "Portal wire protocol logarithmic distance of requested content", - labels = ["protocol_id"], buckets = distanceBuckets + labels = ["protocol_id"], + buckets = distanceBuckets declareHistogram portal_offer_log_distance, "Portal wire protocol logarithmic distance of offered content", - labels = ["protocol_id"], buckets = distanceBuckets + labels = ["protocol_id"], + buckets = distanceBuckets logScope: topics = "portal_wire" @@ -132,16 +146,13 @@ type ToContentIdHandler* = proc(contentKey: ByteList): results.Opt[ContentId] {.raises: [], gcsafe.} - DbGetHandler* = - proc( - contentKey: ByteList, - contentId: ContentId): results.Opt[seq[byte]] {.raises: [], gcsafe.} + DbGetHandler* = proc( + contentKey: ByteList, contentId: ContentId + ): results.Opt[seq[byte]] {.raises: [], gcsafe.} - DbStoreHandler* = - proc( - contentKey: ByteList, - contentId: ContentId, - content: seq[byte]) {.raises: [], gcsafe.} + DbStoreHandler* = proc(contentKey: ByteList, contentId: ContentId, content: seq[byte]) {. + raises: [], gcsafe + .} PortalProtocolId* = array[2, byte] @@ -152,7 +163,8 @@ type content*: seq[byte] OfferRequestType = enum - Direct, Database + Direct + Database OfferRequest = object dst: Node @@ -185,7 +197,7 @@ type PortalResult*[T] = Result[T, string] FoundContentKind* = enum - Nodes, + Nodes Content FoundContent* = object @@ -226,24 +238,19 @@ type utpTransfer*: bool trace*: TraceObject -func init*( - T: type ContentKV, - contentKey: ByteList, - content: seq[byte]): T = - ContentKV( - contentKey: contentKey, - content: content - ) +func init*(T: type ContentKV, contentKey: ByteList, content: seq[byte]): T = + ContentKV(contentKey: contentKey, content: content) func init*( - T: type ContentLookupResult, - content: seq[byte], - utpTransfer: bool, - nodesInterestedInContent: seq[Node]): T = + T: type ContentLookupResult, + content: seq[byte], + utpTransfer: bool, + nodesInterestedInContent: seq[Node], +): T = ContentLookupResult( content: content, utpTransfer: utpTransfer, - nodesInterestedInContent: nodesInterestedInContent + nodesInterestedInContent: nodesInterestedInContent, ) func `$`(id: PortalProtocolId): string = @@ -262,7 +269,8 @@ proc addNode*(p: PortalProtocol, r: Record): bool = func getNode*(p: PortalProtocol, id: NodeId): Opt[Node] = p.routingTable.getNode(id) -func localNode*(p: PortalProtocol): Node = p.baseProtocol.localNode +func localNode*(p: PortalProtocol): Node = + p.baseProtocol.localNode func neighbours*(p: PortalProtocol, id: NodeId, seenOnly = false): seq[Node] = p.routingTable.neighbours(id = id, seenOnly = seenOnly) @@ -274,10 +282,8 @@ func logDistance(p: PortalProtocol, a, b: NodeId): uint16 = p.routingTable.logDistance(a, b) func inRange( - p: PortalProtocol, - nodeId: NodeId, - nodeRadius: UInt256, - contentId: ContentId): bool = + p: PortalProtocol, nodeId: NodeId, nodeRadius: UInt256, contentId: ContentId +): bool = let distance = p.distance(nodeId, contentId) distance <= nodeRadius @@ -285,7 +291,8 @@ func inRange*(p: PortalProtocol, contentId: ContentId): bool = p.inRange(p.localNode.id, p.dataRadius, contentId) func truncateEnrs( - nodes: seq[Node], maxSize: int, enrOverhead: int): List[ByteList, 32] = + nodes: seq[Node], maxSize: int, enrOverhead: int +): List[ByteList, 32] = var enrs: List[ByteList, 32] var totalSize = 0 for n in nodes: @@ -300,21 +307,23 @@ func truncateEnrs( enrs -func handlePing( - p: PortalProtocol, ping: PingMessage, srcId: NodeId): seq[byte] = +func handlePing(p: PortalProtocol, ping: PingMessage, srcId: NodeId): seq[byte] = # TODO: This should become custom per Portal Network # TODO: Need to think about the effect of malicious actor sending lots of # pings from different nodes to clear the LRU. let customPayloadDecoded = - try: SSZ.decode(ping.customPayload.asSeq(), CustomPayload) + try: + SSZ.decode(ping.customPayload.asSeq(), CustomPayload) except SerializationError: # invalid custom payload, send empty back return @[] p.radiusCache.put(srcId, customPayloadDecoded.dataRadius) let customPayload = CustomPayload(dataRadius: p.dataRadius) - let p = PongMessage(enrSeq: p.localNode.record.seqNum, - customPayload: ByteList(SSZ.encode(customPayload))) + let p = PongMessage( + enrSeq: p.localNode.record.seqNum, + customPayload: ByteList(SSZ.encode(customPayload)), + ) encodeMessage(p) @@ -328,9 +337,11 @@ proc handleFindNodes(p: PortalProtocol, fn: FindNodesMessage): seq[byte] = encodeMessage(NodesMessage(total: 1, enrs: List[ByteList, 32](@[enr]))) else: let distances = fn.distances.asSeq() - if distances.all(proc (x: uint16): bool = return x <= 256): - let - nodes = p.routingTable.neighboursAtDistances(distances, seenOnly = true) + if distances.all( + proc(x: uint16): bool = + return x <= 256 + ): + let nodes = p.routingTable.neighboursAtDistances(distances, seenOnly = true) # TODO: Total amount of messages is set fixed to 1 for now, else we would # need to either move the send of the talkresp messages here, or allow for @@ -345,8 +356,7 @@ proc handleFindNodes(p: PortalProtocol, fn: FindNodesMessage): seq[byte] = enrOverhead = 4 # per added ENR, 4 bytes offset overhead let enrs = truncateEnrs(nodes, maxPayloadSize, enrOverhead) - portal_nodes_enrs_packed.observe( - enrs.len().int64, labelValues = [$p.protocolId]) + portal_nodes_enrs_packed.observe(enrs.len().int64, labelValues = [$p.protocolId]) encodeMessage(NodesMessage(total: 1, enrs: enrs)) else: @@ -355,7 +365,8 @@ proc handleFindNodes(p: PortalProtocol, fn: FindNodesMessage): seq[byte] = encodeMessage(NodesMessage(total: 1, enrs: enrs)) proc handleFindContent( - p: PortalProtocol, fc: FindContentMessage, srcId: NodeId): seq[byte] = + p: PortalProtocol, fc: FindContentMessage, srcId: NodeId +): seq[byte] = const contentOverhead = 1 + 1 # msg id + SSZ Union selector maxPayloadSize = maxDiscv5PacketSize - talkRespOverhead - contentOverhead @@ -369,7 +380,8 @@ proc handleFindContent( let logDistance = p.logDistance(contentId, p.localNode.id) portal_find_content_log_distance.observe( - int64(logDistance), labelValues = [$p.protocolId]) + int64(logDistance), labelValues = [$p.protocolId] + ) # Check first if content is in range, as this is a cheaper operation if p.inRange(contentId): @@ -377,22 +389,24 @@ proc handleFindContent( if contentResult.isOk(): let content = contentResult.get() if content.len <= maxPayloadSize: - return encodeMessage(ContentMessage( - contentMessageType: contentType, content: ByteList(content))) + return encodeMessage( + ContentMessage(contentMessageType: contentType, content: ByteList(content)) + ) else: let connectionId = p.stream.addContentRequest(srcId, content) - return encodeMessage(ContentMessage( - contentMessageType: connectionIdType, connectionId: connectionId)) + return encodeMessage( + ContentMessage( + contentMessageType: connectionIdType, connectionId: connectionId + ) + ) # Node does not have the content, or content is not even in radius, # send closest neighbours to the requested content id. let - closestNodes = p.routingTable.neighbours( - NodeId(contentId), seenOnly = true) + closestNodes = p.routingTable.neighbours(NodeId(contentId), seenOnly = true) enrs = truncateEnrs(closestNodes, maxPayloadSize, enrOverhead) - portal_content_enrs_packed.observe( - enrs.len().int64, labelValues = [$p.protocolId]) + portal_content_enrs_packed.observe(enrs.len().int64, labelValues = [$p.protocolId]) encodeMessage(ContentMessage(contentMessageType: enrsType, enrs: enrs)) @@ -401,9 +415,12 @@ proc handleOffer(p: PortalProtocol, o: OfferMessage, srcId: NodeId): seq[byte] = # of content to process and potentially gossip around. Don't accept more # data in this case. if p.stream.contentQueue.full(): - return encodeMessage(AcceptMessage( - connectionId: Bytes2([byte 0x00, 0x00]), - contentKeys: ContentKeysBitList.init(o.contentKeys.len))) + return encodeMessage( + AcceptMessage( + connectionId: Bytes2([byte 0x00, 0x00]), + contentKeys: ContentKeysBitList.init(o.contentKeys.len), + ) + ) var contentKeysBitList = ContentKeysBitList.init(o.contentKeys.len) var contentKeys = ContentKeysList.init(@[]) @@ -419,7 +436,8 @@ proc handleOffer(p: PortalProtocol, o: OfferMessage, srcId: NodeId): seq[byte] = let logDistance = p.logDistance(contentId, p.localNode.id) portal_offer_log_distance.observe( - int64(logDistance), labelValues = [$p.protocolId]) + int64(logDistance), labelValues = [$p.protocolId] + ) if p.inRange(contentId): if p.dbGet(contentKey, contentId).isErr: @@ -439,10 +457,16 @@ proc handleOffer(p: PortalProtocol, o: OfferMessage, srcId: NodeId): seq[byte] = Bytes2([byte 0x00, 0x00]) encodeMessage( - AcceptMessage(connectionId: connectionId, contentKeys: contentKeysBitList)) + AcceptMessage(connectionId: connectionId, contentKeys: contentKeysBitList) + ) -proc messageHandler(protocol: TalkProtocol, request: seq[byte], - srcId: NodeId, srcUdpAddress: Address, nodeOpt: Opt[Node]): seq[byte] = +proc messageHandler( + protocol: TalkProtocol, + request: seq[byte], + srcId: NodeId, + srcUdpAddress: Address, + nodeOpt: Opt[Node], +): seq[byte] = doAssert(protocol of PortalProtocol) logScope: @@ -467,18 +491,15 @@ proc messageHandler(protocol: TalkProtocol, request: seq[byte], if nodeOpt.isSome(): let node = nodeOpt.value() let status = p.addNode(node) - trace "Adding new node to routing table after incoming request", - status, node + trace "Adding new node to routing table after incoming request", status, node else: let nodeOpt = p.baseProtocol.getNode(srcId) if nodeOpt.isSome(): let node = nodeOpt.value() let status = p.addNode(node) - trace "Adding new node to routing table after incoming request", - status, node + trace "Adding new node to routing table after incoming request", status, node - portal_message_requests_incoming.inc( - labelValues = [$p.protocolId, $message.kind]) + portal_message_requests_incoming.inc(labelValues = [$p.protocolId, $message.kind]) case message.kind of MessageKind.ping: @@ -499,7 +520,8 @@ proc messageHandler(protocol: TalkProtocol, request: seq[byte], debug "Packet decoding error", error = decoded.error, srcId, srcUdpAddress @[] -proc new*(T: type PortalProtocol, +proc new*( + T: type PortalProtocol, baseProtocol: protocol.Protocol, protocolId: PortalProtocolId, toContentId: ToContentIdHandler, @@ -507,17 +529,17 @@ proc new*(T: type PortalProtocol, stream: PortalStream, bootstrapRecords: openArray[Record] = [], distanceCalculator: DistanceCalculator = XorDistanceCalculator, - config: PortalProtocolConfig = defaultPortalProtocolConfig - ): T = - + config: PortalProtocolConfig = defaultPortalProtocolConfig, +): T = let initialRadius: UInt256 = config.radiusConfig.getInitialRadius() let proto = PortalProtocol( protocolHandler: messageHandler, protocolId: protocolId, routingTable: RoutingTable.init( - baseProtocol.localNode, config.bitsPerHop, config.tableIpLimits, - baseProtocol.rng, distanceCalculator), + baseProtocol.localNode, config.bitsPerHop, config.tableIpLimits, baseProtocol.rng, + distanceCalculator, + ), baseProtocol: baseProtocol, toContentId: toContentId, dbGet: dbGet, @@ -528,27 +550,27 @@ proc new*(T: type PortalProtocol, radiusCache: RadiusCache.init(256), offerQueue: newAsyncQueue[OfferRequest](concurrentOffers), disablePoke: config.disablePoke, - pingTimings: initTable[NodeId, chronos.Moment]() - ) + pingTimings: initTable[NodeId, chronos.Moment](), + ) proto.baseProtocol.registerTalkProtocol(@(proto.protocolId), proto).expect( - "Only one protocol should have this id") + "Only one protocol should have this id" + ) proto # Sends the discv5 talkreq message with provided Portal message, awaits and # validates the proper response, and updates the Portal Network routing table. proc reqResponse[Request: SomeMessage, Response: SomeMessage]( - p: PortalProtocol, - dst: Node, - request: Request - ): Future[PortalResult[Response]] {.async.} = + p: PortalProtocol, dst: Node, request: Request +): Future[PortalResult[Response]] {.async.} = logScope: protocolId = p.protocolId trace "Send message request", dstId = dst.id, kind = messageKind(Request) portal_message_requests_outgoing.inc( - labelValues = [$p.protocolId, $messageKind(Request)]) + labelValues = [$p.protocolId, $messageKind(Request)] + ) let talkresp = await talkReq(p.baseProtocol, dst, @(p.protocolId), encodeMessage(request)) @@ -558,49 +580,65 @@ proc reqResponse[Request: SomeMessage, Response: SomeMessage]( # an empty response needs to be send in that case. # See: https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire.md#talkreq-request-0x05 - let messageResponse = talkresp.mapErr(proc (x: cstring): string = $x) - .flatMap(proc (x: seq[byte]): Result[Message, string] = decodeMessage(x)) - .flatMap(proc (m: Message): Result[Response, string] = - getInnerMessage[Response](m)) + let messageResponse = talkresp + .mapErr( + proc(x: cstring): string = + $x + ) + .flatMap( + proc(x: seq[byte]): Result[Message, string] = + decodeMessage(x) + ) + .flatMap( + proc(m: Message): Result[Response, string] = + getInnerMessage[Response](m) + ) if messageResponse.isOk(): - trace "Received message response", srcId = dst.id, - srcAddress = dst.address, kind = messageKind(Response) + trace "Received message response", + srcId = dst.id, srcAddress = dst.address, kind = messageKind(Response) portal_message_response_incoming.inc( - labelValues = [$p.protocolId, $messageKind(Response)]) + labelValues = [$p.protocolId, $messageKind(Response)] + ) p.routingTable.setJustSeen(dst) else: - debug "Error receiving message response", error = messageResponse.error, - srcId = dst.id, srcAddress = dst.address + debug "Error receiving message response", + error = messageResponse.error, srcId = dst.id, srcAddress = dst.address p.pingTimings.del(dst.id) p.routingTable.replaceNode(dst) return messageResponse -proc pingImpl*(p: PortalProtocol, dst: Node): - Future[PortalResult[PongMessage]] {.async.} = +proc pingImpl*( + p: PortalProtocol, dst: Node +): Future[PortalResult[PongMessage]] {.async.} = let customPayload = CustomPayload(dataRadius: p.dataRadius) - let ping = PingMessage(enrSeq: p.localNode.record.seqNum, - customPayload: ByteList(SSZ.encode(customPayload))) + let ping = PingMessage( + enrSeq: p.localNode.record.seqNum, + customPayload: ByteList(SSZ.encode(customPayload)), + ) return await reqResponse[PingMessage, PongMessage](p, dst, ping) -proc findNodesImpl*(p: PortalProtocol, dst: Node, distances: List[uint16, 256]): - Future[PortalResult[NodesMessage]] {.async.} = +proc findNodesImpl*( + p: PortalProtocol, dst: Node, distances: List[uint16, 256] +): Future[PortalResult[NodesMessage]] {.async.} = let fn = FindNodesMessage(distances: distances) # TODO Add nodes validation return await reqResponse[FindNodesMessage, NodesMessage](p, dst, fn) -proc findContentImpl*(p: PortalProtocol, dst: Node, contentKey: ByteList): - Future[PortalResult[ContentMessage]] {.async.} = +proc findContentImpl*( + p: PortalProtocol, dst: Node, contentKey: ByteList +): Future[PortalResult[ContentMessage]] {.async.} = let fc = FindContentMessage(contentKey: contentKey) return await reqResponse[FindContentMessage, ContentMessage](p, dst, fc) -proc offerImpl*(p: PortalProtocol, dst: Node, contentKeys: ContentKeysList): - Future[PortalResult[AcceptMessage]] {.async.} = +proc offerImpl*( + p: PortalProtocol, dst: Node, contentKeys: ContentKeysList +): Future[PortalResult[AcceptMessage]] {.async.} = let offer = OfferMessage(contentKeys: contentKeys) return await reqResponse[OfferMessage, AcceptMessage](p, dst, offer) @@ -618,8 +656,7 @@ proc recordsFromBytes*(rawRecords: List[ByteList, 32]): PortalResult[seq[Record] ok(records) -proc ping*(p: PortalProtocol, dst: Node): - Future[PortalResult[PongMessage]] {.async.} = +proc ping*(p: PortalProtocol, dst: Node): Future[PortalResult[PongMessage]] {.async.} = let pongResponse = await p.pingImpl(dst) if pongResponse.isOk(): @@ -629,7 +666,8 @@ proc ping*(p: PortalProtocol, dst: Node): let pong = pongResponse.get() # TODO: This should become custom per Portal Network let customPayloadDecoded = - try: SSZ.decode(pong.customPayload.asSeq(), CustomPayload) + try: + SSZ.decode(pong.customPayload.asSeq(), CustomPayload) except MalformedSszError, SszSizeMismatchError: # invalid custom payload return err("Pong message contains invalid custom payload") @@ -639,22 +677,22 @@ proc ping*(p: PortalProtocol, dst: Node): return pongResponse proc findNodes*( - p: PortalProtocol, dst: Node, distances: seq[uint16]): - Future[PortalResult[seq[Node]]] {.async.} = + p: PortalProtocol, dst: Node, distances: seq[uint16] +): Future[PortalResult[seq[Node]]] {.async.} = let nodesMessage = await p.findNodesImpl(dst, List[uint16, 256](distances)) if nodesMessage.isOk(): let records = recordsFromBytes(nodesMessage.get().enrs) if records.isOk(): # TODO: distance function is wrong here for state, fix + tests - return ok(verifyNodesRecords( - records.get(), dst, enrsResultLimit, distances)) + return ok(verifyNodesRecords(records.get(), dst, enrsResultLimit, distances)) else: return err(records.error) else: return err(nodesMessage.error) -proc findContent*(p: PortalProtocol, dst: Node, contentKey: ByteList): - Future[PortalResult[FoundContent]] {.async.} = +proc findContent*( + p: PortalProtocol, dst: Node, contentKey: ByteList +): Future[PortalResult[FoundContent]] {.async.} = logScope: node = dst contentKey @@ -663,22 +701,21 @@ proc findContent*(p: PortalProtocol, dst: Node, contentKey: ByteList): if contentMessageResponse.isOk(): let m = contentMessageResponse.get() - case m.contentMessageType: + case m.contentMessageType of connectionIdType: let nodeAddress = NodeAddress.init(dst) if nodeAddress.isNone(): # It should not happen as we are already after the succesfull # talkreq/talkresp cycle - error "Trying to connect to node with unknown address", - id = dst.id + error "Trying to connect to node with unknown address", id = dst.id return err("Trying to connect to node with unknown address") # uTP protocol uses BE for all values in the header, incl. connection id - let socket = - (await p.stream.connectTo( - nodeAddress.unsafeGet(), - uint16.fromBytesBE(m.connectionId) - )).valueOr: + let socket = ( + await p.stream.connectTo( + nodeAddress.unsafeGet(), uint16.fromBytesBE(m.connectionId) + ) + ).valueOr: debug "uTP connection error for find content", error return err("Error connecting uTP socket") @@ -691,8 +728,7 @@ proc findContent*(p: PortalProtocol, dst: Node, contentKey: ByteList): let readFut = socket.read() readFut.cancelCallback = proc(udate: pointer) {.gcsafe.} = - debug "Socket read cancelled", - socketKey = socket.socketKey + debug "Socket read cancelled", socketKey = socket.socketKey # In case this `findContent` gets cancelled while reading the data, # send a FIN and clean up the socket. socket.close() @@ -701,14 +737,13 @@ proc findContent*(p: PortalProtocol, dst: Node, contentKey: ByteList): let content = readFut.read # socket received remote FIN and drained whole buffer, it can be # safely destroyed without notifing remote - debug "Socket read fully", - socketKey = socket.socketKey + debug "Socket read fully", socketKey = socket.socketKey socket.destroy() - return ok(FoundContent( - src: dst, kind: Content, content: content, utpTransfer: true)) - else : - debug "Socket read time-out", - socketKey = socket.socketKey + return ok( + FoundContent(src: dst, kind: Content, content: content, utpTransfer: true) + ) + else: + debug "Socket read time-out", socketKey = socket.socketKey # Note: This might look a bit strange, but not doing a socket.close() # here as this is already done internally. utp_socket `checkTimeouts` # already does a socket.destroy() on timeout. Might want to change the @@ -718,20 +753,20 @@ proc findContent*(p: PortalProtocol, dst: Node, contentKey: ByteList): # even though we already installed cancelCallback on readFut, it is worth # catching CancelledError in case that withTimeout throws CancelledError # but readFut have already finished. - debug "Socket read cancelled", - socketKey = socket.socketKey + debug "Socket read cancelled", socketKey = socket.socketKey socket.close() raise exc of contentType: - return ok(FoundContent( - src: dst, - kind: Content, content: m.content.asSeq(), utpTransfer: false)) + return ok( + FoundContent( + src: dst, kind: Content, content: m.content.asSeq(), utpTransfer: false + ) + ) of enrsType: let records = recordsFromBytes(m.enrs) if records.isOk(): - let verifiedNodes = - verifyNodesRecords(records.get(), dst, enrsResultLimit) + let verifiedNodes = verifyNodesRecords(records.get(), dst, enrsResultLimit) return ok(FoundContent(src: dst, kind: Nodes, nodes: verifiedNodes)) else: @@ -745,7 +780,7 @@ proc findContent*(p: PortalProtocol, dst: Node, contentKey: ByteList): proc getContentKeys(o: OfferRequest): ContentKeysList = case o.kind of Direct: - var contentKeys:ContentKeysList + var contentKeys: ContentKeysList for info in o.contentList: discard contentKeys.add(info.contentKey) return contentKeys @@ -765,12 +800,11 @@ func getMaxOfferedContentKeys*(protocolIdLen: uint32, maxKeySize: uint32): int = # to calculate maximal number of keys which will will given space this can be # transformed to: # n = trunc((bytes - offerMessageOverhead) / (maxKeySize + perContentKeyOverhead)) - return ( - (maxTalkReqPayload - 5) div (int(maxKeySize) + 4) - ) + return ((maxTalkReqPayload - 5) div (int(maxKeySize) + 4)) -proc offer(p: PortalProtocol, o: OfferRequest): - Future[PortalResult[ContentKeysBitList]] {.async.} = +proc offer( + p: PortalProtocol, o: OfferRequest +): Future[PortalResult[ContentKeysBitList]] {.async.} = ## Offer triggers offer-accept interaction with one peer ## Whole flow has two phases: ## 1. Come to an agreement on what content to transfer, by using offer and @@ -797,7 +831,8 @@ proc offer(p: PortalProtocol, o: OfferRequest): debug "Offering content" portal_content_keys_offered.observe( - contentKeys.len().int64, labelValues = [$p.protocolId]) + contentKeys.len().int64, labelValues = [$p.protocolId] + ) let acceptMessageResponse = await p.offerImpl(o.dst, contentKeys) @@ -819,7 +854,8 @@ proc offer(p: PortalProtocol, o: OfferRequest): let acceptedKeysAmount = m.contentKeys.countOnes() portal_content_keys_accepted.observe( - acceptedKeysAmount.int64, labelValues = [$p.protocolId]) + acceptedKeysAmount.int64, labelValues = [$p.protocolId] + ) if acceptedKeysAmount == 0: debug "No content accepted" # Don't open an uTP stream if no content was requested @@ -829,15 +865,14 @@ proc offer(p: PortalProtocol, o: OfferRequest): if nodeAddress.isNone(): # It should not happen as we are already after succesfull talkreq/talkresp # cycle - error "Trying to connect to node with unknown address", - id = o.dst.id + error "Trying to connect to node with unknown address", id = o.dst.id return err("Trying to connect to node with unknown address") - let socket = - (await p.stream.connectTo( - nodeAddress.unsafeGet(), - uint16.fromBytesBE(m.connectionId) - )).valueOr: + let socket = ( + await p.stream.connectTo( + nodeAddress.unsafeGet(), uint16.fromBytesBE(m.connectionId) + ) + ).valueOr: debug "uTP connection error for offer content", error return err("Error connecting uTP socket") @@ -898,13 +933,15 @@ proc offer(p: PortalProtocol, o: OfferRequest): error = acceptMessageResponse.error return err("No accept response") -proc offer*(p: PortalProtocol, dst: Node, contentKeys: ContentKeysList): - Future[PortalResult[ContentKeysBitList]] {.async.} = +proc offer*( + p: PortalProtocol, dst: Node, contentKeys: ContentKeysList +): Future[PortalResult[ContentKeysBitList]] {.async.} = let req = OfferRequest(dst: dst, kind: Database, contentKeys: contentKeys) return await p.offer(req) -proc offer*(p: PortalProtocol, dst: Node, content: seq[ContentKV]): - Future[PortalResult[ContentKeysBitList]] {.async.} = +proc offer*( + p: PortalProtocol, dst: Node, content: seq[ContentKV] +): Future[PortalResult[ContentKeysBitList]] {.async.} = if len(content) > contentKeysLimit: return err("Cannot offer more than 64 content items") @@ -926,7 +963,8 @@ proc offerQueueEmpty*(p: PortalProtocol): bool = p.offerQueue.empty() proc lookupWorker( - p: PortalProtocol, dst: Node, target: NodeId): Future[seq[Node]] {.async.} = + p: PortalProtocol, dst: Node, target: NodeId +): Future[seq[Node]] {.async.} = let distances = lookupDistances(target, dst.id) let nodesMessage = await p.findNodes(dst, distances) if nodesMessage.isOk(): @@ -943,8 +981,7 @@ proc lookup*(p: PortalProtocol, target: NodeId): Future[seq[Node]] {.async.} = ## target. Maximum value for n is `BUCKET_SIZE`. # `closestNodes` holds the k closest nodes to target found, sorted by distance # Unvalidated nodes are used for requests as a form of validation. - var closestNodes = p.routingTable.neighbours(target, BUCKET_SIZE, - seenOnly = false) + var closestNodes = p.routingTable.neighbours(target, BUCKET_SIZE, seenOnly = false) var asked, seen = initHashSet[NodeId]() asked.incl(p.localNode.id) # No need to ask our own node @@ -985,25 +1022,26 @@ proc lookup*(p: PortalProtocol, target: NodeId): Future[seq[Node]] {.async.} = for n in nodes: if not seen.containsOrIncl(n.id): # If it wasn't seen before, insert node while remaining sorted - closestNodes.insert(n, closestNodes.lowerBound(n, - proc(x: Node, n: Node): int = - cmp(p.distance(x.id, target), - p.distance(n.id, target)) - )) + closestNodes.insert( + n, + closestNodes.lowerBound( + n, + proc(x: Node, n: Node): int = + cmp(p.distance(x.id, target), p.distance(n.id, target)) + , + ), + ) if closestNodes.len > BUCKET_SIZE: closestNodes.del(closestNodes.high()) - portal_lookup_node_requests.observe( - requestAmount, labelValues = [$p.protocolId]) + portal_lookup_node_requests.observe(requestAmount, labelValues = [$p.protocolId]) p.lastLookup = now(chronos.Moment) return closestNodes proc triggerPoke*( - p: PortalProtocol, - nodes: seq[Node], - contentKey: ByteList, - content: seq[byte]) = + p: PortalProtocol, nodes: seq[Node], contentKey: ByteList, content: seq[byte] +) = ## In order to properly test gossip mechanisms (e.g. in Portal Hive), ## we need the option to turn off the POKE functionality as it influences ## how data moves around the network. @@ -1029,14 +1067,14 @@ proc triggerPoke*( # TODO ContentLookup and Lookup look almost exactly the same, also lookups in other # networks will probably be very similar. Extract lookup function to separate module # and make it more generaic -proc contentLookup*(p: PortalProtocol, target: ByteList, targetId: UInt256): - Future[Opt[ContentLookupResult]] {.async.} = +proc contentLookup*( + p: PortalProtocol, target: ByteList, targetId: UInt256 +): Future[Opt[ContentLookupResult]] {.async.} = ## Perform a lookup for the given target, return the closest n nodes to the ## target. Maximum value for n is `BUCKET_SIZE`. # `closestNodes` holds the k closest nodes to target found, sorted by distance # Unvalidated nodes are used for requests as a form of validation. - var closestNodes = p.routingTable.neighbours( - targetId, BUCKET_SIZE, seenOnly = false) + var closestNodes = p.routingTable.neighbours(targetId, BUCKET_SIZE, seenOnly = false) # Shuffling the order of the nodes in order to not always hit the same node # first for the same request. p.baseProtocol.rng[].shuffle(closestNodes) @@ -1096,23 +1134,30 @@ proc contentLookup*(p: PortalProtocol, target: ByteList, targetId: UInt256): if not seen.containsOrIncl(n.id): discard p.addNode(n) # If it wasn't seen before, insert node while remaining sorted - closestNodes.insert(n, closestNodes.lowerBound(n, - proc(x: Node, n: Node): int = - cmp(p.distance(x.id, targetId), - p.distance(n.id, targetId)) - )) + closestNodes.insert( + n, + closestNodes.lowerBound( + n, + proc(x: Node, n: Node): int = + cmp(p.distance(x.id, targetId), p.distance(n.id, targetId)) + , + ), + ) if closestNodes.len > BUCKET_SIZE: closestNodes.del(closestNodes.high()) - of Content: # cancel any pending queries as the content has been found for f in pendingQueries: f.cancelSoon() portal_lookup_content_requests.observe( - requestAmount, labelValues = [$p.protocolId]) - return Opt.some(ContentLookupResult.init( - content.content, content.utpTransfer, nodesWithoutContent)) + requestAmount, labelValues = [$p.protocolId] + ) + return Opt.some( + ContentLookupResult.init( + content.content, content.utpTransfer, nodesWithoutContent + ) + ) else: # TODO: Should we do something with the node that failed responding our # query? @@ -1121,14 +1166,14 @@ proc contentLookup*(p: PortalProtocol, target: ByteList, targetId: UInt256): portal_lookup_content_failures.inc(labelValues = [$p.protocolId]) return Opt.none(ContentLookupResult) -proc traceContentLookup*(p: PortalProtocol, target: ByteList, targetId: UInt256): - Future[TraceContentLookupResult] {.async.} = +proc traceContentLookup*( + p: PortalProtocol, target: ByteList, targetId: UInt256 +): Future[TraceContentLookupResult] {.async.} = ## Perform a lookup for the given target, return the closest n nodes to the ## target. Maximum value for n is `BUCKET_SIZE`. # `closestNodes` holds the k closest nodes to target found, sorted by distance # Unvalidated nodes are used for requests as a form of validation. - var closestNodes = p.routingTable.neighbours( - targetId, BUCKET_SIZE, seenOnly = false) + var closestNodes = p.routingTable.neighbours(targetId, BUCKET_SIZE, seenOnly = false) # Shuffling the order of the nodes in order to not always hit the same node # first for the same request. p.baseProtocol.rng[].shuffle(closestNodes) @@ -1144,23 +1189,18 @@ proc traceContentLookup*(p: PortalProtocol, target: ByteList, targetId: UInt256) seen.incl(node.id) # Local node should be part of the responses - responses["0x" & $p.localNode.id] = TraceResponse( - durationMs: 0, - respondedWith: seen.toSeq() - ) + responses["0x" & $p.localNode.id] = + TraceResponse(durationMs: 0, respondedWith: seen.toSeq()) metadata["0x" & $p.localNode.id] = NodeMetadata( - enr: p.localNode.record, - distance: p.distance(p.localNode.id, targetId) + enr: p.localNode.record, distance: p.distance(p.localNode.id, targetId) ) # We should also have metadata for all the closes nodes # in order to be able to show cancelled requests for cn in closestNodes: - metadata["0x" & $cn.id] = NodeMetadata( - enr: cn.record, - distance: p.distance(cn.id, targetId) - ) + metadata["0x" & $cn.id] = + NodeMetadata(enr: cn.record, distance: p.distance(cn.id, targetId)) var pendingQueries = newSeqOfCap[Future[PortalResult[FoundContent]]](alpha) var pendingNodes = newSeq[Node]() @@ -1217,35 +1257,32 @@ proc traceContentLookup*(p: PortalProtocol, target: ByteList, targetId: UInt256) for n in content.nodes: let dist = p.distance(n.id, targetId) - metadata["0x" & $n.id] = NodeMetadata( - enr: n.record, - distance: dist, - ) + metadata["0x" & $n.id] = NodeMetadata(enr: n.record, distance: dist) respondedWith.add(n.id) if not seen.containsOrIncl(n.id): discard p.addNode(n) # If it wasn't seen before, insert node while remaining sorted - closestNodes.insert(n, closestNodes.lowerBound(n, - proc(x: Node, n: Node): int = - cmp(p.distance(x.id, targetId), dist) - )) + closestNodes.insert( + n, + closestNodes.lowerBound( + n, + proc(x: Node, n: Node): int = + cmp(p.distance(x.id, targetId), dist) + , + ), + ) if closestNodes.len > BUCKET_SIZE: closestNodes.del(closestNodes.high()) let distance = p.distance(content.src.id, targetId) - responses["0x" & $content.src.id] = TraceResponse( - durationMs: duration, - respondedWith: respondedWith, - ) - - metadata["0x" & $content.src.id] = NodeMetadata( - enr: content.src.record, - distance: distance, - ) + responses["0x" & $content.src.id] = + TraceResponse(durationMs: duration, respondedWith: respondedWith) + metadata["0x" & $content.src.id] = + NodeMetadata(enr: content.src.record, distance: distance) of Content: let duration = chronos.milliseconds(now(chronos.Moment) - ts) @@ -1253,28 +1290,23 @@ proc traceContentLookup*(p: PortalProtocol, target: ByteList, targetId: UInt256) for f in pendingQueries: f.cancelSoon() portal_lookup_content_requests.observe( - requestAmount, labelValues = [$p.protocolId]) + requestAmount, labelValues = [$p.protocolId] + ) let distance = p.distance(content.src.id, targetId) - responses["0x" & $content.src.id] = TraceResponse( - durationMs: duration, - respondedWith: newSeq[NodeId](), - ) + responses["0x" & $content.src.id] = + TraceResponse(durationMs: duration, respondedWith: newSeq[NodeId]()) - metadata["0x" & $content.src.id] = NodeMetadata( - enr: content.src.record, - distance: distance, - ) + metadata["0x" & $content.src.id] = + NodeMetadata(enr: content.src.record, distance: distance) var pendingNodeIds = newSeq[NodeId]() for pn in pendingNodes: pendingNodeIds.add(pn.id) - metadata["0x" & $pn.id] = NodeMetadata( - enr: pn.record, - distance: p.distance(pn.id, targetId) - ) + metadata["0x" & $pn.id] = + NodeMetadata(enr: pn.record, distance: p.distance(pn.id, targetId)) return TraceContentLookupResult( content: Opt.some(content.content), @@ -1286,8 +1318,9 @@ proc traceContentLookup*(p: PortalProtocol, target: ByteList, targetId: UInt256) responses: responses, metadata: metadata, cancelled: pendingNodeIds, - startedAtMs: chronos.epochNanoSeconds(ts) div 1_000_000 # nanoseconds to milliseconds - ) + startedAtMs: chronos.epochNanoSeconds(ts) div 1_000_000, + # nanoseconds to milliseconds + ), ) else: # TODO: Should we do something with the node that failed responding our @@ -1305,12 +1338,14 @@ proc traceContentLookup*(p: PortalProtocol, target: ByteList, targetId: UInt256) responses: responses, metadata: metadata, cancelled: newSeq[NodeId](), - startedAtMs: chronos.epochNanoSeconds(ts) div 1_000_000 # nanoseconds to milliseconds - ) + startedAtMs: chronos.epochNanoSeconds(ts) div 1_000_000, + # nanoseconds to milliseconds + ), ) -proc query*(p: PortalProtocol, target: NodeId, k = BUCKET_SIZE): Future[seq[Node]] - {.async.} = +proc query*( + p: PortalProtocol, target: NodeId, k = BUCKET_SIZE +): Future[seq[Node]] {.async.} = ## Query k nodes for the given target, returns all nodes found, including the ## nodes queried. ## @@ -1363,12 +1398,10 @@ proc queryRandom*(p: PortalProtocol): Future[seq[Node]] = p.query(NodeId.random(p.baseProtocol.rng[])) proc getNClosestNodesWithRadius*( - p: PortalProtocol, - targetId: NodeId, - n: int, - seenOnly: bool = false): seq[(Node, UInt256)] = - let closestLocalNodes = p.routingTable.neighbours( - targetId, k = n, seenOnly = seenOnly) + p: PortalProtocol, targetId: NodeId, n: int, seenOnly: bool = false +): seq[(Node, UInt256)] = + let closestLocalNodes = + p.routingTable.neighbours(targetId, k = n, seenOnly = seenOnly) var nodesWithRadiuses: seq[(Node, UInt256)] for node in closestLocalNodes: @@ -1381,7 +1414,8 @@ proc neighborhoodGossip*( p: PortalProtocol, srcNodeId: Opt[NodeId], contentKeys: ContentKeysList, - content: seq[seq[byte]]): Future[int] {.async.} = + content: seq[seq[byte]], +): Future[int] {.async.} = ## Run neighborhood gossip for provided content. ## Returns the number of peers to which content was attempted to be gossiped. if content.len() == 0: @@ -1389,8 +1423,7 @@ proc neighborhoodGossip*( var contentList = List[ContentKV, contentKeysLimit].init(@[]) for i, contentItem in content: - let contentKV = - ContentKV(contentKey: contentKeys[i], content: contentItem) + let contentKV = ContentKV(contentKey: contentKeys[i], content: contentItem) discard contentList.add(contentKV) # Just taking the first content item as target id. @@ -1414,8 +1447,8 @@ proc neighborhoodGossip*( # It might still cause issues in data getting propagated in a wider id range. const maxGossipNodes = 8 - let closestLocalNodes = p.routingTable.neighbours( - NodeId(contentId), k = 16, seenOnly = true) + let closestLocalNodes = + p.routingTable.neighbours(NodeId(contentId), k = 16, seenOnly = true) var gossipNodes: seq[Node] for node in closestLocalNodes: @@ -1430,7 +1463,7 @@ proc neighborhoodGossip*( if gossipNodes.len >= 8: # use local nodes for gossip portal_gossip_without_lookup.inc(labelValues = [$p.protocolId]) let numberOfGossipedNodes = min(gossipNodes.len, maxGossipNodes) - for node in gossipNodes[0.. 256: - raise newException( - ValueError, "Provided logRadius should be <= 256" - ) + raise newException(ValueError, "Provided logRadius should be <= 256") RadiusConfig(kind: Static, logRadius: parsed) else: @@ -113,8 +105,7 @@ proc parseCmdArg*(T: type RadiusConfig, p: string): T raise newException(ValueError, msg) if parsed > 256: - raise newException( - ValueError, "Provided logRadius should be <= 256") + raise newException(ValueError, "Provided logRadius should be <= 256") RadiusConfig(kind: Static, logRadius: parsed) diff --git a/fluffy/network/wire/portal_stream.nim b/fluffy/network/wire/portal_stream.nim index a80ce38189..d1b47f63ec 100644 --- a/fluffy/network/wire/portal_stream.nim +++ b/fluffy/network/wire/portal_stream.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -9,7 +9,9 @@ import std/sequtils, - chronos, stew/[byteutils, leb128, endians2], chronicles, + chronos, + stew/[byteutils, leb128, endians2], + chronicles, eth/utp/utp_discv5_protocol, # even though utp_discv5_protocol exports this, import is still needed, # perhaps protocol.Protocol type of usage? @@ -78,13 +80,18 @@ proc pruneAllowedConnections(stream: PortalStream) = # Prune requests and offers that didn't receive a connection request # before `connectionTimeout`. let now = Moment.now() - stream.contentRequests.keepIf(proc(x: ContentRequest): bool = - x.timeout > now) - stream.contentOffers.keepIf(proc(x: ContentOffer): bool = - x.timeout > now) + stream.contentRequests.keepIf( + proc(x: ContentRequest): bool = + x.timeout > now + ) + stream.contentOffers.keepIf( + proc(x: ContentOffer): bool = + x.timeout > now + ) proc addContentOffer*( - stream: PortalStream, nodeId: NodeId, contentKeys: ContentKeysList): Bytes2 = + stream: PortalStream, nodeId: NodeId, contentKeys: ContentKeysList +): Bytes2 = stream.pruneAllowedConnections() # TODO: Should we check if `NodeId` & `connectionId` combo already exists? @@ -101,13 +108,15 @@ proc addContentOffer*( connectionId: id, nodeId: nodeId, contentKeys: contentKeys, - timeout: Moment.now() + stream.connectionTimeout) + timeout: Moment.now() + stream.connectionTimeout, + ) stream.contentOffers.add(contentOffer) return connectionId proc addContentRequest*( - stream: PortalStream, nodeId: NodeId, content: seq[byte]): Bytes2 = + stream: PortalStream, nodeId: NodeId, content: seq[byte] +): Bytes2 = stream.pruneAllowedConnections() # TODO: Should we check if `NodeId` & `connectionId` combo already exists? @@ -121,16 +130,15 @@ proc addContentRequest*( connectionId: id, nodeId: nodeId, content: content, - timeout: Moment.now() + stream.connectionTimeout) + timeout: Moment.now() + stream.connectionTimeout, + ) stream.contentRequests.add(contentRequest) return connectionId proc connectTo*( - stream: PortalStream, - nodeAddress: NodeAddress, - connectionId: uint16): - Future[Result[UtpSocket[NodeAddress], string]] {.async.} = + stream: PortalStream, nodeAddress: NodeAddress, connectionId: uint16 +): Future[Result[UtpSocket[NodeAddress], string]] {.async.} = let connectRes = await stream.transport.connectTo(nodeAddress, connectionId) if connectRes.isErr(): case connectRes.error.kind @@ -138,8 +146,9 @@ proc connectTo*( # This means that there is already a socket to this nodeAddress with given # connection id. This means that a peer sent us a connection id which is # already in use. The connection is failed and an error returned. - let msg = "Socket to " & $nodeAddress & "with connection id: " & - $connectionId & " already exists" + let msg = + "Socket to " & $nodeAddress & "with connection id: " & $connectionId & + " already exists" return err(msg) of ConnectionTimedOut: # A time-out here means that a uTP SYN packet was re-sent 3 times and @@ -151,20 +160,18 @@ proc connectTo*( return ok(connectRes.get()) proc writeContentRequest( - socket: UtpSocket[NodeAddress], stream: PortalStream, - request: ContentRequest) {.async.} = - let dataWritten = await socket.write(request.content) + socket: UtpSocket[NodeAddress], stream: PortalStream, request: ContentRequest +) {.async.} = + let dataWritten = await socket.write(request.content) if dataWritten.isErr(): debug "Error writing requested data", error = dataWritten.error await socket.closeWait() -proc readVarint(socket: UtpSocket[NodeAddress]): - Future[Opt[uint32]] {.async.} = - var - buffer: array[5, byte] +proc readVarint(socket: UtpSocket[NodeAddress]): Future[Opt[uint32]] {.async.} = + var buffer: array[5, byte] - for i in 0.. NodeInfo: return d.routingTable.getNodeInfo() - rpcServer.rpc("discv5_updateNodeInfo") do( - kvPairs: seq[(string, string)]) -> NodeInfo: + rpcServer.rpc("discv5_updateNodeInfo") do(kvPairs: seq[(string, string)]) -> NodeInfo: # TODO: Not according to spec, as spec only allows socket address. # portal-specs PR has been created with suggested change as is here. let enrFields = kvPairs.map( proc(n: (string, string)): (string, seq[byte]) {.raises: [ValueError].} = (n[0], hexToSeqByte(n[1])) - ) + ) let updated = d.updateRecord(enrFields) if updated.isErr(): raise newException(ValueError, $updated.error) @@ -104,28 +104,26 @@ proc installDiscoveryApiHandlers*(rpcServer: RpcServer|RpcProxy, raise newException(ValueError, $pong.error) else: let p = pong.get() - return PongResponse( - enrSeq: p.enrSeq, - recipientIP: $p.ip, - recipientPort: p.port - ) + return PongResponse(enrSeq: p.enrSeq, recipientIP: $p.ip, recipientPort: p.port) rpcServer.rpc("discv5_findNode") do( - enr: Record, distances: seq[uint16]) -> seq[Record]: + enr: Record, distances: seq[uint16] + ) -> seq[Record]: let node = toNodeWithAddress(enr) nodes = await d.findNode(node, distances) if nodes.isErr(): raise newException(ValueError, $nodes.error) else: - return nodes.get().map(proc(n: Node): Record = n.record) + return nodes.get().map( + proc(n: Node): Record = + n.record + ) - rpcServer.rpc("discv5_talkReq") do( - enr: Record, protocol, payload: string) -> string: + rpcServer.rpc("discv5_talkReq") do(enr: Record, protocol, payload: string) -> string: let node = toNodeWithAddress(enr) - talkresp = await d.talkReq( - node, hexToSeqByte(protocol), hexToSeqByte(payload)) + talkresp = await d.talkReq(node, hexToSeqByte(protocol), hexToSeqByte(payload)) if talkresp.isErr(): raise newException(ValueError, $talkresp.error) else: @@ -133,4 +131,7 @@ proc installDiscoveryApiHandlers*(rpcServer: RpcServer|RpcProxy, rpcServer.rpc("discv5_recursiveFindNodes") do(nodeId: NodeId) -> seq[Record]: let discovered = await d.lookup(nodeId) - return discovered.map(proc(n: Node): Record = n.record) + return discovered.map( + proc(n: Node): Record = + n.record + ) diff --git a/fluffy/rpc/rpc_eth_api.nim b/fluffy/rpc/rpc_eth_api.nim index a15d22ae41..b50bca74fe 100644 --- a/fluffy/rpc/rpc_eth_api.nim +++ b/fluffy/rpc/rpc_eth_api.nim @@ -40,8 +40,10 @@ func toHash*(value: rpc_types.Hash256): eth_types.Hash256 = func init*( T: type TransactionObject, - tx: eth_types.Transaction, header: eth_types.BlockHeader, txIndex: int): - T {.raises: [ValidationError].} = + tx: eth_types.Transaction, + header: eth_types.BlockHeader, + txIndex: int, +): T {.raises: [ValidationError].} = TransactionObject( blockHash: some(w3Hash header.blockHash), blockNumber: some(Quantity(header.blockNumber.truncate(uint64))), @@ -66,9 +68,11 @@ func init*( # total difficulty func init*( T: type BlockObject, - header: eth_types.BlockHeader, body: BlockBody, - fullTx = true, isUncle = false): - T {.raises: [ValidationError].} = + header: eth_types.BlockHeader, + body: BlockBody, + fullTx = true, + isUncle = false, +): T {.raises: [ValidationError].} = let blockHash = header.blockHash var blockObject = BlockObject( @@ -90,15 +94,17 @@ func init*( totalDifficulty: UInt256.low(), gasLimit: Quantity(header.gasLimit.uint64), gasUsed: Quantity(header.gasUsed.uint64), - timestamp: Quantity(header.timestamp.uint64) + timestamp: Quantity(header.timestamp.uint64), ) let size = sizeof(BlockHeader) - sizeof(Blob) + header.extraData.len blockObject.size = Quantity(size.uint) if not isUncle: - blockObject.uncles = - body.uncles.map(proc(h: BlockHeader): rpc_types.Hash256 = w3Hash h.blockHash) + blockObject.uncles = body.uncles.map( + proc(h: BlockHeader): rpc_types.Hash256 = + w3Hash h.blockHash + ) if fullTx: var i = 0 @@ -115,9 +121,10 @@ func init*( proc installEthApiHandlers*( # Currently only HistoryNetwork needed, later we might want a master object # holding all the networks. - rpcServerWithProxy: var RpcProxy, historyNetwork: HistoryNetwork, - beaconLightClient: Opt[LightClient]) = - + rpcServerWithProxy: var RpcProxy, + historyNetwork: HistoryNetwork, + beaconLightClient: Opt[LightClient], +) = # Supported API rpcServerWithProxy.registerProxyMethod("eth_blockNumber") @@ -201,7 +208,8 @@ proc installEthApiHandlers*( return Quantity(uint64(1)) rpcServerWithProxy.rpc("eth_getBlockByHash") do( - data: rpc_types.Hash256, fullTransactions: bool) -> Option[BlockObject]: + data: rpc_types.Hash256, fullTransactions: bool + ) -> Option[BlockObject]: ## Returns information about a block by hash. ## ## data: Hash of a block. @@ -217,8 +225,8 @@ proc installEthApiHandlers*( return some(BlockObject.init(header, body, fullTransactions)) rpcServerWithProxy.rpc("eth_getBlockByNumber") do( - quantityTag: BlockTag, fullTransactions: bool) -> Option[BlockObject]: - + quantityTag: BlockTag, fullTransactions: bool + ) -> Option[BlockObject]: if quantityTag.kind == bidAlias: let tag = quantityTag.alias.toLowerAscii case tag @@ -243,8 +251,7 @@ proc installEthApiHandlers*( return some(BlockObject.init(header, body, fullTransactions)) else: - raise newException( - ValueError, "Not available before Capella - not synced?") + raise newException(ValueError, "Not available before Capella - not synced?") of "finalized": if beaconLightClient.isNone(): raise newException(ValueError, "Finalized tag not yet implemented") @@ -258,8 +265,7 @@ proc installEthApiHandlers*( return some(BlockObject.init(header, body, fullTransactions)) else: - raise newException( - ValueError, "Not available before Capella - not synced?") + raise newException(ValueError, "Not available before Capella - not synced?") of "pending": raise newException(ValueError, "Pending tag not yet implemented") else: @@ -277,7 +283,8 @@ proc installEthApiHandlers*( return some(BlockObject.init(header, body, fullTransactions)) rpcServerWithProxy.rpc("eth_getBlockTransactionCountByHash") do( - data: rpc_types.Hash256) -> Quantity: + data: rpc_types.Hash256 + ) -> Quantity: ## Returns the number of transactions in a block from a block matching the ## given block hash. ## @@ -302,18 +309,20 @@ proc installEthApiHandlers*( # data: EthHashStr) -> Option[ReceiptObject]: rpcServerWithProxy.rpc("eth_getLogs") do( - filterOptions: FilterOptions) -> seq[FilterLog]: + filterOptions: FilterOptions + ) -> seq[FilterLog]: if filterOptions.blockHash.isNone(): # Currently only queries by blockhash are supported. # To support range queries the Indicies network is required. - raise newException(ValueError, - "Unsupported query: Only `blockHash` queries are currently supported") + raise newException( + ValueError, + "Unsupported query: Only `blockHash` queries are currently supported", + ) let hash = ethHash filterOptions.blockHash.unsafeGet() let header = (await historyNetwork.getVerifiedBlockHeader(hash)).valueOr: - raise newException(ValueError, - "Could not find header with requested hash") + raise newException(ValueError, "Could not find header with requested hash") if headerBloomFilter(header, filterOptions.address, filterOptions.topics): # TODO: These queries could be done concurrently, investigate if there @@ -321,15 +330,12 @@ proc installEthApiHandlers*( # wire protocol level let body = (await historyNetwork.getBlockBody(hash, header)).valueOr: - raise newException(ValueError, - "Could not find block body for requested hash") + raise newException(ValueError, "Could not find block body for requested hash") receipts = (await historyNetwork.getReceipts(hash, header)).valueOr: - raise newException(ValueError, - "Could not find receipts for requested hash") + raise newException(ValueError, "Could not find receipts for requested hash") logs = deriveLogs(header, body.transactions, receipts) - filteredLogs = filterLogs( - logs, filterOptions.address, filterOptions.topics) + filteredLogs = filterLogs(logs, filterOptions.address, filterOptions.topics) return filteredLogs else: diff --git a/fluffy/rpc/rpc_portal_api.nim b/fluffy/rpc/rpc_portal_api.nim index 64c68dc654..c8281f8f08 100644 --- a/fluffy/rpc/rpc_portal_api.nim +++ b/fluffy/rpc/rpc_portal_api.nim @@ -15,17 +15,14 @@ import ../network/wire/portal_protocol, ./rpc_types -export - rpcserver, - tables +export rpcserver, tables # Portal Network JSON-RPC impelentation as per specification: # https://github.com/ethereum/portal-network-specs/tree/master/jsonrpc -type - ContentInfo = object - content: string - utpTransfer: bool +type ContentInfo = object + content: string + utpTransfer: bool ContentInfo.useDefaultSerializationIn JrpcConv TraceContentLookupResult.useDefaultSerializationIn JrpcConv @@ -40,8 +37,8 @@ TraceResponse.useDefaultSerializationIn JrpcConv # as the proc becomes generic, where the rpc macro from router.nim can no longer # be found, which is why we export rpcserver which should export router. proc installPortalApiHandlers*( - rpcServer: RpcServer|RpcProxy, p: PortalProtocol, network: static string) = - + rpcServer: RpcServer | RpcProxy, p: PortalProtocol, network: static string +) = rpcServer.rpc("portal_" & network & "NodeInfo") do() -> NodeInfo: return p.routingTable.getNodeInfo() @@ -93,8 +90,7 @@ proc installPortalApiHandlers*( else: raise newException(ValueError, "Record not found in DHT lookup.") - rpcServer.rpc("portal_" & network & "Ping") do( - enr: Record) -> PingResult: + rpcServer.rpc("portal_" & network & "Ping") do(enr: Record) -> PingResult: let node = toNodeWithAddress(enr) pong = await p.ping(node) @@ -107,49 +103,55 @@ proc installPortalApiHandlers*( # Note: the SSZ.decode cannot fail here as it has already been verified # in the ping call. decodedPayload = - try: SSZ.decode(p.customPayload.asSeq(), CustomPayload) + try: + SSZ.decode(p.customPayload.asSeq(), CustomPayload) except MalformedSszError, SszSizeMismatchError: raiseAssert("Already verified") - return ( - p.enrSeq, - decodedPayload.dataRadius - ) + return (p.enrSeq, decodedPayload.dataRadius) rpcServer.rpc("portal_" & network & "FindNodes") do( - enr: Record, distances: seq[uint16]) -> seq[Record]: + enr: Record, distances: seq[uint16] + ) -> seq[Record]: let node = toNodeWithAddress(enr) nodes = await p.findNodes(node, distances) if nodes.isErr(): raise newException(ValueError, $nodes.error) else: - return nodes.get().map(proc(n: Node): Record = n.record) + return nodes.get().map( + proc(n: Node): Record = + n.record + ) rpcServer.rpc("portal_" & network & "FindContent") do( - enr: Record, contentKey: string) -> JsonString: + enr: Record, contentKey: string + ) -> JsonString: let node = toNodeWithAddress(enr) - foundContentResult = await p.findContent( - node, ByteList.init(hexToSeqByte(contentKey))) + foundContentResult = + await p.findContent(node, ByteList.init(hexToSeqByte(contentKey))) if foundContentResult.isErr(): raise newException(ValueError, $foundContentResult.error) else: let foundContent = foundContentResult.get() - case foundContent.kind: + case foundContent.kind of Content: let res = ContentInfo( - content: foundContent.content.to0xHex(), - utpTransfer: foundContent.utpTransfer + content: foundContent.content.to0xHex(), utpTransfer: foundContent.utpTransfer ) return JrpcConv.encode(res).JsonString of Nodes: - let enrs = foundContent.nodes.map(proc(n: Node): Record = n.record) + let enrs = foundContent.nodes.map( + proc(n: Node): Record = + n.record + ) let jsonEnrs = JrpcConv.encode(enrs) return ("{\"enrs\":" & jsonEnrs & "}").JsonString rpcServer.rpc("portal_" & network & "Offer") do( - enr: Record, contentKey: string, contentValue: string) -> string: + enr: Record, contentKey: string, contentValue: string + ) -> string: let node = toNodeWithAddress(enr) key = hexToSeqByte(contentKey) @@ -163,12 +165,17 @@ proc installPortalApiHandlers*( raise newException(ValueError, $res.error) rpcServer.rpc("portal_" & network & "RecursiveFindNodes") do( - nodeId: NodeId) -> seq[Record]: + nodeId: NodeId + ) -> seq[Record]: let discovered = await p.lookup(nodeId) - return discovered.map(proc(n: Node): Record = n.record) + return discovered.map( + proc(n: Node): Record = + n.record + ) rpcServer.rpc("portal_" & network & "RecursiveFindContent") do( - contentKey: string) -> ContentInfo: + contentKey: string + ) -> ContentInfo: let key = ByteList.init(hexToSeqByte(contentKey)) contentId = p.toContentId(key).valueOr: @@ -178,13 +185,12 @@ proc installPortalApiHandlers*( return ContentInfo(content: "0x", utpTransfer: false) return ContentInfo( - content: contentResult.content.to0xHex(), - utpTransfer: contentResult.utpTransfer - ) + content: contentResult.content.to0xHex(), utpTransfer: contentResult.utpTransfer + ) rpcServer.rpc("portal_" & network & "TraceRecursiveFindContent") do( - contentKey: string) -> TraceContentLookupResult: - + contentKey: string + ) -> TraceContentLookupResult: let key = ByteList.init(hexToSeqByte(contentKey)) contentId = p.toContentId(key).valueOr: @@ -193,7 +199,8 @@ proc installPortalApiHandlers*( await p.traceContentLookup(key, contentId) rpcServer.rpc("portal_" & network & "Store") do( - contentKey: string, contentValue: string) -> bool: + contentKey: string, contentValue: string + ) -> bool: let key = ByteList.init(hexToSeqByte(contentKey)) let contentId = p.toContentId(key) @@ -203,8 +210,7 @@ proc installPortalApiHandlers*( else: raise newException(ValueError, "Invalid content key") - rpcServer.rpc("portal_" & network & "LocalContent") do( - contentKey: string) -> string: + rpcServer.rpc("portal_" & network & "LocalContent") do(contentKey: string) -> string: let key = ByteList.init(hexToSeqByte(contentKey)) contentId = p.toContentId(key).valueOr: @@ -216,17 +222,20 @@ proc installPortalApiHandlers*( return contentResult.to0xHex() rpcServer.rpc("portal_" & network & "Gossip") do( - contentKey: string, contentValue: string) -> int: + contentKey: string, contentValue: string + ) -> int: let key = hexToSeqByte(contentKey) content = hexToSeqByte(contentValue) contentKeys = ContentKeysList(@[ByteList.init(key)]) - numberOfPeers = await p.neighborhoodGossip(Opt.none(NodeId), contentKeys, @[content]) + numberOfPeers = + await p.neighborhoodGossip(Opt.none(NodeId), contentKeys, @[content]) return numberOfPeers rpcServer.rpc("portal_" & network & "RandomGossip") do( - contentKey: string, contentValue: string) -> int: + contentKey: string, contentValue: string + ) -> int: let key = hexToSeqByte(contentKey) content = hexToSeqByte(contentValue) diff --git a/fluffy/rpc/rpc_portal_debug_api.nim b/fluffy/rpc/rpc_portal_debug_api.nim index 9e7171d27a..968bb20f88 100644 --- a/fluffy/rpc/rpc_portal_debug_api.nim +++ b/fluffy/rpc/rpc_portal_debug_api.nim @@ -8,7 +8,8 @@ {.push raises: [].} import - json_rpc/[rpcproxy, rpcserver], stew/byteutils, + json_rpc/[rpcproxy, rpcserver], + stew/byteutils, ../network/wire/portal_protocol, ../network/network_seed, ../eth_data/history_data_seeding, @@ -19,20 +20,19 @@ export rpcserver # Non-spec-RPCs that are used for testing, debugging and seeding data without a # bridge. proc installPortalDebugApiHandlers*( - rpcServer: RpcServer|RpcProxy, p: PortalProtocol, network: static string) = - + rpcServer: RpcServer | RpcProxy, p: PortalProtocol, network: static string +) = ## Portal debug API calls related to storage and seeding from Era1 files. rpcServer.rpc("portal_" & network & "GossipHeaders") do( - era1File: string, epochAccumulatorFile: Opt[string]) -> bool: - let res = await p.historyGossipHeadersWithProof( - era1File, epochAccumulatorFile) + era1File: string, epochAccumulatorFile: Opt[string] + ) -> bool: + let res = await p.historyGossipHeadersWithProof(era1File, epochAccumulatorFile) if res.isOk(): return true else: raise newException(ValueError, $res.error) - rpcServer.rpc("portal_" & network & "GossipBlockContent") do( - era1File: string) -> bool: + rpcServer.rpc("portal_" & network & "GossipBlockContent") do(era1File: string) -> bool: let res = await p.historyGossipBlockContent(era1File) if res.isOk(): return true @@ -41,24 +41,21 @@ proc installPortalDebugApiHandlers*( ## Portal debug API calls related to storage and seeding ## TODO: To be removed/replaced with the Era1 versions where applicable. - rpcServer.rpc("portal_" & network & "_storeContent") do( - dataFile: string) -> bool: + rpcServer.rpc("portal_" & network & "_storeContent") do(dataFile: string) -> bool: let res = p.historyStore(dataFile) if res.isOk(): return true else: raise newException(ValueError, $res.error) - rpcServer.rpc("portal_" & network & "_propagate") do( - dataFile: string) -> bool: + rpcServer.rpc("portal_" & network & "_propagate") do(dataFile: string) -> bool: let res = await p.historyPropagate(dataFile) if res.isOk(): return true else: raise newException(ValueError, $res.error) - rpcServer.rpc("portal_" & network & "_propagateHeaders") do( - dataDir: string) -> bool: + rpcServer.rpc("portal_" & network & "_propagateHeaders") do(dataDir: string) -> bool: let res = await p.historyPropagateHeadersWithProof(dataDir) if res.isOk(): return true @@ -66,16 +63,18 @@ proc installPortalDebugApiHandlers*( raise newException(ValueError, $res.error) rpcServer.rpc("portal_" & network & "_propagateHeaders") do( - epochHeadersFile: string, epochAccumulatorFile: string) -> bool: - let res = await p.historyPropagateHeadersWithProof( - epochHeadersFile, epochAccumulatorFile) + epochHeadersFile: string, epochAccumulatorFile: string + ) -> bool: + let res = + await p.historyPropagateHeadersWithProof(epochHeadersFile, epochAccumulatorFile) if res.isOk(): return true else: raise newException(ValueError, $res.error) rpcServer.rpc("portal_" & network & "_propagateBlock") do( - dataFile: string, blockHash: string) -> bool: + dataFile: string, blockHash: string + ) -> bool: let res = await p.historyPropagateBlock(dataFile, blockHash) if res.isOk(): return true @@ -83,7 +82,8 @@ proc installPortalDebugApiHandlers*( raise newException(ValueError, $res.error) rpcServer.rpc("portal_" & network & "_propagateEpochAccumulator") do( - dataFile: string) -> bool: + dataFile: string + ) -> bool: let res = await p.propagateEpochAccumulator(dataFile) if res.isOk(): return true @@ -91,7 +91,8 @@ proc installPortalDebugApiHandlers*( raise newException(ValueError, $res.error) rpcServer.rpc("portal_" & network & "_propagateEpochAccumulators") do( - path: string) -> bool: + path: string + ) -> bool: let res = await p.propagateEpochAccumulators(path) if res.isOk(): return true @@ -99,9 +100,8 @@ proc installPortalDebugApiHandlers*( raise newException(ValueError, $res.error) rpcServer.rpc("portal_" & network & "_storeContentInNodeRange") do( - dbPath: string, - max: uint32, - starting: uint32) -> bool: + dbPath: string, max: uint32, starting: uint32 + ) -> bool: let storeResult = p.storeContentInNodeRange(dbPath, max, starting) if storeResult.isOk(): @@ -110,10 +110,8 @@ proc installPortalDebugApiHandlers*( raise newException(ValueError, $storeResult.error) rpcServer.rpc("portal_" & network & "_offerContentInNodeRange") do( - dbPath: string, - nodeId: NodeId, - max: uint32, - starting: uint32) -> int: + dbPath: string, nodeId: NodeId, max: uint32, starting: uint32 + ) -> int: # waiting for offer result, by the end of this call remote node should # have received offered content let offerResult = await p.offerContentInNodeRange(dbPath, nodeId, max, starting) @@ -124,8 +122,8 @@ proc installPortalDebugApiHandlers*( raise newException(ValueError, $offerResult.error) rpcServer.rpc("portal_" & network & "_depthContentPropagate") do( - dbPath: string, - max: uint32) -> bool: + dbPath: string, max: uint32 + ) -> bool: # TODO Consider making this call asynchronously without waiting for result # as for big seed db size it could take a loot of time. let propagateResult = await p.depthContentPropagate(dbPath, max) @@ -136,7 +134,8 @@ proc installPortalDebugApiHandlers*( raise newException(ValueError, $propagateResult.error) rpcServer.rpc("portal_" & network & "_breadthContentPropagate") do( - dbPath: string) -> bool: + dbPath: string + ) -> bool: # TODO Consider making this call asynchronously without waiting for result # as for big seed db size it could take a loot of time. let propagateResult = await p.breadthContentPropagate(dbPath) diff --git a/fluffy/rpc/rpc_types.nim b/fluffy/rpc/rpc_types.nim index 23fcc352a0..b0dc67cd9c 100644 --- a/fluffy/rpc/rpc_types.nim +++ b/fluffy/rpc/rpc_types.nim @@ -28,7 +28,7 @@ type NodeInfo.useDefaultSerializationIn JrpcConv RoutingTableInfo.useDefaultSerializationIn JrpcConv -(string,string).useDefaultSerializationIn JrpcConv +(string, string).useDefaultSerializationIn JrpcConv func getNodeInfo*(r: RoutingTable): NodeInfo = NodeInfo(enr: r.localNode.record, nodeId: r.localNode.id) @@ -57,61 +57,69 @@ func toNodeWithAddress*(enr: Record): Node {.raises: [ValueError].} = else: node -proc writeValue*(w: var JsonWriter[JrpcConv], v: Record) - {.gcsafe, raises: [IOError].} = +proc writeValue*(w: var JsonWriter[JrpcConv], v: Record) {.gcsafe, raises: [IOError].} = w.writeValue(v.toURI()) -proc readValue*(r: var JsonReader[JrpcConv], val: var Record) - {.gcsafe, raises: [IOError, JsonReaderError].} = +proc readValue*( + r: var JsonReader[JrpcConv], val: var Record +) {.gcsafe, raises: [IOError, JsonReaderError].} = if not fromURI(val, r.parseString()): r.raiseUnexpectedValue("Invalid ENR") -proc writeValue*(w: var JsonWriter[JrpcConv], v: NodeId) - {.gcsafe, raises: [IOError].} = +proc writeValue*(w: var JsonWriter[JrpcConv], v: NodeId) {.gcsafe, raises: [IOError].} = w.writeValue("0x" & v.toHex()) -proc writeValue*(w: var JsonWriter[JrpcConv], v: Opt[NodeId]) - {.gcsafe, raises: [IOError].} = +proc writeValue*( + w: var JsonWriter[JrpcConv], v: Opt[NodeId] +) {.gcsafe, raises: [IOError].} = if v.isSome(): w.writeValue("0x" & v.get().toHex()) else: w.writeValue("0x") -proc readValue*(r: var JsonReader[JrpcConv], val: var NodeId) - {.gcsafe, raises: [IOError, JsonReaderError].} = +proc readValue*( + r: var JsonReader[JrpcConv], val: var NodeId +) {.gcsafe, raises: [IOError, JsonReaderError].} = try: val = NodeId.fromHex(r.parseString()) except ValueError as exc: r.raiseUnexpectedValue("NodeId parser error: " & exc.msg) -proc writeValue*(w: var JsonWriter[JrpcConv], v: Opt[seq[byte]]) - {.gcsafe, raises: [IOError].} = +proc writeValue*( + w: var JsonWriter[JrpcConv], v: Opt[seq[byte]] +) {.gcsafe, raises: [IOError].} = if v.isSome(): w.writeValue(v.get().to0xHex()) else: w.writeValue("0x") -proc readValue*(r: var JsonReader[JrpcConv], val: var seq[byte]) - {.gcsafe, raises: [IOError, JsonReaderError].} = +proc readValue*( + r: var JsonReader[JrpcConv], val: var seq[byte] +) {.gcsafe, raises: [IOError, JsonReaderError].} = try: val = hexToSeqByte(r.parseString()) except ValueError as exc: r.raiseUnexpectedValue("seq[byte] parser error: " & exc.msg) -proc writeValue*(w: var JsonWriter[JrpcConv], v: PingResult) - {.gcsafe, raises: [IOError].} = +proc writeValue*( + w: var JsonWriter[JrpcConv], v: PingResult +) {.gcsafe, raises: [IOError].} = w.beginRecord() w.writeField("enrSeq", v.enrSeq) w.writeField("dataRadius", "0x" & v.dataRadius.toHex) w.endRecord() -proc readValue*(r: var JsonReader[JrpcConv], val: var PingResult) - {.gcsafe, raises: [IOError, SerializationError].} = +proc readValue*( + r: var JsonReader[JrpcConv], val: var PingResult +) {.gcsafe, raises: [IOError, SerializationError].} = try: for field in r.readObjectFields(): - case field: - of "enrSeq": val.enrSeq = r.parseInt(uint64) - of "dataRadius": val.dataRadius = UInt256.fromHex(r.parseString()) - else: discard + case field + of "enrSeq": + val.enrSeq = r.parseInt(uint64) + of "dataRadius": + val.dataRadius = UInt256.fromHex(r.parseString()) + else: + discard except ValueError as exc: r.raiseUnexpectedValue("PingResult parser error: " & exc.msg) diff --git a/fluffy/rpc/rpc_web3_api.nim b/fluffy/rpc/rpc_web3_api.nim index be941ae7a3..9176620ebd 100644 --- a/fluffy/rpc/rpc_web3_api.nim +++ b/fluffy/rpc/rpc_web3_api.nim @@ -7,13 +7,10 @@ {.push raises: [].} -import - json_rpc/[rpcproxy, rpcserver], - ../version +import json_rpc/[rpcproxy, rpcserver], ../version export rpcserver -proc installWeb3ApiHandlers*(rpcServer: RpcServer|RpcProxy) = - +proc installWeb3ApiHandlers*(rpcServer: RpcServer | RpcProxy) = rpcServer.rpc("web3_clientVersion") do() -> string: return clientVersion diff --git a/fluffy/scripts/test_portal_testnet.nim b/fluffy/scripts/test_portal_testnet.nim index 88d4fd3fda..53651eaacf 100644 --- a/fluffy/scripts/test_portal_testnet.nim +++ b/fluffy/scripts/test_portal_testnet.nim @@ -1,5 +1,5 @@ # Fluffy -# Copyright (c) 2021-2023 Status Research & Development GmbH +# Copyright (c) 2021-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -8,59 +8,59 @@ import os, std/sequtils, - unittest2, testutils, confutils, chronos, + unittest2, + testutils, + confutils, + chronos, stew/byteutils, - eth/p2p/discoveryv5/random2, eth/keys, + eth/p2p/discoveryv5/random2, + eth/keys, ../../nimbus/rpc/[rpc_types], ../rpc/portal_rpc_client, ../rpc/eth_rpc_client, - ../eth_data/[ - history_data_seeding, - history_data_json_store, - history_data_ssz_e2s], + ../eth_data/[history_data_seeding, history_data_json_store, history_data_ssz_e2s], ../network/history/[history_content, accumulator], ../database/seed_db, ../tests/test_history_util type - FutureCallback[A] = proc (): Future[A] {.gcsafe, raises: [].} + FutureCallback[A] = proc(): Future[A] {.gcsafe, raises: [].} - CheckCallback[A] = proc (a: A): bool {.gcsafe, raises: [].} + CheckCallback[A] = proc(a: A): bool {.gcsafe, raises: [].} PortalTestnetConf* = object - nodeCount* {. - defaultValue: 17 - desc: "Number of nodes to test" - name: "node-count" .}: int + nodeCount* {.defaultValue: 17, desc: "Number of nodes to test", name: "node-count".}: + int rpcAddress* {. - desc: "Listening address of the JSON-RPC service for all nodes" - defaultValue: "127.0.0.1" - name: "rpc-address" }: string + desc: "Listening address of the JSON-RPC service for all nodes", + defaultValue: "127.0.0.1", + name: "rpc-address" + .}: string baseRpcPort* {. - defaultValue: 10000 - desc: "Port of the JSON-RPC service of the bootstrap (first) node" - name: "base-rpc-port" .}: uint16 + defaultValue: 10000, + desc: "Port of the JSON-RPC service of the bootstrap (first) node", + name: "base-rpc-port" + .}: uint16 -proc connectToRpcServers(config: PortalTestnetConf): - Future[seq[RpcClient]] {.async.} = +proc connectToRpcServers(config: PortalTestnetConf): Future[seq[RpcClient]] {.async.} = var clients: seq[RpcClient] - for i in 0.. numRetries: # if we reached max number of retries fail - let msg = "Call failed with msg: " & exc.msg & ", for node with idx: " & $nodeIdx + let msg = + "Call failed with msg: " & exc.msg & ", for node with idx: " & $nodeIdx raise newException(ValueError, msg) inc tries @@ -91,10 +92,8 @@ proc withRetries[A]( # To avoid long sleeps, this combinator can be used to retry some calls until # success or until some condition hold (or both) proc retryUntil[A]( - f: FutureCallback[A], - c: CheckCallback[A], - checkFailMessage: string, - nodeIdx: int): Future[A] = + f: FutureCallback[A], c: CheckCallback[A], checkFailMessage: string, nodeIdx: int +): Future[A] = # some reasonable limits, which will cause waits as: 1, 2, 4, 8, 16, 32 seconds return withRetries(f, c, 1, seconds(1), checkFailMessage, nodeIdx) @@ -117,7 +116,6 @@ proc retryUntil[A]( # Could also just retry each call on failure, which would set up a new # connection. - # We are kind of abusing the unittest2 here to run json rpc tests against other # processes. Needs to be compiled with `-d:unittest2DisableParamFiltering` or # the confutils cli will not work. @@ -142,8 +140,12 @@ procSuite "Portal testnet tests": # Note 2: One could also ping all nodes but that is much slower and more # error prone for client in clients: - discard await client.discv5_addEnrs(nodeInfos.map( - proc(x: NodeInfo): Record = x.enr)) + discard await client.discv5_addEnrs( + nodeInfos.map( + proc(x: NodeInfo): Record = + x.enr + ) + ) await client.close() for client in clients: @@ -173,8 +175,12 @@ procSuite "Portal testnet tests": nodeInfos.add(nodeInfo) for client in clients: - discard await client.portal_state_addEnrs(nodeInfos.map( - proc(x: NodeInfo): Record = x.enr)) + discard await client.portal_state_addEnrs( + nodeInfos.map( + proc(x: NodeInfo): Record = + x.enr + ) + ) await client.close() for client in clients: @@ -210,8 +216,12 @@ procSuite "Portal testnet tests": nodeInfos.add(nodeInfo) for client in clients: - discard await client.portal_history_addEnrs(nodeInfos.map( - proc(x: NodeInfo): Record = x.enr)) + discard await client.portal_history_addEnrs( + nodeInfos.map( + proc(x: NodeInfo): Record = + x.enr + ) + ) await client.close() for client in clients: @@ -231,8 +241,10 @@ procSuite "Portal testnet tests": asyncTest "Portal History - Propagate blocks and do content lookups": const - headerFile = "./vendor/portal-spec-tests/tests/mainnet/history/headers/1000001-1000010.e2s" - accumulatorFile = "./vendor/portal-spec-tests/tests/mainnet/history/accumulator/epoch-accumulator-00122.ssz" + headerFile = + "./vendor/portal-spec-tests/tests/mainnet/history/headers/1000001-1000010.e2s" + accumulatorFile = + "./vendor/portal-spec-tests/tests/mainnet/history/accumulator/epoch-accumulator-00122.ssz" blockDataFile = "./fluffy/tests/blocks/mainnet_blocks_1000001_1000010.json" let @@ -240,20 +252,18 @@ procSuite "Portal testnet tests": raiseAssert "Invalid header file: " & headerFile epochAccumulator = readEpochAccumulatorCached(accumulatorFile).valueOr: raiseAssert "Invalid epoch accumulator file: " & accumulatorFile - blockHeadersWithProof = - buildHeadersWithProof(blockHeaders, epochAccumulator).valueOr: - raiseAssert "Could not build headers with proof" - blockData = - readJsonType(blockDataFile, BlockDataTable).valueOr: - raiseAssert "Invalid block data file" & blockDataFile + blockHeadersWithProof = buildHeadersWithProof(blockHeaders, epochAccumulator).valueOr: + raiseAssert "Could not build headers with proof" + blockData = readJsonType(blockDataFile, BlockDataTable).valueOr: + raiseAssert "Invalid block data file" & blockDataFile clients = await connectToRpcServers(config) # Gossiping all block headers with proof first, as bodies and receipts # require them for validation. for (content, contentKey) in blockHeadersWithProof: - discard (await clients[0].portal_history_gossip( - content.toHex(), contentKey.toHex())) + discard + (await clients[0].portal_history_gossip(content.toHex(), contentKey.toHex())) # This will fill the first node its db with blocks from the data file. Next, # this node wil offer all these blocks their headers one by one. @@ -268,7 +278,7 @@ procSuite "Portal testnet tests": # add a json-rpc debug proc that returns whether the offer queue is empty or # not. And then poll every node until all nodes have an empty queue. let content = await retryUntil( - proc (): Future[Option[BlockObject]] {.async.} = + proc(): Future[Option[BlockObject]] {.async.} = try: let res = await client.eth_getBlockByHash(w3Hash hash, true) await client.close() @@ -277,9 +287,11 @@ procSuite "Portal testnet tests": await client.close() raise exc , - proc (mc: Option[BlockObject]): bool = return mc.isSome(), + proc(mc: Option[BlockObject]): bool = + return mc.isSome() + , "Did not receive expected Block with hash " & hash.data.toHex(), - i + i, ) check content.isSome() let blockObj = content.get() @@ -289,12 +301,10 @@ procSuite "Portal testnet tests": doAssert(tx.kind == tohTx) check tx.tx.blockHash.get == w3Hash hash - let filterOptions = FilterOptions( - blockHash: some(w3Hash hash) - ) + let filterOptions = FilterOptions(blockHash: some(w3Hash hash)) let logs = await retryUntil( - proc (): Future[seq[FilterLog]] {.async.} = + proc(): Future[seq[FilterLog]] {.async.} = try: let res = await client.eth_getLogs(filterOptions) await client.close() @@ -303,9 +313,11 @@ procSuite "Portal testnet tests": await client.close() raise exc , - proc (mc: seq[FilterLog]): bool = return true, + proc(mc: seq[FilterLog]): bool = + return true + , "", - i + i, ) for l in logs: diff --git a/fluffy/tests/all_fluffy_tests.nim b/fluffy/tests/all_fluffy_tests.nim index 238efd49f5..2aefa4bca6 100644 --- a/fluffy/tests/all_fluffy_tests.nim +++ b/fluffy/tests/all_fluffy_tests.nim @@ -5,7 +5,7 @@ # * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) # at your option. This file may not be copied, modified, or distributed except according to those terms. -{. warning[UnusedImport]:off .} +{.warning[UnusedImport]: off.} import ./test_portal_wire_protocol, diff --git a/fluffy/tests/beacon_network_tests/all_beacon_network_tests.nim b/fluffy/tests/beacon_network_tests/all_beacon_network_tests.nim index 663d332072..36e2ba7ab1 100644 --- a/fluffy/tests/beacon_network_tests/all_beacon_network_tests.nim +++ b/fluffy/tests/beacon_network_tests/all_beacon_network_tests.nim @@ -1,13 +1,10 @@ # Nimbus -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) # * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) # at your option. This file may not be copied, modified, or distributed except according to those terms. -{. warning[UnusedImport]:off .} +{.warning[UnusedImport]: off.} -import - ./test_beacon_content, - ./test_beacon_network, - ./test_beacon_light_client +import ./test_beacon_content, ./test_beacon_network, ./test_beacon_light_client diff --git a/fluffy/tests/beacon_network_tests/beacon_test_helpers.nim b/fluffy/tests/beacon_network_tests/beacon_test_helpers.nim index 1b723c58da..6f226acad1 100644 --- a/fluffy/tests/beacon_network_tests/beacon_test_helpers.nim +++ b/fluffy/tests/beacon_network_tests/beacon_test_helpers.nim @@ -1,5 +1,5 @@ # Nimbus - Portal Network -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -10,10 +10,7 @@ import eth/p2p/discoveryv5/protocol as discv5_protocol, beacon_chain/spec/forks, ../../network/wire/[portal_protocol, portal_stream], - ../../network/beacon/[ - beacon_init_loader, - beacon_network - ], + ../../network/beacon/[beacon_init_loader, beacon_network], ../test_helpers type BeaconNode* = ref object @@ -21,9 +18,8 @@ type BeaconNode* = ref object beaconNetwork*: BeaconNetwork proc newLCNode*( - rng: ref HmacDrbgContext, - port: int, - networkData: NetworkInitData): BeaconNode = + rng: ref HmacDrbgContext, port: int, networkData: NetworkInitData +): BeaconNode = let node = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(port)) db = BeaconDb.new(networkData, "", inMemory = true) diff --git a/fluffy/tests/beacon_network_tests/light_client_test_data.nim b/fluffy/tests/beacon_network_tests/light_client_test_data.nim index d63b1b767c..46d01e92e4 100644 --- a/fluffy/tests/beacon_network_tests/light_client_test_data.nim +++ b/fluffy/tests/beacon_network_tests/light_client_test_data.nim @@ -1,5 +1,5 @@ # Nimbus - Portal Network -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -10,21 +10,25 @@ import stew/byteutils # bootstrap for epoch with merge block -const bootstrapHex = "60b747000000000018d305000000000023fd2bf54d3f71f13ee0ced4ab42970231bc67fc7fd1e933bb38cd184a90ac6df1bcb3404de7155e2b51f942f091b7d699bd3c32ee8ab3e30e64474f8e90196faebeb48401ced0bd795d19fd1162654192220d7c3f4bbfc93fcd7a76b43b3a6480ef074cf85a1fa38e704c05073f2def4b6ab67fe958fcdd5bc42573f1cb82eac1657ed399ee4e3cda0d3d7ac1e2429f8bfb483ca6701d63ee4fff867044a4f50342b0381e2570301b4057c0ac9360340bf1b776d8d06a80919c7369c54a3fbbb7014062e9150c2941cabbd6e9e2712cc6bd596801682ade219364a22f7dd2a5a30c7394268d17fa48e371c7b41e5a88a0df47bf53dd729eeae58b487dbe06f7cfa855bcf78396bdcab622852c3caa5d1d6c556788c857b689c711792de936228d929afaf940cf30295d28748a395b0f01097a4519d1977e8292d8ec8966943840dfbef204c99e27a94c426fb768fe59a7873450f2e85ed0b66596391649b7e3b4b660a6138e58526cdaff38c70d63e1e6a56f54c24181b920c65aae87a333968137214d4eced416b6fc0f8075c7855838aa0aec26dd41b079d61deff66e1edcfd146b5f03d38e8d2d5815b54db6b7b092ff63e4119e57fc324a6431efc00d13af7b8e0addda3a239abd219968bedc5ad81f36c7cf387e0f80e4bd29d2e73f2fa6f550d46176b706a2b776a5b301608acf26746b94342a3132a5705e45b0fc8a0405c713249c6e0305294139278d7901876e441c36d56a7ef4f6c632cfc018dae58bca6c1fb4e43fb6af1a8c97ae240ace999c8bf899105ff18b09277f778216825ef4bf2a7a1472b4dc1a5290ab73c20b89708eacc893cb348262995f7f5e576d933f7501d10599f9396b0076341e0ab9dcf24dd6edb163d9d7d5d3fc02a1327925bfbbc8e976db08db405761b8b5ff37194dc984281cc904a411d7ce33314e94950a8f71de218196eb577139272ddfe260c01aebcd89548a365d6214d6059442618dbe44db1392ac72e94371985171b2b58415c42a78ca0007eba179c5df23ff201bc684affa197f33ac5c443706d8bd8db6ca5dffa34127118d78d09fbebcacb100748600587d7d032cde218c41916b644f51dbfebab668054986aee1e0165c7b775fb2493eda3cd86eddade62657808d159a7afc773e66fc565d514e0fd43d477494f6d4d7a425420b0fd6b4662bb3171f7f2c8530619244c4fefe438965b822aa787228d51fb5084f5b7e87399e871325de4d3279cf56431d672899d3efcdd30be64b70b9482da4355a8f09fe8596710ca0b24ddccac9091b7ca51d59f87dee4c4e355ca878ce6e9eb27383c3f84f48e7d2401fa20f854db84b3a4476bc977ad8976e5ff4f556fe68ef31a65fd831188dc4541edf50662d006258951d714b45576f60cdd37719e7898bcf76a7d9866afa86dfe3a4e42e4f217a327b17b790ea83adce07961357c4194f419525c034a512d81bd30db6bde2c866a2dce69d8ca5a2557174597f03fca3379e424c5286005ba2f0cc0948d693ed67c0ecea8186ad4c0927f5ef7790c9c4463dc3e28faaeb1eab018c125311db30f58cc90a4ab42c2c2ef5bf12d592b16540be2615883cab02544e3f44bba43531ecc93d9ce18349bf39da6e5e5e90c9586fd2e4d7dfffa9f867a494c3de7e145b6f2e3ca2662112be61dbac9911702a1e25ba7f9bcfaad1285f71f633c01722c58556e0359c543906a0e8da2486e8e6ba730dbd44b4dd6c8ea7d2789a3519d6aebf6ec65a7fa39a1ea7d7950b61292c0850f16378fef0ee52ceb144ce6a3dc57f7e12c2b3233bd04b4a2c4222dc3eb972c447eac7da942bd6a744959b041e31a3ad567724536af288b4cb280a77e873b9d511b203f0a759ebff2ed659612295e37b96ec673aaa998b96de5a1a1d5a0cab83c1d7f00f6f9ef1f4906d59ba4d3b46bb8a4d602014c4ee2b1e21fc92f7a9e0e01a99fc50b72a60a33d0914e02500166aa2f18fabf37bf2944fb73ddbcd7cdc33b8606514254418c48b9ecc1b36700480bad674c68692d6b1608b8c68897a53ffe1db908a7156a069e25392757c8179a981bfbe6e3f2ee20f65a07d34dc6d3c302bf57048862d740f35051e32776ebd2f46f775867c8218b5501500a58d414fdcb2a78dd6f55a278045cc607c72be0903da0ab6e98edeb78499ac3b336f806dcc16b2d1f56c84089989fa612d56c17eb6a5702b34a83878bc43cc3d52a0dbe9ff2fc2fe10881facbe09f777f4cb6ab0b4530bfb0bfcfdbdadf2d0f7f354c412315d4f59053dd78b3ad2727ab093680a018807dd4cb88a794bb8ec7bda26f041f40c0501910e190f57961b960b7ba48a9a0176db376b3501a0a6552958c31089d5e53bd1f8958f99c39975af625f4b6cd290aa43415952a9a8899262e54d7e450ca1cc5259e340fab10b52c855f844112a264b29fc98dcadeb3d180c00684733931135b09d895922e6713911c4ab79afc257b13c2f16b6a1f0a005db87ee057b0a027f37ffaa94ef3f59a5664f3bfa5da0c13d86a7126b1f1e68a4d7aeacaca43970a28930589e7eb8afbfa5cc99e30955ae4c130ca2c34a6635ca73fad659fa79006b36bb8713a158d3133fd392e2bf0e59eea9db54c0c7588899351490ea9b72671e6d508fd49af8975b0cd6ad188082ea7120e114c230dcd0fc28501257c4fb12cae06759dc361a19de8f8f89c73c0f6fdb3244a01300c9740a765e4c66addfef5b28770b7e39a9665cbe0ac67a68112c28ed800bad82beedc23d8874be169997be7aa898c1f239faf6d551eec025191fd5a384ad2158d953a526f110d413e2559474695df7217dc1c3782971e995226b054712b1570013916bc729000d46927590f17e0533c04331ef372ef0770ca18db143377706b1a034831918720fb8912d2e4a68adb54368e5d20a5cbc6a12abdf46dc4a48b9dee95d8b4fcceddc3fc5c5de7da35c59951f7f03023a36f7a03a087d89518cee00934039eea01df9786de93791a50354b14316ccd8f185ce78a8d8199e09d97a6f0c322e4b01d830287f103de22cb8f93c8188071781bb0a0e965b09e1e6f04ecb88c093c35a5467a08341c2154c538d323911b1fa1ee9fe8016627faabcb9caca5ef19daa57d6a292ba38865b02aa959dbf2b184b80c31385378965aa984104af574f3d37f2c480e3f3a60792e9a5c65e101e6dc75688b1d4271713375892337f2f3ae9c05f516e9df29551cc4381bd48eb21233bf2c3b17f886561d359805579825b3ad53641eb0f9237f3362e787181ae9e943efcf627169a0ef82d261cb4c6665c49ea663eb5f216033d28228f0e896b4d683fdf869a9e55ff0e1d62dc6865a311668e3712b089b9c245c18dbed3c5ff32e1698f103b7638191a943fa2e8e5c88cc12c257ac47506d53d7186f3e60a2f3ae8ba38ae6a33ed79243c616baa95b6d1d148fe065244b76b5fb13a9376eeab37578136ca4a23b636d0d472a7c6926825dcee21e04805481e8ccf1daf26d8391c1fa8adcd0b48e6f70af81aa349507c6843a4447924b2edd1446fb4726cb024a79a95a0984fee0753141afdcb776d6d520c1b3a8fa1d9e43dd9d2fab9343431e64fdb05099450e1334b1e3f73105fdbb432b726ff0eedb3fc24ec369f3711d6ec3836db75b4868442261930b2e930f7955a4f46d7154d5b4e1e372bdaed686070e1b4609ea86277cfb186b53a5ea41b2ed3bb4bb0eb5d89f6c15cfa960bc3239be23d4469e779739da3cea3c695ad1442cc506853099bb45552a466ab780f8b9e229eef82867ef05b7d665281f5da80bd89028cc1734b6da0fe95b431afc3978b68e44510e4de674d2efde1d56f1b9e2ff1828e8be8bc5addf0689e9665f3735556b72675cfa48712f220103b18c16e58e5104a200e8101f7212d5a6061014acc18b59728a8b9f7d1ab4eb0aa244a4434a7af9122e2572464d25e09e2f0a97c9f9682f6760bad911f15819d2f7bc458ba2c434deffaabc956ce83438ec9a94e297f9223d2b70f511f3ddb6a2da1b4565d04e792de131e850d1b9aa4d9db7dc61322bbbbebdf7ce5aa61a116a5addbcff6d8ee559c4195a7cec17148454e88d06a9c233f423dfb14530c402a57d77df9bb1e9650381bc1c2ecabdbb6b65828d50ad605f0ceed577f76f4b4126d643956fc1d1c845c72b44f75b388909e3a6859dfcc9814f4f943335c1151a2899a6068b8268e8965552a158096c2158e6d012be0cdd9c94be7b7669da3ba17735b67a6103fc806475db2e17dc0655b90b58e8e7e15758646cb963f5eb17b8366c75e0c93d851ddde63512fe1c5970651138c72618dc6a11e490ee25c66376d8a227346d87b6abacde2b3963fcf879ef7af2ed7c2153ed56dd2e6a8030abf0822c6729ab97214cec5e837ec949fb9108d4aac541466a0fa3faa242d26bdcbe1786ca1eabf19a1f3d2f0ec4c6cc72c3cd57105fd1f382734640dfacd63a130229024c09cc112a00331de36d742ce871415a91ad840574056f1f9b8991e706c734ec88e3a3424291a837f5ff9930357a1a3fef060959e1ccef1d2258e2248bf232ef9bc812bc0b217ba6bf1162a6a8be4245bc5c6e761262be829caeb35c25204a3c15eb628476dbe0ae495519809d9d250473de1fbd7013950a1052ba28f97a2a44d3c3edf13e43c8513d5441c4fa1b88a4c7f21cbbb9d4fa1284adf5231b281183f1696f46c01dbecacc677d1ae0bbbb4296b5710e5989017fbab541a6a8bb2a69514c7fd42a991118b299d1d30fe47bfa941528e6920d92d07b1f51afd26eba83389e57a36653269999e89173e7ab083128f91d70726389221e60db7b9d1f044dbad1de0fdd1dd6757ae7330d08dafccedea648b134c86cc9c57ce830abc6a8869dd8b96150fac6b37f42dd2dca8c0222b2e7e0b65969b8e137a42556f991269e5475c93390d62cbc385d30544c1cbab95c019f3083cbe545b10ba6a5c55174100279cf787babe24fd599048ab1e2f47b7feab6dddac2805425541c905f541a5a43c60c11e8293812c6737e3b8233988c08a2599a89605be2776e9d264c626ca691e48e224e266b11514756c23d58894c855dc5838c66690133ce9c98089df06b29ba572015cc378d77500cda567d6971a515e9d01b34c5ccefa1f1f60441da2ded8408337639698e7ae46b923ceed1b9b3a1ae5c847321e9c8810612ba86c1b1de6ef854f42aba1d87cd896faec6d89a3b49c36a9336cc747f7387853554a9f64bb5dedea2ad6dc5f615a8024abe8acd71b876dbbaff804b955c37e270eaf892d30fe26c4591e25041ccd7e187277f18200c21ecd9e379f98b39262b690a6f902d682526e98c620b49bd8f4566f04b31cecbc8247e364c1404ce28b1703516f1894747ef39c13bce189830e50eaa4bc37694cd4a4e21ed68e321f16eb35bfaff1be1aa5339961ef993423873b7c73e92dd6a92ad23a0f51e121220b8fadec0a288c0b2f0581ed524244fb95ae3eadb36c6c4c8ab4f82d4d608b2a0bd18f1bcb253798506268e2e53ccf84a39677e42752dbfb393de66a81455666f66d560f98062e8dc72a3789c532e9deeb05dd813fbf03337a93d244faa5e9c5f9d24b3695b00cb4acedb04f0185c267ba3bed40af0c264dc3165f518f8128fc13d5163729ace450c4da714a0aa17f4373cb2b3e06ca950569ea5f1e9f9095d162bfe148989569a893750b68bb1223c732d50390c22eef927092adf5cd00dc0d252a34ad6dcb8bc804646d0530962af9a162b24e922a35107e40b58ccdbd1c0a7432284fb8b8e848fa87aa65bc1cc08b4a61d8b7c7bcd051a24067cd4d8e26b693089d6f8b02f7d3e780e3e15a5a1844b4dadb1ebf538eba3a8c1052dea10fe475dea98b23875f430772579ea212e97bccfe75faa662c5688767577167c66993811a889b7a9e90ed804f8433d6a60d55359ba1a5486dfac85097e28bbfcd961864fca39da51235ff5d3a58dd208ceab56d879e5eabf0a153bd68fb1cda47d08ff708a86440bf86c6d5b13932e8a88ee7b95b72cd908025597620f51ac79b0f47f60fb683ac83ae62b7c44b3e3f939135152f0369f7c696fdeba0b17f296df6dd604eee26aac020cab75468c6ec74e538b1902c687fa828be3014869450547d1da4fdbeb15924361fff69b7946baa9d373c7ef070b5947928af2c826f2fd61ca4ebe553f5c3c50dbc70e456cee6c0a6dbc2848dc0448e32d6d47fbc354bb797a41c3dce359364b4409572daf2ca989540714acc4c62b819f241b2055932e5b9128fbab2ce52ae9ab9889dbf0ae66aa2aebb55442f98c083aeb4b758d689a9894f74e1c61dc37c544890d49b812549d3713c9a7114f8c4151c42ccdf7ea4ffa2fe00651ba0832136b6da4bfae0602ede4f2714302bca0ed4d9cd1fc449fe9317c1cb7338426358bbb870a4a29d4c967e2dc21851748d9be72cec41f8b18851d21c4dd8614ea2b91d689b6a03331bc7a448cd0935ffb793834b2e2bc2651a1453047ae89c87b91b7e383d9807213f742fce4a4e008c6080dd11ea8366dd331c4c2181a6f9211fd176980e5c3279da47aa44fa7980d7a47c0cc41d6e9ba5cdccb5aa110ae977838b46cc2f31220433e1447cc30a93e69dce2a00d1bd04c0b6ef34ac9d535a1e96051526104a9e891d52ffe4ad1b4cc2730eb46269121a241e52c39cb2f71c9a06a88169afb99e617da1454db450964180f76c2e7e824ed1c0d2e19eb2eda1d4ca7f40d600c7f5026b1a451825efd38271c70f3dcc9a9ff7e76d18753cc082eeaa0f4c1cd8d8c84097356ac8d39385d4afa1c531d415b4a6f64f51d53183715fdb29d76766f5fbd5a669c4788771879bb0df805d559b0755a87fa16f0740ee2dc66a396e0660419c73582c78a4c4fe59be48291ee11cb169cdedf3bda6c6b02caaeaaed426545d4ce34e1372e3e99e5f4faa543b761b7359f5155735faeaaa5dbce62e52949d7f23c7d6586c4355322d946ec21927a99d0c86cd63a0fd37cc378f869aae83098fac68d41e3fb58326ce2e9f81f1d116d14d1c0bd50cb61f0e35ab02eacfcce5db0b01ec8e69fdc120f2b9729a826586f80239317a662cd7c9b80f06c45f582e86e75359bb03498e229792e5165a854a93b5e2d9a2441f4c994649ae8449279022fb10d5a7159e489a3d4b7e2a4fc9b02a295f5df5b1986020da8511a6681881ab187af1d90b617ce2bda95cc61313516b38994a4e0fe8dfeff0db7f9496c433cb03b8cae8abbae48f5e8c7fd274fb74de66a5f8be68c39f46dd889325591f47fe5f6dd374d7860abd55ba05d3ef89f923f4abed4f82c8a907b28c898252a42d18410c78b4b3204fcd635f714aa5badd937979103a9724147912af8df9ecb9ad721390c5f1ee597e3e2b88ce759bbcae1a9bf53c1092fc98b3c4fc2984089ad74f29ac2de3ecf53172975f621c3873041dac31a7194dec95dedb80b03a46dd826cb58340ad785dee503743f6f55f0d9c40d2d96c418e775fb0a99bd1e821fcd16e0dd5311e50bee9e7f6a344d8f32512470e88ab46765ba47a9f79a371058ace15842b0a4c380ee5c2c7a941da3621fb0facba8ece2c00e222648aded76010de1bdc2bcc90407253e761ad82cf2ccc40bf4cd820d4ed8ed09040970b78bbffb25579cd208f505bf6f14385eec727ff3a72c5d758628fd3eaba630bd0aff4271d08e0597e7d0c6d9d9b064429f437d622e5104e1776a59441db9eb901c4b55d95d4e9e51ef91be90775a02213b32e151023d3568232ffee05f7b3e92bd927a9546927376ae0652bf35745a5a9d91c29f3c40938741e83f3f4a90441450ec84294d9b9505e0436f259444c77cd3d09e2b170ca7bf3038ddd7ac2c2b9d59c973b1d9f2f23e5cbf217b90bd20aa8806f4bce50c031d5ffc9b61f662572709086b14bcf357772b2c69220ae30b971a4e2f28973657854746d9a7664e1838aabdfe999622ab5c1b58dcb328d938839b35979171788bbc72aefbe88bd90a9b2ff379e8fce2a7dffa46b7d8cf4e39e23ef260603b1b039a9674179988e24e2876671f9b41ae1889e2231a4483be1b6b9d64ea5ee77558019ca064a4cc3b4e7d7d65e5faec8214a26d93ea99012853c0b96cad3f53bc30e492e7774c9081f922fd7c1982ba9c329dbcd0d9eedc61d081b65533a556a60e8db32ed813ea79a0b4025a9ed0d6f6cf657b87fddeb661db0959de80829388cb7eff31a1568b6ef19abcd3b861e631f338870984cec2f900aacc82661b3f857b0594055d5c88863a2558f82db9bc0a2a23d0b143591d44ee02e377b9defbb4ea4a1c87fad094c60c54d98e31f6180b772d4e7dc62497d4a8a8e22e4c260397718712849518261a0ed86d8bf0917b89eb1de69a03094117c22c868e136da00eb6e830929aa31c321b20b10b4db998e7ff9606653021418b8355220f0735675628798399824809a60cf70c1c24af476673b24e419bdcd47b7a6bdffee132861b49c053f3980342e1c13f84fbc9050b5e6066fa53e453ec2de04dbddd61bcf6dbef726eb61462f3db68a10739f0ed5e87a48b4655abc03ccf3cbc78dee298acffad27355506ef8f3f731c1dee151674e5d2b1c48914f194e6cb44acbe106454f17454e8f1a8cd2b92b80b674fac1828ab064d04f2ccddd9c072c5b4becc4a908cc1c95496850fea37f986d37e65114b339786a07549bd1c054827a37856552fad0533c453ab50c5d89b818eb3cb2c579aadcdbb70fc4521489aa84cc2db200a4126a6d6e22790ce00e7559946033d80516425b722b156f5a2adb2acf4cb7e3aa23e0f48a77604cddd4a7cc6eaacabf91c45a995c546c57d7756501328bc878bfd8177eee9c1dbc603894464d1174f0c9ebcd23e0c5358a5331991deee84292439b0f8a9cb04c8acc166612ce196f0a0b7d7d566f2d6063974d529ae5501259e1b9d7a5e3385c61445baa6658f60246efa4c34e44e02555e5c438912142528d4ceb0951b5699ba61a97e284acfc0318e45771524ae10e7163a4b6e23fcca8517904d0e9754ef5de80872bf77fbf37e56d65fc7d172de65832bfae3a44d518f37d51af9cba7de4ac709085c8b436e8e4f4400276a6698d75676a2d631900a5d5fbddb32f8c3d02b985c91f2ac93fd81bf235d8c0d7c5bbc56aafb4daec6c075c3ea686b4cfb298ac6ed5e6d4ee7319d755ea4558858ab74b9792cb4bb8730a7ee1173eb2090d7a09daefb12df7338d8a1695949dff452df796cc9c42e2b8a747d62a2a051990ad0726dce1f1ba93618e669add26b1718dd893b989109ca2f4736f74ba50aaf0b47bf3267d54dc32ce4a310c78878ac384a5c35ac4ed3730f5072ea98f66fa6268386d6fb0004cf900cc8f0acbd8d9a7fd80dbd2cf6552918d1277d47600730120ae96e433e94ee7f5fa8a25a31f96186f5244fc8f1580b9fb2c252cdc9c0d01323f6296086a5abe4472dcdddc9d85bd98f32345d0a68f6f44b970f7e313d89edd1493dfa87fd66287409129ca260d1e9aa34c0976f8cb5bd8d2daaf2761b36a5d3001bcf5fbe1c9b2b3c2b59013d6249a97f170848ccb0d8ae988b32dfffc0183bfc1636e3ca861889be8b50c016c49158d83e519405f7f9cd7518da5554c05323061eaa767066053057bdba91219a2f84bee9a736435fef2ca0688cd46bcd6e03c5ff1105523b3f45db8a939b0b9b7023540d1a533333a143f4bf18ee3a34289012f87157928c50ef0306b63f3830514691cd5bef4fd86496af89dd7f0bd97bbe61a018915ccbf4e13389a98d020499230e56619bf08d1c96d943f01eac120c30fccbb79d9c576b3d14cd3c97fd1b89bc885e7b57669eaddc31e1989ea03ff28b702a14b77575ecdbe7a862476fee5b1d38760e7eb38b4eae5eaa5fe8eccf523bf77fc9072c681f4d2a0a44b378322505b808acaafed73ff6518679ad7e756753446af4ea8665b579d6c972df6f896660c21b597cf7e51508e5cb95b760c063c2e58cbc84a5628ce0c9ab29e5ce39a22d587f57899435a027d6f560744e9f2c73c475080077ea9dd43b10dc80efac040db0a1089b0424ed1c6960dc163a979381369bc15ce09b4f41c94c30a528d8e94dad36e842322173e07a0d971b907f38190fc59bebd2f77aeaef7ca81ccbd608ae4ee42ce860f9eea1bc6e29f797dde02c04786a8392d0bb6b20fbdd64c328a8f02292fa03a6ab5b0434ae8fe8558930f79b60eb8b8e89da13272472bd3505fc64cb4078764e6905add84f9e6a6ad4a1964edc5e59ca3bd2e2729d9026ca1d3d687dfe5eda2c403b0b6262a1b9a3d92fedd8310855eea87946fe7ac4f22e6729036419c96ebaf3a8465877774ae6888540f73aaf18b22110cceb6180034880568d36834921018f8e8e90222e83c76fe76792416c24c53a96094fd1aa9381cfe4aa673fa662bfee6728f7e1da313c3059ec2e583b346a474552703a517ee9d92273aed23b3f9397ee94403fb8c939498f095bb1df4ee1480b0510aee02918975f1056c2c824730fe9ead1bf9c09572983f31c718601a697cb0620d163125edc7589ee28eccec201e3da4c9025cf0bbd16c410200af3d256ab2af39f89d4e613b9f22e7cede0a8b9fe55a1c35e2fc67a6b1c4dd4ff892d4e7ddfa37f5cb898ffc0d53d7ae8466f09fe5918c8ce7278d2081ab237dc287c5fdefd719e371b8a4d0af713a00645bc79891765de90cbff4c170a7fcc9ad4657d8943d4cc9e90ce3ddccec671bc2ee25794d192116411994ad282b2640d9e00d4cbc665ec30db10de8a4f047c4861b9fdd9ab2f4bc1e4b7cd9d2517f77bedbdef190616fe6d35232befaf4ca1fc72e23d4df02db6057d74778ce192699b31bf8c93f0ceb24d8c0e3743077f4c05cd542f6aa5e7484d81caa0a041b3384beaca732f8ff6ea6acb2f25a8503a52d99349b029270bf952c586802f1c5864be53a9e6ed400b197145592f006f7481804844028f30d12c49825969d59f03c1989e8e4f4c560030227e0855761536aaae538e2bec4a2c1f2e344c86068546d7d61480f0095de1ca2f411d460199bb543a985271b991e7171d118a94af369656f2ac79b9975b3358e76e1804c0e73f16e49663a560df4807497b974247a4c9d1990118d4cd042b44ab04de62d846323d3933da57f959dac8be59bf04343c81c55648128b9a004a82c85701ffa6a6730bbb36f55691cf9ee5d0762fe17b531cb80b0d9890ac36a466c0f5f2335a7c2950fea36a8456f18c056c850ea69f7f57e20999c02fab97b1239c9b653ee2923a9c5d119069f9671b1570fd7ad961dd0490be24b2a59a4d285e804a2ac7333587c58b55e592112bfb7b74798564df4292242b3c5e5f384bf7ef510e2d950908384ea63b40b0241f592ae54064eafa205b99ab26eb5f591b62fd651c3daad53662a6940f2ebca76a8d03f1ac02a31269793619aad7ce78c2a06797abaf303545acac98283e0ae7ef52f20cc5b993e2c9eb00e56bd86ebbf444e3dce7ada7b7b1f250cc246813133a627d488fd35787467dc8dac495297d20445e03034600bb32b50ed503add25bdf908e9782a139515236de7421c47d6009b7764bbe0972b65824248a5d267f60bf2b51fc2ec5895f87ab4ca42715c28b4991a017efac291237ecb9f6260558a3dab48475f83841e62416f4d94ae466a47972cfe4594e19b6c33c7d30117b737483750c15fe2326b56265aab121cd72f1d12b771e3584e1023155e2e8c137b3223a6504ca462cbfb96fa21f0c14a8cd8d403fa76543012edaba5a59195aa8b4be963d25e149191b04375e53eae32951553e01df67549c573a76fe5e5f2d7bae671fdb1c44e329446335553a50ff1927e092a11f128df1e78c51531ce9347780d7d0f05100323ce1634657563e4495942312daec23ee8b9f518e2d88d27e021089b1c6d09419580a255c09e20abbc8341cf2507bf0c25c7f07d7f95b8914069cddc4ce5acbba3314fd84dd80c3d484d4e4e2504bb924b6c1ccd122e39836a882625094c7222c114139dd87ff665d8c3c70c5eaaf8b99598fabdf292aad3697dea70bff67e32f9d878a3f8b5d9a032f91b03e13de67140540b4225fe0f3db84104da9b2a4a794c148d74b700d97891e3bbd2c1666dd28cf5d6a7570a9483051757751f265e96a21091c0fc015821b54e3cb3e0d3a455c17934ef3a6d08ea4dc401e55dd6b9c22c1d9e4e448dc495e77872ffa0e7f2ece311728072de14715e2109a708bd935bf04f39ecbfee42292ecb78e02f5b8a6ac727bad2614edea14383edca45130ede3f68567670c174810a5ee0b41c437e0aaaa65394d6aaad51cb538c7428d7b37fd33576ca253996b23cb849ba91b23abe2bf156df4204c880bb06cd99812c5eb5efbaa284efc85472cc063f7e21da1d30d93308a0231249b721f088da6c5d668f6408e019e87b17e3bb3e89965fd6e8aa93fd21a098bc8faa9a144bcb394da07c109dbda97330dda78e042d4e04d5da08c274c25eaa98bca69b727eec7e9c6d5f02f844694c8628eb5e537a51a8a3870e121cbd7e177afca8d68b0ad03e4c149ae921194ea4bb2a6d3af56366377b258fc97819fd826ea154e7b55475d00562b625b1efba6207ed97404a289282fc8496b2ff013303a85edd915299c1e498d4d6caaa48bf0128bb914e5b6e53cb6058e3b02e873fc861cba28d3767f73780680a16278845ff7ea8ad5521a0e7afac5c37a76249f64254ca8e7c86548d01f42dd66c0690254390c7a9fc7472938ce3691be3c9592d6f9ea10da5957f3a80e77288585f1d2099011ddb11e24288d91302286f1823bef1a62f8bed2716529ce5a9b007bf4e563c0b799e1eb0d9771dca0f87da16118877beb241889ea46ce0002f8c7bb164017b4a9284e730a18b84b7d3421d8a8ecb41c9c3955aec7dfe695c599f4d49dcf60816b88324670c3814df6647f681943655ac9690914f5b5ac4ad9b92e1efe1f63f8ad508ce02ddcc6cab984f7ad0094d3f8d83287308b0e298c489a033a8fd1e56ef88966546c254fbbd299b66bdc22a11c65d32b6f68323ac20984fd63563ce0de4f8002e17fdf56c3f994db4b06ae6024c6ba38289740979382097b66ca6de2bd1aaead75bb0f996ab4d16d6ff3b4b3be5ec42dabd3d2e7dfe61e8c0f8e8db9bdd1fb99d49a90e590d5be8bcd3bbf16f35d3ed769b7337fb5a03111f82427a5501f59ee7bbf2639c4e40cdde60c66cf00fb3926151f3617cdcdf79811aa8f0e6fe24accac15f4d559c3f8c45f20294098846dd67a6cfe23e01d5916c9db98ca128b080aedcd09224df3774faa07e4c841a2164d5399abeab14fa6f0e29fe9508daf286f81a76917ae7cfdf3c5c2d7c40ff499005481cc871be6ccdaeb7dc9baf298be1090d917454c8c3c98fb7145aec4c3b790209e947f59bafa7e0bc61e8775a5f809a79d782640116f6a5231a50fad419f4802380f54f369a5e75526d078cae16a138228c20a03536680773383521f09cb992ad20ef3bcc905c8ebdf11eed3390023a358118cd1eae1756a759bc2f4ea67c1dfff7a13c8eb073185ec7371e5732961cf63007b57ffd0f5c3f8b21dd77a5e9c08138fa8618cd3ef11418ce607e2310df5587099ec6f155ab4ee54812e67da41fba2ffc54ec7f6368cc34814fd1af4170f64b02249c9147a65011d38ffc6cd794731fb313b64808b1402bd7e1e60a937afc96564ac8ffd875e31aae65072f28de1a6b1cc758683f45058c7791c4a02e5f6ffbf90fbc0b84d01caa094ebb7499be254caa54bc7cc60c1db8a4077af7b62ce2899dcb6c3c061e0a8e6b1ce8ef0d03e3596e29074cc43420993725d16eaf54ea7c5de53efbb20911c8fb9caa3ed067803eaf420c73b132815b02cc16ecd3de85b1c4f1e5325e133d2d83a088a4a9efa27dbdd8646693a26559258b85d153f5e59237c5fb849225bc85e20fa8f3530395d540ef7b2fa0dbf992149565a8874671d1683c369f92ab3ef6ed97ee31b2d091f9705d90cea564e7019801d02ea5d4d4ba3edb4143c2a319a79751e30d85e493e176b3f4794f4dfbd67347139601bf0404b56a78eed468760ce39c7ffe575ddc139c7f4c8096199dda20e9a8eaa206b4ffc03983c730900d45f2530d452438c12b07c8871f339e83c33eb8975ce6655eabcc6fc423b927121a5f2b83a8a477ba6cf0e3eb00afc90ca23cd408bcc35bb63acfe018ba9abd070b854ee0ee4d28cb89b79e6a26cef34821f7f4fd298e27e953253a07ced4771de818a631584eb72186bb2a4e337907638c4e95ce1fd2e560a2737cb10bff9a61934d01372b8155834deee7826c3a49416bfda6e73c430948726c9ec93693310994fe8a25d787755e7ccf6801925f6fa727ea4e1b6bac1bed39078f38c93f83230f1e14da55872bbc59e53814eb4cdef0bdf3fad83a8e0bdfeb6c9a0fe6602923c24c802c088e2766e0e8a4096e994d82b883156628a0d7fffeda30d2f4e87ca245e4e166e7b36431c645027a789b7c326a590a8bdf841f760e272e53fc27524780f139d5377a402570c7f87f984787ae0b3701b06e3f6a792921bba07bc5841a08ca5a10588c569abaa31f03b7f8237f4b61ab746186f76d5aa02b4dc62e4c3d8619d5ecda950e42ed6748912ff5c657eca410e0db90813a650b564ba46bb423b7957765c41d1ee86dd0d58aa73f7c120382996467acb3b2956cd537052b3995e52ef260d48b38c00c80772d1a33c82b00834869d59d0a2a3531f9beb1aafba38c72ae9ed148b35c87dce35ebad2fd373c0991f83eaaa288a5f3080dfa2113711b9b2926f871b1479916fb7e853a6f083abe845f279c167f76838ac6913c4a05efab2009e98513c067eeac752324111316138cd57f1d5f34651ffc02e3b3a41ac8963535cbb9c45c290edbd451af16a3319378222aafc315dab9fc87359a8e2c484f5cb9f520f8e6199955bf75901691acd895e1329c0f63775eeb58740d3bf7421201eb399025b08032d07ae7b3e62a5f79fc62c4a4e1c3b157bc3c57a87b8b6ef1ac9b30919e1fdbf2ff857b31788cc004e148239973a9cce0f1d0f2fc40dc5f696f17eb1cd2a126603827cd634f67a7a6684a61290b5f35bbad3709e4bc40e4174d5373a3dbf518a283153f2a1b39b79954d651cea0e1d078e1702f6a2f4a5db369f9167e7848b2bb50c5fe3c0e63d8f26614a9a91801a3f1d1cc3403aa6f04ec0a4478c8978bbcccd1a7104d1cddfbbbce4f1d1ecb2ccc4de8466c26c90430d956b0cca45af1f1b8d457719f192138e576168b4499d878e1494dea863ef06df3a63bd826651c860e8fc1f5c32d49ee7f45e27fa277ef4a2f8d95bab6830eabba573f999060e472aa2f82d62a9995b46bdfd74c49c69e9be84bf69b33c4fbc38fe6b15685599e83ab72c7aeca933d9372545e6477c49e6d5627c98ca28e1617dcc7054b6227c30fdb5b312d726168c7ce0dd54a995f15b85edaf798181b1ecbab33b5cf4badf41a461d5f37c2986ac063bd01d0912da0ae2e5601638f741fbc242b9d7989e5eb8b4a38a052a1ae8c036e6b3fd80ff7c66fe71d8a3fadbac5c6fcaa9c244fb0ce1fd739b638bc6af5ca59c5d58f853b863610c567e0bf08ed2df9977aa6f268fb990816d236a05b69235786a1fa1da90a1b7c7cd0f6c7c47a6c5ac729fbad3542e43ba2959458d7699a5be0e985255e8e4d0d373e0f03ebfe4f7d6885eea1560af00620083ffe3dbc0ae09affd9930ad8a7b5984e7c472db19e5c3aa4e5850b659dd3a2b8f63b0dbc00104d3430a3a6025d72c2be56b14e351a4a1cbdff96ad4160c9167430e824769ef9002efe6a9a2b9afc86ed43e3deb5e90f2c33b1abb90dba5379c1bd70dbe108bb3ab48b97d8bbfde5c7637dfd5ecb6b1a8aab4a71840f17369dbe5ea75f81afadd184386423b8828893e447b1f9b4d7409cb5328c6b422648533cbb99b9e4dfd58f781ee1bb78e011bc73dd1037b2ca8e84f084e93e29a10ee587baf9722bf07f59c4baaefc20603749629359a19eee3e8add23e0e121b3d9a914db234b0034079388d09228341ba3a995972ed230ed3ade481b964e646a91feeea15075dd21834ade173452abd970f62557133cb23a675de546abca8405bb258b9c5115ddf64854767d95e60bf160adafdcf60194de4e928a535b05746402c9ca585ee5ccaeac07e805afaaa20e1c01e5e313054060eedd75e0a6fbecac9e3ee9e134b4391eda1199e2f987dcc95aea5fa6eb4c84c2bb1fcd6c5cee1ad160c0f9ce40ff6ceec2b7f380b221273ebeef9d256378fd440dcb26fad38a380b65692f79e90b48df2dff414ad73436237d58464958ace8e349dbdb82a3be5364959103071f27388eef27c5d968d446c7b0302b5bb58d7c7b796dc37d3b9d6ffbf4d64b227e9c25e90450479097da5cbf077514f985e9da2988e7b99ed3cb37a9d871efa56a1c188daf35aa567f1cbc2ad0cc5f8c4c8aed69665bddb0b262cfbd72ddcbc4c75739f8d81e9c1eed42f4dce0d3e25bf293dca46d851357a96ebd1953a32eb48f76de382d13693892ec8cd94e3078e10e897b425578c4e9f8db270141ccf777c31b7f197731b2d4c590d0279b58c14807b03396a20daa598472aa2acc5538cdb8564bc525b39a675e8f3687c30bd97174459696efb41eb13a91c384309341fb3b320008f5ba813088329b48b834daff3dcc954a7df3f990316deb39a548731aa4d0989797f24be90ba82b7846cb65a6c16b004605535071b43dfdfa84a1dfd0bfea59b1987e7d612cb3744933ff3d575905835e214a69e8c5ee566be1c8134399a4a8d964dfcb4da32e7607b31180e7f2e3d12ce6fb3bf2602d13a322d61e776e765c17f72748c667c5b58491846c802e39ae7a0774ee2eb4e6b449f030f9addd45a3b00056320a6341b00054f0d5ed03feb454d4b05264223b3a0c6f23b9f26384093fb84ba5c98583bc59667fa86dca70bad5042aa56a850b320144730a050a3fb733968c6153d02395262ba4e60b654d6d8e521cbea5b97a9dc41bcf24d4e47c292f7d2819459077d58d86bf0d4439b7fa199ab49f5a227c1282fd03ba3a353394722192f43082d36efad1f0ba3dcad94d4d712f783ed0724341b66958ff36799112c10b456f08380292a46c72acf2116ea8e0db117d8555a9020df97eed37008c9c85dd346fe5ccd0d15ea2ebf36a4c9024c73cada1c1c41e97b832e4dc72e705bc5135a2e58d8ad697cd51e7d73b2518302d3744636f3d1bcec26f0c08447eac5096fc20606f38e97139c12fb546bc9eb03c82d6df8f3b77efc8a5647264d820d26fa0d0b888fddc8daefc51aded5fc1069a60fb2c569759530d07318b48a889ab50011d40ae733ba837801949e825ae0c97e4c0ef27f9eb633674ff473bfac1a238b4506c2c0c45b1a5d82f87cbb5a61b500588d5ad2b2452cbfbba8bdc7d6d1935de96becbb602b7b0753dba1f9dcf7b5ccf674546ca1d02ee7d0363b1a51ae96b7a4f3e969d6fe78080e55c45554227a310fb9317a41b9aa16b019a2a223be2a0aadc9a4a19d750cc08c1d83db730eddc394c4788648c54ea55d8196af9aa167d1586d3653eaae0acde0587164c93df15a43bf2e40ab0ec51053899c7889ac9333fcdc4b1f4899a8b34038a0fc1f95cff61b38df7977deb75977fa8e9863c8fa32850ac813c3619b6ac2a7c6b0625fb4144f02bacb58608cb41585e3abf22a0ac2fdfa73533acaa1ca5c35cb0bcc5498d96b4f2c4b243cfe9939bb6a6888023678cc71294a664cd2e135e945389bd411ab34e0d727d12fda18abc20c15286b6aae4e7bfaa7e4b7ef0ae7b7bda6295dce08c8b7490132b9dfadfb1d7f0be5c88ef472fabbd2d0fa63f010a6173f29cd74383ac767e0d08558c6e2bdd597b681ab8449b10b92a7feea6d880c7425af9f1719f1441a6d6e19d7551d41f494460a0a8297338dc7ea72de680e47b78672cb218987d46ad21dde09adc976bad1fa6ab0749eee7c048603f3aabd6790d2d8c026f7ade7b0abb4c391f6b81a28528db886a84ed078bb186e44d36d29f24d8eafa4e88b908598dd02030cdbaaadabd0de874774fd2e455170815b265802285536915b35cec8426f5bc9b5a8a52c899f99cb9a4d69e45f0adc3998074ff15972a205d7df649f251241187fcd81eb9b48b4f9b0bc78696681ccb5d05ed3c8b1de67a3ffae5b7e9e990e31063ba7ae4453453dfd67358cddb4e89e9665fea02d19e5440e95ee2a3f572d63bd9f9babb3b0e050fa4ac1f40ccec2da1020e9a2e55fb098d8f46323c58461f3dfb7e2aaf513bc13755d871a4a495bb93ec482d69abe7b8960318a8d695b79d15e48519d3b3f0a7bf7337617bc8f0e67c90b7c719bea3c82374a042a4768687e764ebe392cff95005734dc3bdaa4fd82c4f492a6bedcfb6dd6f689e114a7381bbfdf43c32d99857ec0b2cbeb39e5df6e649fd938296336d096a0ef86256b69f749b0713721c5043d88abeb98cf2677e4787a3fc90ea0de491807cc9b4633c6ab708d4065a521be820c1f3fcb28f810ee1d64a773175ae556b0424afe06778294e62e6ad27cf4d14d07d62459792ce5cb15ffd048002eec4c7e834b0f56ef73b6ab424d2aa3f9bed778f57b310d933cddb4cafcdcee8a427b06d4d22b55bf020feb2030d79828606f1555285382a5efc3f4fe1b5bc716f62182699dd5dbb3ce69d04d4e22ba1191f05e0a052ad5900adfff96a5e14b92d58bf41eac27145f8d7c97fdd49dcbdb18b550bf718af306dea86524df878e107d2b55223208773dd22be76ef2a072c063c3e128f58e3299bf1238f5ef98372adeda2a96b20f2c838f997576f9be7d87a2ceefcb181a11e7972f2b32aba1108545082de11dbb30b10b9fb32b6c169463da83d73ec5a5f8c35208acf987cab67ff6ba1df1e59833b8f77f9d1194f74c99278bb885a2355cd28cf0cee03fe6591059de4caf6a3906e4815c43bd930c04636dc805be07397a16dc5e7ff41c7ceabfc8e255bce609de0813978c81161bd432e64bbb70d5265b76be7538511c1011433ef1fe81541970208f2d3de44542c60867fe811c57ee8e7bb9ba39f1b69e20befb9893f3a9d62d6e7323bd26ceeba9e0b796f7a484286f0f562a92e92094bc84523ab58080a7b379b3592bab3e57abeca79f8b5fc44ffef99aaa31bd064dd17615ae7149858b105fd8b844e75d1c6fd7b40c95158c803a2f17397f56ccc9205f5b31752571e50a0347a1f1b500461e0c0c831b3eb838f2d3f4ac47b0cfda77c5520d052c6b9cf208f381968c3ed9dc349d18b4eeb705910fcb3a55f610f5b234a34fc1651b5b9593d5a6894635c2a77f3f0e4d00e319540eba0aaef904a8405d039abf8366be752ce22ba1e67bd5bfe8b6a06c71483b00c26270c51a5b770017fa1b6dcaf19647bc871cd53347664387f13b003a8230d34985d3aa5248792027bf254d5e4988aae8a8af426cea3e285d01a93f6b0a40659fba768887d98d678971d59ad61a2da56822570179bf02afa84b7ea7727da8c0bc4f8fb78a0e913af6c2ea0f42adc047ef7991b358ce0b4ebc883479c5ab1695ec75b117770af3ccaed32b9f9b97598ddc02b6757b046a32b57ed44b0aadbc04ed5dd52fe2caf78d1ac8e7b7248cdfed59465c4181598bccae3ec898711389590444a31ac6543514b176324d0b164021b0ab439003f4d50ea6ca2024e2f3ca642ba4e47ad485294ab398b88499a928f476a1e3c2280ec0ff2a274aa9efccca77a4b00aa42172ba908d3be88b988387fa9049f0cf4720129b418e9777338e7aebe5d37a97332c9d342151d699ff99c2682bb84fbbfd7a08f5721920a5257c472117617ec07dd2fb8344aaeb6b161c880525c6c3da9fc20000c96d68637300dbf2a334e2786014bf068a91c21d48782e3c005271bd590efe4b8ffcd5dfbe1c7af320580bbf5cfcfd2b3df7e80ca94c645b613e585124b150352e01f0f0ebacee79c6a16efd025884861fc07e21c673badf12c3ab1dbe38e9bc7a1ac1c93fbd001a497dd182ebf60bb67e7f4e124d2e48babaf4e319608d9b4f2e7e24663f7c998d7bfbd2b9d0613fdfd0402325727ba3d28accaef2bc84556d73eb6ddf5c12762857ba1f0bb13530af081a29f9950868c5f60e7c6e29ae39583db6ed3acd5ce810d01e468d4e277a6c0eba36dcbc9e69acea78529d6755ec11cb42eb6a347eca18d16a0bfb4df7009ea93d956e5c77a635ad0d41dd5459754217808cc7c41cbb1c11d5ce1531ecac5ed31309df05d8b8d45324dddafaf17f3bb02fa07ca87a62dbe4631599ee66bf4e4d86af0584aff8e0262884cda927cdf4833b272f19894b5c97c7cc5bc65e4109cba7e40d8108e083e61af093219b51a87a62c6e0632e09c7b494249fa36533c19d1289da85614b20fd6470d930b8cf6d7aba6336ac1391aac7ccab343fd40a48747a3a19d36e586754e9958e297b8fee36bfa9fbf286ca5a10e6a44b44ef231497e1a978612700fc9bb662c485215261911bc57398472921551da28f6ecd640e565d68e006ee393ae37e90eb6b0c5c61f43781b0d028232d64cf3bb92865ddc34a98ede7b4429d830e3d5cd77a067eadff0e66a53b9e8965591c75f91a5fdacdc15dc361b0ca676693575d5c20afe518c2c316fda117bcc2fb649eff58b664e5116f409f1b45d8b631d2d9427dcaa2bd0d7daa2f2f2c5423076e5466ad846775bb9e1c1d7d954ad9d29dc420f5a4f7855cb4d64fd5bca901def994dfe2c9ee5e5a34c732552b801f0a27d9fcd142dfce796b7e7209bcf20a5e021312bc96dd0918d61331557b3af4315181c948a99e8e8a60ec35abc02bd48388cdc3cdd99e90a10a275c80dc5f223bb747d0afa049518f89eaa894545b211f217768e57b9ad2e608142a8b826ae051e4d39313b76d01d6778cc7d6ac430d96c061c3e2ca84f0e3b5341cf210288edf16e0b6fb86d4f6ed549a2c86460429e748b07172f754d769857a3b9e784ac1537453cdfa0a3aa04d1b5fc2d584fb00d1c91deae435f7014597b89c0fe723e30e1497c0e7d6393ec9429858fd8583f50e88bec04a2a88a01b08e15313497b38e723181431c4c215c8756cb32a9c227774607ef49bffb2dc3e7e30d05b1b64550c6a3222c69e38f077d2415809ce78de27cfa99aa524e4bdf2df0fe340fa4e6ce2dcf00cbe76124c9898882cec62d4de203e332813f48af1243b1e25f1b60b71eac2248a85a7101694a253e57e70a63661b09d9083fdac5ea6b17d407725bf8af0005da72d142bf0ba83f6029135d91d16642f4a86ea8638981b79bc9e21ccae64aec3b0c9be7693904a4a860795f9020aa53a7bd96e36d77c52814ca9b208303b9b345a9153b1cf8a1c0bec2f2ba5e20341f1b25ba6f5cfd2b00104a49e499b8113df6bd7191df55b9605b2eb186ab130d06d74d0e112e8486566084429978b96227ee48a83fb50a7ad4107dff959c7e9a985441204f6f587952919476ee848d88f86d7f0d4ffd06c87b4296d6b37c3a43be528d5393fb76911b3b4d11f246c898caa160d3a8c6951c1678bbd9d48cb69dae697b2bd6a9e2f4ea0e2badf1000186861e28737ff84318959228c5c6edb4f5aaabafdce807980f1ca6b281ed809e4d81c016b6f0d8c20a829d0243c75c897cb4d3c6f19f8acf8042d8e6b09dc4862b45f4555f18760ff89b1103f20288e13b3778f0c788dea4603e9f59efaa19ae8677c2a0794e65715aba14f266e4e33b195e74da5ea6c20765a0ee426efab271c1e098491ca4339b57f56a7cfcfb3848fb7c314c684bf3a332f2f3029933de9dc7f9a8d8d2fb47aeb4b46ba3d820a3ab019e9f7b8e4a9d9776c70ee3ec15b2e7fdec3db08583cb9a8dd4aa53949d714eb826be799857a9f9a1da64ea8ffd993749d585580869c2f9a6c3ce6ff70df5d7d034801fa48a0bc4e945e6963ce90c17599f6ad34c8a485a822650e016138158158026620f0f5fcadccdf41bcefcd22f49694b4613233a6be7a97009297421de8f3c4b4f85d604a3c7daf8ddf577a0597cb48e9a80617e12108586e95f62403115e10bdf5f8b20a6809da3945eb3e5227e48e65cbdec43cc7f6ec36860cd934fb51a563f79b1be06fc349144dc34766f3d6c8f205ad9ac7f6801a972cecdcd4c35b0060cc26951650015a63c82e3afc6543e13644de5087e556c8a4a3eeaf94e42fe93ff66fa1934c0c78144b55006828cc67dd29adcf11299460402f615874806a45bd90cabb273e06706491d7748529589f347b12079caf6cb34539be602044d64a303f578fdf3e9956f9a663493e3354ec6729b1b4855c926da90e08af81b02462ff40f848d9549956e9dc3ecbedf99ea5bcb55e9268c9d36d71d8bbd865b1ce0130b2d4c3c6a84dd2eb6ee863aee65e8f10682a0f8706d105a0de00f505c712fc30edcf88ca775b7045371338922a7ec8bec6f9c54c31284f164f453b6b95aae6fa94a71719501b318c029b4d07882294cbdf0284875cb32ecd89ddba3aa8a25982207c782469f6706d8d54483b26182d7a174638af0b719f6730becbe4988e25d8626f64f0fc3280fae1ef3912c187935945489eefba8394c63a6ab35245c3b00d448095af376753dc4f37a543f523e8437b0c696ad5e83cb4626c0b0cd19eac6d52c904f8c9b277eff2a085f4938b5b90b6c6c1ae2796b5c810ba0d4b42bfca9e4ae2cb02288bde28950f5b5df2598c99ca84d9b8c69403d679d139f9c1ad1e46558f590a53fe310d22fbd5c7d8ea93d0ad0cc37751b775710ce66a2da2c316e290d483f08783d6516c8ccdce8fb059c882bcd81e969933ce37d36135bfd194806b3e080b5e5c3d56d5d09834ee6bf1f4a0c85a2e8d12e3a69eb1a275827501f28cd411bc43ab7776ba32721971c369b17480521006c89965ab0e195605a8b41e834f3f04a01b2a876b123db44b470b7639042fb1f46b8f4c92934b5bad983184d7072d8fcc04ff8640348ac488461df5c1943caee576cd97a70d1e336f4bee1450438f1498923f75a10a01c5afde18375234b18856b43f038a132982f7c63fd6a0d53c44ecec4b7a3a5ef047bcfcd652c161c224c009d20dd93fd27c85a86fec0be25e94ec7e7bdbf4f888747282e1bdb9a95d786e2003844b358f3dfae89ae876139364809a0dac924e282b8fe98f3d3909ecd8e01dad6aa108fb71f7fe83089045c40cd7e631ef8b916799edfdc012a8812d9a507fb94748fdb27844ba12da8e08fd052a102e155eaeb857544c617f8021c3d76a790bcf74bab182779e3af60900590e581a7a977b8150768dfee2352233e282fd77c69e1f6798e56ef10bf2426ad6eb85e78b7dd1ffad5b77a3f1e5b754d87a3c8820772de3a88a2eab24d4b6dc5fa5e548d4b06cc4693e6413618a7eef56dc14880d797f10259fff1a08f4e8305650b12e9432b2a7517297fcba51e347b664b284d9cd7a39daf486e16cc19296b35522c081e0fe18ad22c8eaded912325c0c57d1a5ba4525006db8063cd789c9e69a9f7ad0051bd25aed5d44433e0242a731c07602df696df8049b83b82d41fb28107226329ce64cfbc8eb1027254316dbf4af221316f3d048af79fc76cdfaeae66c011ffdca94fc1a89d7eb0fdb9e0db3baa1efbe562c698851fd45584cbadc86e83a89f730c31cf97c32d1b5ccb3a28942aafc81c51cc09fed0e63937eb39296fd81c0e55d1fe45833cdff2fa6d2e0c806e3905ca0292a6b25a649c1125af439f848fc14547f98a0b2b9a574bb81ab4195834f915672d870fe452471b6d076139af9ca3f28f067896484c187b596c8d84768c40d0cdd96ce6abb77039da04537b295ed020cbc7e7bf16d511a65dce051f056b312a66cad2b2841d0c264ba48bbc5d14955b5ed5a08aab4c7d927236b7ed25c71eec5c6360d20df2bbe54055413a68402877d6fbabb50f1ba495f428050b0095795711174f9f3bfe5d32e1e73b7ce51e1c6f258d26a23996d239f0c865bc64767b9d8f7d4d95c9ff3cdb6ae3c48d619af9b14e281986f33270fd36da49a5166f922cba29ea0966d91af8a9290bb08da83ad2d64c718f64e70b7994d23862857c0e4513ef6537932c11368b85b284e5bcc2273412cfadae1cdc1adc19ed6982570d307c676b972e04de29972cd8beb04153c21a1ddd737b51a5db74295af1d50815699480231bb36becf79cc9164cf27417dda5c7e7897d75b950251e253d189fb961268741944e7a63c1073c0fecd8dd799401cdb37c5fb279c75cff181d7217c459f688bd91c7b227d2452ef82282e3ee0dc274b5b31f5578fd3aac891b2c506322864a65de74a350617bec7b49a850b077a935d89266a0538190b2061d9442c879628302b37244456405577d1da712575097a6eee011e4b30771717e63be0121667721518f2f20d476e446a9b4a2653f7567bc22f1f34dc19a090f4083bd60c6df8f941eaaa7093e85d03c7977e0b06e69c5ea9482a458aabd6c3dbe786146e7d5553ee1bd0b6e4a2f5bd730af9e89232a29deed005773d956add2ca99dd6526db5690978f8b4b988dff6a2bae6c2dd52889be3feac6fda6fd8e8ee7881872ab23aa459859e18a6aba81288e1530e7c1c34f3c61b454c16d1d03bfe074d5584f7ebeab8c37b62beb02c13b54701eea4b627543dbc3cfc308ca15c46b66955e4982157dde8c0eefe4ed487c675cc13a1b3e2068c9f5142934c67bc100bf5e2eb90206a6472cb068fe2b99fa28bb6d9274047f97a8abc5ba1eb141e48d0c0018505605a77db0c9fa2dfea1dc3efa5077914d2c5b812914e8d922893541bf80ec4621c4259097b9d825a36f5cadcfab3d0ec3415d7d5c184115ba6657a6e649e02c6784353166d3d0a49d421a7a1eb5780aa25a6e60b06132adb52044d120098c1dc861d9193c6632a0f28df5a417f8d9dbe9a041681f3bdac592b2dc16152a9b1359a05b44ad98d01f8c628ce475c0b7711ae1acc95b7e811150f6f6154fbe050ceb3746cbaf29fc3e44d42b47af1c9b2ee7935be78d17515f0c2f876e0697ad5239832debfb68cdedacad7f0719beb8324dbca95cad705feec90c81b566b8f6ea9bf3a80cb28b4d0f76f6fabd6c7a2780efab082aeeff53d9d72dfc770e1b685ce1d83fcc7d834940b23a749a047f70f11c86d3e88cf1b67046c658a36b61646bf464603160641843940a526f0be25ea7c009c97b316f535d82a3e525537f939db6a149148f9dfc5beac7f0baf0bd0af810c427168df488571130404f54e3a997ddb0969d6a6fd47ae355f9a0e92a5430dbdc33fbac979d0371f18b227f477457825aafec262955dfd8a7f84f02aad25d3e01508231f712b01bc18e13cf683b7f2106735fb74b237fc6360657cbdbb19a13aaf44eea48d18cb3f486fc547edd9961a9871e73a3c459d9bb9f8db34405923cc4a4298d500c32c67736fda9e4f461dd2051734dd7d0160eb8390cafd0103dc2d0620cd8c35f18526c41a585cc3bef48119a3a845ccea8554f5c821bb6641218d3b2fc9f6e8d134b8df4ca6409c9267cfe4e630a9093d1daa58f6560d1df1b1d994c378331bdc8d90ed29745eef52499969e9557539006acd613b5c1c3aad8682ae79d5a9f11a6befbad2fb5bad9079aa16e3ba320b71075d7e61e0e6d4d445685be64d58b91b4069a8d6112d46bc8372b6af395afce01b6fe1750e4662feeb15c990791a7d7353e28989731b9b45bbaecd2d1afd5c36f579020dc7b9e439a1b0192e326b98a9a43e185f895826b38999d2ab1afbd61d3d4445737f716f8595da2f5d7c434edc979f946c1244497f30a7ea4d29a5c245f54bf65b812fa1d00ee627a7d848ac7984b60d6221585fbd29d917d00105fb9adb8cee6fe2d4772ee156a586d101b317a9b17e7f6374d5ca6f8e6d0e7aa2cf899acb34e1f87ddb92d8022795084460b349c0f2ae87ac8dd262283f0c54a85bb27a4709d25d8c888b65164581aa7078b6cdb3a632c2c7fb6ef020fd8f02f4bdce0e63eb66e4e18080fc1bbc2123b64b1d315a06edc3b4f5061baa95c5e99f1fa6de61d3d433b4722599182c0d8f06f9bf61faa6273911a55726089c4024b05960f00d7e37c73e2f482436762c7903bd312d831fdf2f5df7b62062ec7354e566f28b1216b9eda562c0a1fa0a0c0baf0cfda4384283001c65e4e3db42a0a8f5ce096028a58829e08a509d7f4e4a9a7812b3d55b03c1d927742a815e006d81e7afba7701489655293042847ef90d1a85c0efa2e4bcd292652a8a4112b275e9d6d8bd65958562e63c5adce5d769cdb0443766effb07796f6e19984f988114aab829508f40866ca713d360e2590649f12b11c8d0402406ca515528f91be163780dfa5a0476ffd1a54fc54ec600327a48aa3d9b5d95f2ed61775f4e8a24cee6436e214bb79b38d8ad7a55468091e6843dac43977b84b14f3d6b3b4d8b910cc53b6a8e34a8ced9af3bd510fbad1fd6411b39d44492a08fc851945f4beb4ee88ad44aff9f8afd3da1828f33888a923905a8a774afd2eea57c71d37c7b7805f2130069a48f7660348206a26b7fcd702c6d74d71dacf727501109b7ed4b71461a210a9d7173ddab010b72bc3eff4ab6f3dff233fc604538a764f968fbcb6b1c6f4bcdce114496800daf04cd1a9e3c50a3df2afb592feef14855b7f2edea4080e4f770f540fabc989629344aca4eaff35ad4955dd907e4f3a64ab0548287ddae7a4a4967bf0c9f4366128a7f3f367d762fbe37f8e10ae8c11cc45e7a15e6d64f8f449c98f541000996e3cd5723868767bcaec9739d01d872c42871b242271d974e9384ec0881a5b1b7adb36e7108ec14dcf32db43ab654029a162cd945fa707db1884941563b8f492b840f491d339b3c67eefd530948380319f66f27925c5e89693becf6bd1dcdbc94b69f1e472e78bc9c410b7d3c047be6378ca6754cda103aa9eb925f40daf1d43b6c9049ddeeaf60819e431543933415970378cb38d361d15cff48554bde1f17fe79628eadf3861ec768448eaa1b234a18222753314ffeaac414f7c350690cc7f42a6aeab6d29be731fe188bce32f9548c026535ccfa8bbe4bcbcb5d23fff11ac1d8777f26ce188788dfc0e902cbb97fa29e36b3a540ea9c8bc31a816cc1683b218313934b7b8824ca8d3337efac8b286e3bfe4497737fd2befd24932d592570bcdd9cf6fd9846120c840875667d30a353cbe8803e203a01f162bf63d42a7bc1b297875d595f21456b61036fb28091ca0f5979ebcf0deab6e37939557f94d53fe623f4137ba1ed41df649cbc3e717ac67197cbcc98512e0f3ac1168c503d9b902722a165a7115ef7e1631a4dab37d8691ea005a300a8d611959b0a753d6de18d5da7dcaf03fe03a5c6933b7b91e2585e7d2526f91e38970506762ac48a1df5b991fd0de987e8c4242bf6f93c43b7d8eadf245c35b513dec4e88c10448df52670b31955f19ddbe25d7de5e8dc5ee907b0c9f86addc8e0c2e1380519b1217f5a03665aa2327025db5fb9bedb06623813edf88dc2b9dc78e44ea5c30b29bb690293d729e06bebe625a5cb94c5b9340d6157897db3ab8b9bb0b36bf870074914af9abc36f958f95dd97b9cfe0abaa0ea981946ab7a0212e64fc6465d0cb9c7cf6f6196cbc085889a53e49c2468a6d1643d58072c7f586b2c52447a875a0040979639fcd7b7af57c6d395586667015f005c14bd22853ca279d77de9afc69400353fb0b41aec6cad3f893b246dd97a67cc8c18b606c322f21ed2738a0a62a6aa95e59f6a5ea005e25894d2e4da21b423ae7f9d3b2dab8ef4566cac69ed0aa3a4a24629210bb5d42d977f53c03c032c481ce71f14a24c364a5072fad75a111f89a8ac839c67c00e41b1529117518b8958f139972c83a238806f1aaa50272feb8210f63cd3a6f4478339562fc4b047dbc3c059ab435bbb10569cb5652a1cfb3d1e48636da6de3c08bfc86bb8764a3129d5f41acddb88a49acdcd0560f6be534129ee2e7de42bb12673475117fe6b1b425a35d016e9012005ebd9aefb4aaf11d34eb29d923274da3a455634df650fed0972525c65de2d40980a1d20649a6579815c3e2644dc7e86ba948ffa546f69480a388abf67b441a9e2dc06d4d6986e7e58fd829b058e2ac25c8932b389e64abae8cc7ca60fa8c7ca23eaad2fe3ffa7823ee2b4d54e7ed3eac5e306f9056dc82ccec9cda358a7bccbf956ebbcfa67697a13716bbbd2882e5f9233ad30d8f604b37b5556e285d447e79bb5fe7cd66f2c772fc11e41b37a818016d073119970943a104e2972f9885d60d7aa25057f02298c791d3d223e4fd2dc7a638d0aa7c29a5d7899e8f9700585c741fd803cc61cfaeb1513de2e0802616364a6fc4c0a2166fc558219570f0665b88d0a62687a62b6c1f376bcdae6de709f0a3d07260204ef084ede7864711c7309730f66d405b3369e10b8fa83877c5cb3dfbd1bd61dde0a377970779cca27e9dc891f2bf0508cb58987b65e8e874f4bc3d7bdd64884fce15a28fbe0e5c23409119e7a33392522deb714e286ee3ebc71326e0942445231eac98e5cb4069d377253e865bfb083647b3a93d2efddec28d471e72c5e121fe8a5369e8a4568c3726ed617f83f013227134a3b45267c8209b3895c10751838dd5686d0b6214ad0420f9ce8474d54dff9ff738513e3849f6ebab9befd145c6bf41cea35b84a94e04898c9716f39b11372809c0015adc1c9ed00a29fbfd589664abcfc0b7d088b215d72db406a10121ef4bcc92ea0b329f74d4ed825cd7bab3a1ad2c9fb8579a37a904cd0f084f556d35b1ad42c6d29df1eac32e1de11489c3a2390286580f91e3ce6a90f80c2ab23d3a9ae52cd19a5eca452e20ffd1e3332f120693f15275da28595c5f02dc097c3dffb217973225bac00c9f1f8f766c6392fbd4f3011d10d8496376c53889d917184e93605707da14ed8ccfc19b5fc8e5ad4cccd187ffb96afb5c90c108e8b9de222ab6d42be8c2477a6921a2bd2c2b071d5d6afafc109b62535b932712a112873a09c470a8ccda4bdf65fd942d9d1f87e2545f161425e7f00364b50ca5cb0eb86a037245bb7c530f90411c17a7226a095f5fd64fa56716858f63c8ffa1f6771277983191ec940b3535f77344cbf4fea71a1d69f73c100123fcab64b1b7dfcc79753d0860b318db9c6e951c11c41ac0b43cf13eecbf2fc596a3b4b6298848fb5b8fdcae7f77ccea2e36748aad864b5829bfc67931b4748d5bacb10b40e970cd0d4ed52010a113ceddad89efb9c09bf142fdebbc4cfdd0b4ac7c03df925f652a8a34423602a26d82d42b2011d5234a7093edaaa4d1f06fab8c54a574792faecc1780a2c6e9359e149cdc46cc4cb909932e72ffbbe39063ef7791dc39b3bbd33885928996cec1b0a45a8f13d7af759fa95acc7e539b3c4f39a0076719fe075aa0a691ffff6ca85d576327df77557daef4ba8f0ce5804352aea26fe239736a8cf565dd4a96bc083bf6a0b669522827dffa0bd4490d399856dfadb8cae25cd08ae06a4095034af3f27d1c3225b39b31992fe2b65609e897cd06cb1f233fba5cf578deff866a7d832617e401dbe28ee37e41a04eb14ddd322721fad293df5d3ee924cf3f77e2df5219b4278acd3404eaef2e14a4da4b018a1d0f131504bde095a8f3141d907fc401fc3888ece189c4dc83bbb7a0b02c7fea5b78112d2bd6f728810a7d653b7ebdb73b61495ccfcb65e0e400f90730873164e0d4e809ad0097c716086bc8ed8ddcf2402598c331424928fa02f8ae704ed88baf16341ddd0ea38700b569e9d5f4d6d0101a34b71961de70acca60648dfa962258fd68aa15218cbf537710d43105b4a50ee5101eef6330726b3a958aab23cf8e33724aa58cb4fb071c129fe26ecef3f970db79050b19196dcc41fc134dae65a370f54cc18d3527e2816e96c42b958f79163ae25f6748deef95dde08f3925a6fd80f7852446fc74f0e6fcd5af51b5d9850946c5d1487fe0997dfd7996003ba07022624d604f864cb3b6481b26eb528e83cf1c31a2950f37d22f4fc90c234209a4d5ee6d7ffc4323c06d9b1cabf54f7e1fdc7ace0e641f70c197afee93a21b0e0351257494a58a3a7e977dec9e28268e83d4bdd54ec56a269233609ad7d619a597b0433a839990fc5ae75295d1c60b532f8fc4e70046fbf994d313cf38a3be45abbe54e11b0a4ac44794e9ca1bdf2eb0aca86ff5ac60d30c322780fbac1af1c270119fe374caf62f07c26379ff8e56c7a452ea30a7aec92c904ecc626fbd3be8b177f3e3f5cacefa2edec57f919b7bf59306fb7e333b01044e3d9e995e2439d5b50b6c1685266eaf35273faff6651c4e6db5c50a06990456bfa7e742bc5ef2fb45e75875e5b3ef4821ed0d2a4a86828799ab21721ff41b43db4cd0041e528938e919fe7a653d04c253bd1b1825ee7548977438ead3b1676620bccf7205b70be8adf53795803d68ad4ea7925d7bf650479610a1294ff98ae37c90ffbd3f2bdc885b179f44ddafbce2b02b8b39adf8fd25a20a3c7b0c17b46390a483e6026570c0a45b7a55480c658972e3af5f59d1895d70674605f25ca641bf43ce880bd40fdb976124eee8ca290c5093abf30dec0e79458aec198811a8ad21b85c16f87605f236c669af832cb0352a08d2587bb309d1a27950cec8d9836054af7da5bb3761c3a21083540d6bf134d698b5df9b51972f0bb3635249c2d51ba6296adf6b3f78b9b110b56c3b02eec372780d346fc3e5f18ba0687fddd510a315a7ed692a52eafa4f5f10eb866ca8a351065a7e8069fc79b18401270c5ec1a3d77705fca9e9fe7da53ee0a5396b33f7a92aa6afb6a8c84e78f6e284afbeed7039c5a867b2c5bc1e80583afeabd37144e7059f9578d8ac3f2df27f7052fc5893e82392f05cfe72236683255b03f908a61f567fe1352a189aad6c16abe09f0422502372d98142beda337a77020a7de78b2ac6adb6d731f2e53181dd50f643a74c2a5408f8f1b0b5738f6f9020d895875bfaf76c373ed88e20d4c94fa2bde339c8cd5f66a8d3ff11b281ebd7d06318b48d5f0f82616177983eb885be8a37c2b04e977c9f0aa23f0328607c696f03c2c271fe5ab6f2d514517e94eddf85994c75f44438ba159bf0005986e5fd4eb682091cba7ca463b00f152eca09c76cd798608193fa0e55529694d8fa1bc4443566756ba290b50fe6fc8dbfad24ad4f7b3f83c984b94f7789e6966914a9aa95d3c6c8de19634e9f8fa30cbefd6db93bf3b9cccb755a246f3415542895630012dd1e394c286cf3436fc35e6c4ec60fce0e0b2021e0de417d8b54b784d6e3bcd70f663921c6b5b101b565ae35a923d9b427d78df63df14a05cb71a5db4e8bda7b408b8b31573808bd45f5243ce21ea7b705349bb8d230622f8bc23081b8aeaa640da410260b380261e1b05f3e8baa7bfc3bf1c7a28d878ffd25285afb6337694470e0dac8e143c978bde18f99922b6ea839df53cb0e9d02d75234013fe529dbb423e7c3e28c86d2e2b98058998139eac90125edce4c60ceea0d3fc6a8af5e881f7710021bd1ae756c1babd304a1991ca369702b0011f679cf1b2f7a04a14db09f489a3d3fc7bbf2d62d9c9be8999c470fa74d0d4860f81c2eeda3d356019e6e5dc4d3a7e21823f206dc2773b3a89bbea9f1f9436abbcd2fc026285e8aa4b91dead1473ff2e89c78b771690254c26dbbc7af586bff68ea59247c4921e3506b2d43cfb1f5ad69985a7cfc3ac977a25f9d3581d485b05a8a27720f97bff0bdb04f3c12e33037599957b57534c7e9d098ce801c7fc1bbc59cb6a864d79f4186edfdfbf77d37f3b91d942c71185e094d2ae4a787cad0fd0f73e027eb6c2c81edfc192fc8d42374426a5b6b4ba11063b0642bea07797023004d7c066f6eb62236852014ea564e100353a250ceceb8dc205f159e10f97c3b16f2fe43e473a6028cc47db19193d80531d7e614e9a14f180f26b049b701aa8194ec48debcb48ddee294ac41751f0c4277767c1f5a5ee3468557e4b1449ff6343a879b87cae6272f53021a95020c97016418e8cd053c9843dc007d4ff9f24154acbb1c8a2c5b19f1a4f3f13c781db6094aea599e2d0283fbd4fb5072b4b65685da6a917614b5a2e503e7382d8eaa748cbb7ec83afaea07beb4cdd3598496d9887ec0e00259b44dd9ef6dc1b2a86ef6dd8999c172f064962774ae1d08e357a628861b2e69ce03cc69822f0703c9866441d8a0bae7e6394a4527b73f610c0fee1fb344ae2003a354172195178313c84549fe547a946ce8d0348f75b62eee006f7b7585efbb616b4015a840c10a2ed7f414066e3ebf2cfcfaf179e22eda8d8fb4a94aee86787a0c860da452af733f1c6ca9eac8c7b9943ee339fe6c6e18cd62c877c4261a262a0bb8eda30afffb175660d587669b5567aff769b1ee27735dab6316838d736796ab5d11a5a6e0e78ed880c275d9417194da03e9f8ee56d7691147d3ed61638b507be7e083b615e5bdee4071f0e75811d481ebb53e369bcdf5cae5c29e0b6cda9b164164e0a5a97dd2e3cfd2c86fbd8c6b11c5ffa78d0889e58bbf0846d31dd9dba36a4b6f6e42a99a6c101345bf6cb6f513d42a9ae2614391931b8e9ada869968707ac7b026bcd3298809d6191a902edb5e8f93a7100861bcfe4760c88b5e5d9f6d9f7f764b3d6a602417dd90c3b9c9dd4eb757aa713f62ca41f021ab6179a8f9d1c15c73f08b28825705005a7cddf8b2a5c4f7c008ef74fe8b2a0cede40b9e860da143ab344540cdad11184b0c2f66326e4e386265d959606b66fc3ce057d1019b100262c9808f86f22179d9aeeba7ec56ece1b82dec7486f1adc0b205d637d199e6e79e37356c9652e8d53387509dd88a3867a761923529cea7819092fabcc0e58a2293530a5cd07ec17d58ba8f99911c89c1b3bf45a332de7bd2b228fb422a65b749e07796a43f01eacd7544d57974d0103b877f59cc51fea8e6b77c0c3d8bc17d88806b4198fab58bb4ac953a542df9759b5083cabba48fc6bd3968792faa840401a581125d33e810ed90f095022115da9e65698fc0c696a388202cb90ba1174aef8f877239228d963a505ea615fbd5892eac14e16270e20964f0efa56e5b1322dbc52cf17596bb7c337b221af3c0d6f7157f166853211916e96ae40407c69477c3ac31a6739fb5348ece5aff34b6f2f75ddcc7e8bc94f2f549de8664f04cfd32fedb095266a119dfcebdf55a17e8026a1791f7a22cb1cd1d13efeb6ed7d6db0e2d29dde2f976b404629f79c53bd21e63dc40200c918e2bb2f565fb99209600c5b2879227bc2a2299358276c5070dc2a94f81384ce8d5dcb1c098ac8429cb8d169502722fc4036dc729c34650546e40c5e6a99d459047769d1a08aadfc41aa7be75fab0fd17bf3b01d848ee90a54a1a1b61c50f1d184d5b25e0adfc59c631c45f3196a2c45301b8a4986da961114b68b6c1da0139e746f7402abd9f9a277915cff7777080310075d473f1ca4f018b8e6f55aaa372cd3e2360f3377f586c18f0aa38e6aa9eb42c4f3bba5d1158049e31f50086454b0bed41c1db8646c3069608231484885fdfc2765b998abbfd65a7ecbc0afc0384edaa70e1f0a3997bb64000104134afe18c685ed242ff1ca974be5b7079acb777ab6031ea7abc6a6a5b25caa607ff893881c8c48a0b2fe62ec0654582dde44a135317cccbbb33c61ce91f761b1f8c9ee3bc68b8b717f680c5b791097a880b41def0aa627cf6256377eb7f69ec3e7c78e43f7a4b0c981dc4a62cb525dca1940915a5d096b953581653959d86f00223c5b6beb15ff6a0eeda268ee063d6ac91944308bf975c0c36156cb4cf0b07af8cfe06be2abfe936b0f697dce3ee2641b51458be2652f4c12699016d5c30d7ae0f67b08fbc677b96e1c5f8d350d48402932680c73add5d2f0452194f74a6f7a74c87c37703811f0c3548746efb6afe2d339b3c599b2df5b3e02b177e76251f858054a409c1c61c647d11b7a03b58fb6f2711aaa5649a9c414e0c29588c252ffc22134396a223aa99e539edb5db8a7cdeaeb990b20a47c2a5ad71a2619844d32d1319bace1ce205b34e78852773fc19f256bf8db9a63b3cc6df2b4fff9fe222e3a3008256fb0dc236b5ff2d7bbd03f5c6d65d0d758adfb4a8faba41b9c4ce1b330ce2fda58f4f2e2c36f7d8eb458b3257aab101de9ed60baf65a3b0a7f829a1fdd528012260b220bde84d93c3930aa0b109c3ab77401ed06a2f86de2f44e0864ba49989c526532fdd9bba30efc5284dfac6b21e88b17c316d4683c84cf2b33e70dc787898a736c813c354893cdbfb80eaa140c67de5b3b909e40d0308277cb9eb44e30b62bb319d29b420bea13f6cf692be028a64a36588f1d95c94299cecdde18803ea669f8eed5423b9a85b57ab6095df33a0abe4151abfaa43810929ba9ab91a700425c11ef757a3da7f45a8f2449aad441009243a345214f06a5d3789c1316d779ba3c1ffa90aa9af44a83960ab9392f2ecd9e087100669595b623736148ca8b09d4ecb368bad03e61a3c809c172906afb572f69c2021db4f630953390beb5e533eb28e4b59ce2505de4f84929e73a46d8964d9854114e82f0d98562e7f3b0b0f38037f9050a47645cece99f75d9cdafb304a8bf7dcc1049ead05a6086eee953d66f35461fe3887b3a5512325befb7c05c1907c3227b5fd6c77fd3f42b0b336802408eac554c750e1e28c03d87141a308da4afa1d1be842ca910f850c4b91821aeeb8f71672807a80951dd4f6bca98e95afb514618f7051a2db50d9619d629593fdc1ad38f449d441bc00dcf85f75567864660f623e3faef20ee50c68d9bd0b14aef039e05161c2369e0e6fb11af5a76ae132483d5d32f02f779eec1bb7202de9422c90c419dfd12ebc63f9bdb5406907d253b7d56c72a79d2b3a6a5b90ae18e2222a2c6cb284a682f8cfe4cfaec678aad20c15006d8ac1925182cd86c5d9a8c511ea598a6343105f8d8d03be8ee787f42267ddc7dc847c3c834284ca48792309c845d53ccab8e27849a893901a6b9148a9a778d44162f6dd7ee07c52a346a83dc5a6222b6e5d5f11115ec4ba4035512c060e74908c56ebc25ad74dd25c1867c2ff1bea5c6010aee24e63ad82cb6c743c1ab33ce35ec8d8e44076a642bfd5" +const bootstrapHex = + "60b747000000000018d305000000000023fd2bf54d3f71f13ee0ced4ab42970231bc67fc7fd1e933bb38cd184a90ac6df1bcb3404de7155e2b51f942f091b7d699bd3c32ee8ab3e30e64474f8e90196faebeb48401ced0bd795d19fd1162654192220d7c3f4bbfc93fcd7a76b43b3a6480ef074cf85a1fa38e704c05073f2def4b6ab67fe958fcdd5bc42573f1cb82eac1657ed399ee4e3cda0d3d7ac1e2429f8bfb483ca6701d63ee4fff867044a4f50342b0381e2570301b4057c0ac9360340bf1b776d8d06a80919c7369c54a3fbbb7014062e9150c2941cabbd6e9e2712cc6bd596801682ade219364a22f7dd2a5a30c7394268d17fa48e371c7b41e5a88a0df47bf53dd729eeae58b487dbe06f7cfa855bcf78396bdcab622852c3caa5d1d6c556788c857b689c711792de936228d929afaf940cf30295d28748a395b0f01097a4519d1977e8292d8ec8966943840dfbef204c99e27a94c426fb768fe59a7873450f2e85ed0b66596391649b7e3b4b660a6138e58526cdaff38c70d63e1e6a56f54c24181b920c65aae87a333968137214d4eced416b6fc0f8075c7855838aa0aec26dd41b079d61deff66e1edcfd146b5f03d38e8d2d5815b54db6b7b092ff63e4119e57fc324a6431efc00d13af7b8e0addda3a239abd219968bedc5ad81f36c7cf387e0f80e4bd29d2e73f2fa6f550d46176b706a2b776a5b301608acf26746b94342a3132a5705e45b0fc8a0405c713249c6e0305294139278d7901876e441c36d56a7ef4f6c632cfc018dae58bca6c1fb4e43fb6af1a8c97ae240ace999c8bf899105ff18b09277f778216825ef4bf2a7a1472b4dc1a5290ab73c20b89708eacc893cb348262995f7f5e576d933f7501d10599f9396b0076341e0ab9dcf24dd6edb163d9d7d5d3fc02a1327925bfbbc8e976db08db405761b8b5ff37194dc984281cc904a411d7ce33314e94950a8f71de218196eb577139272ddfe260c01aebcd89548a365d6214d6059442618dbe44db1392ac72e94371985171b2b58415c42a78ca0007eba179c5df23ff201bc684affa197f33ac5c443706d8bd8db6ca5dffa34127118d78d09fbebcacb100748600587d7d032cde218c41916b644f51dbfebab668054986aee1e0165c7b775fb2493eda3cd86eddade62657808d159a7afc773e66fc565d514e0fd43d477494f6d4d7a425420b0fd6b4662bb3171f7f2c8530619244c4fefe438965b822aa787228d51fb5084f5b7e87399e871325de4d3279cf56431d672899d3efcdd30be64b70b9482da4355a8f09fe8596710ca0b24ddccac9091b7ca51d59f87dee4c4e355ca878ce6e9eb27383c3f84f48e7d2401fa20f854db84b3a4476bc977ad8976e5ff4f556fe68ef31a65fd831188dc4541edf50662d006258951d714b45576f60cdd37719e7898bcf76a7d9866afa86dfe3a4e42e4f217a327b17b790ea83adce07961357c4194f419525c034a512d81bd30db6bde2c866a2dce69d8ca5a2557174597f03fca3379e424c5286005ba2f0cc0948d693ed67c0ecea8186ad4c0927f5ef7790c9c4463dc3e28faaeb1eab018c125311db30f58cc90a4ab42c2c2ef5bf12d592b16540be2615883cab02544e3f44bba43531ecc93d9ce18349bf39da6e5e5e90c9586fd2e4d7dfffa9f867a494c3de7e145b6f2e3ca2662112be61dbac9911702a1e25ba7f9bcfaad1285f71f633c01722c58556e0359c543906a0e8da2486e8e6ba730dbd44b4dd6c8ea7d2789a3519d6aebf6ec65a7fa39a1ea7d7950b61292c0850f16378fef0ee52ceb144ce6a3dc57f7e12c2b3233bd04b4a2c4222dc3eb972c447eac7da942bd6a744959b041e31a3ad567724536af288b4cb280a77e873b9d511b203f0a759ebff2ed659612295e37b96ec673aaa998b96de5a1a1d5a0cab83c1d7f00f6f9ef1f4906d59ba4d3b46bb8a4d602014c4ee2b1e21fc92f7a9e0e01a99fc50b72a60a33d0914e02500166aa2f18fabf37bf2944fb73ddbcd7cdc33b8606514254418c48b9ecc1b36700480bad674c68692d6b1608b8c68897a53ffe1db908a7156a069e25392757c8179a981bfbe6e3f2ee20f65a07d34dc6d3c302bf57048862d740f35051e32776ebd2f46f775867c8218b5501500a58d414fdcb2a78dd6f55a278045cc607c72be0903da0ab6e98edeb78499ac3b336f806dcc16b2d1f56c84089989fa612d56c17eb6a5702b34a83878bc43cc3d52a0dbe9ff2fc2fe10881facbe09f777f4cb6ab0b4530bfb0bfcfdbdadf2d0f7f354c412315d4f59053dd78b3ad2727ab093680a018807dd4cb88a794bb8ec7bda26f041f40c0501910e190f57961b960b7ba48a9a0176db376b3501a0a6552958c31089d5e53bd1f8958f99c39975af625f4b6cd290aa43415952a9a8899262e54d7e450ca1cc5259e340fab10b52c855f844112a264b29fc98dcadeb3d180c00684733931135b09d895922e6713911c4ab79afc257b13c2f16b6a1f0a005db87ee057b0a027f37ffaa94ef3f59a5664f3bfa5da0c13d86a7126b1f1e68a4d7aeacaca43970a28930589e7eb8afbfa5cc99e30955ae4c130ca2c34a6635ca73fad659fa79006b36bb8713a158d3133fd392e2bf0e59eea9db54c0c7588899351490ea9b72671e6d508fd49af8975b0cd6ad188082ea7120e114c230dcd0fc28501257c4fb12cae06759dc361a19de8f8f89c73c0f6fdb3244a01300c9740a765e4c66addfef5b28770b7e39a9665cbe0ac67a68112c28ed800bad82beedc23d8874be169997be7aa898c1f239faf6d551eec025191fd5a384ad2158d953a526f110d413e2559474695df7217dc1c3782971e995226b054712b1570013916bc729000d46927590f17e0533c04331ef372ef0770ca18db143377706b1a034831918720fb8912d2e4a68adb54368e5d20a5cbc6a12abdf46dc4a48b9dee95d8b4fcceddc3fc5c5de7da35c59951f7f03023a36f7a03a087d89518cee00934039eea01df9786de93791a50354b14316ccd8f185ce78a8d8199e09d97a6f0c322e4b01d830287f103de22cb8f93c8188071781bb0a0e965b09e1e6f04ecb88c093c35a5467a08341c2154c538d323911b1fa1ee9fe8016627faabcb9caca5ef19daa57d6a292ba38865b02aa959dbf2b184b80c31385378965aa984104af574f3d37f2c480e3f3a60792e9a5c65e101e6dc75688b1d4271713375892337f2f3ae9c05f516e9df29551cc4381bd48eb21233bf2c3b17f886561d359805579825b3ad53641eb0f9237f3362e787181ae9e943efcf627169a0ef82d261cb4c6665c49ea663eb5f216033d28228f0e896b4d683fdf869a9e55ff0e1d62dc6865a311668e3712b089b9c245c18dbed3c5ff32e1698f103b7638191a943fa2e8e5c88cc12c257ac47506d53d7186f3e60a2f3ae8ba38ae6a33ed79243c616baa95b6d1d148fe065244b76b5fb13a9376eeab37578136ca4a23b636d0d472a7c6926825dcee21e04805481e8ccf1daf26d8391c1fa8adcd0b48e6f70af81aa349507c6843a4447924b2edd1446fb4726cb024a79a95a0984fee0753141afdcb776d6d520c1b3a8fa1d9e43dd9d2fab9343431e64fdb05099450e1334b1e3f73105fdbb432b726ff0eedb3fc24ec369f3711d6ec3836db75b4868442261930b2e930f7955a4f46d7154d5b4e1e372bdaed686070e1b4609ea86277cfb186b53a5ea41b2ed3bb4bb0eb5d89f6c15cfa960bc3239be23d4469e779739da3cea3c695ad1442cc506853099bb45552a466ab780f8b9e229eef82867ef05b7d665281f5da80bd89028cc1734b6da0fe95b431afc3978b68e44510e4de674d2efde1d56f1b9e2ff1828e8be8bc5addf0689e9665f3735556b72675cfa48712f220103b18c16e58e5104a200e8101f7212d5a6061014acc18b59728a8b9f7d1ab4eb0aa244a4434a7af9122e2572464d25e09e2f0a97c9f9682f6760bad911f15819d2f7bc458ba2c434deffaabc956ce83438ec9a94e297f9223d2b70f511f3ddb6a2da1b4565d04e792de131e850d1b9aa4d9db7dc61322bbbbebdf7ce5aa61a116a5addbcff6d8ee559c4195a7cec17148454e88d06a9c233f423dfb14530c402a57d77df9bb1e9650381bc1c2ecabdbb6b65828d50ad605f0ceed577f76f4b4126d643956fc1d1c845c72b44f75b388909e3a6859dfcc9814f4f943335c1151a2899a6068b8268e8965552a158096c2158e6d012be0cdd9c94be7b7669da3ba17735b67a6103fc806475db2e17dc0655b90b58e8e7e15758646cb963f5eb17b8366c75e0c93d851ddde63512fe1c5970651138c72618dc6a11e490ee25c66376d8a227346d87b6abacde2b3963fcf879ef7af2ed7c2153ed56dd2e6a8030abf0822c6729ab97214cec5e837ec949fb9108d4aac541466a0fa3faa242d26bdcbe1786ca1eabf19a1f3d2f0ec4c6cc72c3cd57105fd1f382734640dfacd63a130229024c09cc112a00331de36d742ce871415a91ad840574056f1f9b8991e706c734ec88e3a3424291a837f5ff9930357a1a3fef060959e1ccef1d2258e2248bf232ef9bc812bc0b217ba6bf1162a6a8be4245bc5c6e761262be829caeb35c25204a3c15eb628476dbe0ae495519809d9d250473de1fbd7013950a1052ba28f97a2a44d3c3edf13e43c8513d5441c4fa1b88a4c7f21cbbb9d4fa1284adf5231b281183f1696f46c01dbecacc677d1ae0bbbb4296b5710e5989017fbab541a6a8bb2a69514c7fd42a991118b299d1d30fe47bfa941528e6920d92d07b1f51afd26eba83389e57a36653269999e89173e7ab083128f91d70726389221e60db7b9d1f044dbad1de0fdd1dd6757ae7330d08dafccedea648b134c86cc9c57ce830abc6a8869dd8b96150fac6b37f42dd2dca8c0222b2e7e0b65969b8e137a42556f991269e5475c93390d62cbc385d30544c1cbab95c019f3083cbe545b10ba6a5c55174100279cf787babe24fd599048ab1e2f47b7feab6dddac2805425541c905f541a5a43c60c11e8293812c6737e3b8233988c08a2599a89605be2776e9d264c626ca691e48e224e266b11514756c23d58894c855dc5838c66690133ce9c98089df06b29ba572015cc378d77500cda567d6971a515e9d01b34c5ccefa1f1f60441da2ded8408337639698e7ae46b923ceed1b9b3a1ae5c847321e9c8810612ba86c1b1de6ef854f42aba1d87cd896faec6d89a3b49c36a9336cc747f7387853554a9f64bb5dedea2ad6dc5f615a8024abe8acd71b876dbbaff804b955c37e270eaf892d30fe26c4591e25041ccd7e187277f18200c21ecd9e379f98b39262b690a6f902d682526e98c620b49bd8f4566f04b31cecbc8247e364c1404ce28b1703516f1894747ef39c13bce189830e50eaa4bc37694cd4a4e21ed68e321f16eb35bfaff1be1aa5339961ef993423873b7c73e92dd6a92ad23a0f51e121220b8fadec0a288c0b2f0581ed524244fb95ae3eadb36c6c4c8ab4f82d4d608b2a0bd18f1bcb253798506268e2e53ccf84a39677e42752dbfb393de66a81455666f66d560f98062e8dc72a3789c532e9deeb05dd813fbf03337a93d244faa5e9c5f9d24b3695b00cb4acedb04f0185c267ba3bed40af0c264dc3165f518f8128fc13d5163729ace450c4da714a0aa17f4373cb2b3e06ca950569ea5f1e9f9095d162bfe148989569a893750b68bb1223c732d50390c22eef927092adf5cd00dc0d252a34ad6dcb8bc804646d0530962af9a162b24e922a35107e40b58ccdbd1c0a7432284fb8b8e848fa87aa65bc1cc08b4a61d8b7c7bcd051a24067cd4d8e26b693089d6f8b02f7d3e780e3e15a5a1844b4dadb1ebf538eba3a8c1052dea10fe475dea98b23875f430772579ea212e97bccfe75faa662c5688767577167c66993811a889b7a9e90ed804f8433d6a60d55359ba1a5486dfac85097e28bbfcd961864fca39da51235ff5d3a58dd208ceab56d879e5eabf0a153bd68fb1cda47d08ff708a86440bf86c6d5b13932e8a88ee7b95b72cd908025597620f51ac79b0f47f60fb683ac83ae62b7c44b3e3f939135152f0369f7c696fdeba0b17f296df6dd604eee26aac020cab75468c6ec74e538b1902c687fa828be3014869450547d1da4fdbeb15924361fff69b7946baa9d373c7ef070b5947928af2c826f2fd61ca4ebe553f5c3c50dbc70e456cee6c0a6dbc2848dc0448e32d6d47fbc354bb797a41c3dce359364b4409572daf2ca989540714acc4c62b819f241b2055932e5b9128fbab2ce52ae9ab9889dbf0ae66aa2aebb55442f98c083aeb4b758d689a9894f74e1c61dc37c544890d49b812549d3713c9a7114f8c4151c42ccdf7ea4ffa2fe00651ba0832136b6da4bfae0602ede4f2714302bca0ed4d9cd1fc449fe9317c1cb7338426358bbb870a4a29d4c967e2dc21851748d9be72cec41f8b18851d21c4dd8614ea2b91d689b6a03331bc7a448cd0935ffb793834b2e2bc2651a1453047ae89c87b91b7e383d9807213f742fce4a4e008c6080dd11ea8366dd331c4c2181a6f9211fd176980e5c3279da47aa44fa7980d7a47c0cc41d6e9ba5cdccb5aa110ae977838b46cc2f31220433e1447cc30a93e69dce2a00d1bd04c0b6ef34ac9d535a1e96051526104a9e891d52ffe4ad1b4cc2730eb46269121a241e52c39cb2f71c9a06a88169afb99e617da1454db450964180f76c2e7e824ed1c0d2e19eb2eda1d4ca7f40d600c7f5026b1a451825efd38271c70f3dcc9a9ff7e76d18753cc082eeaa0f4c1cd8d8c84097356ac8d39385d4afa1c531d415b4a6f64f51d53183715fdb29d76766f5fbd5a669c4788771879bb0df805d559b0755a87fa16f0740ee2dc66a396e0660419c73582c78a4c4fe59be48291ee11cb169cdedf3bda6c6b02caaeaaed426545d4ce34e1372e3e99e5f4faa543b761b7359f5155735faeaaa5dbce62e52949d7f23c7d6586c4355322d946ec21927a99d0c86cd63a0fd37cc378f869aae83098fac68d41e3fb58326ce2e9f81f1d116d14d1c0bd50cb61f0e35ab02eacfcce5db0b01ec8e69fdc120f2b9729a826586f80239317a662cd7c9b80f06c45f582e86e75359bb03498e229792e5165a854a93b5e2d9a2441f4c994649ae8449279022fb10d5a7159e489a3d4b7e2a4fc9b02a295f5df5b1986020da8511a6681881ab187af1d90b617ce2bda95cc61313516b38994a4e0fe8dfeff0db7f9496c433cb03b8cae8abbae48f5e8c7fd274fb74de66a5f8be68c39f46dd889325591f47fe5f6dd374d7860abd55ba05d3ef89f923f4abed4f82c8a907b28c898252a42d18410c78b4b3204fcd635f714aa5badd937979103a9724147912af8df9ecb9ad721390c5f1ee597e3e2b88ce759bbcae1a9bf53c1092fc98b3c4fc2984089ad74f29ac2de3ecf53172975f621c3873041dac31a7194dec95dedb80b03a46dd826cb58340ad785dee503743f6f55f0d9c40d2d96c418e775fb0a99bd1e821fcd16e0dd5311e50bee9e7f6a344d8f32512470e88ab46765ba47a9f79a371058ace15842b0a4c380ee5c2c7a941da3621fb0facba8ece2c00e222648aded76010de1bdc2bcc90407253e761ad82cf2ccc40bf4cd820d4ed8ed09040970b78bbffb25579cd208f505bf6f14385eec727ff3a72c5d758628fd3eaba630bd0aff4271d08e0597e7d0c6d9d9b064429f437d622e5104e1776a59441db9eb901c4b55d95d4e9e51ef91be90775a02213b32e151023d3568232ffee05f7b3e92bd927a9546927376ae0652bf35745a5a9d91c29f3c40938741e83f3f4a90441450ec84294d9b9505e0436f259444c77cd3d09e2b170ca7bf3038ddd7ac2c2b9d59c973b1d9f2f23e5cbf217b90bd20aa8806f4bce50c031d5ffc9b61f662572709086b14bcf357772b2c69220ae30b971a4e2f28973657854746d9a7664e1838aabdfe999622ab5c1b58dcb328d938839b35979171788bbc72aefbe88bd90a9b2ff379e8fce2a7dffa46b7d8cf4e39e23ef260603b1b039a9674179988e24e2876671f9b41ae1889e2231a4483be1b6b9d64ea5ee77558019ca064a4cc3b4e7d7d65e5faec8214a26d93ea99012853c0b96cad3f53bc30e492e7774c9081f922fd7c1982ba9c329dbcd0d9eedc61d081b65533a556a60e8db32ed813ea79a0b4025a9ed0d6f6cf657b87fddeb661db0959de80829388cb7eff31a1568b6ef19abcd3b861e631f338870984cec2f900aacc82661b3f857b0594055d5c88863a2558f82db9bc0a2a23d0b143591d44ee02e377b9defbb4ea4a1c87fad094c60c54d98e31f6180b772d4e7dc62497d4a8a8e22e4c260397718712849518261a0ed86d8bf0917b89eb1de69a03094117c22c868e136da00eb6e830929aa31c321b20b10b4db998e7ff9606653021418b8355220f0735675628798399824809a60cf70c1c24af476673b24e419bdcd47b7a6bdffee132861b49c053f3980342e1c13f84fbc9050b5e6066fa53e453ec2de04dbddd61bcf6dbef726eb61462f3db68a10739f0ed5e87a48b4655abc03ccf3cbc78dee298acffad27355506ef8f3f731c1dee151674e5d2b1c48914f194e6cb44acbe106454f17454e8f1a8cd2b92b80b674fac1828ab064d04f2ccddd9c072c5b4becc4a908cc1c95496850fea37f986d37e65114b339786a07549bd1c054827a37856552fad0533c453ab50c5d89b818eb3cb2c579aadcdbb70fc4521489aa84cc2db200a4126a6d6e22790ce00e7559946033d80516425b722b156f5a2adb2acf4cb7e3aa23e0f48a77604cddd4a7cc6eaacabf91c45a995c546c57d7756501328bc878bfd8177eee9c1dbc603894464d1174f0c9ebcd23e0c5358a5331991deee84292439b0f8a9cb04c8acc166612ce196f0a0b7d7d566f2d6063974d529ae5501259e1b9d7a5e3385c61445baa6658f60246efa4c34e44e02555e5c438912142528d4ceb0951b5699ba61a97e284acfc0318e45771524ae10e7163a4b6e23fcca8517904d0e9754ef5de80872bf77fbf37e56d65fc7d172de65832bfae3a44d518f37d51af9cba7de4ac709085c8b436e8e4f4400276a6698d75676a2d631900a5d5fbddb32f8c3d02b985c91f2ac93fd81bf235d8c0d7c5bbc56aafb4daec6c075c3ea686b4cfb298ac6ed5e6d4ee7319d755ea4558858ab74b9792cb4bb8730a7ee1173eb2090d7a09daefb12df7338d8a1695949dff452df796cc9c42e2b8a747d62a2a051990ad0726dce1f1ba93618e669add26b1718dd893b989109ca2f4736f74ba50aaf0b47bf3267d54dc32ce4a310c78878ac384a5c35ac4ed3730f5072ea98f66fa6268386d6fb0004cf900cc8f0acbd8d9a7fd80dbd2cf6552918d1277d47600730120ae96e433e94ee7f5fa8a25a31f96186f5244fc8f1580b9fb2c252cdc9c0d01323f6296086a5abe4472dcdddc9d85bd98f32345d0a68f6f44b970f7e313d89edd1493dfa87fd66287409129ca260d1e9aa34c0976f8cb5bd8d2daaf2761b36a5d3001bcf5fbe1c9b2b3c2b59013d6249a97f170848ccb0d8ae988b32dfffc0183bfc1636e3ca861889be8b50c016c49158d83e519405f7f9cd7518da5554c05323061eaa767066053057bdba91219a2f84bee9a736435fef2ca0688cd46bcd6e03c5ff1105523b3f45db8a939b0b9b7023540d1a533333a143f4bf18ee3a34289012f87157928c50ef0306b63f3830514691cd5bef4fd86496af89dd7f0bd97bbe61a018915ccbf4e13389a98d020499230e56619bf08d1c96d943f01eac120c30fccbb79d9c576b3d14cd3c97fd1b89bc885e7b57669eaddc31e1989ea03ff28b702a14b77575ecdbe7a862476fee5b1d38760e7eb38b4eae5eaa5fe8eccf523bf77fc9072c681f4d2a0a44b378322505b808acaafed73ff6518679ad7e756753446af4ea8665b579d6c972df6f896660c21b597cf7e51508e5cb95b760c063c2e58cbc84a5628ce0c9ab29e5ce39a22d587f57899435a027d6f560744e9f2c73c475080077ea9dd43b10dc80efac040db0a1089b0424ed1c6960dc163a979381369bc15ce09b4f41c94c30a528d8e94dad36e842322173e07a0d971b907f38190fc59bebd2f77aeaef7ca81ccbd608ae4ee42ce860f9eea1bc6e29f797dde02c04786a8392d0bb6b20fbdd64c328a8f02292fa03a6ab5b0434ae8fe8558930f79b60eb8b8e89da13272472bd3505fc64cb4078764e6905add84f9e6a6ad4a1964edc5e59ca3bd2e2729d9026ca1d3d687dfe5eda2c403b0b6262a1b9a3d92fedd8310855eea87946fe7ac4f22e6729036419c96ebaf3a8465877774ae6888540f73aaf18b22110cceb6180034880568d36834921018f8e8e90222e83c76fe76792416c24c53a96094fd1aa9381cfe4aa673fa662bfee6728f7e1da313c3059ec2e583b346a474552703a517ee9d92273aed23b3f9397ee94403fb8c939498f095bb1df4ee1480b0510aee02918975f1056c2c824730fe9ead1bf9c09572983f31c718601a697cb0620d163125edc7589ee28eccec201e3da4c9025cf0bbd16c410200af3d256ab2af39f89d4e613b9f22e7cede0a8b9fe55a1c35e2fc67a6b1c4dd4ff892d4e7ddfa37f5cb898ffc0d53d7ae8466f09fe5918c8ce7278d2081ab237dc287c5fdefd719e371b8a4d0af713a00645bc79891765de90cbff4c170a7fcc9ad4657d8943d4cc9e90ce3ddccec671bc2ee25794d192116411994ad282b2640d9e00d4cbc665ec30db10de8a4f047c4861b9fdd9ab2f4bc1e4b7cd9d2517f77bedbdef190616fe6d35232befaf4ca1fc72e23d4df02db6057d74778ce192699b31bf8c93f0ceb24d8c0e3743077f4c05cd542f6aa5e7484d81caa0a041b3384beaca732f8ff6ea6acb2f25a8503a52d99349b029270bf952c586802f1c5864be53a9e6ed400b197145592f006f7481804844028f30d12c49825969d59f03c1989e8e4f4c560030227e0855761536aaae538e2bec4a2c1f2e344c86068546d7d61480f0095de1ca2f411d460199bb543a985271b991e7171d118a94af369656f2ac79b9975b3358e76e1804c0e73f16e49663a560df4807497b974247a4c9d1990118d4cd042b44ab04de62d846323d3933da57f959dac8be59bf04343c81c55648128b9a004a82c85701ffa6a6730bbb36f55691cf9ee5d0762fe17b531cb80b0d9890ac36a466c0f5f2335a7c2950fea36a8456f18c056c850ea69f7f57e20999c02fab97b1239c9b653ee2923a9c5d119069f9671b1570fd7ad961dd0490be24b2a59a4d285e804a2ac7333587c58b55e592112bfb7b74798564df4292242b3c5e5f384bf7ef510e2d950908384ea63b40b0241f592ae54064eafa205b99ab26eb5f591b62fd651c3daad53662a6940f2ebca76a8d03f1ac02a31269793619aad7ce78c2a06797abaf303545acac98283e0ae7ef52f20cc5b993e2c9eb00e56bd86ebbf444e3dce7ada7b7b1f250cc246813133a627d488fd35787467dc8dac495297d20445e03034600bb32b50ed503add25bdf908e9782a139515236de7421c47d6009b7764bbe0972b65824248a5d267f60bf2b51fc2ec5895f87ab4ca42715c28b4991a017efac291237ecb9f6260558a3dab48475f83841e62416f4d94ae466a47972cfe4594e19b6c33c7d30117b737483750c15fe2326b56265aab121cd72f1d12b771e3584e1023155e2e8c137b3223a6504ca462cbfb96fa21f0c14a8cd8d403fa76543012edaba5a59195aa8b4be963d25e149191b04375e53eae32951553e01df67549c573a76fe5e5f2d7bae671fdb1c44e329446335553a50ff1927e092a11f128df1e78c51531ce9347780d7d0f05100323ce1634657563e4495942312daec23ee8b9f518e2d88d27e021089b1c6d09419580a255c09e20abbc8341cf2507bf0c25c7f07d7f95b8914069cddc4ce5acbba3314fd84dd80c3d484d4e4e2504bb924b6c1ccd122e39836a882625094c7222c114139dd87ff665d8c3c70c5eaaf8b99598fabdf292aad3697dea70bff67e32f9d878a3f8b5d9a032f91b03e13de67140540b4225fe0f3db84104da9b2a4a794c148d74b700d97891e3bbd2c1666dd28cf5d6a7570a9483051757751f265e96a21091c0fc015821b54e3cb3e0d3a455c17934ef3a6d08ea4dc401e55dd6b9c22c1d9e4e448dc495e77872ffa0e7f2ece311728072de14715e2109a708bd935bf04f39ecbfee42292ecb78e02f5b8a6ac727bad2614edea14383edca45130ede3f68567670c174810a5ee0b41c437e0aaaa65394d6aaad51cb538c7428d7b37fd33576ca253996b23cb849ba91b23abe2bf156df4204c880bb06cd99812c5eb5efbaa284efc85472cc063f7e21da1d30d93308a0231249b721f088da6c5d668f6408e019e87b17e3bb3e89965fd6e8aa93fd21a098bc8faa9a144bcb394da07c109dbda97330dda78e042d4e04d5da08c274c25eaa98bca69b727eec7e9c6d5f02f844694c8628eb5e537a51a8a3870e121cbd7e177afca8d68b0ad03e4c149ae921194ea4bb2a6d3af56366377b258fc97819fd826ea154e7b55475d00562b625b1efba6207ed97404a289282fc8496b2ff013303a85edd915299c1e498d4d6caaa48bf0128bb914e5b6e53cb6058e3b02e873fc861cba28d3767f73780680a16278845ff7ea8ad5521a0e7afac5c37a76249f64254ca8e7c86548d01f42dd66c0690254390c7a9fc7472938ce3691be3c9592d6f9ea10da5957f3a80e77288585f1d2099011ddb11e24288d91302286f1823bef1a62f8bed2716529ce5a9b007bf4e563c0b799e1eb0d9771dca0f87da16118877beb241889ea46ce0002f8c7bb164017b4a9284e730a18b84b7d3421d8a8ecb41c9c3955aec7dfe695c599f4d49dcf60816b88324670c3814df6647f681943655ac9690914f5b5ac4ad9b92e1efe1f63f8ad508ce02ddcc6cab984f7ad0094d3f8d83287308b0e298c489a033a8fd1e56ef88966546c254fbbd299b66bdc22a11c65d32b6f68323ac20984fd63563ce0de4f8002e17fdf56c3f994db4b06ae6024c6ba38289740979382097b66ca6de2bd1aaead75bb0f996ab4d16d6ff3b4b3be5ec42dabd3d2e7dfe61e8c0f8e8db9bdd1fb99d49a90e590d5be8bcd3bbf16f35d3ed769b7337fb5a03111f82427a5501f59ee7bbf2639c4e40cdde60c66cf00fb3926151f3617cdcdf79811aa8f0e6fe24accac15f4d559c3f8c45f20294098846dd67a6cfe23e01d5916c9db98ca128b080aedcd09224df3774faa07e4c841a2164d5399abeab14fa6f0e29fe9508daf286f81a76917ae7cfdf3c5c2d7c40ff499005481cc871be6ccdaeb7dc9baf298be1090d917454c8c3c98fb7145aec4c3b790209e947f59bafa7e0bc61e8775a5f809a79d782640116f6a5231a50fad419f4802380f54f369a5e75526d078cae16a138228c20a03536680773383521f09cb992ad20ef3bcc905c8ebdf11eed3390023a358118cd1eae1756a759bc2f4ea67c1dfff7a13c8eb073185ec7371e5732961cf63007b57ffd0f5c3f8b21dd77a5e9c08138fa8618cd3ef11418ce607e2310df5587099ec6f155ab4ee54812e67da41fba2ffc54ec7f6368cc34814fd1af4170f64b02249c9147a65011d38ffc6cd794731fb313b64808b1402bd7e1e60a937afc96564ac8ffd875e31aae65072f28de1a6b1cc758683f45058c7791c4a02e5f6ffbf90fbc0b84d01caa094ebb7499be254caa54bc7cc60c1db8a4077af7b62ce2899dcb6c3c061e0a8e6b1ce8ef0d03e3596e29074cc43420993725d16eaf54ea7c5de53efbb20911c8fb9caa3ed067803eaf420c73b132815b02cc16ecd3de85b1c4f1e5325e133d2d83a088a4a9efa27dbdd8646693a26559258b85d153f5e59237c5fb849225bc85e20fa8f3530395d540ef7b2fa0dbf992149565a8874671d1683c369f92ab3ef6ed97ee31b2d091f9705d90cea564e7019801d02ea5d4d4ba3edb4143c2a319a79751e30d85e493e176b3f4794f4dfbd67347139601bf0404b56a78eed468760ce39c7ffe575ddc139c7f4c8096199dda20e9a8eaa206b4ffc03983c730900d45f2530d452438c12b07c8871f339e83c33eb8975ce6655eabcc6fc423b927121a5f2b83a8a477ba6cf0e3eb00afc90ca23cd408bcc35bb63acfe018ba9abd070b854ee0ee4d28cb89b79e6a26cef34821f7f4fd298e27e953253a07ced4771de818a631584eb72186bb2a4e337907638c4e95ce1fd2e560a2737cb10bff9a61934d01372b8155834deee7826c3a49416bfda6e73c430948726c9ec93693310994fe8a25d787755e7ccf6801925f6fa727ea4e1b6bac1bed39078f38c93f83230f1e14da55872bbc59e53814eb4cdef0bdf3fad83a8e0bdfeb6c9a0fe6602923c24c802c088e2766e0e8a4096e994d82b883156628a0d7fffeda30d2f4e87ca245e4e166e7b36431c645027a789b7c326a590a8bdf841f760e272e53fc27524780f139d5377a402570c7f87f984787ae0b3701b06e3f6a792921bba07bc5841a08ca5a10588c569abaa31f03b7f8237f4b61ab746186f76d5aa02b4dc62e4c3d8619d5ecda950e42ed6748912ff5c657eca410e0db90813a650b564ba46bb423b7957765c41d1ee86dd0d58aa73f7c120382996467acb3b2956cd537052b3995e52ef260d48b38c00c80772d1a33c82b00834869d59d0a2a3531f9beb1aafba38c72ae9ed148b35c87dce35ebad2fd373c0991f83eaaa288a5f3080dfa2113711b9b2926f871b1479916fb7e853a6f083abe845f279c167f76838ac6913c4a05efab2009e98513c067eeac752324111316138cd57f1d5f34651ffc02e3b3a41ac8963535cbb9c45c290edbd451af16a3319378222aafc315dab9fc87359a8e2c484f5cb9f520f8e6199955bf75901691acd895e1329c0f63775eeb58740d3bf7421201eb399025b08032d07ae7b3e62a5f79fc62c4a4e1c3b157bc3c57a87b8b6ef1ac9b30919e1fdbf2ff857b31788cc004e148239973a9cce0f1d0f2fc40dc5f696f17eb1cd2a126603827cd634f67a7a6684a61290b5f35bbad3709e4bc40e4174d5373a3dbf518a283153f2a1b39b79954d651cea0e1d078e1702f6a2f4a5db369f9167e7848b2bb50c5fe3c0e63d8f26614a9a91801a3f1d1cc3403aa6f04ec0a4478c8978bbcccd1a7104d1cddfbbbce4f1d1ecb2ccc4de8466c26c90430d956b0cca45af1f1b8d457719f192138e576168b4499d878e1494dea863ef06df3a63bd826651c860e8fc1f5c32d49ee7f45e27fa277ef4a2f8d95bab6830eabba573f999060e472aa2f82d62a9995b46bdfd74c49c69e9be84bf69b33c4fbc38fe6b15685599e83ab72c7aeca933d9372545e6477c49e6d5627c98ca28e1617dcc7054b6227c30fdb5b312d726168c7ce0dd54a995f15b85edaf798181b1ecbab33b5cf4badf41a461d5f37c2986ac063bd01d0912da0ae2e5601638f741fbc242b9d7989e5eb8b4a38a052a1ae8c036e6b3fd80ff7c66fe71d8a3fadbac5c6fcaa9c244fb0ce1fd739b638bc6af5ca59c5d58f853b863610c567e0bf08ed2df9977aa6f268fb990816d236a05b69235786a1fa1da90a1b7c7cd0f6c7c47a6c5ac729fbad3542e43ba2959458d7699a5be0e985255e8e4d0d373e0f03ebfe4f7d6885eea1560af00620083ffe3dbc0ae09affd9930ad8a7b5984e7c472db19e5c3aa4e5850b659dd3a2b8f63b0dbc00104d3430a3a6025d72c2be56b14e351a4a1cbdff96ad4160c9167430e824769ef9002efe6a9a2b9afc86ed43e3deb5e90f2c33b1abb90dba5379c1bd70dbe108bb3ab48b97d8bbfde5c7637dfd5ecb6b1a8aab4a71840f17369dbe5ea75f81afadd184386423b8828893e447b1f9b4d7409cb5328c6b422648533cbb99b9e4dfd58f781ee1bb78e011bc73dd1037b2ca8e84f084e93e29a10ee587baf9722bf07f59c4baaefc20603749629359a19eee3e8add23e0e121b3d9a914db234b0034079388d09228341ba3a995972ed230ed3ade481b964e646a91feeea15075dd21834ade173452abd970f62557133cb23a675de546abca8405bb258b9c5115ddf64854767d95e60bf160adafdcf60194de4e928a535b05746402c9ca585ee5ccaeac07e805afaaa20e1c01e5e313054060eedd75e0a6fbecac9e3ee9e134b4391eda1199e2f987dcc95aea5fa6eb4c84c2bb1fcd6c5cee1ad160c0f9ce40ff6ceec2b7f380b221273ebeef9d256378fd440dcb26fad38a380b65692f79e90b48df2dff414ad73436237d58464958ace8e349dbdb82a3be5364959103071f27388eef27c5d968d446c7b0302b5bb58d7c7b796dc37d3b9d6ffbf4d64b227e9c25e90450479097da5cbf077514f985e9da2988e7b99ed3cb37a9d871efa56a1c188daf35aa567f1cbc2ad0cc5f8c4c8aed69665bddb0b262cfbd72ddcbc4c75739f8d81e9c1eed42f4dce0d3e25bf293dca46d851357a96ebd1953a32eb48f76de382d13693892ec8cd94e3078e10e897b425578c4e9f8db270141ccf777c31b7f197731b2d4c590d0279b58c14807b03396a20daa598472aa2acc5538cdb8564bc525b39a675e8f3687c30bd97174459696efb41eb13a91c384309341fb3b320008f5ba813088329b48b834daff3dcc954a7df3f990316deb39a548731aa4d0989797f24be90ba82b7846cb65a6c16b004605535071b43dfdfa84a1dfd0bfea59b1987e7d612cb3744933ff3d575905835e214a69e8c5ee566be1c8134399a4a8d964dfcb4da32e7607b31180e7f2e3d12ce6fb3bf2602d13a322d61e776e765c17f72748c667c5b58491846c802e39ae7a0774ee2eb4e6b449f030f9addd45a3b00056320a6341b00054f0d5ed03feb454d4b05264223b3a0c6f23b9f26384093fb84ba5c98583bc59667fa86dca70bad5042aa56a850b320144730a050a3fb733968c6153d02395262ba4e60b654d6d8e521cbea5b97a9dc41bcf24d4e47c292f7d2819459077d58d86bf0d4439b7fa199ab49f5a227c1282fd03ba3a353394722192f43082d36efad1f0ba3dcad94d4d712f783ed0724341b66958ff36799112c10b456f08380292a46c72acf2116ea8e0db117d8555a9020df97eed37008c9c85dd346fe5ccd0d15ea2ebf36a4c9024c73cada1c1c41e97b832e4dc72e705bc5135a2e58d8ad697cd51e7d73b2518302d3744636f3d1bcec26f0c08447eac5096fc20606f38e97139c12fb546bc9eb03c82d6df8f3b77efc8a5647264d820d26fa0d0b888fddc8daefc51aded5fc1069a60fb2c569759530d07318b48a889ab50011d40ae733ba837801949e825ae0c97e4c0ef27f9eb633674ff473bfac1a238b4506c2c0c45b1a5d82f87cbb5a61b500588d5ad2b2452cbfbba8bdc7d6d1935de96becbb602b7b0753dba1f9dcf7b5ccf674546ca1d02ee7d0363b1a51ae96b7a4f3e969d6fe78080e55c45554227a310fb9317a41b9aa16b019a2a223be2a0aadc9a4a19d750cc08c1d83db730eddc394c4788648c54ea55d8196af9aa167d1586d3653eaae0acde0587164c93df15a43bf2e40ab0ec51053899c7889ac9333fcdc4b1f4899a8b34038a0fc1f95cff61b38df7977deb75977fa8e9863c8fa32850ac813c3619b6ac2a7c6b0625fb4144f02bacb58608cb41585e3abf22a0ac2fdfa73533acaa1ca5c35cb0bcc5498d96b4f2c4b243cfe9939bb6a6888023678cc71294a664cd2e135e945389bd411ab34e0d727d12fda18abc20c15286b6aae4e7bfaa7e4b7ef0ae7b7bda6295dce08c8b7490132b9dfadfb1d7f0be5c88ef472fabbd2d0fa63f010a6173f29cd74383ac767e0d08558c6e2bdd597b681ab8449b10b92a7feea6d880c7425af9f1719f1441a6d6e19d7551d41f494460a0a8297338dc7ea72de680e47b78672cb218987d46ad21dde09adc976bad1fa6ab0749eee7c048603f3aabd6790d2d8c026f7ade7b0abb4c391f6b81a28528db886a84ed078bb186e44d36d29f24d8eafa4e88b908598dd02030cdbaaadabd0de874774fd2e455170815b265802285536915b35cec8426f5bc9b5a8a52c899f99cb9a4d69e45f0adc3998074ff15972a205d7df649f251241187fcd81eb9b48b4f9b0bc78696681ccb5d05ed3c8b1de67a3ffae5b7e9e990e31063ba7ae4453453dfd67358cddb4e89e9665fea02d19e5440e95ee2a3f572d63bd9f9babb3b0e050fa4ac1f40ccec2da1020e9a2e55fb098d8f46323c58461f3dfb7e2aaf513bc13755d871a4a495bb93ec482d69abe7b8960318a8d695b79d15e48519d3b3f0a7bf7337617bc8f0e67c90b7c719bea3c82374a042a4768687e764ebe392cff95005734dc3bdaa4fd82c4f492a6bedcfb6dd6f689e114a7381bbfdf43c32d99857ec0b2cbeb39e5df6e649fd938296336d096a0ef86256b69f749b0713721c5043d88abeb98cf2677e4787a3fc90ea0de491807cc9b4633c6ab708d4065a521be820c1f3fcb28f810ee1d64a773175ae556b0424afe06778294e62e6ad27cf4d14d07d62459792ce5cb15ffd048002eec4c7e834b0f56ef73b6ab424d2aa3f9bed778f57b310d933cddb4cafcdcee8a427b06d4d22b55bf020feb2030d79828606f1555285382a5efc3f4fe1b5bc716f62182699dd5dbb3ce69d04d4e22ba1191f05e0a052ad5900adfff96a5e14b92d58bf41eac27145f8d7c97fdd49dcbdb18b550bf718af306dea86524df878e107d2b55223208773dd22be76ef2a072c063c3e128f58e3299bf1238f5ef98372adeda2a96b20f2c838f997576f9be7d87a2ceefcb181a11e7972f2b32aba1108545082de11dbb30b10b9fb32b6c169463da83d73ec5a5f8c35208acf987cab67ff6ba1df1e59833b8f77f9d1194f74c99278bb885a2355cd28cf0cee03fe6591059de4caf6a3906e4815c43bd930c04636dc805be07397a16dc5e7ff41c7ceabfc8e255bce609de0813978c81161bd432e64bbb70d5265b76be7538511c1011433ef1fe81541970208f2d3de44542c60867fe811c57ee8e7bb9ba39f1b69e20befb9893f3a9d62d6e7323bd26ceeba9e0b796f7a484286f0f562a92e92094bc84523ab58080a7b379b3592bab3e57abeca79f8b5fc44ffef99aaa31bd064dd17615ae7149858b105fd8b844e75d1c6fd7b40c95158c803a2f17397f56ccc9205f5b31752571e50a0347a1f1b500461e0c0c831b3eb838f2d3f4ac47b0cfda77c5520d052c6b9cf208f381968c3ed9dc349d18b4eeb705910fcb3a55f610f5b234a34fc1651b5b9593d5a6894635c2a77f3f0e4d00e319540eba0aaef904a8405d039abf8366be752ce22ba1e67bd5bfe8b6a06c71483b00c26270c51a5b770017fa1b6dcaf19647bc871cd53347664387f13b003a8230d34985d3aa5248792027bf254d5e4988aae8a8af426cea3e285d01a93f6b0a40659fba768887d98d678971d59ad61a2da56822570179bf02afa84b7ea7727da8c0bc4f8fb78a0e913af6c2ea0f42adc047ef7991b358ce0b4ebc883479c5ab1695ec75b117770af3ccaed32b9f9b97598ddc02b6757b046a32b57ed44b0aadbc04ed5dd52fe2caf78d1ac8e7b7248cdfed59465c4181598bccae3ec898711389590444a31ac6543514b176324d0b164021b0ab439003f4d50ea6ca2024e2f3ca642ba4e47ad485294ab398b88499a928f476a1e3c2280ec0ff2a274aa9efccca77a4b00aa42172ba908d3be88b988387fa9049f0cf4720129b418e9777338e7aebe5d37a97332c9d342151d699ff99c2682bb84fbbfd7a08f5721920a5257c472117617ec07dd2fb8344aaeb6b161c880525c6c3da9fc20000c96d68637300dbf2a334e2786014bf068a91c21d48782e3c005271bd590efe4b8ffcd5dfbe1c7af320580bbf5cfcfd2b3df7e80ca94c645b613e585124b150352e01f0f0ebacee79c6a16efd025884861fc07e21c673badf12c3ab1dbe38e9bc7a1ac1c93fbd001a497dd182ebf60bb67e7f4e124d2e48babaf4e319608d9b4f2e7e24663f7c998d7bfbd2b9d0613fdfd0402325727ba3d28accaef2bc84556d73eb6ddf5c12762857ba1f0bb13530af081a29f9950868c5f60e7c6e29ae39583db6ed3acd5ce810d01e468d4e277a6c0eba36dcbc9e69acea78529d6755ec11cb42eb6a347eca18d16a0bfb4df7009ea93d956e5c77a635ad0d41dd5459754217808cc7c41cbb1c11d5ce1531ecac5ed31309df05d8b8d45324dddafaf17f3bb02fa07ca87a62dbe4631599ee66bf4e4d86af0584aff8e0262884cda927cdf4833b272f19894b5c97c7cc5bc65e4109cba7e40d8108e083e61af093219b51a87a62c6e0632e09c7b494249fa36533c19d1289da85614b20fd6470d930b8cf6d7aba6336ac1391aac7ccab343fd40a48747a3a19d36e586754e9958e297b8fee36bfa9fbf286ca5a10e6a44b44ef231497e1a978612700fc9bb662c485215261911bc57398472921551da28f6ecd640e565d68e006ee393ae37e90eb6b0c5c61f43781b0d028232d64cf3bb92865ddc34a98ede7b4429d830e3d5cd77a067eadff0e66a53b9e8965591c75f91a5fdacdc15dc361b0ca676693575d5c20afe518c2c316fda117bcc2fb649eff58b664e5116f409f1b45d8b631d2d9427dcaa2bd0d7daa2f2f2c5423076e5466ad846775bb9e1c1d7d954ad9d29dc420f5a4f7855cb4d64fd5bca901def994dfe2c9ee5e5a34c732552b801f0a27d9fcd142dfce796b7e7209bcf20a5e021312bc96dd0918d61331557b3af4315181c948a99e8e8a60ec35abc02bd48388cdc3cdd99e90a10a275c80dc5f223bb747d0afa049518f89eaa894545b211f217768e57b9ad2e608142a8b826ae051e4d39313b76d01d6778cc7d6ac430d96c061c3e2ca84f0e3b5341cf210288edf16e0b6fb86d4f6ed549a2c86460429e748b07172f754d769857a3b9e784ac1537453cdfa0a3aa04d1b5fc2d584fb00d1c91deae435f7014597b89c0fe723e30e1497c0e7d6393ec9429858fd8583f50e88bec04a2a88a01b08e15313497b38e723181431c4c215c8756cb32a9c227774607ef49bffb2dc3e7e30d05b1b64550c6a3222c69e38f077d2415809ce78de27cfa99aa524e4bdf2df0fe340fa4e6ce2dcf00cbe76124c9898882cec62d4de203e332813f48af1243b1e25f1b60b71eac2248a85a7101694a253e57e70a63661b09d9083fdac5ea6b17d407725bf8af0005da72d142bf0ba83f6029135d91d16642f4a86ea8638981b79bc9e21ccae64aec3b0c9be7693904a4a860795f9020aa53a7bd96e36d77c52814ca9b208303b9b345a9153b1cf8a1c0bec2f2ba5e20341f1b25ba6f5cfd2b00104a49e499b8113df6bd7191df55b9605b2eb186ab130d06d74d0e112e8486566084429978b96227ee48a83fb50a7ad4107dff959c7e9a985441204f6f587952919476ee848d88f86d7f0d4ffd06c87b4296d6b37c3a43be528d5393fb76911b3b4d11f246c898caa160d3a8c6951c1678bbd9d48cb69dae697b2bd6a9e2f4ea0e2badf1000186861e28737ff84318959228c5c6edb4f5aaabafdce807980f1ca6b281ed809e4d81c016b6f0d8c20a829d0243c75c897cb4d3c6f19f8acf8042d8e6b09dc4862b45f4555f18760ff89b1103f20288e13b3778f0c788dea4603e9f59efaa19ae8677c2a0794e65715aba14f266e4e33b195e74da5ea6c20765a0ee426efab271c1e098491ca4339b57f56a7cfcfb3848fb7c314c684bf3a332f2f3029933de9dc7f9a8d8d2fb47aeb4b46ba3d820a3ab019e9f7b8e4a9d9776c70ee3ec15b2e7fdec3db08583cb9a8dd4aa53949d714eb826be799857a9f9a1da64ea8ffd993749d585580869c2f9a6c3ce6ff70df5d7d034801fa48a0bc4e945e6963ce90c17599f6ad34c8a485a822650e016138158158026620f0f5fcadccdf41bcefcd22f49694b4613233a6be7a97009297421de8f3c4b4f85d604a3c7daf8ddf577a0597cb48e9a80617e12108586e95f62403115e10bdf5f8b20a6809da3945eb3e5227e48e65cbdec43cc7f6ec36860cd934fb51a563f79b1be06fc349144dc34766f3d6c8f205ad9ac7f6801a972cecdcd4c35b0060cc26951650015a63c82e3afc6543e13644de5087e556c8a4a3eeaf94e42fe93ff66fa1934c0c78144b55006828cc67dd29adcf11299460402f615874806a45bd90cabb273e06706491d7748529589f347b12079caf6cb34539be602044d64a303f578fdf3e9956f9a663493e3354ec6729b1b4855c926da90e08af81b02462ff40f848d9549956e9dc3ecbedf99ea5bcb55e9268c9d36d71d8bbd865b1ce0130b2d4c3c6a84dd2eb6ee863aee65e8f10682a0f8706d105a0de00f505c712fc30edcf88ca775b7045371338922a7ec8bec6f9c54c31284f164f453b6b95aae6fa94a71719501b318c029b4d07882294cbdf0284875cb32ecd89ddba3aa8a25982207c782469f6706d8d54483b26182d7a174638af0b719f6730becbe4988e25d8626f64f0fc3280fae1ef3912c187935945489eefba8394c63a6ab35245c3b00d448095af376753dc4f37a543f523e8437b0c696ad5e83cb4626c0b0cd19eac6d52c904f8c9b277eff2a085f4938b5b90b6c6c1ae2796b5c810ba0d4b42bfca9e4ae2cb02288bde28950f5b5df2598c99ca84d9b8c69403d679d139f9c1ad1e46558f590a53fe310d22fbd5c7d8ea93d0ad0cc37751b775710ce66a2da2c316e290d483f08783d6516c8ccdce8fb059c882bcd81e969933ce37d36135bfd194806b3e080b5e5c3d56d5d09834ee6bf1f4a0c85a2e8d12e3a69eb1a275827501f28cd411bc43ab7776ba32721971c369b17480521006c89965ab0e195605a8b41e834f3f04a01b2a876b123db44b470b7639042fb1f46b8f4c92934b5bad983184d7072d8fcc04ff8640348ac488461df5c1943caee576cd97a70d1e336f4bee1450438f1498923f75a10a01c5afde18375234b18856b43f038a132982f7c63fd6a0d53c44ecec4b7a3a5ef047bcfcd652c161c224c009d20dd93fd27c85a86fec0be25e94ec7e7bdbf4f888747282e1bdb9a95d786e2003844b358f3dfae89ae876139364809a0dac924e282b8fe98f3d3909ecd8e01dad6aa108fb71f7fe83089045c40cd7e631ef8b916799edfdc012a8812d9a507fb94748fdb27844ba12da8e08fd052a102e155eaeb857544c617f8021c3d76a790bcf74bab182779e3af60900590e581a7a977b8150768dfee2352233e282fd77c69e1f6798e56ef10bf2426ad6eb85e78b7dd1ffad5b77a3f1e5b754d87a3c8820772de3a88a2eab24d4b6dc5fa5e548d4b06cc4693e6413618a7eef56dc14880d797f10259fff1a08f4e8305650b12e9432b2a7517297fcba51e347b664b284d9cd7a39daf486e16cc19296b35522c081e0fe18ad22c8eaded912325c0c57d1a5ba4525006db8063cd789c9e69a9f7ad0051bd25aed5d44433e0242a731c07602df696df8049b83b82d41fb28107226329ce64cfbc8eb1027254316dbf4af221316f3d048af79fc76cdfaeae66c011ffdca94fc1a89d7eb0fdb9e0db3baa1efbe562c698851fd45584cbadc86e83a89f730c31cf97c32d1b5ccb3a28942aafc81c51cc09fed0e63937eb39296fd81c0e55d1fe45833cdff2fa6d2e0c806e3905ca0292a6b25a649c1125af439f848fc14547f98a0b2b9a574bb81ab4195834f915672d870fe452471b6d076139af9ca3f28f067896484c187b596c8d84768c40d0cdd96ce6abb77039da04537b295ed020cbc7e7bf16d511a65dce051f056b312a66cad2b2841d0c264ba48bbc5d14955b5ed5a08aab4c7d927236b7ed25c71eec5c6360d20df2bbe54055413a68402877d6fbabb50f1ba495f428050b0095795711174f9f3bfe5d32e1e73b7ce51e1c6f258d26a23996d239f0c865bc64767b9d8f7d4d95c9ff3cdb6ae3c48d619af9b14e281986f33270fd36da49a5166f922cba29ea0966d91af8a9290bb08da83ad2d64c718f64e70b7994d23862857c0e4513ef6537932c11368b85b284e5bcc2273412cfadae1cdc1adc19ed6982570d307c676b972e04de29972cd8beb04153c21a1ddd737b51a5db74295af1d50815699480231bb36becf79cc9164cf27417dda5c7e7897d75b950251e253d189fb961268741944e7a63c1073c0fecd8dd799401cdb37c5fb279c75cff181d7217c459f688bd91c7b227d2452ef82282e3ee0dc274b5b31f5578fd3aac891b2c506322864a65de74a350617bec7b49a850b077a935d89266a0538190b2061d9442c879628302b37244456405577d1da712575097a6eee011e4b30771717e63be0121667721518f2f20d476e446a9b4a2653f7567bc22f1f34dc19a090f4083bd60c6df8f941eaaa7093e85d03c7977e0b06e69c5ea9482a458aabd6c3dbe786146e7d5553ee1bd0b6e4a2f5bd730af9e89232a29deed005773d956add2ca99dd6526db5690978f8b4b988dff6a2bae6c2dd52889be3feac6fda6fd8e8ee7881872ab23aa459859e18a6aba81288e1530e7c1c34f3c61b454c16d1d03bfe074d5584f7ebeab8c37b62beb02c13b54701eea4b627543dbc3cfc308ca15c46b66955e4982157dde8c0eefe4ed487c675cc13a1b3e2068c9f5142934c67bc100bf5e2eb90206a6472cb068fe2b99fa28bb6d9274047f97a8abc5ba1eb141e48d0c0018505605a77db0c9fa2dfea1dc3efa5077914d2c5b812914e8d922893541bf80ec4621c4259097b9d825a36f5cadcfab3d0ec3415d7d5c184115ba6657a6e649e02c6784353166d3d0a49d421a7a1eb5780aa25a6e60b06132adb52044d120098c1dc861d9193c6632a0f28df5a417f8d9dbe9a041681f3bdac592b2dc16152a9b1359a05b44ad98d01f8c628ce475c0b7711ae1acc95b7e811150f6f6154fbe050ceb3746cbaf29fc3e44d42b47af1c9b2ee7935be78d17515f0c2f876e0697ad5239832debfb68cdedacad7f0719beb8324dbca95cad705feec90c81b566b8f6ea9bf3a80cb28b4d0f76f6fabd6c7a2780efab082aeeff53d9d72dfc770e1b685ce1d83fcc7d834940b23a749a047f70f11c86d3e88cf1b67046c658a36b61646bf464603160641843940a526f0be25ea7c009c97b316f535d82a3e525537f939db6a149148f9dfc5beac7f0baf0bd0af810c427168df488571130404f54e3a997ddb0969d6a6fd47ae355f9a0e92a5430dbdc33fbac979d0371f18b227f477457825aafec262955dfd8a7f84f02aad25d3e01508231f712b01bc18e13cf683b7f2106735fb74b237fc6360657cbdbb19a13aaf44eea48d18cb3f486fc547edd9961a9871e73a3c459d9bb9f8db34405923cc4a4298d500c32c67736fda9e4f461dd2051734dd7d0160eb8390cafd0103dc2d0620cd8c35f18526c41a585cc3bef48119a3a845ccea8554f5c821bb6641218d3b2fc9f6e8d134b8df4ca6409c9267cfe4e630a9093d1daa58f6560d1df1b1d994c378331bdc8d90ed29745eef52499969e9557539006acd613b5c1c3aad8682ae79d5a9f11a6befbad2fb5bad9079aa16e3ba320b71075d7e61e0e6d4d445685be64d58b91b4069a8d6112d46bc8372b6af395afce01b6fe1750e4662feeb15c990791a7d7353e28989731b9b45bbaecd2d1afd5c36f579020dc7b9e439a1b0192e326b98a9a43e185f895826b38999d2ab1afbd61d3d4445737f716f8595da2f5d7c434edc979f946c1244497f30a7ea4d29a5c245f54bf65b812fa1d00ee627a7d848ac7984b60d6221585fbd29d917d00105fb9adb8cee6fe2d4772ee156a586d101b317a9b17e7f6374d5ca6f8e6d0e7aa2cf899acb34e1f87ddb92d8022795084460b349c0f2ae87ac8dd262283f0c54a85bb27a4709d25d8c888b65164581aa7078b6cdb3a632c2c7fb6ef020fd8f02f4bdce0e63eb66e4e18080fc1bbc2123b64b1d315a06edc3b4f5061baa95c5e99f1fa6de61d3d433b4722599182c0d8f06f9bf61faa6273911a55726089c4024b05960f00d7e37c73e2f482436762c7903bd312d831fdf2f5df7b62062ec7354e566f28b1216b9eda562c0a1fa0a0c0baf0cfda4384283001c65e4e3db42a0a8f5ce096028a58829e08a509d7f4e4a9a7812b3d55b03c1d927742a815e006d81e7afba7701489655293042847ef90d1a85c0efa2e4bcd292652a8a4112b275e9d6d8bd65958562e63c5adce5d769cdb0443766effb07796f6e19984f988114aab829508f40866ca713d360e2590649f12b11c8d0402406ca515528f91be163780dfa5a0476ffd1a54fc54ec600327a48aa3d9b5d95f2ed61775f4e8a24cee6436e214bb79b38d8ad7a55468091e6843dac43977b84b14f3d6b3b4d8b910cc53b6a8e34a8ced9af3bd510fbad1fd6411b39d44492a08fc851945f4beb4ee88ad44aff9f8afd3da1828f33888a923905a8a774afd2eea57c71d37c7b7805f2130069a48f7660348206a26b7fcd702c6d74d71dacf727501109b7ed4b71461a210a9d7173ddab010b72bc3eff4ab6f3dff233fc604538a764f968fbcb6b1c6f4bcdce114496800daf04cd1a9e3c50a3df2afb592feef14855b7f2edea4080e4f770f540fabc989629344aca4eaff35ad4955dd907e4f3a64ab0548287ddae7a4a4967bf0c9f4366128a7f3f367d762fbe37f8e10ae8c11cc45e7a15e6d64f8f449c98f541000996e3cd5723868767bcaec9739d01d872c42871b242271d974e9384ec0881a5b1b7adb36e7108ec14dcf32db43ab654029a162cd945fa707db1884941563b8f492b840f491d339b3c67eefd530948380319f66f27925c5e89693becf6bd1dcdbc94b69f1e472e78bc9c410b7d3c047be6378ca6754cda103aa9eb925f40daf1d43b6c9049ddeeaf60819e431543933415970378cb38d361d15cff48554bde1f17fe79628eadf3861ec768448eaa1b234a18222753314ffeaac414f7c350690cc7f42a6aeab6d29be731fe188bce32f9548c026535ccfa8bbe4bcbcb5d23fff11ac1d8777f26ce188788dfc0e902cbb97fa29e36b3a540ea9c8bc31a816cc1683b218313934b7b8824ca8d3337efac8b286e3bfe4497737fd2befd24932d592570bcdd9cf6fd9846120c840875667d30a353cbe8803e203a01f162bf63d42a7bc1b297875d595f21456b61036fb28091ca0f5979ebcf0deab6e37939557f94d53fe623f4137ba1ed41df649cbc3e717ac67197cbcc98512e0f3ac1168c503d9b902722a165a7115ef7e1631a4dab37d8691ea005a300a8d611959b0a753d6de18d5da7dcaf03fe03a5c6933b7b91e2585e7d2526f91e38970506762ac48a1df5b991fd0de987e8c4242bf6f93c43b7d8eadf245c35b513dec4e88c10448df52670b31955f19ddbe25d7de5e8dc5ee907b0c9f86addc8e0c2e1380519b1217f5a03665aa2327025db5fb9bedb06623813edf88dc2b9dc78e44ea5c30b29bb690293d729e06bebe625a5cb94c5b9340d6157897db3ab8b9bb0b36bf870074914af9abc36f958f95dd97b9cfe0abaa0ea981946ab7a0212e64fc6465d0cb9c7cf6f6196cbc085889a53e49c2468a6d1643d58072c7f586b2c52447a875a0040979639fcd7b7af57c6d395586667015f005c14bd22853ca279d77de9afc69400353fb0b41aec6cad3f893b246dd97a67cc8c18b606c322f21ed2738a0a62a6aa95e59f6a5ea005e25894d2e4da21b423ae7f9d3b2dab8ef4566cac69ed0aa3a4a24629210bb5d42d977f53c03c032c481ce71f14a24c364a5072fad75a111f89a8ac839c67c00e41b1529117518b8958f139972c83a238806f1aaa50272feb8210f63cd3a6f4478339562fc4b047dbc3c059ab435bbb10569cb5652a1cfb3d1e48636da6de3c08bfc86bb8764a3129d5f41acddb88a49acdcd0560f6be534129ee2e7de42bb12673475117fe6b1b425a35d016e9012005ebd9aefb4aaf11d34eb29d923274da3a455634df650fed0972525c65de2d40980a1d20649a6579815c3e2644dc7e86ba948ffa546f69480a388abf67b441a9e2dc06d4d6986e7e58fd829b058e2ac25c8932b389e64abae8cc7ca60fa8c7ca23eaad2fe3ffa7823ee2b4d54e7ed3eac5e306f9056dc82ccec9cda358a7bccbf956ebbcfa67697a13716bbbd2882e5f9233ad30d8f604b37b5556e285d447e79bb5fe7cd66f2c772fc11e41b37a818016d073119970943a104e2972f9885d60d7aa25057f02298c791d3d223e4fd2dc7a638d0aa7c29a5d7899e8f9700585c741fd803cc61cfaeb1513de2e0802616364a6fc4c0a2166fc558219570f0665b88d0a62687a62b6c1f376bcdae6de709f0a3d07260204ef084ede7864711c7309730f66d405b3369e10b8fa83877c5cb3dfbd1bd61dde0a377970779cca27e9dc891f2bf0508cb58987b65e8e874f4bc3d7bdd64884fce15a28fbe0e5c23409119e7a33392522deb714e286ee3ebc71326e0942445231eac98e5cb4069d377253e865bfb083647b3a93d2efddec28d471e72c5e121fe8a5369e8a4568c3726ed617f83f013227134a3b45267c8209b3895c10751838dd5686d0b6214ad0420f9ce8474d54dff9ff738513e3849f6ebab9befd145c6bf41cea35b84a94e04898c9716f39b11372809c0015adc1c9ed00a29fbfd589664abcfc0b7d088b215d72db406a10121ef4bcc92ea0b329f74d4ed825cd7bab3a1ad2c9fb8579a37a904cd0f084f556d35b1ad42c6d29df1eac32e1de11489c3a2390286580f91e3ce6a90f80c2ab23d3a9ae52cd19a5eca452e20ffd1e3332f120693f15275da28595c5f02dc097c3dffb217973225bac00c9f1f8f766c6392fbd4f3011d10d8496376c53889d917184e93605707da14ed8ccfc19b5fc8e5ad4cccd187ffb96afb5c90c108e8b9de222ab6d42be8c2477a6921a2bd2c2b071d5d6afafc109b62535b932712a112873a09c470a8ccda4bdf65fd942d9d1f87e2545f161425e7f00364b50ca5cb0eb86a037245bb7c530f90411c17a7226a095f5fd64fa56716858f63c8ffa1f6771277983191ec940b3535f77344cbf4fea71a1d69f73c100123fcab64b1b7dfcc79753d0860b318db9c6e951c11c41ac0b43cf13eecbf2fc596a3b4b6298848fb5b8fdcae7f77ccea2e36748aad864b5829bfc67931b4748d5bacb10b40e970cd0d4ed52010a113ceddad89efb9c09bf142fdebbc4cfdd0b4ac7c03df925f652a8a34423602a26d82d42b2011d5234a7093edaaa4d1f06fab8c54a574792faecc1780a2c6e9359e149cdc46cc4cb909932e72ffbbe39063ef7791dc39b3bbd33885928996cec1b0a45a8f13d7af759fa95acc7e539b3c4f39a0076719fe075aa0a691ffff6ca85d576327df77557daef4ba8f0ce5804352aea26fe239736a8cf565dd4a96bc083bf6a0b669522827dffa0bd4490d399856dfadb8cae25cd08ae06a4095034af3f27d1c3225b39b31992fe2b65609e897cd06cb1f233fba5cf578deff866a7d832617e401dbe28ee37e41a04eb14ddd322721fad293df5d3ee924cf3f77e2df5219b4278acd3404eaef2e14a4da4b018a1d0f131504bde095a8f3141d907fc401fc3888ece189c4dc83bbb7a0b02c7fea5b78112d2bd6f728810a7d653b7ebdb73b61495ccfcb65e0e400f90730873164e0d4e809ad0097c716086bc8ed8ddcf2402598c331424928fa02f8ae704ed88baf16341ddd0ea38700b569e9d5f4d6d0101a34b71961de70acca60648dfa962258fd68aa15218cbf537710d43105b4a50ee5101eef6330726b3a958aab23cf8e33724aa58cb4fb071c129fe26ecef3f970db79050b19196dcc41fc134dae65a370f54cc18d3527e2816e96c42b958f79163ae25f6748deef95dde08f3925a6fd80f7852446fc74f0e6fcd5af51b5d9850946c5d1487fe0997dfd7996003ba07022624d604f864cb3b6481b26eb528e83cf1c31a2950f37d22f4fc90c234209a4d5ee6d7ffc4323c06d9b1cabf54f7e1fdc7ace0e641f70c197afee93a21b0e0351257494a58a3a7e977dec9e28268e83d4bdd54ec56a269233609ad7d619a597b0433a839990fc5ae75295d1c60b532f8fc4e70046fbf994d313cf38a3be45abbe54e11b0a4ac44794e9ca1bdf2eb0aca86ff5ac60d30c322780fbac1af1c270119fe374caf62f07c26379ff8e56c7a452ea30a7aec92c904ecc626fbd3be8b177f3e3f5cacefa2edec57f919b7bf59306fb7e333b01044e3d9e995e2439d5b50b6c1685266eaf35273faff6651c4e6db5c50a06990456bfa7e742bc5ef2fb45e75875e5b3ef4821ed0d2a4a86828799ab21721ff41b43db4cd0041e528938e919fe7a653d04c253bd1b1825ee7548977438ead3b1676620bccf7205b70be8adf53795803d68ad4ea7925d7bf650479610a1294ff98ae37c90ffbd3f2bdc885b179f44ddafbce2b02b8b39adf8fd25a20a3c7b0c17b46390a483e6026570c0a45b7a55480c658972e3af5f59d1895d70674605f25ca641bf43ce880bd40fdb976124eee8ca290c5093abf30dec0e79458aec198811a8ad21b85c16f87605f236c669af832cb0352a08d2587bb309d1a27950cec8d9836054af7da5bb3761c3a21083540d6bf134d698b5df9b51972f0bb3635249c2d51ba6296adf6b3f78b9b110b56c3b02eec372780d346fc3e5f18ba0687fddd510a315a7ed692a52eafa4f5f10eb866ca8a351065a7e8069fc79b18401270c5ec1a3d77705fca9e9fe7da53ee0a5396b33f7a92aa6afb6a8c84e78f6e284afbeed7039c5a867b2c5bc1e80583afeabd37144e7059f9578d8ac3f2df27f7052fc5893e82392f05cfe72236683255b03f908a61f567fe1352a189aad6c16abe09f0422502372d98142beda337a77020a7de78b2ac6adb6d731f2e53181dd50f643a74c2a5408f8f1b0b5738f6f9020d895875bfaf76c373ed88e20d4c94fa2bde339c8cd5f66a8d3ff11b281ebd7d06318b48d5f0f82616177983eb885be8a37c2b04e977c9f0aa23f0328607c696f03c2c271fe5ab6f2d514517e94eddf85994c75f44438ba159bf0005986e5fd4eb682091cba7ca463b00f152eca09c76cd798608193fa0e55529694d8fa1bc4443566756ba290b50fe6fc8dbfad24ad4f7b3f83c984b94f7789e6966914a9aa95d3c6c8de19634e9f8fa30cbefd6db93bf3b9cccb755a246f3415542895630012dd1e394c286cf3436fc35e6c4ec60fce0e0b2021e0de417d8b54b784d6e3bcd70f663921c6b5b101b565ae35a923d9b427d78df63df14a05cb71a5db4e8bda7b408b8b31573808bd45f5243ce21ea7b705349bb8d230622f8bc23081b8aeaa640da410260b380261e1b05f3e8baa7bfc3bf1c7a28d878ffd25285afb6337694470e0dac8e143c978bde18f99922b6ea839df53cb0e9d02d75234013fe529dbb423e7c3e28c86d2e2b98058998139eac90125edce4c60ceea0d3fc6a8af5e881f7710021bd1ae756c1babd304a1991ca369702b0011f679cf1b2f7a04a14db09f489a3d3fc7bbf2d62d9c9be8999c470fa74d0d4860f81c2eeda3d356019e6e5dc4d3a7e21823f206dc2773b3a89bbea9f1f9436abbcd2fc026285e8aa4b91dead1473ff2e89c78b771690254c26dbbc7af586bff68ea59247c4921e3506b2d43cfb1f5ad69985a7cfc3ac977a25f9d3581d485b05a8a27720f97bff0bdb04f3c12e33037599957b57534c7e9d098ce801c7fc1bbc59cb6a864d79f4186edfdfbf77d37f3b91d942c71185e094d2ae4a787cad0fd0f73e027eb6c2c81edfc192fc8d42374426a5b6b4ba11063b0642bea07797023004d7c066f6eb62236852014ea564e100353a250ceceb8dc205f159e10f97c3b16f2fe43e473a6028cc47db19193d80531d7e614e9a14f180f26b049b701aa8194ec48debcb48ddee294ac41751f0c4277767c1f5a5ee3468557e4b1449ff6343a879b87cae6272f53021a95020c97016418e8cd053c9843dc007d4ff9f24154acbb1c8a2c5b19f1a4f3f13c781db6094aea599e2d0283fbd4fb5072b4b65685da6a917614b5a2e503e7382d8eaa748cbb7ec83afaea07beb4cdd3598496d9887ec0e00259b44dd9ef6dc1b2a86ef6dd8999c172f064962774ae1d08e357a628861b2e69ce03cc69822f0703c9866441d8a0bae7e6394a4527b73f610c0fee1fb344ae2003a354172195178313c84549fe547a946ce8d0348f75b62eee006f7b7585efbb616b4015a840c10a2ed7f414066e3ebf2cfcfaf179e22eda8d8fb4a94aee86787a0c860da452af733f1c6ca9eac8c7b9943ee339fe6c6e18cd62c877c4261a262a0bb8eda30afffb175660d587669b5567aff769b1ee27735dab6316838d736796ab5d11a5a6e0e78ed880c275d9417194da03e9f8ee56d7691147d3ed61638b507be7e083b615e5bdee4071f0e75811d481ebb53e369bcdf5cae5c29e0b6cda9b164164e0a5a97dd2e3cfd2c86fbd8c6b11c5ffa78d0889e58bbf0846d31dd9dba36a4b6f6e42a99a6c101345bf6cb6f513d42a9ae2614391931b8e9ada869968707ac7b026bcd3298809d6191a902edb5e8f93a7100861bcfe4760c88b5e5d9f6d9f7f764b3d6a602417dd90c3b9c9dd4eb757aa713f62ca41f021ab6179a8f9d1c15c73f08b28825705005a7cddf8b2a5c4f7c008ef74fe8b2a0cede40b9e860da143ab344540cdad11184b0c2f66326e4e386265d959606b66fc3ce057d1019b100262c9808f86f22179d9aeeba7ec56ece1b82dec7486f1adc0b205d637d199e6e79e37356c9652e8d53387509dd88a3867a761923529cea7819092fabcc0e58a2293530a5cd07ec17d58ba8f99911c89c1b3bf45a332de7bd2b228fb422a65b749e07796a43f01eacd7544d57974d0103b877f59cc51fea8e6b77c0c3d8bc17d88806b4198fab58bb4ac953a542df9759b5083cabba48fc6bd3968792faa840401a581125d33e810ed90f095022115da9e65698fc0c696a388202cb90ba1174aef8f877239228d963a505ea615fbd5892eac14e16270e20964f0efa56e5b1322dbc52cf17596bb7c337b221af3c0d6f7157f166853211916e96ae40407c69477c3ac31a6739fb5348ece5aff34b6f2f75ddcc7e8bc94f2f549de8664f04cfd32fedb095266a119dfcebdf55a17e8026a1791f7a22cb1cd1d13efeb6ed7d6db0e2d29dde2f976b404629f79c53bd21e63dc40200c918e2bb2f565fb99209600c5b2879227bc2a2299358276c5070dc2a94f81384ce8d5dcb1c098ac8429cb8d169502722fc4036dc729c34650546e40c5e6a99d459047769d1a08aadfc41aa7be75fab0fd17bf3b01d848ee90a54a1a1b61c50f1d184d5b25e0adfc59c631c45f3196a2c45301b8a4986da961114b68b6c1da0139e746f7402abd9f9a277915cff7777080310075d473f1ca4f018b8e6f55aaa372cd3e2360f3377f586c18f0aa38e6aa9eb42c4f3bba5d1158049e31f50086454b0bed41c1db8646c3069608231484885fdfc2765b998abbfd65a7ecbc0afc0384edaa70e1f0a3997bb64000104134afe18c685ed242ff1ca974be5b7079acb777ab6031ea7abc6a6a5b25caa607ff893881c8c48a0b2fe62ec0654582dde44a135317cccbbb33c61ce91f761b1f8c9ee3bc68b8b717f680c5b791097a880b41def0aa627cf6256377eb7f69ec3e7c78e43f7a4b0c981dc4a62cb525dca1940915a5d096b953581653959d86f00223c5b6beb15ff6a0eeda268ee063d6ac91944308bf975c0c36156cb4cf0b07af8cfe06be2abfe936b0f697dce3ee2641b51458be2652f4c12699016d5c30d7ae0f67b08fbc677b96e1c5f8d350d48402932680c73add5d2f0452194f74a6f7a74c87c37703811f0c3548746efb6afe2d339b3c599b2df5b3e02b177e76251f858054a409c1c61c647d11b7a03b58fb6f2711aaa5649a9c414e0c29588c252ffc22134396a223aa99e539edb5db8a7cdeaeb990b20a47c2a5ad71a2619844d32d1319bace1ce205b34e78852773fc19f256bf8db9a63b3cc6df2b4fff9fe222e3a3008256fb0dc236b5ff2d7bbd03f5c6d65d0d758adfb4a8faba41b9c4ce1b330ce2fda58f4f2e2c36f7d8eb458b3257aab101de9ed60baf65a3b0a7f829a1fdd528012260b220bde84d93c3930aa0b109c3ab77401ed06a2f86de2f44e0864ba49989c526532fdd9bba30efc5284dfac6b21e88b17c316d4683c84cf2b33e70dc787898a736c813c354893cdbfb80eaa140c67de5b3b909e40d0308277cb9eb44e30b62bb319d29b420bea13f6cf692be028a64a36588f1d95c94299cecdde18803ea669f8eed5423b9a85b57ab6095df33a0abe4151abfaa43810929ba9ab91a700425c11ef757a3da7f45a8f2449aad441009243a345214f06a5d3789c1316d779ba3c1ffa90aa9af44a83960ab9392f2ecd9e087100669595b623736148ca8b09d4ecb368bad03e61a3c809c172906afb572f69c2021db4f630953390beb5e533eb28e4b59ce2505de4f84929e73a46d8964d9854114e82f0d98562e7f3b0b0f38037f9050a47645cece99f75d9cdafb304a8bf7dcc1049ead05a6086eee953d66f35461fe3887b3a5512325befb7c05c1907c3227b5fd6c77fd3f42b0b336802408eac554c750e1e28c03d87141a308da4afa1d1be842ca910f850c4b91821aeeb8f71672807a80951dd4f6bca98e95afb514618f7051a2db50d9619d629593fdc1ad38f449d441bc00dcf85f75567864660f623e3faef20ee50c68d9bd0b14aef039e05161c2369e0e6fb11af5a76ae132483d5d32f02f779eec1bb7202de9422c90c419dfd12ebc63f9bdb5406907d253b7d56c72a79d2b3a6a5b90ae18e2222a2c6cb284a682f8cfe4cfaec678aad20c15006d8ac1925182cd86c5d9a8c511ea598a6343105f8d8d03be8ee787f42267ddc7dc847c3c834284ca48792309c845d53ccab8e27849a893901a6b9148a9a778d44162f6dd7ee07c52a346a83dc5a6222b6e5d5f11115ec4ba4035512c060e74908c56ebc25ad74dd25c1867c2ff1bea5c6010aee24e63ad82cb6c743c1ab33ce35ec8d8e44076a642bfd5" -const lightClientUpdateHex = "6cac470000000000a2a6050000000000989604e65d66d380233f025b10f7b99b8bdf8610ae627ed61825266fbe1aa15a9e33aeeb92443af6d49f7b1b4f6c8f973be374b548db40b5f32f82db57cd29349a4fe9c882bde3ad2ac67b67795c1f89de57c6f1e361c60415375ad0d19605d0928540d3a8819e60842076668e9907f9d9664c8a8a5a94f658a0da1cdfdac6114a0781f13b5e402fa4283f7f4e2e973c8706beb542fc4c83c7de6b4a3530a8174859a98bfe96587ef166bbae213b268ec21ed0ad166f4e32ff820b37823e533ba3afafa8eea4a9408278602ce5830d474355c50dbe34162e53482aa093de4328f26a602aacc01134d3e3dcc55aed33c48a5442438ea51b9af7fde941360f5506ddfc77f55e341fdfdb41b41f39a6507186e6a3e9131f91f2e48098b4c0fa502caf3f28449cbe9e540ea50c70e1f899e77f365758565a2f344d90d558d9b8002a6b6cdf844269a9551ee0f6bde6f3135b917295a0d935c5031fa7e44dbb28cdd37a6c2cbd1a949c3f097deacbdd1ea3f29e117efd04989955eaea2e97ad717151b414d12d3a69ed7e083972c1dfd65681ef7ea6ab4f2b549c5032db6a24e07e8f3e52f656835ee7a4ea5ebc6fce106530aecd990d2730d3c207c3191c8cdcf6107052051924391546ab75ef796157635761c6bd1c53c61f1bd437a5b3dfd302178876951e388070b161655f2cf9a9bd11d96590b37315cb562c33aac5067d21d943d2c01d0e1f6773412e5474fa102fc7afcfad0448288767069998d41a123701208c11414adcbf37e0f6748f4a27eebe5bf65d155039bbd90b027a0e27aaf327895f1f6fa1cc47b23b08edb3f0b375768782422c85b45643f051da52e3e584fc4ff84aaf8dcceec257db7a0a516669a3b4664e87cf747da6738cea5163c3ebdabeb7b9292e1cd7e56933362502b8139cfa51fd254011acaf58d0782904db71dca1bcad4fe4630905aba558094175007610d574702ab4be6b9a743a031f850dcf3a2a6950d30b82c76ea37dbe7a949cc2897bda78b921684614fbe5f9d59d96b2d0b3e10d4b31ea58439e93ccee6d48cc1e8020e3da96456ca9626ba597943c2fb2d53179105881f8a170251f75d00c493054c770572aee3c8da2b73558ba6e3640e61210417d0c0bf9954d4e0a02fb15b7fb043a90b09539a73c67c1b9a4a33fb11987960547e4a7e08cbd1083d340ad9df29ff42d94c44f02301d1c41539adb93013a6a05c30d51e69642808985f82fc3983c2e4fbc823d8e657b9e316bc56116041d43f0b9d071501790163f6bb83b9223e1ad84213922d889102a230ef141300adc09c0244019f0a694a05d05eaa23e9d6cca506b65dd9529665c38629979a70f04def4a9eab96ae21ff84254c45070cff7430e8fdc3e4d5c2ee6258e8c79b63898185390151b295c2c9d9e2278c6b5c15ec364dbca1fc9d02ce5e60188286fd49e5d28212d0930aad3980451864fa8d4fbf036c50297ea652a5cde1c03d29793a300f7316de7a1a20572320484f55827cb24b4a2617fbcc9fab7fcf2ed34f1f7408d3fb8237a020495f10edff34c8054efe1e58eeec2cf197352a41a1fd84f873378675339974960a6bd4bc604dfe9eeb48c254d79b02c0f599f4015f18da1a566b47c275cf617bdb57dceb1f939fd75d77bdb56db0a19c9cb8ac162876dfba53d7b9f3b5a953c7d3a8b0013c152a2aa4070a8748f16c999e9591dd315ff0a3ded207c0986feba3fd518ebbab34f39d3e1a8c6e20c90528f5c84995c5ef8811ff38b7b4f726a33abd6b81d5e7df4abb0d1679d96b90416e675c5db1c0b33cf83343be00f99e3b6af34bc5975d67990fd1c0eb0c355d0d97c769e0fcdfbb7c443fdc0c4b339f2014618a390da2f4f0c3c12dfb1548ed1db6fc6a8dbcd6ec38d2e76b6119844b9af369940731baafa375490290e463020ab93115c9ef785693020a2ae99cd438902180c130f4f17008e92a88759128fdf186fd923649857903211b5d1f133b3bd30f94825257c3f26510ff784f1a99eaba01364d1b3357173a89629c60667415794271f26d237b16efb497fe0a76a8e4085c22ed8a5787e1ee403abc8987b138fffec05e12eddfc2c807de040c3ae4cc904468eefed8581c8bc07c4280d885be6fa3b226c36b75b4b6af8903f4d06b7fb7cbb04db3ba75ec290e1d2bebc0bb4e572923b1d7ab7fbda1da4239d2b4f3aad0e9df0aa05a713623f83680c58d88c8b980d483f4cfd0b02a5b74018d23f251ed4d56260309905d1f0cc8d2c66dd0e671dc7a8eff07a896eea22f31fa16b182fa2bde02acabb01f3b5ae10f796f43558580b2275dcc203ae38475ee872555d692202e429b5f9ea54aaefedeac8ed7f5c548e33de3d501b68a30ce7442d6f27dd376d380fff252f32bc7f47184c25dbfa5efccabc860082370f13c21149de4819fffc69291916b004ab9f36a79a9f73a625b761f62ae9522dedbfd46270098eb151c5f7a3451f8752804a23e30ac8c7fa86a597bc346b51e1919bb096a58fa5e4bfe19569244af3fb8c8d489c22d1f4880250d302c351d2a811cf11a88fd2c7a6948b9c3dfa474133ab62669f2859821dd9fa6b84b18b781fea0e5204ca05a49fea6eacdd61edff660e8d6e40e1a188ece121fd53942cd75ea111dbbd404d7f119dd76b0933cc8ebc39e7fb0bc067e45c18584c94c380757b45c74254d16a4d330bde257b69e25075a85124a6612ec04a3f0b938298fdb2e914851d2a914041963521755192dd89ec303454a7af020670cdbce2ce1f0b8269b968f439ce66962f3bd6a7c6b54d3cd02e279a313b395368af985f59b6640283f61cc49e40125f0edbfb4120a05583028261e2a873eee391b3b523387d8267196f17ddc77de45bd14659856cd6491fed63291ee821322e1d7bade7d3e96b795f8bb432f39f6b2866ebebe98d31d84f669f4ca9d12e87e99f84b9e146651abf57a43829951db1987d25b24080b084ed6a87d0ce08dc53fb6af4dd53b321164dafe742de9ffdb7403ccacc822e7daaf47891627b340e72858272c232ed2dce2aa2904a35567da0bd2fe78cb90c097ea4efac77648bd359347692a78ec1399306879eeb39035c668527242640baef1d5c37a61103ad663db69574a04265691cc9a97159f6c55f88001f117f6efbb309fb263739da6af3812ab13096d8dd588c6ddd8af230b1df8621f3830d54b24b487b16e583f803f90c457003a3eb60a4e534781393cbb20bf3e013616ca2e38ed5ae9d8216577c26296aa8fe3ae3acd60fcad00febb98494d87eb2b2aed98585bed8bf85aac89d1518e7efb8393f540da6865fb334d250b58546a008351e97ea32aca5796edaa9ab8af11b00cbd34d079be0ced0fb40243c0ca9de6cef725b30e64f49a4cbbc2a1fee815ddf28f093fac49de823515f04adeb007a94d0f1a1dd51345813ddce14ee2364409a02f2df581f10bc8f065afdb458af78a7fedfb51f7d4b465bc0e283505dfe3711cf2ef9d6c41b1b717953873909113deae867faf3b1472687c2d132517b0b0fd2cadc60a418e9dcbf23cab4df275e5f52a7d294e8811e226e84fe0bd1e54c1c62d04b61bb4520faac9e66fcecd23579fd53d995eb60a17c7693863b8d72eda62170ee85540f8554b606ab57dc99ef632aa284766df9d1b6a09f88f5be56b43bf7969b43637101e8b83a695a4ef72103033cdf035aef03c3ba5517a508dc1ab5f3a70dc03e1a80eca160400a6477856c6eb3c9a039837a4166d74f629b684ccd53c1851397765d53ce27a650c38bc765faa6b04b4a55bd22921d530f5185581cb644326cc7295a0b4232f6e5bf99dab39e65efd1ce686c83cfa6e7e282a92d3efb9ca5ccabfd8e48994a6f6457b302145febc135461acf3d0e8895ea17c8b1be2453c8fb93663524d43e0cfa2b82819721ee1c56b47ff2ad85945d551b5fc9594ed39c85c8dac75768c1b3644a1f7789e37b50cdbcff5f723abb777cc31de5c1d685cfceab1a1196a12e906febf46ffe6dd22482d544c78c8ec68cf6017b7774845ecce3f327f9f2b9c33441143609374e19f384ce6d299299faa6c772d54f92a3c0e65aa2fc5bb0e1141ef079951998bc95fd6d83a8f7aee6e69e16710cf545b78e97550fcc9428abb5be97f885bb4fa033f64c6523f705f27e7ec2fc122d10dc7b40f0feebf7dd4c9727ac4177b7b9a4f73ce5bd10d2b28257f80a8d2e0bee75805f2d7b6d6265d9e320e7c2c0f1f6b76b95210fb72855c6ce6cf1c6649280fe7b08d0a1c1ae3b902db2362fc6c793e943ebbb57e6aacd31bd51b97cc6a2748ab08ce337f5866cd48607a55f05da882ebc2f2e98702b96b3a69023a092af098a2ef13db0d9822aaebc5a99d4fd19fdc253fbeab76cddb0e85da6799ea5113223b5a9b151f1308d99d492a9a4e6e57853bd3ecc33956eb761bcc5ebce595bad093802d40f672eaa0222cd29eaec72561ef37ae01f60be77932d3280558fe0e21f119a6b02324037e0eae9060169d244402c65fc1c2e927f073622738b627ea442ffd95a5cf0127d88c25aaa8d5d13f3999c5d8b318e71a12f12cfeb4e6eecdf9b54c8544ab9dc4eec3b528be3065788e047e6f4839dbefb968129fd31294ae006db22ad249ea43ae0e8d9fd613856488cec3da153533c4b6b9965baf87e3533bdec0284a331bb38a1e557fbce279b279c383c84376ba869c2615dcc6ee6954317ef223d0d210abd6b75fc28bf1b409d5686e8c88c214d1a8cd4f31bde26a2bef4781f41d6e0a457a392e7a9ab8b94f0a5048d16d11acd7394a8ee26effca678252c963ea0dcf55cb879430d33461cd966bf588846752e06758c7da2357a9f61973d5b64ea27a2f8d086ae463896e056a56da23a06b9010db0fd4bc51171cf21e61caa91f0ef21132c19a008c0c611cbcd2526affb37384409808ecbf67fa5f32c9f44f7979abd9092b7ea8c804cb42db76c778714c1a01e16630c97b98e0f53e742c05d54be141872f8393ba9fc7110011202d8d98e580ea3bc4324f269d769a094e67ff8aec9de4faf1c7c225b972853f87774514accae12e6156f35ec5341fb97af4ab862bd8aae1c6f1b7bdca51d7d53ca9f06ea43559529b8db44e20019bfa4056f362fd5f731aa6e2323e4d098e6469085efad05c88c65b4a925b3602f8b5ca2ffd16bec00ac00e795b3ce9138c97608fe35293017217f85cda7d34f659e076bdfa23f2e189677ae78ec8412651dc9b90fc94108a778ab5ea6dc236c226ca68a1d171ce103afdaa2a1c3e7deeb0a889a5c431532b7b207a37204fc69b403caeccc042e85d24e7456fbb9713cda2062a6c2fff87825dc07bbdb18319eeaafe3663f9daca6f3aafdd2c59bcd26e56d528569add1d7dd0327b0df8389db1358dd354ca142ad60c0a9569b92a6d5947754d260e933060fb2ace0779262f97b00b1b0f498c021b2e18f87d7067cd37d3b69df3ed64f92b196346c40b2734a06e85b60f4ce41bdf9966d3d0a5bc2495919416777aa32994a4ef1eab7041048c6ccf2e0dcc608dd76a98a2aaef603c2c17b282756e8532571b28eba85720709f1aa782560d44d27efdcc50a98e222dcb10ab432551c3a0fd32cda7c4fa375346a59ca5187426f3626b546be5390476c6d262650cd324e8b6e85305694106343f3370fc90bf7b10900fb117dfcc2b9de55e41fdefbd21fc49c8e4d755a30bb5df072b2742d549d237aa4f8fd8291ad3e6201917cb8c937985132c706037fef55606ab1d1153a5d9dc78bc5f3aad6f9f332b849d31bde7a6d3c2c12c7536c9ad777b172216ce4a23dd6f199b505747abf9dcc443a925a719c808b0a9a24bfe3f1d83c29bb8e55103ade68bf94fb2a9aedc0efe7b1409cf495a61620d8eaccc2072c6157a076af29d1a9acc5dcac5e186f55f87c28d3174e685ef9009bef9ecee703294b256d1e0d1e210c64956acb8f6bce04c97312d34e516183578e3c13a506987037e74e7a36e4bcb85ef816ba8afea4ef5d2f8edb391d2a783aff39b04d416d6a76daa49dd62166a4c8d92c61b839a079e7d3913d87a8b56b0988ff7f5aa5620d5e857b7d579ddfbb021c89c7090b4c1974e6d123f4a4218a20226f01ddd898ca2bfecfd6fb4c5cf0a45cfdac02c947c6f52d0146a292004887adcb74266999cd775b94d66d04e2b7f99dff70e1cf427b9792bff64d3bfa7374df3481669e48d033db4142811aebf91760c2dab732c31103de918b8392038b0270f9dd1f173644a4a9180a2e4ccf6e2cdee877ac0c35776486be8a97de1f39b71fad4f4b0a5aaee2e84124a97a4396fc64e2f1bf2111e96cbde26ccd3e2849dc046c49d1df1d55ed515edf7fa01c9ce18263cd528154246b72977644473a95ebabbe62af81af7880477b52b726ebec0e68546bf56c06593ca1c3b82ec160c270442c2d1ff1be36654c1baf52a7b8b4ee9359937f72389995057aa7df2a0f51913564b7b6fb45f9074e9f654eef7a60402e0d6d8eb7b01aa45aa2942d78fa94a3e064458a23e9791fac0f4d96bdc67ed7cb21b1a05a13089976ff9ae27cf02781a4b374a79168630fbab8d057eb008a7c7829b5d3b7cb3f715444cfa13340a18c3ed4a6f2e2d10612ff655c9686f8e29b9b84d8baf0fd5ed76169b78e62efa86e2d105452518a7a9d53167f590ae79fc847fec6045b21727d667cdc9ef4c0fd30377e17bc33cbbf1ebdda9edaa247b7d99cb7702a1f63c45f64b045c2846df8ffb2501fb8c7bce80948f11bc5091bfec63073651cb939210275829651badca1a4b3d3e5e1b8979326c1ee6246c258967ab58803eae8f917832feb4f3e2a04565c3c776db1b497e976a561d56fbc738eec166937c501b9ad22075a513dc4f6fb533340d4006ab5498715b1bd5c2f2b5c0b159a1944afa758d9118734b3e026b316d60c8c627a7a58e09b6bdbfe9ee58b3df32f36266d3c905de360344cb3bd8bd4309f5dd24c7f9a0e38775a7185f6979fb9a3ab1d483b29cfaaa77f147c9d1163a739d07333a067cf34c1ca539e0e9334eff481041371575a0903eed6d8c6b3ac52ae9ba97c773c375cbf8dcdb362824d5e92f4d0ddbf6359e5baceee078c06d3794ea0d35e32d192411d80930cfc927fe1c72fa81567080f9b89007211f1590452f886993d393e1e373faa673df8b9f8d2483233ce279c0153f583b0410da57983dd9c74cea1f1ed89777e68dc3a6b899c92606c9a2da639829468c760ff15e53fc3071fb7cbf5c0a9881c34b68eb5ec47c7417f26cbfd0fbe65a722a3ab0a0b5903cd5132bc7d783f33c9dafffd1c4f6243a6c215f470ce918ed7ee2742a77cd2eebb52886a310dff99199ba90904ec6393a984871908dec2306a6c9aae99f89ae9523a1ec4c877638421fbb451a3d3ba514cbec20c2d829292b07d2550ab2efbaf6b08c890c278c1e5b02cd34794356488a7bd4ed935433fca3d51d96aafe7259562fa585ffaf8ba4c600004145819f9ff22374c0d575439447c4b8ccdcd5fd7b4cf80efabd7d42f77bbeb5f9ba77ef69af99e7f3be29c0d66cfcf69b141766975d9dc69ef4068a52ca452c268d1410f6ceabfcd176f03f493fe4136a384998e3a188332681c9f5a5e5e32eab8b860beeafebcfda55d11c4076d284ce281701e54e5b510e2d78366355d1e151fa00dfa78e6d5230ca2186493bb87616e616fdc329e8cbe09a8763c536cde893dbc60fdcd222e8d6c11fda7d2a0f9e48da76c64a0f9b4bdc5be52e0d41b52fb0dff33d30fadbd329270cda400cfcd4483af3eda227672276b149e25f8253db629995623cd3b1205d6525a875f33151e1071b9de9249f6b59227123c9897e4e8932719504faddb58624364d404c3373ef98f699f897a79757290734e95dfac9681f2aa29da96980d000ddd9b0efb14b232512a850afc1b162054196f853b02c02fa2920eff35bcd413acbb436c659aeccb17d72f9e395a97c2cd1e6ca6b0c5fdb56657e9c40c5461c6ab42e03e0d2abb648c406828da39d43577ce172d5603a4f3cef643a0cc7ae04b7af2f33877c58875225569bb038bdb261aeb98e9d138e82ca5fe514b8f12d746fe55b201503fd38f6f9016cb52e73df29d66ee98ef71c72258792837a55ffb14749079151e8bb67483ff2a047590fbf43135e73b454cc6b3e266d1b4e96d530785da6d6447fb9e09e70b7e9476cc3bde12d92ade883cde68ad5bd2558c67605e90df61106a4378420bfd4cbf101705080db73d840483867ec8007ab0ccb22362ed6a78e83a5a12f88e57b4916ca5877355be55ccc1b17d99677ba4a07acba89c7ae6f4405abfd1e4690d4fb9d8f17bef5dbd45bf4a75113dad0e392b5ec8550edb751db397079a952d854898b8309f0ee471575fde8434e7e30feada8a1faeeec478f1738fef6433a882491503890f7cc55d3a1a0777366b40c231f7b6ed68e4ab7443ca0d52705b1226b5ecc130ce4083c20ae91a799277a234ba55f0a9ce2735611e2a2c064addb20dbb628ecb096bc4f2eaab8ee5f85212c68ed50f362504f6e45b972f5b3a9bb592069d660c375012d691f36919db26e84ce9de048d93facbe766ae6824542d5c9359e1b1241ecf946f3ce58904e528af4550afe1646c734cf0631a5ad8a1a8dc418480865693766d008b439e6a84336efb88c362e7b5288bf7c03f2d5df260895f043ae9869b4a74f95b3024b03814227b6c79bb06ce9f1af453eac159b9154c3ef93e4b1b084e0c03ea3a65b38808932c549b49f3d94a746872de6e56988fba767128a516b04f87770bb7f3ab51e0f5bc71e38b9fb8ca3eadd74ff40dc121ac8f4399a5373929fde10493efaeeb1a7f8f657163f15ec36fa8b5648be5160c3f6cbd825ec3a88aff0943ddfa9eb6b6b11c9ec1c41b9f3465a0145d76319f3588fccd1ab0dffdd38a245bedfa62077fba704541f78535e4fe1babb52fe374138881774195258f761cec06eb259d5a13f62fd6876b9129dcd96f4d40cf7067556a42c02fd6584818e19fc14a47e4bd5aaa04d580ee60b7ff39a702f4cddb554636639ae482c9eaaefc4a88bc4bc50f9060b382d9420ffa6afe04be19f88e99ff90184f93fb37fdade981b5ae11d6adc1d29c3210826dd9a21abcb733ccf2bdff4a3cb9c9d960ce72809737d4e0de8aaf9656ac945591821636be5792df6902c1346d963e396e82ebca4728c730649825b11f94c3877a8b9cdbf4c11e3d4ba91db16a2bf0ad0669778c5052f60bbe9bfba69cbe9d19dc6aa36cd47ee232ff0253363761aeefc2a5f31b2b81300df76018b31161a973c84483055f25cc726706bfafe7a8f4ae0890d7fe403c3362b58e427fc4ef3d1f0ed9db2798a633817d045581d8422921889db7afa152b86a3ca01e1adb741fecc40c800e03c86e4cc3f54ee80f458ba055865d864406d628d4482d8ad6550e5bcd51b5d8652ece6ee8f4a4deea0b2e712f4f2f492c10f59a602a1eacd987d4f3ed0cb0d414309ddea382cd92d4cd00f83bd0bfdb1e0a16a7b9040cabbc11748c7b5cf1cc5ed465244551a08c7676509dde9356823376fa380dcaf78c2496eb621df058ceb8429658d7972d15e8d88b988100334c2a1d20f0eef7baad080ff87ffd70baada2223984b16476a05bba50aedb5921c712074a6d501db411c5f1d5bc303fab76506a0d21144a7a7ae8782ed581706bf3245d1cd57347a38f331d28f697d3dd3e85e83ac302b54a368ee4a24c8560697aa4a9e7c3d13f88ca1f24a906f20702862db77d57ba4e6596b0d48874caa390051bdd79955a07421ac1d0d5eb6889a5024c9eb0e4303b8fa8056db38fc63b590b81c879a8cae226894cbb9a3cbbfd7285d5be30d1fe2288b2e4d245ed781ea50126223433bf835566b26939e84b43725241c6b8e2f15b1d87a80ccb786c1eb544e169dbec9702227b7ce8a1ed8b68599ff0aa0119b4e4154fc838abf4ddae0df0621ac535cb57b9aee9fbbc4325123ca2e860d62169adcbfdf627e7e32023454d271c18a589dda429c17ed1e9aadcce2111fd907b938c00a9d0ba76c83bbf1e73e63d95707bc96c9c4b5a0fcfc7449f7f44992a533e08fe11e9ac715a67a8520b99935c25a29ff5883e889a44c2ef3b6ed641cbf04c7901c1e25adffd0b0ce6bc5075168e8aaf15a4b4d43e42d9ec5bc16052d71c38e0b3a7e942ec13c699ffcdfbbf8ec6624565ac42b257702e56def74d4501e61f318e23e030ad854033754595f9647508eaa0b01c78f783caacbf18aa87f9d419bbe11f21cd618628372394cc043881455f49d275f46230c08cf2f4ae3a848b1ac198a657c15cb4ee5c3c006d22ccefd06c601942cffcb763aa6b67f4b8aec54a4ea6e66e8b2897ec637c262622e117b7ceaca60759c14f12b8741582bce5cfa0f7d86743e0da364170d9677cf448d7fa3590256ea01dcf11905726b88b15056236d8b996a28bce21fab5aa79bc62e1b61759bfc80d044910d3de3cf7b107d15115ec7285ec4bf3622a33fea896ddeb66c1d8adf4af7bec7d637105aac13d1ecf13708beab130be604f9a2ba28676ea45e1fbc00262fb5ad791ecd3454d807018f14580d9fddc75328df5c2d4fefd13f810716011515ed787f0ab85e36152f0f0699ec0eaffc4fb61e668dc42a607e62b88c3b51bc352694ef931865ac6bd2d62e9a126868963e10f4ca7a2b879b1e462578ac723f9410699f17b25c13f14a37f1de5a08681a0c22943514e36e6fef6dc7f6244af34a3b7565dafdf951ffdba646a16fdef01eddf01278816d357427738ffb9951ed28b8c036c536d0028cbf2f9147e2807b984909cdfce613a7678d77f3cf488280cb79e6b36607a6d419334c7c3b3aa19c621eaa4da59a1257a43ef73245f5f5833e03e83296707755381a98c567251c18327517c24c6e048b1319210f552b9281d95cdc532cfe29ac3b1e53a03b7683bf78e95a9523d063ae14b8ad8a3fff26ea904be2ea6a1d0ae27afe59eefc68be8af098a6308b9f48996e196a501ead76f8302904dd984c65bd90a2287e2144fb2f95b12ebe693cf1dadf3020a37ec8604da968bb1abf61edc3209a3897c2f501e974e6659b2d962aa7f765640a6f1ff541a714e9609261caff65ffedc9e02b7b4092ed6913b19c27eca0ecfa078dbdc5c1aa45c5c0182ac64317ba2f3cd08895e4d451cb781dbe8e0e5863923e05aad7a28fffe8bed937b86ecd98707e963f4c9de740419aaf779a467c2dcc91f5ce61a9c3efa1db6e5b9fe291320ac8bb08c5fee185a9696102254cd1c2846122ab69193b8317ff690c8107204aae1463ea8750dc9f6a3c17653c8bf09e43b99e5a31976898b0991a3227dd04c63e51e21494824e09310dc78632438b1af7ef31b194fd57f1788cd8747e8373a678a8e24a65e1ec77518e951039763738fa331740d06336675452414ca07e20a0d682a255e983097f3ba21802166785f672c5b2d92e1785b6b4981da6d5493ebfd9227fc97533aab03869f0f93f6d95cb276be6a804a5a3a03d17c2f1b9838957bea06b5a85e531fb26977ee664262347abc5cf09badb3f07885e16bb7078ac682399576c1a9d17dc7ad4d3a806e3de40d9bff4ca31dfe0c27c94b5da20693a6c6d36caed4fd12f075efba3a514a9718522b9a3e3b98b1b525bb0d2b3723ebf90e547c09844330a48df5ac3b094d6aa71db616fe67dc08ea7e83e291f9ea20f83556cdfe4b91705eecc4cc3ac5ac8819ce5234a4a780c61d8430a561444c8f1424797dc6197d4e7adc323c1050815dc9e6060f88824182a1d25d396029930317bbaf33b69317cc9317c689a90b4b6c3b10e7e6032f3ce8db91f7ddfdc3a51b5c020410a5454ab3c5ba3a6e55daf3669d091f658da5208550ccf215758c7d9e3fbdc90c017a6e71c0b6cd2d15904e8cd350f972d02a2565b402e53eb64cb5334871924c9fb008f73de8b136b36129c80a986c2d1e120e4aa988d7fdbd67aa04f7df8f5965db71b38383e5b93ecd5563eac558fc308f046355fe8d1873ee4b557287af1660bbad3c42ab90c0fd8379f039645d6d0773a68ffd46fcbb5f29354864e5730284aac4af17fc59cdcc5d4f69a552e2eca2813d554a69059000daf4dfe0793200977913152f7576b670f687422dcf24fe0c883f49533744338cc74326894f7e34a7fda88ac67eea9eb69b3dd1cac8c078075e865389ede7a5b0302b1d7ab1dffeb4b71dc84db4cd3aa6dfdeeb90040f80ce425b607dc81dd4f1698ab2304f78811bcbd56ca4c2726991e862bf39372e741b88c90aa22f55537658934ba63bb62fd31971c3c1429efb83561fec800bb9f54667296ba25869fd3ade5acb1bf7cabaa8b22c1f04951b7c5c0434d3bc7a054882aa9f22e7eeac8822049b9034c1ca0f8330613fc870cc63f80077863b5aa41b47b563cfd6cb87163977582dbbb6a26f10c33792d51cbb50119d82bb3f210ef29d8bf18a05463a766fdf3a9b0f1317a1d4b6e66ca01f6331ff5bcd1821cf35df7e1288e3d72ce727ded3ad0ffee267e28737607c70aa57f72dc4aff85027dd2095b9408dbc0b3eb9f024f8075ef328c8b2a49914b29cb753be1edfab395f3badbadf397ea352a523afedf0298deb013e7baa27ec1efa27a320f32796c1317a4a356324a867749aa1e081199275c8b324d48e9c2839bcdb599f98ef7e232872460ab76f824da5a9e17ba287eda3ace8f83699a3ca49f912812c7d1841bda38bfcc06e148a9339fcdf5d47f7ad712d65324081b40bee1df457c07ba281a5f48213a3e8ab4c5813b23f5801af73f9fc376986ec85bc6c21e5f33fde307736e34fd8afb38c102b0420377f2b825d0df980d97ded8d1e8e1fdf8cd963f3b6db8c5f102e5590321c1e1eeffc18519775d210d0b384c47aef3557f5d1a082632dd4192362fb3ed2a25ad4210e90ad4270318987a4eabbeb747d1c52bc308db8e917dd765480fceaca01ae79554a455be3e9376191685928444832a002c08412887f12ff15446292735484d3c938666a347163f855952c05afde5c7b322303431c67d14966e5c1eda6bee425b1e75b52760e184252081993450621fb65ef0fbd4e0549ac5aa54c4caa6b14169835976a453961fd28271211c813f54df9aa9e1fccb6067dab0567ad0cd0a540b9b29ac72410aad5aa9887697d20622b17dd10e7267ef1e264039f1f863eb56220da771fbb7c05ec7a57b1b43db827d88a411800f77b0d19c6a25e9572d1b8a22c3839043ac4aee01acbf20d237e30bf28a1717e04ba57370ca7add62ee3a66e8a8261bf03d6978c55b31e4fa0d61624ed764e9e873ec5097620a5e071c06908d824b8d4917d3000bff408867a6ef997788657be516240a5f981967b98799fda4fb81f81a74044a7288604eaa84d8b2dabf2af50b7a17d8f4df6ddf06efdcbb45796fd7ba5f60e03fcac007119d3f672102d92c1aa3af351d5b147ed20b4441c2081cc3a750f66dbff850d05cdac64493f065bd762790c2d1b92ccb7c2e2f5a42573c05a693311f79c3d22551559be7b8bd4c5606bfe9f4d0e6eaa532093e86b38e1d3ecf5877ee02ea2afa478ca44d0a3868d8b9237f08e23e4df2a6fba9a0f35d46307ef1777f80e1cbf5925a6cdb413965e8c85010a9b08b5e9c1417b3ba7243c1da1fe949a7513364451e4089415602559946f67bbee3d6b6e9e66b35a3c81d8357b43fdca4b99b6844b7db9bb708e3d5b195460dc9c277410e9c1ebadef76d16198abee591996840f589a05188c49888e78d77b43e4b4b04e66a14bd76c28129bd83bd82b9c845f0e0df532257ccbf6a2199dfd42518feb9a67e1c26dbf626b434887e5ed596191abc9ca00a72449a546156ceb18e0ca7845daa6ef92f8c1650831cb9b04b4554ecbd0fb7747843f25986192e3bb14a78df5045241bb16e71b1bd76f4b64c8e3e4b882b528ff39a11f09c466b317f25220b60417b174ff44cf3211fbbbe29995a928f7606e07399eb85a9e184b8ca20ed9a70503f70cfacbddfad5c4f24d58f37c9c1d7e19337f2528e6c83e2bd8adebaa9034958513b58bf43250a991f450f226725defefcbbcccf21f30db48e0304a7ea60bd7606fe17b929874f10bdc2ebd95d23faca0f469bb91d9d4369ef9b649159a9fbdfb326afeeb6b71d3f08385fbc49633034f1264684c36b78c78c9afc58390c81abf7b6c6a14673b3535c3643ef414814d57e6141118b2bd8e5d89dfd2690958e624b0ab51986ca5891bde40e598762a3690f33e19f3851539632c1df0ec874c972527297aed8063a03e3df65a2284789acf8d0610128a9b9fdc6940939036f1c06c54afdb4b30a72ef94c492e89849b2ce6a585547cf87c82bc1e0713e7745decd69ba0336a15f699a7614991a43b5b96fa3ca15f7ff49ba44a9187d94fcfe7f5a6451528c0e150aabff8484e5b06ffacad688bd6dc618a3cbe021249b94da8d33d44fd1b498b0588adfe7da16caecf738e1d951c4586b6de564c4af90397e5ec6675ecbb0fb91a967d8e947b846c5072ff009610f5c4e2cec3cd7bfe848a22e7113b92d12c7bd919b29f5502cf16eefd03534d726709e7efb24b67bea30ecf902fdbefbc17e67b0d81b2844eaa917f84f01ffb2b8f4c81012cf6ccc5fdc595b6d3358e89e872b53a13b3d4479233f166b4eb4eaf6f170c85941f73ac28c803dff00f7baaa5e2e6b88ace3a78913896516dd1a14d3d2764aa24f7126ea1faa1900f1a07d619606a6784519dbf3bce4df59a5ceb1d47d26a0b09ab216e9a029cc87b7b462ad06ffbd37dda90cf8353758d2a6367ad5f1c399076c9bec0b797cfbb596f7e2d5b69f418ef10e92ed252362d3def656b7466bc6e0a4c628fb9453c66f7be3845d9a6e417616c7fdd31924bed9c28af60a7478587eba1f71e76b240744144e88d4a783730026077ae89dd80d8d4739bb75b99014104cdaaa590b75d657059103486a4169291fd2ea1e18e446b61439018ed558d7045bf0702a0bac1dd2735079f1e04b065024afbbc7f83c91aa2a287e2f38b1f3c1b86eaf6516f971118c14bded051427ee65a23dd8df31bd069ec6ed9fb2049b11898d2d3168685a5a84ba6bd5739849a1d3a5cf35f688e3ade76b87c3b70d1e293a6484fb2b7aaef76fba524219afb1cee19d223fce3cc8ab591a44835d785b9fe028d8a31bd3c9da68ca3e57c3bd462ccbb5024b8aaf8ca4e5d683756c3fc78c147209f7ae30d129ba6474df8bc3f4415924d78d21a881ce40ee88308b7aa6c15610da494caed1ef18cafa686165b04027ae8bc33afd23bec6098195234bb8f6ee6fd330ce4aaa5939c7c30e1eae62d1ea6454bae7bf127c54a57f6a2c135eb47ab63b4677023c5a03a7e3c15b21bbf6369eb47661263235176015984a5eb40a4f8f7b1983d48b597a14a87779103591c670af88bea9887982a98a5940391feedd8d3b3974231577fc5ab43fcdb025ba7bb032aa229ba6b03b31dcee3e62410caf8727bff59c4a0ec0612af84ff7a1e3bc73a94ec190df777d19002e0dd0598d0771fa887d701681b39cefee406560ffe70d5b3fcacc972b2b50743a7c4554c5d23c620f04c9e7459719448a02b3b769ff8e3fba9a048a0dc035b48922428a12a99f37a127443c4f13229a5155e081118cdbcca60e172ac37805500f1f2af6ca196ac55974b9db594d31d71d849d6435da27b90f1fde536b499c60b911c0ca6ae04ea1f2fc037567b3ec2051d8ee9397c2353248f5e07c8438529805aab782f87a997d50834a1c842cb0d7780ead2198dbf77fb8c55c93df4a364213311997e89cabed082e9de036b8209c238d4e9cd4bf5ff17cc1b6ab8f95cc7fa18752e19fa270a123fc2040d41a34e2d296d4cc9adc9c2128101c693c97eec1a117e1f6fc3decd5b97598db0cbc5183b8f64842e7777275d42556c0390fa84398888716cfff0387eaccb2b0689277e6ca85bbf69eb8fadbc9d329518869a1d43e276b934b69f970a92e603282400daa073c6f957fe9fca90853d2d796096047063e95ccb1d8194b68022f977d05276ed850fbf3eda87fb091f6864ca3d7e46c1f91b490c0361961a91f049e1d6e6b97efff89783f1e9235ac000821238766fc6d20efd3e2c95fea742815ee4accceccec1b25a65f1cabc9ab6c17d27e7a040658b1b17911f052f64d8b641b73eb54264e8c492ca5effc9514b1f9c3a0dad9f60a2b9fae596b65aa5a959bf6133203071b7d09354e93d854fb82d04637a8c407b10feb8228b98e4f84f326874d227d3badbc132047b0fecb1b5b10b1a344ba16ebca2b7dace65996b23408a1e2b3f3ba2febae5677735eed3abdba25821a3f91a2de17edfb37cd7b98bef7783e6bccf83ca282873b130ac94f0ce8bebac288408a2a998d21e02ed8cb7667d51703988b1ee2a32c260b93c13909516b75a2bb16e3fd378543c1b55bfd836adc34597190e03bc1342df016610051d2702e2a8b2ad8b98ebb0d317a2f9b7e7200f43dc25ae58855879e2b2c84f5be2a151f3979cef8e202897e8f4a8a5fe1b8a6245397880714687079e65a3e5abdce110f1efb7283f5133140499c11de0e9935210f7bc28e1761a4d697cabe336ba0b6d29ed21fef65349ad9d573ad1b436c3a61e7604e9a0659d95e2af6cb397400f7244bb85c477fc9a0b102c76bf9330327ed9dfd36c65ea3d9f836f1b1d8b2afbfe2130d30074c141f720c9865a396a041ec10af5d1736fdaf630ae1a9c86badf4e4827ed94e205fbd1fe9b9a8fa53b525730323ea45cd2629573303b6bd7c33393181985edaf032aa14960adbf867a7bdb0b15a7529fe59f99c814d0df86f44c5615b0ec5e4b784693169da12abf9b395ecb56094241727478cd8e4fd2fe2d3f2d578996c8f58087d983fbd318aa33579aa7b3b1ceac1ff2be1c0f97fde0eea06c3a08d4634a0a9ecbf1685e1c52014b2b2d418efbaf2e636c5531b8e2ace666dee29c5300d7e9a49116bfc7761204b3c1a5e9f8944ce052a288bf4e1eef63ec586517b360dc42a6d6e4f2814eb8954f544a413aa212d8bc91aea05281fc78985d5288bfd041f846bc43fada26dda50c94a5ad5ae70ca7e35b442660f4a6fdfdb50ac205a2d2b03ea59044ff7144b7226332293b156610a38d7fd40f83d1831885ee55fdf3868f2c6489144cc8973d97fb512898bcb566f0c4b5a04f12e66dbc667bc22f60ee32ae3c51e280393bac9071bdcd5b8ccd1e9eb34d2beb07908a87d936ca4d2067128fbbca1a4b9d3ecc8461eb08ed9efd9275dbb578b6340df6c46ef0d29e948adca6cfa4a3f0e68fe1d0f3c635137cedf747fb3f6b101166b714ab05290101a5a1ed7c12b6b4f2c54fab0900cc98a893b1facf85ff3497a46d22d56a1afbf270c13cb63a0d8021ea9fd3643634626fbb1b187c1877fd3939f89e26ba95fd02caae8a30f45a3a708e9bc1514840c9e133e28a71164192cb315f8814edcadb39e0c133942bcb7920e89a8ba297a583ce3e286ed51cc7aea3b6e48472cfc3db87a8f0483e9e66292c0b9b05bcb988543f4014b7cc922e7579a55d3f6e08a7813bda17a2ffd23f4cdfb05284a9034f912d539988647effff95375bfd539637569a97b2588394f3fb0cb531417d0596c0edf248eda7f185d6fab3203cff721304a020b844a93fddc987b674e49449484aebcef3ab0d4567faccb2ba412c928ecdb027ee353acd574e059471dee857d790e83b06a2ca7b25f4737964e60bf3dc6a60a022189636e383a8d2c23568309568a2b518c4e95bda9a72a10302003b791d5116b603914968d2047c78a8ff0c7263681791063c29db546233cf9105aeac3f7c3efb2a38054620038cf019022ed705a3285c6fe9a1f15b0885d6b343de7fef8a2f840c2560468be4bbae430fa75165c62574eb88e7b0b3f683cdfe6b1049e375f819b139dc69623b35d264bd75a68ca8223849163d9611c3b7855a4f8c89f2b85db49266342a187b822972279b38df9b6d5a281a2b414ad6698175fd51f7b7a009dc70319fbadef87cb970d5c2efee7c834c7fa52e88750db53824e896b051152a3e262eccf8d39eed50f41f76b129358f9332fbac3d624640b3220cc67c4423239bf53892d2777eb16717811c16c63a54a06c0a5b6690d9dbdb3ac94348ee734576634da9794a52f7552dcd7f48be11e8d6af32bd23c0e2957e0d4d0573cba7215d13a6b0b60c9ddfc0e99870dd31a5f06b5051b5caab8d6ce30c1cb94134343765458e6b3749f6b7184c6564ced4ebf4f5a7dfa906672306cb5423819a5be104b5031f6f4610a5ca15a5ecac713c36cc3b57216eacb7c4b64bf9cad9195f5477f3c5b935e44d73f48a557efd754767de722f4c08499b74dbb6dbefa4fd46da27671c68d934e8ce8e0d6fffeab8bebda3dd1ee4157db9a1de5994d1691cbf71429369c219c45a7084ddd2335b41a288f81767e93d33f716aa8741ef71ef55abb53e3a8d393a32ee0edf4e58de2a786daa2264c7881f75eb8b9a91590052ef224e1c8541fcf9b77ba8f88a16234cac97756e4be58487f94827a1ac436b90c645faa8399bb3d715caee996c7a4bfa2b850459cd748dcfa451b96fed0fdb0358fa6ff925aefbc62c037ee2764b65c8a885628ee0f710e45838b97d6124c2dc71684977b56664359344b01c868af85daabcec8e3d71a925e3e6da79e2b3ab19e0c526244398e35746daf8ce10917b1231cccb4e0bedd4b814a4a3abb7ad827cab3d8df1ea6ab58c0f103d5da1888afd8f307134c1a8830644f42dba05e44c71d071bbf72453c33e5a9795fb4091cf32ed8124a7be431f1341d6973db699905ef65c0bf81713043ce7c2fd4269f2abd2c73c5a7b6fd62ef8f052a302ea1ad53470d3df336f99b3a885ffe83597fb889a6f70d8ad8a7fc1064978aa9f30aad9d072fccc173e609ba6e0aeb417d4a4ddea493b8828c1275a34701f40383f5d7bc4566bc46aedb54c8e3c8cf5a7e5d20780fcec2c8cba98fc871a41ad1b41e5b824347519dba98b9be544c8be64e4bb90916285ba5c2295ddc1dc8aeb3c2147e1584ecc9d320ca52725a2758f7c7e2753ce44e5c00c5c853c7fffcd7421713e8256d2457ca375a05a4ba1f148d174f8163341146c8b29cf708f39f9960bb0fc53fab18000b18cc990136155ff01a4d5bf2cd1fcf22e5b87cde9d8271542a1aa3c878ca300004c82a3701419a943575519fbaa02cf49b6328372595d77937cf85b286eca0cf494a8bdcf4dca363bcefbd76cdc239e91bd05052390f9a2f60119e7bcee90977f59df53ae999e6d3fc8cf329365ac0898dac8ed1337f107e236c00a95ea90411b6efa0f99a778ad8321dda8bb5ae7955819f745074c416216f2d18f35d7ae8d930a907145cb0e2b7909bdc5789b268935afdd089227b6b214465350423a14ea7d85cc1e025ffaaec445715b5bee8cc8fde7d025436751de002c8c941ffc539e17329020c37897b7f98cc6afafbe200e88b6064fcb4631bef4c081b18a32aafec766f5c40d66720de5d16e619dd7f6bd36acd2d726b47784cd37ca0d6afc697007cef9efeb85613fc92f53fde44b9651f78d546da46292ecdc49b969569c823328a4133c3f03e1b639d79b1fd0da1e01e5d43c9978a240f703e2f88edd6f384095c6ec71de37d057c4047ad46674ae1d0cd6ac502fea27b15ac3292f999633a02628e04f82b17520df51e1871a029482494c53a9e95ad38cd08b1016ff284c86f2d1f666b1bec8b52d0ba67f89a70ec80f1b425b2fc38dfd462db5377c9e5ae794b84ac840e56f885293cbb55e11f27663668174c603b3b9fa417e257627e62d9968a05835f424c44d287bfbc6c95ab3d409d69fd575060ddc95e4ebbd6d27bc62427503fd4e4ba8fd0bb0f3b4bcaf972783595f895fc12cb5ef3f9ba510481691b6eeb4bdceb3b13f7bab2c90d9c429aa2e4b2803e49487fe4770303d3aebfc8feafa7822afd1729b727f3bcae5ad7dc7ba91876c825292315d2b25f03f5db242a400a80d0917956210bd4c24bbc480ceb41a9f63e199850b3be3565af3a9ffd245a8eb04a9de3f7113e41e26dbbeecc9f5dc3275ec118e8ea9cc5d7e51e73616a89d27100626b22aa065736f5a6f832e65d1d877d7a8c857b835e41933bac9ba42116577774fb04db0f22a8ef00f875bc14cbd921dd31bd5f380335bfbeaad25e1e298f4c23707715f17abb3cce2c318bbc84bb2b14d82eee02cbae9108d9d6f494366d8ebf2315dfbe50ff2f080e87aa86dd663c8560dc6614b390d51c38ed430e1a1990a0a86c52e1cf12f0a50f3e9911e1e704191d47aa861cea94dd2be83776f208afb1ba05f63a3f019539c8a4a9354cc3f6368a550baa9ec25c527c54b1794ba0f45e5c39f2bf5b9236d9fe1ad0774275f542b1cd35ff4cb7d8ca01796072fa7fdc0c1ac7b19e604c4e364a599d6a23253506ae437b4667c74266b360b5cf55b91704de2ac628f618c90f3df367f77471315dc950e45106a248ce434b0e4540d49bb07662776ea1ed5df6c26452cf67bbf360a02516c749b3052a41fb0931e17cd34dc80f2b8951f18e7c2293a73626212cc39d18e62723ab95b195eeebcd1ee512ac80a86289522b2d9380d92b017127335d693b55e5aecb7f70bf0afafba30a0d758ad5615eea3060327fac645f33d0529578503b462f1333d5e0f43c47ae3ac2c40acde57d0dbbb9726481e5e2df33e6ccb4db0302edeb1fe8bec44a78c12c1d017c4be2629a42a1df0421d3944634d9be089c7846fd580de410c1880d0b130f21d435efafac526a8f655fe05162ea7e94db8e3696ceace5ebbc3ce80899317f55d9235cd6bd22590fb77d493cd0ee43b631ae89c8e1689790a5292d6e2fd7b7ea25834133edb1171ed3606d028fa9c6889ae8d59df1a39250a1ceb64bbb12d1702ad08fce413be3ff445d38c85251db75506adbb98733c6ef0ffe92cf74c7f6d3d87d5e54cf4401c5e4a265344f27d8b032a64f18baa11b4668b156772b1f627b2c2eab0c996c35b1df7b3ae0fa70b920fa13b8abaa63f54dcb9d72e3723bd5b83488877d9f8c328a8ee3c6a308d5ba89d9aa098b4b02e8ddc5b4779e928c9f45395fd5cc3853e3244595f8b53d20db7864ca20c0321eb09c52c2b02dab6ab628d321e4cbafb47daeb3cbb8b966804c954a1568db521b4d83ad8ac1fc538ee67af147aa5dd32e382e12a245e4a1fed1e20af202b9b30e0e4afda900a8c503a2a6cb6d6ca5fa4dc470d941e7f9c887baa369470ab84fb6d13bcde663c75784df729ac36f1c6eec39415aabd3853300c1ac1a7b13676a16bdde17cd0298a91cd2bbb0a502996d3f0d021e426cce639d9e9020f828ca0854521738459bd9e375c18e69202e0b2b9553a1fc091f14f1f11a7a206a06fc2131a8fc9539421ec5ebacbbaefd85f7eee22387298f0c73fd906ac92966344fcf2267858cbad80dbeaa54d948d8cd992c0f2852a58f6aa12b282d6b4a2a7334f596c16800abf1b95ff4a6671b2dfd98eb92af7382365613d4e11ad35116b7276806e818dc86e2aa8a931c645e761476ebad0bc9ab226f6c633c80fd6b5672ae4614df4a541e6ca4475a01e8b3a5fe19ad88806c1929eb788acb2dbbabdb65d368e7193e40a380b3c2490677eb1c78be97aa0416af8e7a30a782feb9c57c8f8a3113472f8a08ca5dc39c219ab56824a717cfaa2e5119ef44a62118501b80c7344138d25db1e80014d2a1d66cbc52f5cd7a82de93034a5e8e9a17130cfc95b51a0f27626de0d8d9621b72a27d18ed7f70ca1d0a4e526906e7c38cd0e959469da248c4b314ab41ddeeb4247a4acf9c716de1e5ee220645ee5561f5af8fba63bc23d0bf58c79721d72746bd1e109c4e0e1f70922159be248cfb8b2b386b4d1bd16bb4a8a330762485fbe364b519189e43ac2dc56ec3c62f91ae1ff38ff38b0ee009cb20813310e74bc024012d5013ece11044f4a18758d89560a2a6f95c7a74298870de1281ba0ce7253048ef19d74aa7c9436c3e3fcdec6f305b697d724d24c410e3acee4b6bd5ba35dfc02cefd996502825070937ba676a7e7cc9167261e7bba65bd885812a9273bfa40d925bbda7b92ed68b17212e670d14211102507a715fa5206fa02bdd82f66a91a6fbeae5e03dc93f5e099bd1cbbf231ac60f1d0f621ac092a0fd045062ee0e0bcfb6ce28cc2df1146f6d040b1b8ff34e9d3a09d101284a84530bbd3dbf35bee7f03af19fde394281998d19630843a03451ff639add8f0bfeb4c860c8374bedeaca50ea9a03c994acb14c564a68f1f81eb8a26dd12c52cc41e03c2bddd6ffb516d243fa4b80be6c2a76581209cc2674d813f806e28937b9052f3afe670104bfbb2a678851118c93f8ab0c269e0148e7c16d75d759230a98cde802591b5fc2f483ef953b5c228ccbcc097669d3bf50fdee95f94d2b60bc967357d8ec16177896f0f30d3688fc62dcad702703c52a9809918d3927777fb9f67d1b83582bcc67b084af25080458e5316fc0fd8e5035403e201dc0b7c9656248e2b8ce6294373aed36291036c9a7871c6af1414ae9c4ab0b7be060b6dd3e71941859a33d54a768a0e963a44b98178638df200ff45fd08dda77a141c4be78bf831c072ba5ccb6a22905e677f24e1b4d8149f6bfe609cf7ea2b13278bdbae1fd2bb746f69b4d92077c7c4f1a7e4dd957209a0904439f06a0abeaf0f4d4c7575fa528795e37358dd56ba258c3bb586403107f0c4f0b2d8c638c69043fd6ea45e425737c80af98bb20dae12e0390d06963dd51ab84b9901a692853078d153a37c10374657de40ea9bb0f7876df1d77bfb491b3f492c17974e7668765419f6f615a0e41cf1c0f1c162307bd7550470852c31ca93afd9a8bfdfa27636933bd0e23b96e7b84571cc648762371fc23d1e334da303a4fadc53bace6b5f5d3188b3b90c975a4b4e183d2d15d89e23f2fb0bce8d3511852e46eaa34db82bca4d0f8cfca9f3149121bee0a923457e564ac4a0ae31f2b37961e13e91f7d0623e4017735a643fc3155174f3e098d9300530235b12d2256cd670ce34c553766f324869c180d04c49d0c3a4245e4a1d4a3824921cc3f3a40ea46e58751bc2bc33bb60b95e26d005f73d11c57cadafd29ca243268cad5704716614a8d81221200dbb24fcefed82cceb60ad0d1f434f6bc5d582d0a293b61ef7566187c82894229354ac4acf83ec6aa070c8e839605e3b327e0529e53d828f29a400745926f910c1cc0c377c21a5a8473d69ba8dd334033ba86160b3b57ad2e20f6fe437acaa5702d15bfe62383be8a92d1717fa2b19a83fbb28dfd99c1317953c2db84d6a8e74d3e1ebefa89884e482e42849983bc3c16bd80830e27bd8da85f6e0781e36a65856ec72464652b2b876d526c90b7fde5b063328144d81e71352369e52b7219b9f2a48d01be2b9bbcf876ef51080598ebbdc40d7470ccf7f80abbb3e772c437b5220d50327e181b2ad97d40de12a5cf811c3b34f3fa7af5488c03d845332591213996d5ee5907f4cbf558ce13166bc5cbc3d3ed0a1d5a702d49fb6e0bbe8286fdb289960bd2f49287cdc6a35eaf625892f78c32bac107eab0e27937d96a878b160ab39d4a64ba5f63520acbf55d40d7af3eb9aaef4a69c61a20b48349439252e5829f3f0159ac3a247fbe7610c4275b5f0b9631d5ea5af0d9027a0366415137501813a81604d94795162ae74f031258d83172a2cf94a57e239d296eae15cef7e5117a7e704a4ae9da752c64e89292e12433fd4501d82ca166c2d36765dce5bb9d73777852398cf9f393c517a51804e3b39b1cbb399e98ae8c3d7c67f35e1d275b4e60bfcadfd877bdf5ad3cdece06d8bbbadaf18ad98ff85625fd753bab8393c2839892f8883b2a0c7617af6d04bf29167cfa20580eb55e175e4e350ac277e467929c4a9fd68d1ee50df113116e3c6c1479c623d50aaaf31860454bfeac0726f92e665dca15c4520c3929aa89c8e38e6da88e8f8ab5e3b6292453db63af3a07e5ce5df36cf438f79a3f75451b7089de176cf70f64c357494d885ddecff734db541060cb9d3dbbac5ce6d4607170b14cf037c0198fbb882e07950c3b4821e2616d855b46ea9dfbea1c11917c9dee4a4f314d42cb058a125fb9ba90db899d58bbae6540ae6e0f08acf6a12fd9b468cad52d337b969d6c214eb4af8f48e23b96ffdba3370f98b08e2240074de658d8c1e5464de47e2b20ba9b39977402aeab6c2548360076cdb8bdccbfb95ac21f8f4e3fcda147720e8becec2938ea9d77abf1c321f5184f7d477a7d4d5de851766f005c5782d068cb06a86a2c4778ac77046015a5c66e8e755c2c68cff941ed05f5dc60b41215996f5e58ebfc7775f24fe17ec3bb39fd45960a0826abffd32c446de2b5d25f81b7d6bafb9b4ddf5341a398c9c1f13d65125ed95984233cb7b969fef0f0e48657dad0a2725a84307cab1352deb574e7c3f70982b178d9bf23419e5c024b681b87f7c65ef839ffd01ebff274039f764e6f927e1bf4435065d23d6fbad30522ced613f3ea071c1fe15694b738d123f8557819e9889a78e83a99bc357dfeabe0e2b44bf7732ba93ce0fa12b041be7a15cf2be21fc687e882f8cc6ff1e00a12b9e7e737b434e8e81bb41d886098128f61639bdfd27b1fc703b287ccd6cf67ad1817970a98c963ec857bab86a8268f583fe3109fe5f2b94678a070c0fee912375edab755e57146d8be450435b8929598f2400a542fb2e277a3a354c6c27778593d8431d38ba7a85b9cae6af00be8db87549b26264c49e9f6c462dca3bb9490c072c4722f8355f346833865de614b423e9982475f086648c97c1855b1b7d59fe18b5bc79edab512db87135861b603e66860f2c532d2807db0c51c90cf3c0e1ab68ef22cd6108a4aa782271e3e33868168b2c84664edc508de42a72e531d873d29c9ea5a3b0311873e781108cb3b2a366179ffd7f7fde39915df0356022cc97499b55b5a4b671527abf1268e8afd144f1ac8861c200903c01fb5d0e3301c4e9800a38f1f0c0d2d7aef16579e077fb222a6d1d87a060f7f796cb34dde39eb285b7a1f206e03f4404639be29b4e935b24d4bca42963323aeda4af4a735791b470edd72b33c899b7d176e1440dd3eec9840efde4d9916b2935f3f3b14f4763067ad233698e23dad81b82458396aaf8900e87f80cb259a2452c12c79e4c52ce7b31130b7b24075aeaa2b5a7523559bf7bfcd1505bdf6ccab8fd9274fc5bc9ef866ffb261cd29d06381cb0f0049b360979b252f90583ff591b90bcdfa2dbbb9c6f69a50b7ac6eebe4d1095bab079520d5813401558cc7b152b553cd421dd0ce88086b35d8b4e09947769b6617787a72e616fa4847f46d552487aa6e1166be9a07c0ad47390db5e2aae4e0e90b2ff4d6df69a6ebd200eea5dc20202bbcf05b257100a3a4f81d423b6175daf5801310f3b525663f387a9d33a7c96cd81c881b17a2ae84955a82b5269b22ee2632228d7984d70aef82c53045c0083a9b0e679ea7404864b58487580bd8f333ad9798cabce8b7135b71e7ea3242cb4bb4ea8d1ae7e7cd69293aad2e453159e89b16e6dce94e12786cd2ac2dff3542fa8d2d946645831639efe53635c7ef0e5e2eaa33d08ff0100b47ad0d458671b43b972cdd7822b096f28282750c4d5cd49cb45f9cc8e18d40824f49e613f5dbd358fd7ad9540f4229c493a0724262f8aef8c14fa5ae55ed40433c89d92e6698aebe59c86020427363820ee128c2c483a1d65cf4ec5d060d66113bd6f4f0a614861b0c1bb083316e3db2ef85341a6e722a018cfc60c71867205fc68b0e1b5aba84626799dcc7f53eeb81264fcfafae751a3a782bbc7b684da5dda2ac7b2b11cc976cf49f74036ad3b1445ceeb5119efeee80976153e26328d0eb062d17daa09169d96a9998b6e9f8c5e395c5bb49769b8dab181633b65a399e8cc8b8c0bdd1dbd6f4c56849f54528387733a86ea14eafda3a9ba590ce90425e3316e97a3a68286b729ce5eb6aeaf06b2bf24cbad28dbbe60d448b254d86c3704677d0bad65b053008450ae97445dc7380c241d2deaf5dc3f5157c875340b5dacf7838c794e16da05cb38531e8d963e60e55af21a5f7d9a8b937a4ac7bf4f826f9830e67dd6b11c6ace6669a50ae71c6bbfc62b859256592a232a38046b89908d69edbaacbffe21f792175fc0babd476858f67c19b4a148de36a66314fc63750baa80d4ddc0da1066c58ba96ed53f92602deba3cb51a20e1bb1995f774a5c78976ccc50e9a480902bde436b45a8e5e09bc80cb75ac6c76f0b6bda928ae118dd9b0478a82f210cf234a924498ff4efb6099fb9bc1452f1f41aa90935aadec8eee861ee33f5fd83c6faa7af9324755e0f6e5d7a4fbab36e1f07b5687b39d7a01beb328fc913683866239fc33886bc05c96c6daac1bf329c341617f1840c8ced4edb1b40d8bd0fa014faa785157a582a92e0ceeeb10c8d742fc2e26e2a67ced59d5c2ee48a2c16c94f4090eddcb5a163cb8f73327099949febe6959f7002279728a01243a99e50a287828928e38c284d9a8aaf49bd34099162ca5adf3b31648563ff96673bdfa5ec99088416ff7b7b695a8ff33845d4f7021f560bb46ea73327ad6f7665dafcbc7f232158bf846c16cc7ab5836af8e2a84d2185b221168027af457694138bc6a6ad26144ea1b8ab0d4c37933a8bf0341a812eee894fa88d1454e306fc1be02377a4c9ccb238b2f47975e82a41a9bf80fe276e34fc3f54d2f59fe7a021552b87b1b07c4020b22dc971cec24eb84c32b9cb9adbc9838761d0da3e43a09328db7d1eeb891e33031f4f8cc06ff5d64baaaf13664c3983632579a82e5635ef5362d35009a23b84de8e38cb1aa84b2316c7c15dc8b0f893a0b06731945f6b24e80a3554ae20af4bc78b8c74fddbcf559a7b8011498733ac692a111f96eb5477127b472fcf46299d79251b7c10e14d6121e0e972d88869271a49b1320c4843be7e6bd575f8dee085fa2f2e8e5077729115a46901a0dfd398c60a0363d6bcee0e30a92d4698d126f273c407439ba719688177b44c73b9bcb79e095620a33bd387d455c8a8f6ae2de1f260359ae4283b72e9ccad1238a5bedc66062211e1bd0034f745e271969ebe96d36866e18fdb7c6232ae846832b811c821276091ad08e680cea063599ffa53e71fc591f1281262def91190af2d906db6d60ebfe031c00cd6b667c3b03fc29f51a6439ca7bffb9ceee827b464ecbb03745fac34349edfbcc278bf2ec8519b23b503a83aecb336cc673ae724606388278a3dafdbb83ffcbf5204734f0a61c010ce013a34ab088182ac7a93fa62a97586ae077472c4c3d56783ab20bb7bff83e532df48e24b5abe78dc3d291fedd55991bad05edd46d6f0778ee2f69e67717f43993cb58289eeed8bf5e17f1958f80b52054b5af3a32efa949c375fe1147ca560322393b42f753576be7e2a5ae2267ccbb7243e9668a6849476fa380dbec80389d907f3a70990e305bb8890839649a864a2695ca4a3179c913d82d05c68828b0b994bf54fc29ce1c47b3e99b9706533ea9c42fa8cb4167726fa21181c09f549c44e474a2d366b892bb4bdb860ce2a33788083049d936696b49c82aa82b95a1f576e2174cf6736684a471e47bf5afc84bfc56affdd9933c554510eb4a6df41b8798c6d596029e4cffeb2b350509d505858548bf718ab23383a51200fc75ffe7dec970fe7b0e7b9b68496241653d0c89adcb386a3f474a24d3e33f021348b620b845d2c8ad5f903a19dd96feaf71523b46dbff302144399a5aa1be24c362edd925280c2425204e9513fb4b2a293dc7d8e64fc25c38d23194299fe83be882c87b91cb06d0ea148ed36eb960b18d3f39fdf9ab89b4c32c53944f512c9d354c5d8895bcddffa427a7d0f32d00a8147ddc9d84c2e54e5735e577a0d1ae5ec7acfd09747a72c90930732afc13d6fb84df15ffbf63ab63e2a7ae098be8372ce73089ab2513b9f2ed2f99439295f1ba24bea9cc6099546335c1938213188a06042ba2c116669debaef68d87ee79eca89982971cd24caa30b914bc69ddf88d4e929ddfb56bfb469a5068a4f7bff270619d8e9d1a4651a5f0acc17f581a21aeb1dc816862dec208c0d79dd22066dd27626c3910b9d7480b4654d221103a2b890c1902dc680965a768b4efd982e7c37464597f145ee29c2fa52a4ad5d0dc630bc19697fe11f50b17d01a66146f14cdb8a2c24aee36b8adc131feaf748e4641c31f1139989fd831b49c9ed930ad6714b6b55f9c9556622b1f0ad859527636b6b6480f74743960854b0dbc9ab4531038a1d9d62c286d4a8cae13e805e4c5ead758077ed25d44a8c89e29e98793641da9a224c0a0bb7091bbc8f69135c0e24d64e33969fa927e7e9e73c831cf7942e83628a0bd077ef994d97c3c0614cc7ab287b8de2131823d48e6f3607b424dd4943f8d57c5057db872266d26268aa10a5746e3552f2db260ec18f3324a8ef942276ea6d374e273342c3999e6bff5efd5519a6c4efbb2ae6ce14628f75455b3882f0456167d3871f964a91c5d6d8131ac3b8611148c22882826ec664fedc05a0f326f596b38a3eac04471f717b67f619e1e46e18aa23e5062d3ca4a4605637ba6af0ffc42a3c9030d669216511d94dfede96420fbde3c3bfb6d87ae0a5320c57a15b8f8023b71aba2bdea552cd36879d5a3f1020f709c6794fe90e9542c43b2b64dc8daeac4f99e277485e8be21c13e045731227f0dd67571370b1e314fd2b5a076da0f283f11b4a6cbd4cbb493b68552744f9e9d5aea08f75537d38c3a80e8f7730b5298eff75140d1498b7bbf1c0eb72af59fb4837fb23c8050e49a33558bb0a22a364163a8604750f8bf268120ff998b2704f80ec94ce0f02ae6ebb5f8a9e0987c203bfedaec8b592330ef9f2ada380e61c08bafc37691e5267c4409f43889e50e2715102288c97a394c855dc5838c66690133ce9c98089df06b29ba572015cc378d77500cda567d6971a515e9d01b34c5ccefa1f1f60441db8e7b4df2e220f1b2fe92625ea135feba904319e06b3528b2a60997b0d050ed9d3619bacdcb3d28ec00d94d8d98b09e68e6ad4ccccd0d1577b3eca08bfea469067722d6d74e899d97f8ea65f30531eb53d9c422bc7b201db7ec810b7747466798736480d6fca807b2dc9b48a41b13ee1d17241391f84566f706311c22e89103160826ee5c9ed291acdb56a6a9bc3d14bb83c2ecf64b81b40e638a0d29db4597ed9021fe603df9a696d352115c8a6ad16592051d0531eca9b9d9908c4ef12a8d395686a7dacc1b6d0ffb34146f8d24397f1a6434d255e01976e5296efa969f6c11a91ec0b4f59fc28c86f8e4907d013c2b63663eac11bbb6d63671ebf530736ec6cb93b5a2dcec397c9148792367f1bbe3373af86dc50df6f996aaa2170e1cc378164f2a150bec74269193a315f474e3e3f2f2ef0c58210162ca93b1ae300fb62e3d741fcff3c26eaa0ecc748d59fde7096f175fadf422b1ba0085164ec63969f25fdbd60ddfd6e63d620e5ed4bede8d47118c23a77752af856941f69ea74f9b18cd4146462da2bf390cc6780affc2fab93e4faba560df69e5bd19af7bd11e9f9489b93e8f939c663a3349606fe2865f3b274e903e4b72835c8c34de681ac733e4df5f86daffd1011b76e06a9345f39d910fdd99aa4dac1bf8f99938fd74ed387a4ff87fe9ace6ea6b63cbcfe4abb6470cafb3182b3b1766c6e22895eea852a9445f13df856bb9cea400f8abdbc6218beb80567814e6825fb3b61cd0fa6be5d36e23895bc60690bd6df7f0258022e64b70479af80598f3610c8a59390c4df6ec0b42431fb1d253f54178d209a3235cf2970dd4b8e032e685c2c2d30869b27d09017de0ee87253ab4f0d2ffbebbd6c916aa1c0f03759780328661f3f0d4d2382c3bd573d7c16c4c7c3a3e22d9e3fd6f51ecab0594b3135ef09ac84521b26c3e545932ae0c23cc1f5619b3bcfc6966a78a759187ac214eac5853a0baaf655c1dd34cd3a6e0af8b8401acb0fcf8bd6ac16ecb66073f215222d2d0a534010a245655b4f4cc52b038e21944da6c3337c696fe6e219ffbeaa50b5f384156359ae7204adb511f270314774f1fff1ba9666f5417c4a050f434e36b055f6376d7949219f9e55f50aece49e9ed0caff1e82049c55d0a6acdbf9d18acda80c3edac7d45dd9fb3ddffc4a43ec8a36b582eeffdc709f397e29c7da997f9ad132f6a8964710e2eb873d65aee63f760e138a47c9ef6c1656e289b2402bd2fa41cc401c6edda9cd896080f8e480c92b4e8247c27b2dfe103d80d3ef2709664ec4f7d26187b4afab2ce414f4ee21a89158684bfa894e6db15d4b772e092a6cd858a78814e2ce3fe4b6872a9785bf5da07d000aab8d87f3f2196279d50d33ee7af40641081ff767a6cc563b4f0d39b532da5a829364b216f273a63bc7851a083531bdb6b78c3df3b27115e1c8e2e5ab36137c9230096f557d81a0a1fa373df90a3f20e6733e51d14503a3d37feb75f6eb9670fed7ad24fc6e21abc5b454a7905ad8377a25609e10deb78565a97ce1dcafd0026140389c5a73b6a2891bb9691491769f82a20620e7dc2864f26c57d240f67a9523c3d7512a102218120b89375a95d41d2dd8d7069d7871a36e7996b76069d53c5f78f5d133ecefba704ad3b34e6010ebf880cd62171d6fb8a36a0476d52991e6bee6a3ba04296e936caf145879c567fc1e555090753cdbfefb00decb788676cd30fab7b2bf19ab33267f10dcf6fcfc17b76c6e51e7281a8de37ec3502e33996e1a10bd271be8103e10fc1c45b0b68c436133959bae479cfaffc300883863b58476d8413e5346588e2ecaff6fdca590e248df560cc346507ae26ebd2ff7932d1ed12ea65ae1586cc4d37e6696010fa1dc20548a2791881b809404c87b9b89f983aff1377207f1c12601a2222d27a6d83790199d639d186f6862483e00f92f8534da0aa757cf13e2a67a443d23e411a4f499ce1c2f7224c29befa3bc374775ba616f1fec8ebb8a20e9f3b626b6315d195ff8552aaca0031197d1036c2edf2c7cb9368609ef7583b29b6cd086a3cff8311c8f8191d68f274a3ab95f33ae8491fd419520ec25394698ae48fe4e2b53721a0faf70ddd207f41cd7861ef663c84e6e6e311a3d96fa744e29c66d3424c831d10c34d2b015105c48830db8855489c3a03d52d748183b52ed6b44dc75125f06da53897574ae9fec598eaf757ffca7c0e99995e449d8bf2cb7ac8625bd57bc4772462347e0cc2fcf843c2637c8a37bcaf4b4d6836153c4612377282ddfac97e98ef9b1b36288b174e78f0bcb8a3eb17677cc357b3502e356f66c7cf71a800b84a34d6cadc15d24e8763d76908091121d0d818b8295811a7decaa97672f94e4d0d09b8cd58e4727e95be8728c727fd14144a35041a0072572c33b1cec2806584b3ca2fefca517da0a2fb59a9198335667728f3c4ad17b366b0c406a19490c305bb0c0b52baaced30322ae63bfdcb6d16af41f8fc2ed009159a0926f4d4e78c97b304a2d6a76952dca75fe8e9df9402231ab05fc5525903af1f9d424623ed07d350110f74922f8aed62d939463194c88e64cec41298a7974246ff011b0d1e21eac6d3bfd57291ff12da200bf9ff446e09e0e09c23188253f90fe8d69e3f54d11ee8260d3f2f044ecdc82321e793dd5f84e906c26d10e450b788f5f371a0d6d0ccae2b3bc50ea6e9f7dfdaaaca61fb7ecb1222b982d5d1bb2a201706d44fc35e612e9853b1593747567f1e063b8ba1b17cbce33bc36ad4aa9e471acfb3cdcd4d94b284ba006a470b8a3c013aa745c780e0fb70ee2efd9736699a09c325a7eaa03cf21d03194b943db8f1b8405b6c443a53cfcf0776634449af0f9634b860222f9bb201da1789820dc0a1f886f689daf914f0201f898aae57f50b6a69c0e3861f6ce8406f0d4cd4d1f7f3bca7d5bfab0ee29c96eb156fde8c1eea37e97755905d28ff0b07e183814474e49b8810561c731399237662ba2d0c4ada24295c32696f372c88c623cef47c4e4df0727bbcd8bf7cc947037513edf1489a190e1e2475bdc6ad4c9dafd4ea102dde5543d86f21fd989a96c97d2b35d3fae28ec23cf796d102e1173d5541242fff5b089e0489f1dbcfda3cd83a275496e7f59ea3ab6eaf8c667f70fa3a38580c0113b1c8de6754693c73606073d5e5c36a907a8dc7fcb428f4d9df5b6c8b1ecc6b3829f498b7b2ae73cea3f019434c3102b7068dcaa5fea54d38c78f7a1b925f6d71c8d85e6a88a4a9bbf3b1a7abd447a31ac41fc823ac3394918973aff3a1f94694bf2440c94cc7608a456f250525795b395a04b76f1b3348240cb4206849eb5ff79c745cadc6208c1976937c5a791ac289f88ea898e8c31b46375f4f0a0cbec4f2ea71df3b330ae5a626c73c86f045112c609b008ba4fb6f4665c5949358be602e6375dd41e3437849ced1e2f5ed1b249f18cf713fad5b4dc2b4ac34cfbce9bc7011c8920a53f6d2b3af9a2a6b56f91a6b1ac743be267cf8cda89637746f0cf1fcf88aafeb621ad9f6d1e22eaf6167557ad80b48622261920f60c69b89044c85f92cd063e8352048d4c9d4369a27b1179ea98305196dfeb64105f973b68956896976956d7d954f8b19839531652b0244316214bcdcc6435fa273875cfb943b2e2818fa947f4ae80959b65a0a166b89b3e09a2dcbda58ca33a3d5d1ffe4a1eb1df16611bbf478866c08734f564a9e162362fab2201db3c94a32e81dddbb72c239579c72e3db5fd7208a82ce484da6f76c1033fe493b30d32c137e2e7b2bda67be72cb74391098c5f745a8818f7ab2f085db25acc18d39d4ec99401325255511530364df9910f120a756f4c6b30b3c31faa5da48ce9e737b95a12b85a5f48c4a3782b23d0f38443792adc2becbded6925ae91458aee99d3ac168f5d233942d769376f84312b99dc9571c0cc7c0d8995130ab10ad65fbcd599b8a799431f1f8860ed7b176195ad1437b051f02bc620b4aad6d98301a25a4a1a4a76e3c34c78b01c00ceb1a46d46d6b0ee418257bfab7fed9b9276f3ac9c18063a7eb29533a40bf4673f875411a31fe041c6fe5f2358952fbdc6c60802485f4cc1d0fa204c09cb73991fc9d583732f27c240a3db10c2ead4d794a5fbaa7c1ee90ed6ea2db80799956a711f10bae503ea8c89c5ab6f3d4437d8366c4d7d7e30e00e988189f858b204e295a80937e582bd69f9d0d371104ba78ec63f334e12c9d33a52b7bec9a2c2b3b468c6317ff50a409552c4fa9c0db92e550480912c2b450c564c9127819553bc41883a4a6da5ec09d7c257983a0391ebdac1fbf10e61b4af9c126cff49774bb0c260a81c8f5e1c028f86bcc16dd1dc1d67a864ee0e81991d8127e22baf3e4efefba6efafa3a53c076c243254e4bed099d326843d3a72305db511b7c673f585b74b62a3304c8f1ec9b515428860f1e799bbb38a3956dc083277f344c183bbfc5db1d58136d2108c8ef8daaf7d0d84632529b0e6f38c1f4ca53533217ffa184e1709b72dce5ac899415a34be371104f4a68b378c9a0aeb710f234c344ca06f8e3dfdaef888c4929d1671ed2b4356d05b46cde53e503a141424f8a1234accd0f5521499a9af255b13ae04e5bbee271712c659a00d9b11463515f43b896a6b27bd64c7c357969b07895b5155091b97c9bbd43050a67e3ecf4499c60f6d22eb8316f2c7c7363a6dd3dfa45ed383400e8558b4a9bb086dcaf680d9bb728383805b69a0859892e16654e94afd3c567ab0ac6d55708e9b0e995e16ec18024e403137988c4c9428c294bc0ada9ec8a853d11ebaf323d68d886aee26a9e9564aea863b6301e7216b26c54abe25ec42cfcb49f4f0a406d38e1f9b997368d3cc9825fee6490e498b81a47e50e41b60030a4927154ca6a22349c88ee0e8f75f199319c7e42d34849a533fe8a7106b1aeab25e47a696de3a49949647f96aacc34ff03d0030415eec7f845c6a861b3919db5f4e0a2a3dbaaea7732af1af9b5fe6e4c8a6d875559ac63ca11afc080dac2963b14d6d4bb72cc0a71f330ea02860c9c0b24a03ab0398909d0d19571e27ce8a4aded55a8aba7b0651803f140be6bbede6d375962c3218b1181a7f3d8a4aa814bea8be054ae6359809e4cf3e0f4de7f5d6feb3536cb3709ce6b0e3c6794f3bd1164cac81e2130e09d42754b90cc558d3a7fd415a38f6ea43e3e78cc9eaad7e53b3b17c180d71843707b08812b7cfd77ddded89ed371824cd2ef04e7e135a222ed194a51296797516b0faa39afb6d0cfe061ae9cd33523c38849239b0c2ca2d07184a93ce52fd49dbdc0695a069605858a96d30fd36c23671dcc776bb5546d03d8756d76a301d5482ed8127bf39b03f8ffbd4bd5891e2cb9f536c27e52f31dea2eb804abfaf4eb3c628990fd4cbf5c0e97dfa0db85b6857310781db2b3b450ef25a04a49bbcac9f3ad5a9ae8afb9079e47395151c6038e785d17d44e0ed5e36e5aa5e431dea61a3625795559ceade65927b939a56c780ed95abb31055928d45a1a7586090b8d5bb32451e753bbd33164d36d174d4610d4928c9b8a340c571b76c4fd5402c87d5ea92294b48f7c8cc329a2e70f4271ff6e6bcf63d35d3a91e5728ae1b2af7aee524ddbc87fc41d13b9fd236b56fbff61cc0d41c587923d20f913318a6c5c6c0d4eb2965b48e5636e95b5db7c20e3cfcc4824632a22fbd50cbda72aa661a04d81a7f03798f6303b8939beeb2ac25e8c0beca61b924c2cb501c8ab7156910205ef9597411826ba0dca0e79cb98d203557ce2f8676465b2043489d2222e194983500df0bb9214bbaaf4e06d3e53b1a70aae30374d9912b9603ab2ddfd5bf4cbb2f7364105b69841770e8342a44fcf5850a6590b11c03e190aa22dc7f20f25519fd2cd7df7c92033ea340c01c1f334e701567bddd927bd774dc03523c3c10abb2290de2942a452ecb83b8a99942fdebceee737e89eaadcf9986ea0deb12297420b7b06c49b117fba3727e74487a651cfba29651960fffd728794a0bbd4040eb18837bbbec15a83dc5a6222b6e5d5f11115ec4ba4035512c060e74908c56ebc25ad74dd25c188015974db3f28ee0e607db29c91b82e18502d8d48d971c64877e240ad401c53920ac4700000000003040000000000000fb66426d30534237bc4dd18dc582657ead640e2ca2ac6e1d82f05909683947ecab43e2ae0684fa74bbb547de4b82c677b437270d7e99536ac49bf083d4c997bd74caaf9b424a61134a5366d950c3cfc9a0186b3c83e81d599a5ba454e4d85a97613d0200000000000000000000000000000000000000000000000000000000009ded2fa14005a1f9ac6d628b64a31fe23142dfac71890d0f4e8b29d0da12ddf80e8d8d420e82151ecd560bf1454429ddb256fecfe7def0d60969bcfcf61917fe17fba3727e74487a651cfba29651960fffd728794a0bbd4040eb18837bbbec15a83dc5a6222b6e5d5f11115ec4ba4035512c060e74908c56ebc25ad74dd25c188015974db3f28ee0e607db29c91b82e18502d8d48d971c64877e240ad401c539ffffffffffffffffdfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8ce33564dfa183f877ac5d9433844e90f9e3e7e2a5497dc5214588ba4a19022e7aec3ebe601e7726bf8abe7c0ace3fb717980de06024d04bc3072aa5841b1d75966479419b1234321991a24237ae1d488101a47eae2b6c86a69715ec1adb56246dac470000000000" +const lightClientUpdateHex = + "6cac470000000000a2a6050000000000989604e65d66d380233f025b10f7b99b8bdf8610ae627ed61825266fbe1aa15a9e33aeeb92443af6d49f7b1b4f6c8f973be374b548db40b5f32f82db57cd29349a4fe9c882bde3ad2ac67b67795c1f89de57c6f1e361c60415375ad0d19605d0928540d3a8819e60842076668e9907f9d9664c8a8a5a94f658a0da1cdfdac6114a0781f13b5e402fa4283f7f4e2e973c8706beb542fc4c83c7de6b4a3530a8174859a98bfe96587ef166bbae213b268ec21ed0ad166f4e32ff820b37823e533ba3afafa8eea4a9408278602ce5830d474355c50dbe34162e53482aa093de4328f26a602aacc01134d3e3dcc55aed33c48a5442438ea51b9af7fde941360f5506ddfc77f55e341fdfdb41b41f39a6507186e6a3e9131f91f2e48098b4c0fa502caf3f28449cbe9e540ea50c70e1f899e77f365758565a2f344d90d558d9b8002a6b6cdf844269a9551ee0f6bde6f3135b917295a0d935c5031fa7e44dbb28cdd37a6c2cbd1a949c3f097deacbdd1ea3f29e117efd04989955eaea2e97ad717151b414d12d3a69ed7e083972c1dfd65681ef7ea6ab4f2b549c5032db6a24e07e8f3e52f656835ee7a4ea5ebc6fce106530aecd990d2730d3c207c3191c8cdcf6107052051924391546ab75ef796157635761c6bd1c53c61f1bd437a5b3dfd302178876951e388070b161655f2cf9a9bd11d96590b37315cb562c33aac5067d21d943d2c01d0e1f6773412e5474fa102fc7afcfad0448288767069998d41a123701208c11414adcbf37e0f6748f4a27eebe5bf65d155039bbd90b027a0e27aaf327895f1f6fa1cc47b23b08edb3f0b375768782422c85b45643f051da52e3e584fc4ff84aaf8dcceec257db7a0a516669a3b4664e87cf747da6738cea5163c3ebdabeb7b9292e1cd7e56933362502b8139cfa51fd254011acaf58d0782904db71dca1bcad4fe4630905aba558094175007610d574702ab4be6b9a743a031f850dcf3a2a6950d30b82c76ea37dbe7a949cc2897bda78b921684614fbe5f9d59d96b2d0b3e10d4b31ea58439e93ccee6d48cc1e8020e3da96456ca9626ba597943c2fb2d53179105881f8a170251f75d00c493054c770572aee3c8da2b73558ba6e3640e61210417d0c0bf9954d4e0a02fb15b7fb043a90b09539a73c67c1b9a4a33fb11987960547e4a7e08cbd1083d340ad9df29ff42d94c44f02301d1c41539adb93013a6a05c30d51e69642808985f82fc3983c2e4fbc823d8e657b9e316bc56116041d43f0b9d071501790163f6bb83b9223e1ad84213922d889102a230ef141300adc09c0244019f0a694a05d05eaa23e9d6cca506b65dd9529665c38629979a70f04def4a9eab96ae21ff84254c45070cff7430e8fdc3e4d5c2ee6258e8c79b63898185390151b295c2c9d9e2278c6b5c15ec364dbca1fc9d02ce5e60188286fd49e5d28212d0930aad3980451864fa8d4fbf036c50297ea652a5cde1c03d29793a300f7316de7a1a20572320484f55827cb24b4a2617fbcc9fab7fcf2ed34f1f7408d3fb8237a020495f10edff34c8054efe1e58eeec2cf197352a41a1fd84f873378675339974960a6bd4bc604dfe9eeb48c254d79b02c0f599f4015f18da1a566b47c275cf617bdb57dceb1f939fd75d77bdb56db0a19c9cb8ac162876dfba53d7b9f3b5a953c7d3a8b0013c152a2aa4070a8748f16c999e9591dd315ff0a3ded207c0986feba3fd518ebbab34f39d3e1a8c6e20c90528f5c84995c5ef8811ff38b7b4f726a33abd6b81d5e7df4abb0d1679d96b90416e675c5db1c0b33cf83343be00f99e3b6af34bc5975d67990fd1c0eb0c355d0d97c769e0fcdfbb7c443fdc0c4b339f2014618a390da2f4f0c3c12dfb1548ed1db6fc6a8dbcd6ec38d2e76b6119844b9af369940731baafa375490290e463020ab93115c9ef785693020a2ae99cd438902180c130f4f17008e92a88759128fdf186fd923649857903211b5d1f133b3bd30f94825257c3f26510ff784f1a99eaba01364d1b3357173a89629c60667415794271f26d237b16efb497fe0a76a8e4085c22ed8a5787e1ee403abc8987b138fffec05e12eddfc2c807de040c3ae4cc904468eefed8581c8bc07c4280d885be6fa3b226c36b75b4b6af8903f4d06b7fb7cbb04db3ba75ec290e1d2bebc0bb4e572923b1d7ab7fbda1da4239d2b4f3aad0e9df0aa05a713623f83680c58d88c8b980d483f4cfd0b02a5b74018d23f251ed4d56260309905d1f0cc8d2c66dd0e671dc7a8eff07a896eea22f31fa16b182fa2bde02acabb01f3b5ae10f796f43558580b2275dcc203ae38475ee872555d692202e429b5f9ea54aaefedeac8ed7f5c548e33de3d501b68a30ce7442d6f27dd376d380fff252f32bc7f47184c25dbfa5efccabc860082370f13c21149de4819fffc69291916b004ab9f36a79a9f73a625b761f62ae9522dedbfd46270098eb151c5f7a3451f8752804a23e30ac8c7fa86a597bc346b51e1919bb096a58fa5e4bfe19569244af3fb8c8d489c22d1f4880250d302c351d2a811cf11a88fd2c7a6948b9c3dfa474133ab62669f2859821dd9fa6b84b18b781fea0e5204ca05a49fea6eacdd61edff660e8d6e40e1a188ece121fd53942cd75ea111dbbd404d7f119dd76b0933cc8ebc39e7fb0bc067e45c18584c94c380757b45c74254d16a4d330bde257b69e25075a85124a6612ec04a3f0b938298fdb2e914851d2a914041963521755192dd89ec303454a7af020670cdbce2ce1f0b8269b968f439ce66962f3bd6a7c6b54d3cd02e279a313b395368af985f59b6640283f61cc49e40125f0edbfb4120a05583028261e2a873eee391b3b523387d8267196f17ddc77de45bd14659856cd6491fed63291ee821322e1d7bade7d3e96b795f8bb432f39f6b2866ebebe98d31d84f669f4ca9d12e87e99f84b9e146651abf57a43829951db1987d25b24080b084ed6a87d0ce08dc53fb6af4dd53b321164dafe742de9ffdb7403ccacc822e7daaf47891627b340e72858272c232ed2dce2aa2904a35567da0bd2fe78cb90c097ea4efac77648bd359347692a78ec1399306879eeb39035c668527242640baef1d5c37a61103ad663db69574a04265691cc9a97159f6c55f88001f117f6efbb309fb263739da6af3812ab13096d8dd588c6ddd8af230b1df8621f3830d54b24b487b16e583f803f90c457003a3eb60a4e534781393cbb20bf3e013616ca2e38ed5ae9d8216577c26296aa8fe3ae3acd60fcad00febb98494d87eb2b2aed98585bed8bf85aac89d1518e7efb8393f540da6865fb334d250b58546a008351e97ea32aca5796edaa9ab8af11b00cbd34d079be0ced0fb40243c0ca9de6cef725b30e64f49a4cbbc2a1fee815ddf28f093fac49de823515f04adeb007a94d0f1a1dd51345813ddce14ee2364409a02f2df581f10bc8f065afdb458af78a7fedfb51f7d4b465bc0e283505dfe3711cf2ef9d6c41b1b717953873909113deae867faf3b1472687c2d132517b0b0fd2cadc60a418e9dcbf23cab4df275e5f52a7d294e8811e226e84fe0bd1e54c1c62d04b61bb4520faac9e66fcecd23579fd53d995eb60a17c7693863b8d72eda62170ee85540f8554b606ab57dc99ef632aa284766df9d1b6a09f88f5be56b43bf7969b43637101e8b83a695a4ef72103033cdf035aef03c3ba5517a508dc1ab5f3a70dc03e1a80eca160400a6477856c6eb3c9a039837a4166d74f629b684ccd53c1851397765d53ce27a650c38bc765faa6b04b4a55bd22921d530f5185581cb644326cc7295a0b4232f6e5bf99dab39e65efd1ce686c83cfa6e7e282a92d3efb9ca5ccabfd8e48994a6f6457b302145febc135461acf3d0e8895ea17c8b1be2453c8fb93663524d43e0cfa2b82819721ee1c56b47ff2ad85945d551b5fc9594ed39c85c8dac75768c1b3644a1f7789e37b50cdbcff5f723abb777cc31de5c1d685cfceab1a1196a12e906febf46ffe6dd22482d544c78c8ec68cf6017b7774845ecce3f327f9f2b9c33441143609374e19f384ce6d299299faa6c772d54f92a3c0e65aa2fc5bb0e1141ef079951998bc95fd6d83a8f7aee6e69e16710cf545b78e97550fcc9428abb5be97f885bb4fa033f64c6523f705f27e7ec2fc122d10dc7b40f0feebf7dd4c9727ac4177b7b9a4f73ce5bd10d2b28257f80a8d2e0bee75805f2d7b6d6265d9e320e7c2c0f1f6b76b95210fb72855c6ce6cf1c6649280fe7b08d0a1c1ae3b902db2362fc6c793e943ebbb57e6aacd31bd51b97cc6a2748ab08ce337f5866cd48607a55f05da882ebc2f2e98702b96b3a69023a092af098a2ef13db0d9822aaebc5a99d4fd19fdc253fbeab76cddb0e85da6799ea5113223b5a9b151f1308d99d492a9a4e6e57853bd3ecc33956eb761bcc5ebce595bad093802d40f672eaa0222cd29eaec72561ef37ae01f60be77932d3280558fe0e21f119a6b02324037e0eae9060169d244402c65fc1c2e927f073622738b627ea442ffd95a5cf0127d88c25aaa8d5d13f3999c5d8b318e71a12f12cfeb4e6eecdf9b54c8544ab9dc4eec3b528be3065788e047e6f4839dbefb968129fd31294ae006db22ad249ea43ae0e8d9fd613856488cec3da153533c4b6b9965baf87e3533bdec0284a331bb38a1e557fbce279b279c383c84376ba869c2615dcc6ee6954317ef223d0d210abd6b75fc28bf1b409d5686e8c88c214d1a8cd4f31bde26a2bef4781f41d6e0a457a392e7a9ab8b94f0a5048d16d11acd7394a8ee26effca678252c963ea0dcf55cb879430d33461cd966bf588846752e06758c7da2357a9f61973d5b64ea27a2f8d086ae463896e056a56da23a06b9010db0fd4bc51171cf21e61caa91f0ef21132c19a008c0c611cbcd2526affb37384409808ecbf67fa5f32c9f44f7979abd9092b7ea8c804cb42db76c778714c1a01e16630c97b98e0f53e742c05d54be141872f8393ba9fc7110011202d8d98e580ea3bc4324f269d769a094e67ff8aec9de4faf1c7c225b972853f87774514accae12e6156f35ec5341fb97af4ab862bd8aae1c6f1b7bdca51d7d53ca9f06ea43559529b8db44e20019bfa4056f362fd5f731aa6e2323e4d098e6469085efad05c88c65b4a925b3602f8b5ca2ffd16bec00ac00e795b3ce9138c97608fe35293017217f85cda7d34f659e076bdfa23f2e189677ae78ec8412651dc9b90fc94108a778ab5ea6dc236c226ca68a1d171ce103afdaa2a1c3e7deeb0a889a5c431532b7b207a37204fc69b403caeccc042e85d24e7456fbb9713cda2062a6c2fff87825dc07bbdb18319eeaafe3663f9daca6f3aafdd2c59bcd26e56d528569add1d7dd0327b0df8389db1358dd354ca142ad60c0a9569b92a6d5947754d260e933060fb2ace0779262f97b00b1b0f498c021b2e18f87d7067cd37d3b69df3ed64f92b196346c40b2734a06e85b60f4ce41bdf9966d3d0a5bc2495919416777aa32994a4ef1eab7041048c6ccf2e0dcc608dd76a98a2aaef603c2c17b282756e8532571b28eba85720709f1aa782560d44d27efdcc50a98e222dcb10ab432551c3a0fd32cda7c4fa375346a59ca5187426f3626b546be5390476c6d262650cd324e8b6e85305694106343f3370fc90bf7b10900fb117dfcc2b9de55e41fdefbd21fc49c8e4d755a30bb5df072b2742d549d237aa4f8fd8291ad3e6201917cb8c937985132c706037fef55606ab1d1153a5d9dc78bc5f3aad6f9f332b849d31bde7a6d3c2c12c7536c9ad777b172216ce4a23dd6f199b505747abf9dcc443a925a719c808b0a9a24bfe3f1d83c29bb8e55103ade68bf94fb2a9aedc0efe7b1409cf495a61620d8eaccc2072c6157a076af29d1a9acc5dcac5e186f55f87c28d3174e685ef9009bef9ecee703294b256d1e0d1e210c64956acb8f6bce04c97312d34e516183578e3c13a506987037e74e7a36e4bcb85ef816ba8afea4ef5d2f8edb391d2a783aff39b04d416d6a76daa49dd62166a4c8d92c61b839a079e7d3913d87a8b56b0988ff7f5aa5620d5e857b7d579ddfbb021c89c7090b4c1974e6d123f4a4218a20226f01ddd898ca2bfecfd6fb4c5cf0a45cfdac02c947c6f52d0146a292004887adcb74266999cd775b94d66d04e2b7f99dff70e1cf427b9792bff64d3bfa7374df3481669e48d033db4142811aebf91760c2dab732c31103de918b8392038b0270f9dd1f173644a4a9180a2e4ccf6e2cdee877ac0c35776486be8a97de1f39b71fad4f4b0a5aaee2e84124a97a4396fc64e2f1bf2111e96cbde26ccd3e2849dc046c49d1df1d55ed515edf7fa01c9ce18263cd528154246b72977644473a95ebabbe62af81af7880477b52b726ebec0e68546bf56c06593ca1c3b82ec160c270442c2d1ff1be36654c1baf52a7b8b4ee9359937f72389995057aa7df2a0f51913564b7b6fb45f9074e9f654eef7a60402e0d6d8eb7b01aa45aa2942d78fa94a3e064458a23e9791fac0f4d96bdc67ed7cb21b1a05a13089976ff9ae27cf02781a4b374a79168630fbab8d057eb008a7c7829b5d3b7cb3f715444cfa13340a18c3ed4a6f2e2d10612ff655c9686f8e29b9b84d8baf0fd5ed76169b78e62efa86e2d105452518a7a9d53167f590ae79fc847fec6045b21727d667cdc9ef4c0fd30377e17bc33cbbf1ebdda9edaa247b7d99cb7702a1f63c45f64b045c2846df8ffb2501fb8c7bce80948f11bc5091bfec63073651cb939210275829651badca1a4b3d3e5e1b8979326c1ee6246c258967ab58803eae8f917832feb4f3e2a04565c3c776db1b497e976a561d56fbc738eec166937c501b9ad22075a513dc4f6fb533340d4006ab5498715b1bd5c2f2b5c0b159a1944afa758d9118734b3e026b316d60c8c627a7a58e09b6bdbfe9ee58b3df32f36266d3c905de360344cb3bd8bd4309f5dd24c7f9a0e38775a7185f6979fb9a3ab1d483b29cfaaa77f147c9d1163a739d07333a067cf34c1ca539e0e9334eff481041371575a0903eed6d8c6b3ac52ae9ba97c773c375cbf8dcdb362824d5e92f4d0ddbf6359e5baceee078c06d3794ea0d35e32d192411d80930cfc927fe1c72fa81567080f9b89007211f1590452f886993d393e1e373faa673df8b9f8d2483233ce279c0153f583b0410da57983dd9c74cea1f1ed89777e68dc3a6b899c92606c9a2da639829468c760ff15e53fc3071fb7cbf5c0a9881c34b68eb5ec47c7417f26cbfd0fbe65a722a3ab0a0b5903cd5132bc7d783f33c9dafffd1c4f6243a6c215f470ce918ed7ee2742a77cd2eebb52886a310dff99199ba90904ec6393a984871908dec2306a6c9aae99f89ae9523a1ec4c877638421fbb451a3d3ba514cbec20c2d829292b07d2550ab2efbaf6b08c890c278c1e5b02cd34794356488a7bd4ed935433fca3d51d96aafe7259562fa585ffaf8ba4c600004145819f9ff22374c0d575439447c4b8ccdcd5fd7b4cf80efabd7d42f77bbeb5f9ba77ef69af99e7f3be29c0d66cfcf69b141766975d9dc69ef4068a52ca452c268d1410f6ceabfcd176f03f493fe4136a384998e3a188332681c9f5a5e5e32eab8b860beeafebcfda55d11c4076d284ce281701e54e5b510e2d78366355d1e151fa00dfa78e6d5230ca2186493bb87616e616fdc329e8cbe09a8763c536cde893dbc60fdcd222e8d6c11fda7d2a0f9e48da76c64a0f9b4bdc5be52e0d41b52fb0dff33d30fadbd329270cda400cfcd4483af3eda227672276b149e25f8253db629995623cd3b1205d6525a875f33151e1071b9de9249f6b59227123c9897e4e8932719504faddb58624364d404c3373ef98f699f897a79757290734e95dfac9681f2aa29da96980d000ddd9b0efb14b232512a850afc1b162054196f853b02c02fa2920eff35bcd413acbb436c659aeccb17d72f9e395a97c2cd1e6ca6b0c5fdb56657e9c40c5461c6ab42e03e0d2abb648c406828da39d43577ce172d5603a4f3cef643a0cc7ae04b7af2f33877c58875225569bb038bdb261aeb98e9d138e82ca5fe514b8f12d746fe55b201503fd38f6f9016cb52e73df29d66ee98ef71c72258792837a55ffb14749079151e8bb67483ff2a047590fbf43135e73b454cc6b3e266d1b4e96d530785da6d6447fb9e09e70b7e9476cc3bde12d92ade883cde68ad5bd2558c67605e90df61106a4378420bfd4cbf101705080db73d840483867ec8007ab0ccb22362ed6a78e83a5a12f88e57b4916ca5877355be55ccc1b17d99677ba4a07acba89c7ae6f4405abfd1e4690d4fb9d8f17bef5dbd45bf4a75113dad0e392b5ec8550edb751db397079a952d854898b8309f0ee471575fde8434e7e30feada8a1faeeec478f1738fef6433a882491503890f7cc55d3a1a0777366b40c231f7b6ed68e4ab7443ca0d52705b1226b5ecc130ce4083c20ae91a799277a234ba55f0a9ce2735611e2a2c064addb20dbb628ecb096bc4f2eaab8ee5f85212c68ed50f362504f6e45b972f5b3a9bb592069d660c375012d691f36919db26e84ce9de048d93facbe766ae6824542d5c9359e1b1241ecf946f3ce58904e528af4550afe1646c734cf0631a5ad8a1a8dc418480865693766d008b439e6a84336efb88c362e7b5288bf7c03f2d5df260895f043ae9869b4a74f95b3024b03814227b6c79bb06ce9f1af453eac159b9154c3ef93e4b1b084e0c03ea3a65b38808932c549b49f3d94a746872de6e56988fba767128a516b04f87770bb7f3ab51e0f5bc71e38b9fb8ca3eadd74ff40dc121ac8f4399a5373929fde10493efaeeb1a7f8f657163f15ec36fa8b5648be5160c3f6cbd825ec3a88aff0943ddfa9eb6b6b11c9ec1c41b9f3465a0145d76319f3588fccd1ab0dffdd38a245bedfa62077fba704541f78535e4fe1babb52fe374138881774195258f761cec06eb259d5a13f62fd6876b9129dcd96f4d40cf7067556a42c02fd6584818e19fc14a47e4bd5aaa04d580ee60b7ff39a702f4cddb554636639ae482c9eaaefc4a88bc4bc50f9060b382d9420ffa6afe04be19f88e99ff90184f93fb37fdade981b5ae11d6adc1d29c3210826dd9a21abcb733ccf2bdff4a3cb9c9d960ce72809737d4e0de8aaf9656ac945591821636be5792df6902c1346d963e396e82ebca4728c730649825b11f94c3877a8b9cdbf4c11e3d4ba91db16a2bf0ad0669778c5052f60bbe9bfba69cbe9d19dc6aa36cd47ee232ff0253363761aeefc2a5f31b2b81300df76018b31161a973c84483055f25cc726706bfafe7a8f4ae0890d7fe403c3362b58e427fc4ef3d1f0ed9db2798a633817d045581d8422921889db7afa152b86a3ca01e1adb741fecc40c800e03c86e4cc3f54ee80f458ba055865d864406d628d4482d8ad6550e5bcd51b5d8652ece6ee8f4a4deea0b2e712f4f2f492c10f59a602a1eacd987d4f3ed0cb0d414309ddea382cd92d4cd00f83bd0bfdb1e0a16a7b9040cabbc11748c7b5cf1cc5ed465244551a08c7676509dde9356823376fa380dcaf78c2496eb621df058ceb8429658d7972d15e8d88b988100334c2a1d20f0eef7baad080ff87ffd70baada2223984b16476a05bba50aedb5921c712074a6d501db411c5f1d5bc303fab76506a0d21144a7a7ae8782ed581706bf3245d1cd57347a38f331d28f697d3dd3e85e83ac302b54a368ee4a24c8560697aa4a9e7c3d13f88ca1f24a906f20702862db77d57ba4e6596b0d48874caa390051bdd79955a07421ac1d0d5eb6889a5024c9eb0e4303b8fa8056db38fc63b590b81c879a8cae226894cbb9a3cbbfd7285d5be30d1fe2288b2e4d245ed781ea50126223433bf835566b26939e84b43725241c6b8e2f15b1d87a80ccb786c1eb544e169dbec9702227b7ce8a1ed8b68599ff0aa0119b4e4154fc838abf4ddae0df0621ac535cb57b9aee9fbbc4325123ca2e860d62169adcbfdf627e7e32023454d271c18a589dda429c17ed1e9aadcce2111fd907b938c00a9d0ba76c83bbf1e73e63d95707bc96c9c4b5a0fcfc7449f7f44992a533e08fe11e9ac715a67a8520b99935c25a29ff5883e889a44c2ef3b6ed641cbf04c7901c1e25adffd0b0ce6bc5075168e8aaf15a4b4d43e42d9ec5bc16052d71c38e0b3a7e942ec13c699ffcdfbbf8ec6624565ac42b257702e56def74d4501e61f318e23e030ad854033754595f9647508eaa0b01c78f783caacbf18aa87f9d419bbe11f21cd618628372394cc043881455f49d275f46230c08cf2f4ae3a848b1ac198a657c15cb4ee5c3c006d22ccefd06c601942cffcb763aa6b67f4b8aec54a4ea6e66e8b2897ec637c262622e117b7ceaca60759c14f12b8741582bce5cfa0f7d86743e0da364170d9677cf448d7fa3590256ea01dcf11905726b88b15056236d8b996a28bce21fab5aa79bc62e1b61759bfc80d044910d3de3cf7b107d15115ec7285ec4bf3622a33fea896ddeb66c1d8adf4af7bec7d637105aac13d1ecf13708beab130be604f9a2ba28676ea45e1fbc00262fb5ad791ecd3454d807018f14580d9fddc75328df5c2d4fefd13f810716011515ed787f0ab85e36152f0f0699ec0eaffc4fb61e668dc42a607e62b88c3b51bc352694ef931865ac6bd2d62e9a126868963e10f4ca7a2b879b1e462578ac723f9410699f17b25c13f14a37f1de5a08681a0c22943514e36e6fef6dc7f6244af34a3b7565dafdf951ffdba646a16fdef01eddf01278816d357427738ffb9951ed28b8c036c536d0028cbf2f9147e2807b984909cdfce613a7678d77f3cf488280cb79e6b36607a6d419334c7c3b3aa19c621eaa4da59a1257a43ef73245f5f5833e03e83296707755381a98c567251c18327517c24c6e048b1319210f552b9281d95cdc532cfe29ac3b1e53a03b7683bf78e95a9523d063ae14b8ad8a3fff26ea904be2ea6a1d0ae27afe59eefc68be8af098a6308b9f48996e196a501ead76f8302904dd984c65bd90a2287e2144fb2f95b12ebe693cf1dadf3020a37ec8604da968bb1abf61edc3209a3897c2f501e974e6659b2d962aa7f765640a6f1ff541a714e9609261caff65ffedc9e02b7b4092ed6913b19c27eca0ecfa078dbdc5c1aa45c5c0182ac64317ba2f3cd08895e4d451cb781dbe8e0e5863923e05aad7a28fffe8bed937b86ecd98707e963f4c9de740419aaf779a467c2dcc91f5ce61a9c3efa1db6e5b9fe291320ac8bb08c5fee185a9696102254cd1c2846122ab69193b8317ff690c8107204aae1463ea8750dc9f6a3c17653c8bf09e43b99e5a31976898b0991a3227dd04c63e51e21494824e09310dc78632438b1af7ef31b194fd57f1788cd8747e8373a678a8e24a65e1ec77518e951039763738fa331740d06336675452414ca07e20a0d682a255e983097f3ba21802166785f672c5b2d92e1785b6b4981da6d5493ebfd9227fc97533aab03869f0f93f6d95cb276be6a804a5a3a03d17c2f1b9838957bea06b5a85e531fb26977ee664262347abc5cf09badb3f07885e16bb7078ac682399576c1a9d17dc7ad4d3a806e3de40d9bff4ca31dfe0c27c94b5da20693a6c6d36caed4fd12f075efba3a514a9718522b9a3e3b98b1b525bb0d2b3723ebf90e547c09844330a48df5ac3b094d6aa71db616fe67dc08ea7e83e291f9ea20f83556cdfe4b91705eecc4cc3ac5ac8819ce5234a4a780c61d8430a561444c8f1424797dc6197d4e7adc323c1050815dc9e6060f88824182a1d25d396029930317bbaf33b69317cc9317c689a90b4b6c3b10e7e6032f3ce8db91f7ddfdc3a51b5c020410a5454ab3c5ba3a6e55daf3669d091f658da5208550ccf215758c7d9e3fbdc90c017a6e71c0b6cd2d15904e8cd350f972d02a2565b402e53eb64cb5334871924c9fb008f73de8b136b36129c80a986c2d1e120e4aa988d7fdbd67aa04f7df8f5965db71b38383e5b93ecd5563eac558fc308f046355fe8d1873ee4b557287af1660bbad3c42ab90c0fd8379f039645d6d0773a68ffd46fcbb5f29354864e5730284aac4af17fc59cdcc5d4f69a552e2eca2813d554a69059000daf4dfe0793200977913152f7576b670f687422dcf24fe0c883f49533744338cc74326894f7e34a7fda88ac67eea9eb69b3dd1cac8c078075e865389ede7a5b0302b1d7ab1dffeb4b71dc84db4cd3aa6dfdeeb90040f80ce425b607dc81dd4f1698ab2304f78811bcbd56ca4c2726991e862bf39372e741b88c90aa22f55537658934ba63bb62fd31971c3c1429efb83561fec800bb9f54667296ba25869fd3ade5acb1bf7cabaa8b22c1f04951b7c5c0434d3bc7a054882aa9f22e7eeac8822049b9034c1ca0f8330613fc870cc63f80077863b5aa41b47b563cfd6cb87163977582dbbb6a26f10c33792d51cbb50119d82bb3f210ef29d8bf18a05463a766fdf3a9b0f1317a1d4b6e66ca01f6331ff5bcd1821cf35df7e1288e3d72ce727ded3ad0ffee267e28737607c70aa57f72dc4aff85027dd2095b9408dbc0b3eb9f024f8075ef328c8b2a49914b29cb753be1edfab395f3badbadf397ea352a523afedf0298deb013e7baa27ec1efa27a320f32796c1317a4a356324a867749aa1e081199275c8b324d48e9c2839bcdb599f98ef7e232872460ab76f824da5a9e17ba287eda3ace8f83699a3ca49f912812c7d1841bda38bfcc06e148a9339fcdf5d47f7ad712d65324081b40bee1df457c07ba281a5f48213a3e8ab4c5813b23f5801af73f9fc376986ec85bc6c21e5f33fde307736e34fd8afb38c102b0420377f2b825d0df980d97ded8d1e8e1fdf8cd963f3b6db8c5f102e5590321c1e1eeffc18519775d210d0b384c47aef3557f5d1a082632dd4192362fb3ed2a25ad4210e90ad4270318987a4eabbeb747d1c52bc308db8e917dd765480fceaca01ae79554a455be3e9376191685928444832a002c08412887f12ff15446292735484d3c938666a347163f855952c05afde5c7b322303431c67d14966e5c1eda6bee425b1e75b52760e184252081993450621fb65ef0fbd4e0549ac5aa54c4caa6b14169835976a453961fd28271211c813f54df9aa9e1fccb6067dab0567ad0cd0a540b9b29ac72410aad5aa9887697d20622b17dd10e7267ef1e264039f1f863eb56220da771fbb7c05ec7a57b1b43db827d88a411800f77b0d19c6a25e9572d1b8a22c3839043ac4aee01acbf20d237e30bf28a1717e04ba57370ca7add62ee3a66e8a8261bf03d6978c55b31e4fa0d61624ed764e9e873ec5097620a5e071c06908d824b8d4917d3000bff408867a6ef997788657be516240a5f981967b98799fda4fb81f81a74044a7288604eaa84d8b2dabf2af50b7a17d8f4df6ddf06efdcbb45796fd7ba5f60e03fcac007119d3f672102d92c1aa3af351d5b147ed20b4441c2081cc3a750f66dbff850d05cdac64493f065bd762790c2d1b92ccb7c2e2f5a42573c05a693311f79c3d22551559be7b8bd4c5606bfe9f4d0e6eaa532093e86b38e1d3ecf5877ee02ea2afa478ca44d0a3868d8b9237f08e23e4df2a6fba9a0f35d46307ef1777f80e1cbf5925a6cdb413965e8c85010a9b08b5e9c1417b3ba7243c1da1fe949a7513364451e4089415602559946f67bbee3d6b6e9e66b35a3c81d8357b43fdca4b99b6844b7db9bb708e3d5b195460dc9c277410e9c1ebadef76d16198abee591996840f589a05188c49888e78d77b43e4b4b04e66a14bd76c28129bd83bd82b9c845f0e0df532257ccbf6a2199dfd42518feb9a67e1c26dbf626b434887e5ed596191abc9ca00a72449a546156ceb18e0ca7845daa6ef92f8c1650831cb9b04b4554ecbd0fb7747843f25986192e3bb14a78df5045241bb16e71b1bd76f4b64c8e3e4b882b528ff39a11f09c466b317f25220b60417b174ff44cf3211fbbbe29995a928f7606e07399eb85a9e184b8ca20ed9a70503f70cfacbddfad5c4f24d58f37c9c1d7e19337f2528e6c83e2bd8adebaa9034958513b58bf43250a991f450f226725defefcbbcccf21f30db48e0304a7ea60bd7606fe17b929874f10bdc2ebd95d23faca0f469bb91d9d4369ef9b649159a9fbdfb326afeeb6b71d3f08385fbc49633034f1264684c36b78c78c9afc58390c81abf7b6c6a14673b3535c3643ef414814d57e6141118b2bd8e5d89dfd2690958e624b0ab51986ca5891bde40e598762a3690f33e19f3851539632c1df0ec874c972527297aed8063a03e3df65a2284789acf8d0610128a9b9fdc6940939036f1c06c54afdb4b30a72ef94c492e89849b2ce6a585547cf87c82bc1e0713e7745decd69ba0336a15f699a7614991a43b5b96fa3ca15f7ff49ba44a9187d94fcfe7f5a6451528c0e150aabff8484e5b06ffacad688bd6dc618a3cbe021249b94da8d33d44fd1b498b0588adfe7da16caecf738e1d951c4586b6de564c4af90397e5ec6675ecbb0fb91a967d8e947b846c5072ff009610f5c4e2cec3cd7bfe848a22e7113b92d12c7bd919b29f5502cf16eefd03534d726709e7efb24b67bea30ecf902fdbefbc17e67b0d81b2844eaa917f84f01ffb2b8f4c81012cf6ccc5fdc595b6d3358e89e872b53a13b3d4479233f166b4eb4eaf6f170c85941f73ac28c803dff00f7baaa5e2e6b88ace3a78913896516dd1a14d3d2764aa24f7126ea1faa1900f1a07d619606a6784519dbf3bce4df59a5ceb1d47d26a0b09ab216e9a029cc87b7b462ad06ffbd37dda90cf8353758d2a6367ad5f1c399076c9bec0b797cfbb596f7e2d5b69f418ef10e92ed252362d3def656b7466bc6e0a4c628fb9453c66f7be3845d9a6e417616c7fdd31924bed9c28af60a7478587eba1f71e76b240744144e88d4a783730026077ae89dd80d8d4739bb75b99014104cdaaa590b75d657059103486a4169291fd2ea1e18e446b61439018ed558d7045bf0702a0bac1dd2735079f1e04b065024afbbc7f83c91aa2a287e2f38b1f3c1b86eaf6516f971118c14bded051427ee65a23dd8df31bd069ec6ed9fb2049b11898d2d3168685a5a84ba6bd5739849a1d3a5cf35f688e3ade76b87c3b70d1e293a6484fb2b7aaef76fba524219afb1cee19d223fce3cc8ab591a44835d785b9fe028d8a31bd3c9da68ca3e57c3bd462ccbb5024b8aaf8ca4e5d683756c3fc78c147209f7ae30d129ba6474df8bc3f4415924d78d21a881ce40ee88308b7aa6c15610da494caed1ef18cafa686165b04027ae8bc33afd23bec6098195234bb8f6ee6fd330ce4aaa5939c7c30e1eae62d1ea6454bae7bf127c54a57f6a2c135eb47ab63b4677023c5a03a7e3c15b21bbf6369eb47661263235176015984a5eb40a4f8f7b1983d48b597a14a87779103591c670af88bea9887982a98a5940391feedd8d3b3974231577fc5ab43fcdb025ba7bb032aa229ba6b03b31dcee3e62410caf8727bff59c4a0ec0612af84ff7a1e3bc73a94ec190df777d19002e0dd0598d0771fa887d701681b39cefee406560ffe70d5b3fcacc972b2b50743a7c4554c5d23c620f04c9e7459719448a02b3b769ff8e3fba9a048a0dc035b48922428a12a99f37a127443c4f13229a5155e081118cdbcca60e172ac37805500f1f2af6ca196ac55974b9db594d31d71d849d6435da27b90f1fde536b499c60b911c0ca6ae04ea1f2fc037567b3ec2051d8ee9397c2353248f5e07c8438529805aab782f87a997d50834a1c842cb0d7780ead2198dbf77fb8c55c93df4a364213311997e89cabed082e9de036b8209c238d4e9cd4bf5ff17cc1b6ab8f95cc7fa18752e19fa270a123fc2040d41a34e2d296d4cc9adc9c2128101c693c97eec1a117e1f6fc3decd5b97598db0cbc5183b8f64842e7777275d42556c0390fa84398888716cfff0387eaccb2b0689277e6ca85bbf69eb8fadbc9d329518869a1d43e276b934b69f970a92e603282400daa073c6f957fe9fca90853d2d796096047063e95ccb1d8194b68022f977d05276ed850fbf3eda87fb091f6864ca3d7e46c1f91b490c0361961a91f049e1d6e6b97efff89783f1e9235ac000821238766fc6d20efd3e2c95fea742815ee4accceccec1b25a65f1cabc9ab6c17d27e7a040658b1b17911f052f64d8b641b73eb54264e8c492ca5effc9514b1f9c3a0dad9f60a2b9fae596b65aa5a959bf6133203071b7d09354e93d854fb82d04637a8c407b10feb8228b98e4f84f326874d227d3badbc132047b0fecb1b5b10b1a344ba16ebca2b7dace65996b23408a1e2b3f3ba2febae5677735eed3abdba25821a3f91a2de17edfb37cd7b98bef7783e6bccf83ca282873b130ac94f0ce8bebac288408a2a998d21e02ed8cb7667d51703988b1ee2a32c260b93c13909516b75a2bb16e3fd378543c1b55bfd836adc34597190e03bc1342df016610051d2702e2a8b2ad8b98ebb0d317a2f9b7e7200f43dc25ae58855879e2b2c84f5be2a151f3979cef8e202897e8f4a8a5fe1b8a6245397880714687079e65a3e5abdce110f1efb7283f5133140499c11de0e9935210f7bc28e1761a4d697cabe336ba0b6d29ed21fef65349ad9d573ad1b436c3a61e7604e9a0659d95e2af6cb397400f7244bb85c477fc9a0b102c76bf9330327ed9dfd36c65ea3d9f836f1b1d8b2afbfe2130d30074c141f720c9865a396a041ec10af5d1736fdaf630ae1a9c86badf4e4827ed94e205fbd1fe9b9a8fa53b525730323ea45cd2629573303b6bd7c33393181985edaf032aa14960adbf867a7bdb0b15a7529fe59f99c814d0df86f44c5615b0ec5e4b784693169da12abf9b395ecb56094241727478cd8e4fd2fe2d3f2d578996c8f58087d983fbd318aa33579aa7b3b1ceac1ff2be1c0f97fde0eea06c3a08d4634a0a9ecbf1685e1c52014b2b2d418efbaf2e636c5531b8e2ace666dee29c5300d7e9a49116bfc7761204b3c1a5e9f8944ce052a288bf4e1eef63ec586517b360dc42a6d6e4f2814eb8954f544a413aa212d8bc91aea05281fc78985d5288bfd041f846bc43fada26dda50c94a5ad5ae70ca7e35b442660f4a6fdfdb50ac205a2d2b03ea59044ff7144b7226332293b156610a38d7fd40f83d1831885ee55fdf3868f2c6489144cc8973d97fb512898bcb566f0c4b5a04f12e66dbc667bc22f60ee32ae3c51e280393bac9071bdcd5b8ccd1e9eb34d2beb07908a87d936ca4d2067128fbbca1a4b9d3ecc8461eb08ed9efd9275dbb578b6340df6c46ef0d29e948adca6cfa4a3f0e68fe1d0f3c635137cedf747fb3f6b101166b714ab05290101a5a1ed7c12b6b4f2c54fab0900cc98a893b1facf85ff3497a46d22d56a1afbf270c13cb63a0d8021ea9fd3643634626fbb1b187c1877fd3939f89e26ba95fd02caae8a30f45a3a708e9bc1514840c9e133e28a71164192cb315f8814edcadb39e0c133942bcb7920e89a8ba297a583ce3e286ed51cc7aea3b6e48472cfc3db87a8f0483e9e66292c0b9b05bcb988543f4014b7cc922e7579a55d3f6e08a7813bda17a2ffd23f4cdfb05284a9034f912d539988647effff95375bfd539637569a97b2588394f3fb0cb531417d0596c0edf248eda7f185d6fab3203cff721304a020b844a93fddc987b674e49449484aebcef3ab0d4567faccb2ba412c928ecdb027ee353acd574e059471dee857d790e83b06a2ca7b25f4737964e60bf3dc6a60a022189636e383a8d2c23568309568a2b518c4e95bda9a72a10302003b791d5116b603914968d2047c78a8ff0c7263681791063c29db546233cf9105aeac3f7c3efb2a38054620038cf019022ed705a3285c6fe9a1f15b0885d6b343de7fef8a2f840c2560468be4bbae430fa75165c62574eb88e7b0b3f683cdfe6b1049e375f819b139dc69623b35d264bd75a68ca8223849163d9611c3b7855a4f8c89f2b85db49266342a187b822972279b38df9b6d5a281a2b414ad6698175fd51f7b7a009dc70319fbadef87cb970d5c2efee7c834c7fa52e88750db53824e896b051152a3e262eccf8d39eed50f41f76b129358f9332fbac3d624640b3220cc67c4423239bf53892d2777eb16717811c16c63a54a06c0a5b6690d9dbdb3ac94348ee734576634da9794a52f7552dcd7f48be11e8d6af32bd23c0e2957e0d4d0573cba7215d13a6b0b60c9ddfc0e99870dd31a5f06b5051b5caab8d6ce30c1cb94134343765458e6b3749f6b7184c6564ced4ebf4f5a7dfa906672306cb5423819a5be104b5031f6f4610a5ca15a5ecac713c36cc3b57216eacb7c4b64bf9cad9195f5477f3c5b935e44d73f48a557efd754767de722f4c08499b74dbb6dbefa4fd46da27671c68d934e8ce8e0d6fffeab8bebda3dd1ee4157db9a1de5994d1691cbf71429369c219c45a7084ddd2335b41a288f81767e93d33f716aa8741ef71ef55abb53e3a8d393a32ee0edf4e58de2a786daa2264c7881f75eb8b9a91590052ef224e1c8541fcf9b77ba8f88a16234cac97756e4be58487f94827a1ac436b90c645faa8399bb3d715caee996c7a4bfa2b850459cd748dcfa451b96fed0fdb0358fa6ff925aefbc62c037ee2764b65c8a885628ee0f710e45838b97d6124c2dc71684977b56664359344b01c868af85daabcec8e3d71a925e3e6da79e2b3ab19e0c526244398e35746daf8ce10917b1231cccb4e0bedd4b814a4a3abb7ad827cab3d8df1ea6ab58c0f103d5da1888afd8f307134c1a8830644f42dba05e44c71d071bbf72453c33e5a9795fb4091cf32ed8124a7be431f1341d6973db699905ef65c0bf81713043ce7c2fd4269f2abd2c73c5a7b6fd62ef8f052a302ea1ad53470d3df336f99b3a885ffe83597fb889a6f70d8ad8a7fc1064978aa9f30aad9d072fccc173e609ba6e0aeb417d4a4ddea493b8828c1275a34701f40383f5d7bc4566bc46aedb54c8e3c8cf5a7e5d20780fcec2c8cba98fc871a41ad1b41e5b824347519dba98b9be544c8be64e4bb90916285ba5c2295ddc1dc8aeb3c2147e1584ecc9d320ca52725a2758f7c7e2753ce44e5c00c5c853c7fffcd7421713e8256d2457ca375a05a4ba1f148d174f8163341146c8b29cf708f39f9960bb0fc53fab18000b18cc990136155ff01a4d5bf2cd1fcf22e5b87cde9d8271542a1aa3c878ca300004c82a3701419a943575519fbaa02cf49b6328372595d77937cf85b286eca0cf494a8bdcf4dca363bcefbd76cdc239e91bd05052390f9a2f60119e7bcee90977f59df53ae999e6d3fc8cf329365ac0898dac8ed1337f107e236c00a95ea90411b6efa0f99a778ad8321dda8bb5ae7955819f745074c416216f2d18f35d7ae8d930a907145cb0e2b7909bdc5789b268935afdd089227b6b214465350423a14ea7d85cc1e025ffaaec445715b5bee8cc8fde7d025436751de002c8c941ffc539e17329020c37897b7f98cc6afafbe200e88b6064fcb4631bef4c081b18a32aafec766f5c40d66720de5d16e619dd7f6bd36acd2d726b47784cd37ca0d6afc697007cef9efeb85613fc92f53fde44b9651f78d546da46292ecdc49b969569c823328a4133c3f03e1b639d79b1fd0da1e01e5d43c9978a240f703e2f88edd6f384095c6ec71de37d057c4047ad46674ae1d0cd6ac502fea27b15ac3292f999633a02628e04f82b17520df51e1871a029482494c53a9e95ad38cd08b1016ff284c86f2d1f666b1bec8b52d0ba67f89a70ec80f1b425b2fc38dfd462db5377c9e5ae794b84ac840e56f885293cbb55e11f27663668174c603b3b9fa417e257627e62d9968a05835f424c44d287bfbc6c95ab3d409d69fd575060ddc95e4ebbd6d27bc62427503fd4e4ba8fd0bb0f3b4bcaf972783595f895fc12cb5ef3f9ba510481691b6eeb4bdceb3b13f7bab2c90d9c429aa2e4b2803e49487fe4770303d3aebfc8feafa7822afd1729b727f3bcae5ad7dc7ba91876c825292315d2b25f03f5db242a400a80d0917956210bd4c24bbc480ceb41a9f63e199850b3be3565af3a9ffd245a8eb04a9de3f7113e41e26dbbeecc9f5dc3275ec118e8ea9cc5d7e51e73616a89d27100626b22aa065736f5a6f832e65d1d877d7a8c857b835e41933bac9ba42116577774fb04db0f22a8ef00f875bc14cbd921dd31bd5f380335bfbeaad25e1e298f4c23707715f17abb3cce2c318bbc84bb2b14d82eee02cbae9108d9d6f494366d8ebf2315dfbe50ff2f080e87aa86dd663c8560dc6614b390d51c38ed430e1a1990a0a86c52e1cf12f0a50f3e9911e1e704191d47aa861cea94dd2be83776f208afb1ba05f63a3f019539c8a4a9354cc3f6368a550baa9ec25c527c54b1794ba0f45e5c39f2bf5b9236d9fe1ad0774275f542b1cd35ff4cb7d8ca01796072fa7fdc0c1ac7b19e604c4e364a599d6a23253506ae437b4667c74266b360b5cf55b91704de2ac628f618c90f3df367f77471315dc950e45106a248ce434b0e4540d49bb07662776ea1ed5df6c26452cf67bbf360a02516c749b3052a41fb0931e17cd34dc80f2b8951f18e7c2293a73626212cc39d18e62723ab95b195eeebcd1ee512ac80a86289522b2d9380d92b017127335d693b55e5aecb7f70bf0afafba30a0d758ad5615eea3060327fac645f33d0529578503b462f1333d5e0f43c47ae3ac2c40acde57d0dbbb9726481e5e2df33e6ccb4db0302edeb1fe8bec44a78c12c1d017c4be2629a42a1df0421d3944634d9be089c7846fd580de410c1880d0b130f21d435efafac526a8f655fe05162ea7e94db8e3696ceace5ebbc3ce80899317f55d9235cd6bd22590fb77d493cd0ee43b631ae89c8e1689790a5292d6e2fd7b7ea25834133edb1171ed3606d028fa9c6889ae8d59df1a39250a1ceb64bbb12d1702ad08fce413be3ff445d38c85251db75506adbb98733c6ef0ffe92cf74c7f6d3d87d5e54cf4401c5e4a265344f27d8b032a64f18baa11b4668b156772b1f627b2c2eab0c996c35b1df7b3ae0fa70b920fa13b8abaa63f54dcb9d72e3723bd5b83488877d9f8c328a8ee3c6a308d5ba89d9aa098b4b02e8ddc5b4779e928c9f45395fd5cc3853e3244595f8b53d20db7864ca20c0321eb09c52c2b02dab6ab628d321e4cbafb47daeb3cbb8b966804c954a1568db521b4d83ad8ac1fc538ee67af147aa5dd32e382e12a245e4a1fed1e20af202b9b30e0e4afda900a8c503a2a6cb6d6ca5fa4dc470d941e7f9c887baa369470ab84fb6d13bcde663c75784df729ac36f1c6eec39415aabd3853300c1ac1a7b13676a16bdde17cd0298a91cd2bbb0a502996d3f0d021e426cce639d9e9020f828ca0854521738459bd9e375c18e69202e0b2b9553a1fc091f14f1f11a7a206a06fc2131a8fc9539421ec5ebacbbaefd85f7eee22387298f0c73fd906ac92966344fcf2267858cbad80dbeaa54d948d8cd992c0f2852a58f6aa12b282d6b4a2a7334f596c16800abf1b95ff4a6671b2dfd98eb92af7382365613d4e11ad35116b7276806e818dc86e2aa8a931c645e761476ebad0bc9ab226f6c633c80fd6b5672ae4614df4a541e6ca4475a01e8b3a5fe19ad88806c1929eb788acb2dbbabdb65d368e7193e40a380b3c2490677eb1c78be97aa0416af8e7a30a782feb9c57c8f8a3113472f8a08ca5dc39c219ab56824a717cfaa2e5119ef44a62118501b80c7344138d25db1e80014d2a1d66cbc52f5cd7a82de93034a5e8e9a17130cfc95b51a0f27626de0d8d9621b72a27d18ed7f70ca1d0a4e526906e7c38cd0e959469da248c4b314ab41ddeeb4247a4acf9c716de1e5ee220645ee5561f5af8fba63bc23d0bf58c79721d72746bd1e109c4e0e1f70922159be248cfb8b2b386b4d1bd16bb4a8a330762485fbe364b519189e43ac2dc56ec3c62f91ae1ff38ff38b0ee009cb20813310e74bc024012d5013ece11044f4a18758d89560a2a6f95c7a74298870de1281ba0ce7253048ef19d74aa7c9436c3e3fcdec6f305b697d724d24c410e3acee4b6bd5ba35dfc02cefd996502825070937ba676a7e7cc9167261e7bba65bd885812a9273bfa40d925bbda7b92ed68b17212e670d14211102507a715fa5206fa02bdd82f66a91a6fbeae5e03dc93f5e099bd1cbbf231ac60f1d0f621ac092a0fd045062ee0e0bcfb6ce28cc2df1146f6d040b1b8ff34e9d3a09d101284a84530bbd3dbf35bee7f03af19fde394281998d19630843a03451ff639add8f0bfeb4c860c8374bedeaca50ea9a03c994acb14c564a68f1f81eb8a26dd12c52cc41e03c2bddd6ffb516d243fa4b80be6c2a76581209cc2674d813f806e28937b9052f3afe670104bfbb2a678851118c93f8ab0c269e0148e7c16d75d759230a98cde802591b5fc2f483ef953b5c228ccbcc097669d3bf50fdee95f94d2b60bc967357d8ec16177896f0f30d3688fc62dcad702703c52a9809918d3927777fb9f67d1b83582bcc67b084af25080458e5316fc0fd8e5035403e201dc0b7c9656248e2b8ce6294373aed36291036c9a7871c6af1414ae9c4ab0b7be060b6dd3e71941859a33d54a768a0e963a44b98178638df200ff45fd08dda77a141c4be78bf831c072ba5ccb6a22905e677f24e1b4d8149f6bfe609cf7ea2b13278bdbae1fd2bb746f69b4d92077c7c4f1a7e4dd957209a0904439f06a0abeaf0f4d4c7575fa528795e37358dd56ba258c3bb586403107f0c4f0b2d8c638c69043fd6ea45e425737c80af98bb20dae12e0390d06963dd51ab84b9901a692853078d153a37c10374657de40ea9bb0f7876df1d77bfb491b3f492c17974e7668765419f6f615a0e41cf1c0f1c162307bd7550470852c31ca93afd9a8bfdfa27636933bd0e23b96e7b84571cc648762371fc23d1e334da303a4fadc53bace6b5f5d3188b3b90c975a4b4e183d2d15d89e23f2fb0bce8d3511852e46eaa34db82bca4d0f8cfca9f3149121bee0a923457e564ac4a0ae31f2b37961e13e91f7d0623e4017735a643fc3155174f3e098d9300530235b12d2256cd670ce34c553766f324869c180d04c49d0c3a4245e4a1d4a3824921cc3f3a40ea46e58751bc2bc33bb60b95e26d005f73d11c57cadafd29ca243268cad5704716614a8d81221200dbb24fcefed82cceb60ad0d1f434f6bc5d582d0a293b61ef7566187c82894229354ac4acf83ec6aa070c8e839605e3b327e0529e53d828f29a400745926f910c1cc0c377c21a5a8473d69ba8dd334033ba86160b3b57ad2e20f6fe437acaa5702d15bfe62383be8a92d1717fa2b19a83fbb28dfd99c1317953c2db84d6a8e74d3e1ebefa89884e482e42849983bc3c16bd80830e27bd8da85f6e0781e36a65856ec72464652b2b876d526c90b7fde5b063328144d81e71352369e52b7219b9f2a48d01be2b9bbcf876ef51080598ebbdc40d7470ccf7f80abbb3e772c437b5220d50327e181b2ad97d40de12a5cf811c3b34f3fa7af5488c03d845332591213996d5ee5907f4cbf558ce13166bc5cbc3d3ed0a1d5a702d49fb6e0bbe8286fdb289960bd2f49287cdc6a35eaf625892f78c32bac107eab0e27937d96a878b160ab39d4a64ba5f63520acbf55d40d7af3eb9aaef4a69c61a20b48349439252e5829f3f0159ac3a247fbe7610c4275b5f0b9631d5ea5af0d9027a0366415137501813a81604d94795162ae74f031258d83172a2cf94a57e239d296eae15cef7e5117a7e704a4ae9da752c64e89292e12433fd4501d82ca166c2d36765dce5bb9d73777852398cf9f393c517a51804e3b39b1cbb399e98ae8c3d7c67f35e1d275b4e60bfcadfd877bdf5ad3cdece06d8bbbadaf18ad98ff85625fd753bab8393c2839892f8883b2a0c7617af6d04bf29167cfa20580eb55e175e4e350ac277e467929c4a9fd68d1ee50df113116e3c6c1479c623d50aaaf31860454bfeac0726f92e665dca15c4520c3929aa89c8e38e6da88e8f8ab5e3b6292453db63af3a07e5ce5df36cf438f79a3f75451b7089de176cf70f64c357494d885ddecff734db541060cb9d3dbbac5ce6d4607170b14cf037c0198fbb882e07950c3b4821e2616d855b46ea9dfbea1c11917c9dee4a4f314d42cb058a125fb9ba90db899d58bbae6540ae6e0f08acf6a12fd9b468cad52d337b969d6c214eb4af8f48e23b96ffdba3370f98b08e2240074de658d8c1e5464de47e2b20ba9b39977402aeab6c2548360076cdb8bdccbfb95ac21f8f4e3fcda147720e8becec2938ea9d77abf1c321f5184f7d477a7d4d5de851766f005c5782d068cb06a86a2c4778ac77046015a5c66e8e755c2c68cff941ed05f5dc60b41215996f5e58ebfc7775f24fe17ec3bb39fd45960a0826abffd32c446de2b5d25f81b7d6bafb9b4ddf5341a398c9c1f13d65125ed95984233cb7b969fef0f0e48657dad0a2725a84307cab1352deb574e7c3f70982b178d9bf23419e5c024b681b87f7c65ef839ffd01ebff274039f764e6f927e1bf4435065d23d6fbad30522ced613f3ea071c1fe15694b738d123f8557819e9889a78e83a99bc357dfeabe0e2b44bf7732ba93ce0fa12b041be7a15cf2be21fc687e882f8cc6ff1e00a12b9e7e737b434e8e81bb41d886098128f61639bdfd27b1fc703b287ccd6cf67ad1817970a98c963ec857bab86a8268f583fe3109fe5f2b94678a070c0fee912375edab755e57146d8be450435b8929598f2400a542fb2e277a3a354c6c27778593d8431d38ba7a85b9cae6af00be8db87549b26264c49e9f6c462dca3bb9490c072c4722f8355f346833865de614b423e9982475f086648c97c1855b1b7d59fe18b5bc79edab512db87135861b603e66860f2c532d2807db0c51c90cf3c0e1ab68ef22cd6108a4aa782271e3e33868168b2c84664edc508de42a72e531d873d29c9ea5a3b0311873e781108cb3b2a366179ffd7f7fde39915df0356022cc97499b55b5a4b671527abf1268e8afd144f1ac8861c200903c01fb5d0e3301c4e9800a38f1f0c0d2d7aef16579e077fb222a6d1d87a060f7f796cb34dde39eb285b7a1f206e03f4404639be29b4e935b24d4bca42963323aeda4af4a735791b470edd72b33c899b7d176e1440dd3eec9840efde4d9916b2935f3f3b14f4763067ad233698e23dad81b82458396aaf8900e87f80cb259a2452c12c79e4c52ce7b31130b7b24075aeaa2b5a7523559bf7bfcd1505bdf6ccab8fd9274fc5bc9ef866ffb261cd29d06381cb0f0049b360979b252f90583ff591b90bcdfa2dbbb9c6f69a50b7ac6eebe4d1095bab079520d5813401558cc7b152b553cd421dd0ce88086b35d8b4e09947769b6617787a72e616fa4847f46d552487aa6e1166be9a07c0ad47390db5e2aae4e0e90b2ff4d6df69a6ebd200eea5dc20202bbcf05b257100a3a4f81d423b6175daf5801310f3b525663f387a9d33a7c96cd81c881b17a2ae84955a82b5269b22ee2632228d7984d70aef82c53045c0083a9b0e679ea7404864b58487580bd8f333ad9798cabce8b7135b71e7ea3242cb4bb4ea8d1ae7e7cd69293aad2e453159e89b16e6dce94e12786cd2ac2dff3542fa8d2d946645831639efe53635c7ef0e5e2eaa33d08ff0100b47ad0d458671b43b972cdd7822b096f28282750c4d5cd49cb45f9cc8e18d40824f49e613f5dbd358fd7ad9540f4229c493a0724262f8aef8c14fa5ae55ed40433c89d92e6698aebe59c86020427363820ee128c2c483a1d65cf4ec5d060d66113bd6f4f0a614861b0c1bb083316e3db2ef85341a6e722a018cfc60c71867205fc68b0e1b5aba84626799dcc7f53eeb81264fcfafae751a3a782bbc7b684da5dda2ac7b2b11cc976cf49f74036ad3b1445ceeb5119efeee80976153e26328d0eb062d17daa09169d96a9998b6e9f8c5e395c5bb49769b8dab181633b65a399e8cc8b8c0bdd1dbd6f4c56849f54528387733a86ea14eafda3a9ba590ce90425e3316e97a3a68286b729ce5eb6aeaf06b2bf24cbad28dbbe60d448b254d86c3704677d0bad65b053008450ae97445dc7380c241d2deaf5dc3f5157c875340b5dacf7838c794e16da05cb38531e8d963e60e55af21a5f7d9a8b937a4ac7bf4f826f9830e67dd6b11c6ace6669a50ae71c6bbfc62b859256592a232a38046b89908d69edbaacbffe21f792175fc0babd476858f67c19b4a148de36a66314fc63750baa80d4ddc0da1066c58ba96ed53f92602deba3cb51a20e1bb1995f774a5c78976ccc50e9a480902bde436b45a8e5e09bc80cb75ac6c76f0b6bda928ae118dd9b0478a82f210cf234a924498ff4efb6099fb9bc1452f1f41aa90935aadec8eee861ee33f5fd83c6faa7af9324755e0f6e5d7a4fbab36e1f07b5687b39d7a01beb328fc913683866239fc33886bc05c96c6daac1bf329c341617f1840c8ced4edb1b40d8bd0fa014faa785157a582a92e0ceeeb10c8d742fc2e26e2a67ced59d5c2ee48a2c16c94f4090eddcb5a163cb8f73327099949febe6959f7002279728a01243a99e50a287828928e38c284d9a8aaf49bd34099162ca5adf3b31648563ff96673bdfa5ec99088416ff7b7b695a8ff33845d4f7021f560bb46ea73327ad6f7665dafcbc7f232158bf846c16cc7ab5836af8e2a84d2185b221168027af457694138bc6a6ad26144ea1b8ab0d4c37933a8bf0341a812eee894fa88d1454e306fc1be02377a4c9ccb238b2f47975e82a41a9bf80fe276e34fc3f54d2f59fe7a021552b87b1b07c4020b22dc971cec24eb84c32b9cb9adbc9838761d0da3e43a09328db7d1eeb891e33031f4f8cc06ff5d64baaaf13664c3983632579a82e5635ef5362d35009a23b84de8e38cb1aa84b2316c7c15dc8b0f893a0b06731945f6b24e80a3554ae20af4bc78b8c74fddbcf559a7b8011498733ac692a111f96eb5477127b472fcf46299d79251b7c10e14d6121e0e972d88869271a49b1320c4843be7e6bd575f8dee085fa2f2e8e5077729115a46901a0dfd398c60a0363d6bcee0e30a92d4698d126f273c407439ba719688177b44c73b9bcb79e095620a33bd387d455c8a8f6ae2de1f260359ae4283b72e9ccad1238a5bedc66062211e1bd0034f745e271969ebe96d36866e18fdb7c6232ae846832b811c821276091ad08e680cea063599ffa53e71fc591f1281262def91190af2d906db6d60ebfe031c00cd6b667c3b03fc29f51a6439ca7bffb9ceee827b464ecbb03745fac34349edfbcc278bf2ec8519b23b503a83aecb336cc673ae724606388278a3dafdbb83ffcbf5204734f0a61c010ce013a34ab088182ac7a93fa62a97586ae077472c4c3d56783ab20bb7bff83e532df48e24b5abe78dc3d291fedd55991bad05edd46d6f0778ee2f69e67717f43993cb58289eeed8bf5e17f1958f80b52054b5af3a32efa949c375fe1147ca560322393b42f753576be7e2a5ae2267ccbb7243e9668a6849476fa380dbec80389d907f3a70990e305bb8890839649a864a2695ca4a3179c913d82d05c68828b0b994bf54fc29ce1c47b3e99b9706533ea9c42fa8cb4167726fa21181c09f549c44e474a2d366b892bb4bdb860ce2a33788083049d936696b49c82aa82b95a1f576e2174cf6736684a471e47bf5afc84bfc56affdd9933c554510eb4a6df41b8798c6d596029e4cffeb2b350509d505858548bf718ab23383a51200fc75ffe7dec970fe7b0e7b9b68496241653d0c89adcb386a3f474a24d3e33f021348b620b845d2c8ad5f903a19dd96feaf71523b46dbff302144399a5aa1be24c362edd925280c2425204e9513fb4b2a293dc7d8e64fc25c38d23194299fe83be882c87b91cb06d0ea148ed36eb960b18d3f39fdf9ab89b4c32c53944f512c9d354c5d8895bcddffa427a7d0f32d00a8147ddc9d84c2e54e5735e577a0d1ae5ec7acfd09747a72c90930732afc13d6fb84df15ffbf63ab63e2a7ae098be8372ce73089ab2513b9f2ed2f99439295f1ba24bea9cc6099546335c1938213188a06042ba2c116669debaef68d87ee79eca89982971cd24caa30b914bc69ddf88d4e929ddfb56bfb469a5068a4f7bff270619d8e9d1a4651a5f0acc17f581a21aeb1dc816862dec208c0d79dd22066dd27626c3910b9d7480b4654d221103a2b890c1902dc680965a768b4efd982e7c37464597f145ee29c2fa52a4ad5d0dc630bc19697fe11f50b17d01a66146f14cdb8a2c24aee36b8adc131feaf748e4641c31f1139989fd831b49c9ed930ad6714b6b55f9c9556622b1f0ad859527636b6b6480f74743960854b0dbc9ab4531038a1d9d62c286d4a8cae13e805e4c5ead758077ed25d44a8c89e29e98793641da9a224c0a0bb7091bbc8f69135c0e24d64e33969fa927e7e9e73c831cf7942e83628a0bd077ef994d97c3c0614cc7ab287b8de2131823d48e6f3607b424dd4943f8d57c5057db872266d26268aa10a5746e3552f2db260ec18f3324a8ef942276ea6d374e273342c3999e6bff5efd5519a6c4efbb2ae6ce14628f75455b3882f0456167d3871f964a91c5d6d8131ac3b8611148c22882826ec664fedc05a0f326f596b38a3eac04471f717b67f619e1e46e18aa23e5062d3ca4a4605637ba6af0ffc42a3c9030d669216511d94dfede96420fbde3c3bfb6d87ae0a5320c57a15b8f8023b71aba2bdea552cd36879d5a3f1020f709c6794fe90e9542c43b2b64dc8daeac4f99e277485e8be21c13e045731227f0dd67571370b1e314fd2b5a076da0f283f11b4a6cbd4cbb493b68552744f9e9d5aea08f75537d38c3a80e8f7730b5298eff75140d1498b7bbf1c0eb72af59fb4837fb23c8050e49a33558bb0a22a364163a8604750f8bf268120ff998b2704f80ec94ce0f02ae6ebb5f8a9e0987c203bfedaec8b592330ef9f2ada380e61c08bafc37691e5267c4409f43889e50e2715102288c97a394c855dc5838c66690133ce9c98089df06b29ba572015cc378d77500cda567d6971a515e9d01b34c5ccefa1f1f60441db8e7b4df2e220f1b2fe92625ea135feba904319e06b3528b2a60997b0d050ed9d3619bacdcb3d28ec00d94d8d98b09e68e6ad4ccccd0d1577b3eca08bfea469067722d6d74e899d97f8ea65f30531eb53d9c422bc7b201db7ec810b7747466798736480d6fca807b2dc9b48a41b13ee1d17241391f84566f706311c22e89103160826ee5c9ed291acdb56a6a9bc3d14bb83c2ecf64b81b40e638a0d29db4597ed9021fe603df9a696d352115c8a6ad16592051d0531eca9b9d9908c4ef12a8d395686a7dacc1b6d0ffb34146f8d24397f1a6434d255e01976e5296efa969f6c11a91ec0b4f59fc28c86f8e4907d013c2b63663eac11bbb6d63671ebf530736ec6cb93b5a2dcec397c9148792367f1bbe3373af86dc50df6f996aaa2170e1cc378164f2a150bec74269193a315f474e3e3f2f2ef0c58210162ca93b1ae300fb62e3d741fcff3c26eaa0ecc748d59fde7096f175fadf422b1ba0085164ec63969f25fdbd60ddfd6e63d620e5ed4bede8d47118c23a77752af856941f69ea74f9b18cd4146462da2bf390cc6780affc2fab93e4faba560df69e5bd19af7bd11e9f9489b93e8f939c663a3349606fe2865f3b274e903e4b72835c8c34de681ac733e4df5f86daffd1011b76e06a9345f39d910fdd99aa4dac1bf8f99938fd74ed387a4ff87fe9ace6ea6b63cbcfe4abb6470cafb3182b3b1766c6e22895eea852a9445f13df856bb9cea400f8abdbc6218beb80567814e6825fb3b61cd0fa6be5d36e23895bc60690bd6df7f0258022e64b70479af80598f3610c8a59390c4df6ec0b42431fb1d253f54178d209a3235cf2970dd4b8e032e685c2c2d30869b27d09017de0ee87253ab4f0d2ffbebbd6c916aa1c0f03759780328661f3f0d4d2382c3bd573d7c16c4c7c3a3e22d9e3fd6f51ecab0594b3135ef09ac84521b26c3e545932ae0c23cc1f5619b3bcfc6966a78a759187ac214eac5853a0baaf655c1dd34cd3a6e0af8b8401acb0fcf8bd6ac16ecb66073f215222d2d0a534010a245655b4f4cc52b038e21944da6c3337c696fe6e219ffbeaa50b5f384156359ae7204adb511f270314774f1fff1ba9666f5417c4a050f434e36b055f6376d7949219f9e55f50aece49e9ed0caff1e82049c55d0a6acdbf9d18acda80c3edac7d45dd9fb3ddffc4a43ec8a36b582eeffdc709f397e29c7da997f9ad132f6a8964710e2eb873d65aee63f760e138a47c9ef6c1656e289b2402bd2fa41cc401c6edda9cd896080f8e480c92b4e8247c27b2dfe103d80d3ef2709664ec4f7d26187b4afab2ce414f4ee21a89158684bfa894e6db15d4b772e092a6cd858a78814e2ce3fe4b6872a9785bf5da07d000aab8d87f3f2196279d50d33ee7af40641081ff767a6cc563b4f0d39b532da5a829364b216f273a63bc7851a083531bdb6b78c3df3b27115e1c8e2e5ab36137c9230096f557d81a0a1fa373df90a3f20e6733e51d14503a3d37feb75f6eb9670fed7ad24fc6e21abc5b454a7905ad8377a25609e10deb78565a97ce1dcafd0026140389c5a73b6a2891bb9691491769f82a20620e7dc2864f26c57d240f67a9523c3d7512a102218120b89375a95d41d2dd8d7069d7871a36e7996b76069d53c5f78f5d133ecefba704ad3b34e6010ebf880cd62171d6fb8a36a0476d52991e6bee6a3ba04296e936caf145879c567fc1e555090753cdbfefb00decb788676cd30fab7b2bf19ab33267f10dcf6fcfc17b76c6e51e7281a8de37ec3502e33996e1a10bd271be8103e10fc1c45b0b68c436133959bae479cfaffc300883863b58476d8413e5346588e2ecaff6fdca590e248df560cc346507ae26ebd2ff7932d1ed12ea65ae1586cc4d37e6696010fa1dc20548a2791881b809404c87b9b89f983aff1377207f1c12601a2222d27a6d83790199d639d186f6862483e00f92f8534da0aa757cf13e2a67a443d23e411a4f499ce1c2f7224c29befa3bc374775ba616f1fec8ebb8a20e9f3b626b6315d195ff8552aaca0031197d1036c2edf2c7cb9368609ef7583b29b6cd086a3cff8311c8f8191d68f274a3ab95f33ae8491fd419520ec25394698ae48fe4e2b53721a0faf70ddd207f41cd7861ef663c84e6e6e311a3d96fa744e29c66d3424c831d10c34d2b015105c48830db8855489c3a03d52d748183b52ed6b44dc75125f06da53897574ae9fec598eaf757ffca7c0e99995e449d8bf2cb7ac8625bd57bc4772462347e0cc2fcf843c2637c8a37bcaf4b4d6836153c4612377282ddfac97e98ef9b1b36288b174e78f0bcb8a3eb17677cc357b3502e356f66c7cf71a800b84a34d6cadc15d24e8763d76908091121d0d818b8295811a7decaa97672f94e4d0d09b8cd58e4727e95be8728c727fd14144a35041a0072572c33b1cec2806584b3ca2fefca517da0a2fb59a9198335667728f3c4ad17b366b0c406a19490c305bb0c0b52baaced30322ae63bfdcb6d16af41f8fc2ed009159a0926f4d4e78c97b304a2d6a76952dca75fe8e9df9402231ab05fc5525903af1f9d424623ed07d350110f74922f8aed62d939463194c88e64cec41298a7974246ff011b0d1e21eac6d3bfd57291ff12da200bf9ff446e09e0e09c23188253f90fe8d69e3f54d11ee8260d3f2f044ecdc82321e793dd5f84e906c26d10e450b788f5f371a0d6d0ccae2b3bc50ea6e9f7dfdaaaca61fb7ecb1222b982d5d1bb2a201706d44fc35e612e9853b1593747567f1e063b8ba1b17cbce33bc36ad4aa9e471acfb3cdcd4d94b284ba006a470b8a3c013aa745c780e0fb70ee2efd9736699a09c325a7eaa03cf21d03194b943db8f1b8405b6c443a53cfcf0776634449af0f9634b860222f9bb201da1789820dc0a1f886f689daf914f0201f898aae57f50b6a69c0e3861f6ce8406f0d4cd4d1f7f3bca7d5bfab0ee29c96eb156fde8c1eea37e97755905d28ff0b07e183814474e49b8810561c731399237662ba2d0c4ada24295c32696f372c88c623cef47c4e4df0727bbcd8bf7cc947037513edf1489a190e1e2475bdc6ad4c9dafd4ea102dde5543d86f21fd989a96c97d2b35d3fae28ec23cf796d102e1173d5541242fff5b089e0489f1dbcfda3cd83a275496e7f59ea3ab6eaf8c667f70fa3a38580c0113b1c8de6754693c73606073d5e5c36a907a8dc7fcb428f4d9df5b6c8b1ecc6b3829f498b7b2ae73cea3f019434c3102b7068dcaa5fea54d38c78f7a1b925f6d71c8d85e6a88a4a9bbf3b1a7abd447a31ac41fc823ac3394918973aff3a1f94694bf2440c94cc7608a456f250525795b395a04b76f1b3348240cb4206849eb5ff79c745cadc6208c1976937c5a791ac289f88ea898e8c31b46375f4f0a0cbec4f2ea71df3b330ae5a626c73c86f045112c609b008ba4fb6f4665c5949358be602e6375dd41e3437849ced1e2f5ed1b249f18cf713fad5b4dc2b4ac34cfbce9bc7011c8920a53f6d2b3af9a2a6b56f91a6b1ac743be267cf8cda89637746f0cf1fcf88aafeb621ad9f6d1e22eaf6167557ad80b48622261920f60c69b89044c85f92cd063e8352048d4c9d4369a27b1179ea98305196dfeb64105f973b68956896976956d7d954f8b19839531652b0244316214bcdcc6435fa273875cfb943b2e2818fa947f4ae80959b65a0a166b89b3e09a2dcbda58ca33a3d5d1ffe4a1eb1df16611bbf478866c08734f564a9e162362fab2201db3c94a32e81dddbb72c239579c72e3db5fd7208a82ce484da6f76c1033fe493b30d32c137e2e7b2bda67be72cb74391098c5f745a8818f7ab2f085db25acc18d39d4ec99401325255511530364df9910f120a756f4c6b30b3c31faa5da48ce9e737b95a12b85a5f48c4a3782b23d0f38443792adc2becbded6925ae91458aee99d3ac168f5d233942d769376f84312b99dc9571c0cc7c0d8995130ab10ad65fbcd599b8a799431f1f8860ed7b176195ad1437b051f02bc620b4aad6d98301a25a4a1a4a76e3c34c78b01c00ceb1a46d46d6b0ee418257bfab7fed9b9276f3ac9c18063a7eb29533a40bf4673f875411a31fe041c6fe5f2358952fbdc6c60802485f4cc1d0fa204c09cb73991fc9d583732f27c240a3db10c2ead4d794a5fbaa7c1ee90ed6ea2db80799956a711f10bae503ea8c89c5ab6f3d4437d8366c4d7d7e30e00e988189f858b204e295a80937e582bd69f9d0d371104ba78ec63f334e12c9d33a52b7bec9a2c2b3b468c6317ff50a409552c4fa9c0db92e550480912c2b450c564c9127819553bc41883a4a6da5ec09d7c257983a0391ebdac1fbf10e61b4af9c126cff49774bb0c260a81c8f5e1c028f86bcc16dd1dc1d67a864ee0e81991d8127e22baf3e4efefba6efafa3a53c076c243254e4bed099d326843d3a72305db511b7c673f585b74b62a3304c8f1ec9b515428860f1e799bbb38a3956dc083277f344c183bbfc5db1d58136d2108c8ef8daaf7d0d84632529b0e6f38c1f4ca53533217ffa184e1709b72dce5ac899415a34be371104f4a68b378c9a0aeb710f234c344ca06f8e3dfdaef888c4929d1671ed2b4356d05b46cde53e503a141424f8a1234accd0f5521499a9af255b13ae04e5bbee271712c659a00d9b11463515f43b896a6b27bd64c7c357969b07895b5155091b97c9bbd43050a67e3ecf4499c60f6d22eb8316f2c7c7363a6dd3dfa45ed383400e8558b4a9bb086dcaf680d9bb728383805b69a0859892e16654e94afd3c567ab0ac6d55708e9b0e995e16ec18024e403137988c4c9428c294bc0ada9ec8a853d11ebaf323d68d886aee26a9e9564aea863b6301e7216b26c54abe25ec42cfcb49f4f0a406d38e1f9b997368d3cc9825fee6490e498b81a47e50e41b60030a4927154ca6a22349c88ee0e8f75f199319c7e42d34849a533fe8a7106b1aeab25e47a696de3a49949647f96aacc34ff03d0030415eec7f845c6a861b3919db5f4e0a2a3dbaaea7732af1af9b5fe6e4c8a6d875559ac63ca11afc080dac2963b14d6d4bb72cc0a71f330ea02860c9c0b24a03ab0398909d0d19571e27ce8a4aded55a8aba7b0651803f140be6bbede6d375962c3218b1181a7f3d8a4aa814bea8be054ae6359809e4cf3e0f4de7f5d6feb3536cb3709ce6b0e3c6794f3bd1164cac81e2130e09d42754b90cc558d3a7fd415a38f6ea43e3e78cc9eaad7e53b3b17c180d71843707b08812b7cfd77ddded89ed371824cd2ef04e7e135a222ed194a51296797516b0faa39afb6d0cfe061ae9cd33523c38849239b0c2ca2d07184a93ce52fd49dbdc0695a069605858a96d30fd36c23671dcc776bb5546d03d8756d76a301d5482ed8127bf39b03f8ffbd4bd5891e2cb9f536c27e52f31dea2eb804abfaf4eb3c628990fd4cbf5c0e97dfa0db85b6857310781db2b3b450ef25a04a49bbcac9f3ad5a9ae8afb9079e47395151c6038e785d17d44e0ed5e36e5aa5e431dea61a3625795559ceade65927b939a56c780ed95abb31055928d45a1a7586090b8d5bb32451e753bbd33164d36d174d4610d4928c9b8a340c571b76c4fd5402c87d5ea92294b48f7c8cc329a2e70f4271ff6e6bcf63d35d3a91e5728ae1b2af7aee524ddbc87fc41d13b9fd236b56fbff61cc0d41c587923d20f913318a6c5c6c0d4eb2965b48e5636e95b5db7c20e3cfcc4824632a22fbd50cbda72aa661a04d81a7f03798f6303b8939beeb2ac25e8c0beca61b924c2cb501c8ab7156910205ef9597411826ba0dca0e79cb98d203557ce2f8676465b2043489d2222e194983500df0bb9214bbaaf4e06d3e53b1a70aae30374d9912b9603ab2ddfd5bf4cbb2f7364105b69841770e8342a44fcf5850a6590b11c03e190aa22dc7f20f25519fd2cd7df7c92033ea340c01c1f334e701567bddd927bd774dc03523c3c10abb2290de2942a452ecb83b8a99942fdebceee737e89eaadcf9986ea0deb12297420b7b06c49b117fba3727e74487a651cfba29651960fffd728794a0bbd4040eb18837bbbec15a83dc5a6222b6e5d5f11115ec4ba4035512c060e74908c56ebc25ad74dd25c188015974db3f28ee0e607db29c91b82e18502d8d48d971c64877e240ad401c53920ac4700000000003040000000000000fb66426d30534237bc4dd18dc582657ead640e2ca2ac6e1d82f05909683947ecab43e2ae0684fa74bbb547de4b82c677b437270d7e99536ac49bf083d4c997bd74caaf9b424a61134a5366d950c3cfc9a0186b3c83e81d599a5ba454e4d85a97613d0200000000000000000000000000000000000000000000000000000000009ded2fa14005a1f9ac6d628b64a31fe23142dfac71890d0f4e8b29d0da12ddf80e8d8d420e82151ecd560bf1454429ddb256fecfe7def0d60969bcfcf61917fe17fba3727e74487a651cfba29651960fffd728794a0bbd4040eb18837bbbec15a83dc5a6222b6e5d5f11115ec4ba4035512c060e74908c56ebc25ad74dd25c188015974db3f28ee0e607db29c91b82e18502d8d48d971c64877e240ad401c539ffffffffffffffffdfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8ce33564dfa183f877ac5d9433844e90f9e3e7e2a5497dc5214588ba4a19022e7aec3ebe601e7726bf8abe7c0ace3fb717980de06024d04bc3072aa5841b1d75966479419b1234321991a24237ae1d488101a47eae2b6c86a69715ec1adb56246dac470000000000" -const lightClientUpdate1Hex = "f6c34700000000006db605000000000032e35433b6bd8d415ed3df8dd54570e3e238ae6334e8899a54bf9804515d8d58293fcbbadbcdd05ed855e2b2286654030221f4177adc7cfa7d39ac414de06a9aa3eeee65a64fb83399522005b11170f228b21eef30be6f481d30695695064ac29484fced5c619a27d9d61af73af33ab3ac72b64b0928facaf8ebc967d657d9e0f3623e752709992bad1bdf9c3a3b253b8ab10cc8099a7e8d2ad1a4d0a537aa7bb3f04d57f877d3da9e2762f3d479cc18e154a9f4e7f5879ba029387f5916cc8bb74902e3aa238a69197b56f619216f2cc099eb2778eb3129e54913705613889720b33d18ba1e4e636dbc917e3d12552a8b80447b98a89ca3417b5d669cb5e45cd772619820b6071e80efd9cfe8c3898889c38f2a1be7513813b59493a43578e584370e937d211f8fa5ba772f1bb8aa43b4917f39846f1870cea8d2fcb2759643a6c8330ddfa5df4868ddc5e2458f7026aede0afd24a01fb8c7eec60cf2feec1eb19a364573d1dbb1d954a0a22498bac237db96ec3ed7e9c90e1cd1f0b59b9ba8966c02169af100668639cc15f518d7e45b3eb1286a64086dd1c0e68e099bd7689dbef697b17bbb01cf902dade9678a0eb6078f523423e9ff2b4ecda9ec00c58cf0f0aa7d8504585d2c379a404c8e4e7ceb6c82ed434838c70eeabfec212238a68210d2ddff385fddf235d9aa5d4be5cbc8953925ed17f66dcf1e6af1cc16533526000037f902d3b21cb9a5563b3816c0af8eb9496c74abfbb3ca2a98c02162d7715f846aa5051680e8f0bed3ab191a47d683d70b6fcaa5f9e82012948e68b501b20dfeeb9c8f121e30c4943390afe5445053be1d1535c13b35abb366e637cdc34f6691ceab31ecd561eb429a484ce7bfae2c7e23a996eaa1785474108c574d28ec1068df44d404de6302560acf7c3c62f06727f84b2f56814e23402ca6c4d2f38293d1f59db356c0020bc43bc6fcdf69a53c417a06c1a59f38e931796bc7318621aeb6ae4f6cebde9141055d25eb10e9a1211fdc56cb6d8ce08d9e2e8d488c17394730f062800345ef513ded3bf3ed27315aadde8bb01b80ec025097c547ad8f949ec33d116fadda6b5c39b84339279693a5d20179606df0659c30386c9e03fe133228c83a372fe2158aa19c3e621159ae5af7b06bf86f2a7aabb9f87c0a56d3707002f68cbe835f7045466a7dab4a718356bc8616b4dd23eb0ffad1c5dc33d9b07eb9a2aa665fd27941d2b045d85ae7537078a2065f19ebb362735ad313e850d80cc397c046ac630b7aa9098668744e8b9118c8f2aa24799d11c9ca0ee6f4abc31ae3072896ead170f1cc93ac4969f7fa4864f210debad59b6f494c85d11f3fab98ba26154f66a6f3fe4c0254b48af4293a32b0a3b9fb21d0065f3ad6bcda4709cc1178052e1d9b31f4b09a4e525861b416d89637d266b1a099b3fba6f7d60978a8e20e47eb246defc4e2d75863d2e357dac3398fb5020b88937f69e84311cd950e37096e24b3a18138cec0e0f17eff12109fe5df224cf86f8f18d38cb1fe66e0e19977a7cb6509eb9473b319f2e8daa3d84ec5f8b4482fd9205f043e37888d5c1c3788f6a24d1daaaa659f9325e0710d5e3a3898e5a14a7d08f7c8cc042ba7a216185a27afe610556e006a583f1ca53056b534873fbd1ecc4463108f3b1ee13c2ba260c7d354884796a22f1ecdbe779362dc25b24dfea81bd98bf945ba71e5bf86a82aef21816bc143de5c83353288672b1dc781a1714596225e88ada77e2db35e04b9bfe2d59b2ab60deb465f9284016ae28e5b82a39ae32fe74eaf1724cc13a5f361ff74c764afdb8c300d0475769735f38f616a14e876b06ef1c5bc7ee953679e64bfe6e9200b00789a632d9da537bcf58bdb157b6f39e224ea4209b22a99934c6b5dc996b7dc8b1c3ccecbf74688bc1d217fa4a76c3cdd709a61610c6a148bcbbe4f4de1e6c4f9496e0a1071aba36c3117980cc0d59a004d16f62c4889334be3ee652029d71b8020b317be7900dac2837f7a12422ecfe1c4830e7edcf7b349c221064799c9d410e1570747061c3f9ff97428aebeef4ac99cb3ac49d1fad4084458abe87b9f5ade10d1023575fa977025dd44e3ad1a925ea947350a93b742e2a16ada62a2c5311d8f931ce0e0829b405f1947a732a7cc201aa751e425bab1f6821539a72b9f2ff667763c57a82fd3f73bf0be6e9e458c74d0350c706fbe23c6a263333fc947e97ba1a68185ae56a7ec28e79d7b28b9f5e755e476f5156da4cdad44cc8235c3cdbe5e365b21bce38c98411bbdfd53d2bac0f210a159599091970832e8646b51029a89cc31e80d55eefa375c2f80a29ceb466bd39b0c7307719e3d46e2cfd5378eacd712b76aca5eab2f7a0027f02c433e07605355ecbf268f4393351e426f72751fd35869dde03a939e2d3c6edf48cc41992d9008973a1c877bfd105f584e9a8f9a80773ed04978bb4e7c75e40a17c832b97da60077309f9107dc4867c8a9846fe570f4199528d9854696627b9e38edfd106a21fe75b2b1acc0bdfadeac40b6a1f9049d4565a52072ef85df826be94f54da5c1307415b2b974fde09add587db05d351d8fb5ad0cc146551d4b4b4e9d6f50d5eacb8bf183075a8afddd728f3f64a587f0d1d22128994f2e7e59583177e8d9c35c28f7ea49a8cf9d6829baa7bb06206cc0d50ee0b9e7fa12d8206a6f8d85a58c3e321e35fb98711def57fcf0710348a5343d2f8668daf4cb9174df54fe82ee32e08208bd0a25949e55661f8615ceec260b5fe632996a7e0970d24e3eb7eade45fd4f7941a0474b12448cd20d4f74f9d9335f4f66d5900cd65d9237d0d84f8613d6782b3e9ec86f4dfc35ed35c1c527a882dadf739a9c2b2fe9bd2e06ffc48ae60e8848c40ec2c8247d1b20ac5fa3be82a9039bd60b4a2df1da0744320c958453c939e257895c6ba0fd071dc53f958b25a1624264842efca70ec9e76b7da5c8470407a60d7e5b5713890aec8117ce515c81738a54be557ae3d4a21757cbefe33ba4bae817c0e7f71cb9c1e148e7de0d8da05140e54abb778107227890e8d6fd86720cb82f9d291f8a548e8435dc5386b26989b6ff9216950b78c62b1fc431de15f7eee2778ea805f27a1e959d510453c7da837c74fa0dca389e8d13847450ea785243ae10404d416266a1bc932f083f92fe3e164a068b3e49df8be84e157d6356ea3ed35946b55ff0c99d1b823976d808cc773b42c307202251a3f8471c1c109df4c9aabed858924ac1436d5d8cc138ccfaa9e75c0690371aac2f857ee745e3c52d867456397a9cd0faf88784c2098ab03657d73e6229622180fdc6517b38a6aae550db6b452e4fc1c96a72fca3df2a4524139126170624f7ab45bf06b62ac9668c4984a5a7e99cb26a1e6d34b32711c364e51c302f7a595459eabdf7378ebd1aaff0dcc4f005139b6f87de4539f1e8afe920764b234945744d0c10cd3fa025cd2ab4234b243ee1230ace37531767a8e24b8e0c55e0378c2f0c766faa3458172d77d536475d186b8566bb49e13c333e2db0aaaef312f4e156c6ed74ca41b607bf34e23262bfbcc2ea6282179cae7e97a5cbdaf932621af355c50b2ec2f8fa0cc93694632e517e74edf1060dc138bf9f2c6c26d77d32decde9738235d7fc7f6a1cc0f5d07b0ef94175f9d39522ba04364bdbde6f39e1b411aa038ecc4c063a34157a31a23fca632dba59c403f8e416fac066c8fcf3924ac85853387585251477310b38d9b94432c7720713e5a312c373c19e5eec495979b1cf6ed387d9a6fc2492e278395346b924c271aa5253eed306f4c38456b347a22f1b29fc4e6c4f790487a89ce60a9539d358d2e8227fa265941748e1fab3d0ba0939bdac9d6474618e48b83efa3c8a2b5681060cf523f14dade122af7e506fe14288997c1bc9b39933582b63039b238aae2e1f9368b46e32e950bacbc1c3b4013143685b1db6abd1f01c1008fd523e23a7cca9d45d8f468dce35a55a9ceb160a2589d70f62b5f95c1d3c016796b41062093c44df5bfe7faa062e362ef3ea9f6f45b2a67481d7a5706071b27ef49fbbaa75410b26975dd33010014069b451d690bcbf52c3ff674238628d2543dfdea9fc787c893b83623bd7f4361592ee79120aa5b26fec714fa64177814a47c713fdaaaec749fe54baf97e06b47aef5017a25d23bf9938bfade298406394c9b93a63291852c2baeb0b37cc963b5b9c4f27851d2e270005a80749f758ba559cb6b49f87498fbd5d7cb73b0b7c80d1b090b8357af9d839fcedd639d8550964d27ee9515b1a8b41e5e0c043c6c17d1d720c1937135f001c9206246a596b5fd44171c08758410c5b4732d9f6cc1630579b042716d435c4a28d4357ac3ce6f3e82a8271bd0dd4c2a651f76e9f27e0296a02754d9e4821f9e097df6cb2bd1dbb71b3062cbfe6bff628230a77040ac1eb7de4366a8f36279bab06d2bd344c9bb90d6fcddf755922ccc7a3e349c0a560913ad49709e7efcf41943f5c8e4e922df14523d51e6b30db99d724fcfc7d7fc48a9613a9f5b3e8038d477ea53de48236c4d4cd771e6876ce995a75d9a6f3f6fc4ff7b881bdbb4e0fb8df6ca9b77470b0aa01460c224c980e35da171b24cede15c1751bced4eeaa8324c68e6695dfbf9e86357b593d6bff145d6361aaf310f9e82b29058534855b6b1f3bb09e90639c4eec1c7b8164b599222c1437a68f53f5d15627ec35b08b1bb0a1a6d6b23fb08dfda5d20b381396fab8b83b5dedb1179b39afc4e9c379214432f1fe394fb870a8af78fea7ea6349152ab9596fe060445963623cd14471de897a03b98a08374549c7e201fdc6c741191459c5e38dc8ac6692f117536df5d63537bcd49b1432a0730011e0de910010fb191a793be051c315be14efcf7ed8c8a94def8a42479f19dcbbe2e182ed24c22fa9e6027490215dd70f05b476b0a118998f6adf755cbb8ead017433182a4b12ccdae2802070b48c8cb10a2978dddad9660d352f703b760026d8fbc270348097682c682de1ab12f707bb6e4495613f1b935b5369a2468c7dcfa30ce9e10bc52b0249fc80d078075da3453fcf040d6b785b94cfc6c9105180101533436b352732f56d1fde3462bbe4a1ddedac24528543807a828fe1c79f67c3922dd1b5a37ed76b6a8889d47190a2b1afa3f35aabf67588577963485a1ba82c3aeeff7a3cc4507b8cd9aa3b8b02ca24321b8936b087d1ea3d5a0cb4ceaddb59845c7e9b654116c4ad6209b57eb53f7eb79b9abbc16864e34d3f184a61d1dbcd870a4c5d771b669b6ed51b915739a8223d60807575f5f1b85b659b4d9c608e9de4c6bad60b9a9c8f268cc13a53ebae4eba007dfe11f279696b598db9480ddd603540ff834a838447bd1226a8e42afed5fd0485ee6c92b53f0044ec7e4d01ba22d2f89b34d2a4573aa03d96d580699d7289c6438ce0ae04f35b721f04bf410c252afc0966a12d693050e6b408220b641bd4997de8df95a9eae45e2919c30ddfc079ae7aa809cc68fda05d7872fdbe1804c1a9fdd6d7c9c4eeeae74f65b1c1f9723e035644a337f0c846cf489e0f9abb4501486e6cabcb18a598489a6c6c7f4ad36c0dcf083acc5d83e858e03f25155b6d2552f9f6916bf81937a8e3c457a89b6418b125b56bbc7ca40c16a0247baa8b2126aa35048cb0f151f212abaef935f6dc199fbd55f575142acf9231989f1a7e64c73786aa8b4ea76ad171207b61d3f9cb2ddc6cf9dede907bf709828c845f1e11627ab8dbaf06946b30528d16a5a8b50d3a2d9946b8f37640fb734245d3bdde526dea2787d604d48ab9c5dd79af0df69914444899cc0aeaf93bda573f4aac640160086ffd4985e566b43c360a3258a5c80f04d305797ca9be2457a02279a988804d2ae15b38a3b66821b5d3fea6fd7deefd65b6f5fe7a97e5140fb36e035803a568aa7274b15a5fcb30e452a15372abb8257d7f7a91437aeb60612d341e62ea8fec26b447e8d2e02a49a3983c6d4a9a060662a4ed93d0ed874074083be5d712bf4051f37d3194e51a5f674573985c94e6867a601746b418d17e6fa28eebee53e670d26742ea7559e60b5afbd552b804cd94cfff0f48d8f91b34b3c63630326d266ca1ef364999907dfc51b8e59c590507a664d19fd0d188cea0d3c67b62a7e199995903acf0337ec8d5aa2dde760d3b30671d529a47a039d9233a93243c8ad3d6089d8d24ac41c3efb3b1f0e0c3b0bda3efe2baabb3938bdad08d0d73ff9842f154a0b1615d013959b2d400d7483c226897bc49b0571ad312fda3781f4e8607f93177f7b4112691680af579cccebe0270b727460432e0356b1383287a6e07235ac22188699db5978fbdb45f4710520662a4265f751f03e9b94b4b68df23528e91e3de52e73d96f5ca3e7b06d499cd06cad4ec84b17e4248865add0854361946a6e868799a30bcd4ea115b6dd14b6594d9f52a05c47370422af37eb131044a4a9cc510ba3d60849f086dba13e49f7238a3b6f27343ba4b7528d71c87f4ec5e2029ec4ee4503eacf71ca1ae03b61822bb1e3a78aa70e6d46ef51d65911da29342256767ec036a5c232b74e0083fa3086372b28cf318a59bffab3d10d3cc876815d96f132223d816a5bc2a68ac4714f15f01c9ca2d2e2fb0b25b824f1e5b2687ddab6427501e9f63a1a4ff472b444874f6f55b821d61aee035360fe7971e749c469c2492e1950ab62f5a6b87d135f701f5fe5fa878cd769f03774f420070f13837604b533d44f4ccffae1f5ad1c7b6624f6c0914bfdb9dcbe3389ca76205560567cfd2925608b89bb1cdc7df4aa6dac912db40d9746a31c6bb130e26aaff0d8feafc0c9d09e99eb8d03887a10fbbc9ee3bcef5e6b5b3889d4fa0b3e4be2cd1d60b273530cddd6eb1a7f8bc5b8facaa8d9de1189427b2fc55d81a48a7465f7d537a3e9b8c245893c52271d7e89f594e2947ff743bcefd27a467aa0ccb8fb6cb0657c520a02723f838b6ab49305f5526f6bfa44bc2c7d27a7e13d75808659c0818f818aa666d17f72bbf2b86f1bcbb8c045c61bb0d3a0507b5bd7935697dc861d27c3d35c256a4c7ae9bb1939dc565e2b3bb0b7a2adefe7c318897a767e456147ab2dd2aed00d27f32aea89c5443910c70fe742f32fe7e452a5bced9be5e76ce756155fb5d084c485afae9880576cf8c1af7596b0224a08ff954d99e6f9ee3fccb44e756e492f5ac80a87d21df3ab536193d5c6b1c19d48969825ec7719a53722858600b59850014003bdb4dc8f1c703380a2e229528a6e391212d7a051511f4b27d474259403e748dfc9e8b5a900820dc452dd4d2138ffc2ff9795bf904cd0da91b37c7f05ad7c066904d7d2c91ece42ea3d0b115529acd585c8dbb03cced93f5385e6918acbaca1dbe88f044c0b1069ab98ff34c0b0cb5c920346a8136318e0bb3f12df460c26d052be573a6cb0ed285d2e4925f7ac32c280a1b23a46e6509cf6fb58379f2f5c55a66c751bf53aade024a03611a6c3f2db3ab48e73b2538e8209eba66b79cfa8e42ecf842b90cf63b9e77c2acebd63916f325bc31de03bdff649111c67f774bdbf06805da37e4655089905f540771970218e0a933092c006b6bb1fae36cc863f28973b581cf00a8f744bed81215e35e56997326330b47457a0dfcfd7b50d35259fd920b8e911efdaea9b739f7f0e99b278b1c0247d63f35ae923b3e68c9054391542eca3201a6c1afc221e4d6fb2c9f1f2fe0da54c86a45aba97960b5605dc7cad330eb9432cd3c6be9427529323880ae704bf37da52f795beac0d8931d15b62e9b25db86d8dba2dc6cca755800c0ec8bddb39df325b8b15e195dd90d06d6b823b4ce0111e685957cfbabf7c9ae2fe1e7d1bc48234d30e6d8e2cc0382b7b5d03b689157722fc9ff9d0d7bcc12456455f172498a236044ae3e4cb55ddb2fac4259558f582e847d85c790620cd5e270df8d4f7edef99427525ddcdfc8854f56a142a3f3c94f6967df211f61ddb211be9780b626c955b30d911cd66780bbb176b4543e0ccb88ff36a1a2f468cba53253a5a69da3c30dc9103bb35959ead6e1f652de9c2d193f6dc3e8ce19268922105688011b41e54e4a74845bf0d3fb49a74b3d6a6184f96ed4b142f66ae948ec8975de37333f58bb4412da2bec0821e0f455cb05f4ca7e5b5217f756cd80a3b5a331093a38e15d7c78d2e632520476d0fa9f793da3307861c359ec1b91a0db6eb2c71c85768d87c85f1bf72aa25d8ffa833e006701993b24003759f7c20b39112535ed1493d78996ececcb57c2d0ccb5641239fd1847967f6a5e2655998ecab3f367ecfd6aa4437b413c7263d2fa2fd5228466d3e2b95a5f9edebe5e0d18dda6d3845f01852a777c03321121f1b685d9c65def15df64589f24cc1cbdc3ec663cf36d270877b46a00013ea3825571e62552903f5d770abf2d538af220a88fd5af7d4445bdb4259b2afd54409717e60338523df8c6ae1f8840abe9b3fa3389871df2eab6ebb99d9109195df1de925a2837741a3ad8fa5af0da51e3dabe0a8c7158ef753f1adee7baa9d926919c8d70ae202efbadb1665924dacd8bffb33d3099562801f7a68df2b83a6458a8d536dfa80c73d69b1a54788a506750792626105c389231848caa0d37c1890a9932cec256c32eadd663c90cc5e5500ef5a8cf33c03e490ad1d58fcf0a77969b51c4e1606ba59fe7473cf28427cf1e2b7684e53b2bc3a80f52e652357d834f076345fa2db454cb55fe7cadbdfa0fb1395f4a1321d16090afeba4f39b46327cb33abb6ead875cb80ed17d3de211d0a238632159dc9bd603ce7884185c1a2dc851707f9f6f9ba68592313a38ec485566dd206a2c2251eafe026583f166db15d6ad9fe38b4690be591f012656a2fb41217dbfe43ec0d8a9e0670fc160ed2867386e0e29d191880dd1e53d69ade05265d112e26588bc807f69343964144b9b8482f555a530d9f646c2b75f8587e3849f7fc15498f42c25fdf21074589c863ec75e6c1359ddf4330509b916e58b4beafbf6db8017993271072ae1d53fe51a36f845b110755541634f2d5324fa390506dc64a5548d57a32d65cfbacd66e7281a33fee60aee3a5c80e1190618dd96d350810bc88cd549ebea0728c3d9cdef033e6f498851e7c386158f5a725748e4b748e0795154fcac1bf7cb26acbba894b1c2252bad372b37591fd529dbb481a77b03e4a7fc11cb0c63ab5dba3007f3203ca8cc4b41ef5363dd634bc10e69186f4507c714e84c910b4df0dc1e494e31a5bda06701eaedd21717fbaf98804ef60d14ab072095bb63d73398cbc61c0a53f73aa03fabd3c2948cc2eb3c96bd18fcb9208d635a750abdb038c2a1c2e7da5b7b84aa50d04dfc4e7d3fdd93304d8c94f519dad2918a24ed45721f4d55af460e955e90b8927ab8f74ff5cb86208ba6fded969b7f9b688aa6f14bf353a4e1fc6fb9f6c3c2454b6b06311155161ee5dc52b1f7162c63002a89231887c2fb20d2138cecab157b91a00ad0f81562f2b269aab830ebdf9b774432ec92134e7e43a3070a6c9a878fc57e288550751d9bec7e4685d05ac2e8be7b564a96b6a9a2bf4c16cb91d0c79fa21f6b6b84b70c2265c1810346c47bd87fa1de178353620ee8a74e6d64294f7fe35e02c74d87db70a91250ecc411bd537870cfbbd187fa919e51d527279915ad435f3a8568e92d80e6519b97c09a5535ac594530b84dac45898f3b9c4d564c7e8e2acb421ce93cd6f0402cc1f017af6fc93754b75a7c7378a0bf9701895893f5bf2eca9291b27f7b86b84d33cf6d2df00db8147088c4146d9e1b33cfe3c2955456d856da07cf59de97487072fe1a446c8e22e6c5e78dfe2dc95ef7dcbb9520ceabba3495d9c6f56d671645536097db6a9b8e9aa9ac7daa7935f69a6f70992b091943ce6b8ca773e0aa43572577cb1490704099bc2328c37dc7936758182000e19ed5ecff66ca5d0c6ca5a91fe36aa6770421ef18841a1a99f5b29ebab323bdd47cf5fca51f5cb22763be647d6e05cbe9bd462fa71e2c19d05b3c0329ab7a5ac4fa458a30879aa8c1a3b5536af2eb5400715448928231964e4d2d8b64fdbc0c5eb2f231aa185501316fca6d07026821e85326a0feeedffce9bca1274a77ce682a9d9ab5a0e1daa93d5d08bc7291dbe15829b4eb76ca7ebc60d9d862c7dbd86e3a337a429b635cd765191d74a4d52fa9b4a12f0d65d782c235fadd74ef46e3441264a7d5290d91d6eece78a0cb38c83b18d425a96968eb4b8254648b15ae9699ca43bde24a90443c6e05b0842bbdaac6448fcdb3d0ba93667597493bee287a47b47ce9f6e792af3fb8c24e839ad35f9456dde606de28d418473d46311037d5c30358c7916f877e769a6a1519db4ec99fa2d2e289468bb766a910000fd71fd01676f8fe269ab9243f8b0d74611ec4a4ebafa18ee1603c32474da1e3eda9a57910b57d85b7f669226d47182cde2433b5b613961e4da1a6038767decad62303bb7a8996528c46caba5e360f3b97f5885a9410c86f687ff8fc2527250f9d0bd7042c5ec7f8f1f69f72b200cb9ab1d74af5c1605f19b18a0106dfc1c3d9c51888db12a9732d8a0ac4a18ddeaff383445fb63394d44cec0f1e3850dbc2687ccd11f932abf5c8086b96717bcc7dbb5338da6a22ed35f07dd9dc36f60acf4fc0ad69bcf95d350968e3d4a4487627a57eed5945faf94721303c9270ad79e3143a7649cb1724b358e697c8ac70a14b105d21d29af7ad7c445f0c9558b25aca616620d66637a896a21d2d86912a5bdeb692bd21f8dd6237c2eb70132ad6e97ddbb25bbe23d75fa848615f2fbf998a199e31519fbea2db7bd311e10ca26003bbc4df5c4bf84bfd3149370a6e0daef76c59e7a7730d4c891bf347e8cc35d864b30dd0dc72ae61b2a83a26280ab290559f7595c743e8a78b054313a5f2fc41da5fc60101267ee07bbaea42caf25b2553cc62278eccf9bf0b7b6e3f739db078cb78e6499619b8a5c71be54d0673845f2ca04331b5bcb8beab78a480299561b1f57611adf31a9edc0fe8ec7177b63e5828b8ab3735bc4ae781305b4203f53040c2f2d7cdb06706d6b1ab1aede9606b100ddf7b1a8314ddd200b67c51466a6cbd810566907d484a61c86efacd1f3abe1c8f92a9a6dd978af58135ec4cf85e7eb2d0f2edc745ab5be4eb7ef47257c4ef39bc5d333d719dd88a3c74eeb509a358900533de09eb4e8690806f362cb2f78ab9a792a65d3fa560934208ce4cf68da1aac757765157d0d9004804ddfb254b1ce76e0523b2fd3d83cdd004b15bb8261cf2d16875f3f65e7de583172a49039d5276560b793c0a71aae33978d56020abf02d66b70ee0a88a5d02deca6dbed53588f1f6922d9dcf874da1e1497a5ae9baffbb2602b45e0ed398595d397aafdfebe5806c0f596e203029b5de38b5c7a5ffe00847f04404fc725b20434dc2ed5b470d56db1c39dc80d56a951df5b7ba3aa497d5297eeec7a673f0d4904a348b902bec02480193bc278fd6e7ded2f07b9fe1f4dd95f1dfa63f669b53cbf385599d1aa2b9a53a823b00ae82d41c215de29c961f56db95498b03724b9fbc762d9aac7d961b8df7b9a1518efa169b060e7e41f990995e7682ba365f382816c8cf3368ecc0cf9f7b7fa49fc5845ad3e98c57cf1a19acf7fbe3dfeb34eb0b8ee8fbc1c2bb7ef3be1362b846171859d6113277c5933647fd6fd8b1bc8dc2c6d8e42dc68692390babaa3b09b6e3c8275949221453016d079a457e9855101d25cc259873f9334510f0f0169840c85d148a0fa2036cf3f04a38b13d860e214a429b92f73748cce24e56cffa91fcf2f4207be70e2d056fec9dc3f81d2cf2f6351d5ec3cd4c01ee3e8c58ac2dfdaf031992f21ffb514f995a7441accc2134ca29a0b0e721a964d1a302a7b5832b9698a92f83ac50087fa70ead562385cceae94965bf4bd1e40054c969fedee0c73fbda48f42f070e5bfa98dd9783c1ebd1aa9da6de2acf0a284fd691da5126bc77b26ab0ae956a31551cdc0ffffe9630a2cc0cca9dafc125604b607b33cf40db292369f8577a8679cad4b7c9d30152eeec7c00ac196e823985b8d27361ff1b0f2bd7c9f027834ec0dacad695abf774879b661fb77a65af20d6a87d386fafd64f218103a8d30f5905b4d2b386a2021802fbb8ce30f435aff6a19ba7ab541493793b071fa215df6da81567345f3d1d1b9f098c2fb63338975e37525797c6d2a7774c1ddd2da3d0730a80521077bd6edcd4670039d98e792f6d98e89224eff20e05864520801a1b1f2bf3eb4dc794eea899bee13ccc5d5a5328060d0b2bc53af654fabcf56a76b36e68a7c770c5558b7df0869ce78fc836a2cec7e67afed6162ff257a89f2eb12f36972a40e9ca0e88281179a861484094304c8034abb01d8cb0eaa633f59335f58da35be6100de84b8af34a9a7a80857f8c43f27ac19d8fa3b1e9c4c5a7cf68053aa4e87a6bd916d6301d290d038f0431c28ec38b78da5d9be2daa819ed2ea76079dcab9450045fc661fe036d9b63d8c31a9d6ca2b8cce56ccd136e81f781e24bc387dbdfa4c9cd5b632d4a0a3c6423b39800587eaeee3c8d23c5dffae1ccf7e89381c7b7af40170d29968ee68cb002d1866978edd21975c8e5a835ae7d1146f998b7d6c2bc9463b70caf4ff49c244c1173f9b9e950d627edd5cae3eb0092130fde91174dd36c423d96238c4b0af1477e00298bb1535a0273110eb0b9a2d4ec9cd4e42b9e86ed4c170c8e8edd2aa29536564028052e53f9acca43320815697f1fa8b5b9dc8116131ed31ebc54b84250174640329257329e0914229462b799fb09404b67932e95f8055a97fe60afcfbeaf164b7b38a0a79f0ac9218aec0e07942eec2e04f5f3e1c79eb4162d297fb43bf7bc854f8e3674dfa9edf173376e8ac50512eb506f465b3396fe3281f3fa92ee2aa6bed1b221a7baf4152764fb168751e216ce04d966bc04ffb494fe6008016349571a26cf2ec571ea37be58c98296ae442262ce6ac41b256f273a1310e9acea82bdeef3134169ee75321e8fc7e959682940eb6cab0caa646ee37ef63fb1846fcefb281ac5641e9ba10e70edae8b92eb66101064b5ddd0f828555273492a89a08c3db032f0686ac5e1c8d43abd777ee3a09b6dd93aa6215b7cea0653268f8b49c2f1b46cab6e0788b0eebc0aca0ba3f58f825b736bb5031fa4bd713c04f02bcf5cbcf439c75359188b6b1e2c9f1f99af10bb425d2d49f8af6cc9cdff844d3912aa59ed8d8b0a7abdb2e00a4400c48c140c15b60fceac620cc35f20f6ddc8891d99daa946a876aa485c3514241ff4397731445ec9ac67815c2d1a24a5e1de1d71ef641228de34831eb923b4bda27fb188a78b80c7cc6dda188dc5f25104fb1e66bdbd917d1971740f43cf99a80976d267617e2b984d674dd5d2f15199cb97b5c5d724f5e33827b8ec14a8e9283a5ccedf7fcef8b3384c74aa2c4e36d32036ce82478d9eff8e070b069497a80ce1079108321f8a4d273486f3f981b7d405824a8effbd4343c53e94058fcc57505f4cb428435d122015961d1d40d2df31ad73a65dea329b6a76e48240166b1d0e2c85d83f8eb4439e4446d8106e1e43d7ac5ddb3e034b4ce7db5c3c3e8f7a811a2171af0aa35295881310def98ea8b3ca0e98f2bd88b2a6f9528074737eb7f9e577e007a694b3b83e40a1a8d2b0fd1994ef91b9a67759e230c06da53b0c16d4749af6749fde0b8fe1d291e68d1eb185a84b83863418c0dffd173ffc49307b9121cd6a8eced3ce6f0d9d112dc8088cb8adcae6ad2dcc3205e86d081c8628a9d60ccdb97a9b8ce6ee2f1ed57680c92a903d2f578abd3391820c93aae86fb3a08a00516344692de11387cb5c7fb3d0d5a6a66e6e58fdd2aa7c5ea9aa6716d68685d2ea5b8e4b001d3f299e108e3dfba8e8ae7bd343cc2563b231334a2eadb44222c4b7fb238f1a70868ec721d077f08c7d5de6af915d44b3e55aa8e2a374997c2ba4dc703073afa0c7fbc48c69381ef048b8be35faac45ae2568390d5e12e05ce5ee687d8d8f3c90a52edd72df6af5976053a4c24f023995d02dea832fdd6a768cc9a73cc27e12fd3367b68af12fe8a7f43776829830df69d66fa84826b105fc84adf5afc6a0a4dbc508306d062583e7fb62c4cfdea1807f6819d40db7cbadc764253a71ae75e6e9ab4b377733534940af150f0649305779aff699c0cac23ad819b7927c4a009e03b2949c48885ba0d009380f278ad901fa2685f3349ddd39b4c22854ce0b3aa4eeef056979a0d019a88438b4e16ae6e4bd7a0f1dea7ea3b4df9625b0ffab7a7e65f1df3eeb713bbaf2649b36244d6ff4b1d185bc423a572f869531d0cda5720ed51feebaa3aee028b5f87a1c42b4f39e4d2b5c1747e95e2883cc8b894dc8ef69806fd808f95df787d023226c06ca657ce3b9fd17864c743e6d24a64f85b72aac2897971de5e66873fbfdbf1b0ec91209d6c1a546057ebbe768202cb159e51a288b7a18c8200280d472074d0e55aa5018aa443ebac85750c1cb74519c9973637a4641f379e44e4a04cb5930337bf4d6404210aad5db5af4b1056cb51ec8b9e5c188f9e4f3c3f33ddd6e052fd6c14934ea984c2c31066dd8275dfc4aa194d25222ea8cfa31e93793c2c034709019a930d623a43563537c2d38eb61a1861a59e90e4eefa4b78cee46b2f9ef3864c1e68f04b5aa16ab3fc2200e84bf0499fc8778f1ea931156267cbf89ed483c072703aa4fd7340cc53a3f1ccd42889b7ba7cacfbb82d220e62d75314e779312e2d693d6dbdca422c686941817793dd5e7a31842e0c111a5c351a671c65a062f056537e4e9def9f1a5b3cd63ed9ffd3b6875af53a1ba25cda9c673f38c2b02fc644cfffbf37b65e178a2b9aeb962961a796147c436d51317115877879ba374149278b6b10c31d0cca967818c6328f7f10ce26b28d1156a3e77c21e395812e22c9bdd28e1e4bb360c7b2cac99c4d8f60b7ea88ab1b34d91bc3461af2855dfcf886e1cca278d40ce604714d6734647c2fa280204fdb41c69c544f2e7c8be1f1aa10829b1afc13c9fff0f42b9f0693fdeab1530cd0947c2bcb9b0d0730fdef7c13f7824ba93df57f8be50b6637e4cd7dd0156bd9417568d5e5e8cf55118b00095ce6d05469aa8c8b3c86bd5a72c9896574366ecc197affb314dbedf636220f6d846cef5b0700f6341dff0fb99a9c9a575b4baa095dd123154f43bb3cb8e738ea2c3029681fd4e286a5d037043bfc2cee4c0eae8944685361daada9d9f7d227acc8a37e818f48a2f166b5bf323e65d04e1044ce6a70612478614646818b6fda13140e6eb98b2933cb21fdf69c2522f11c766ce234b52ac713df0fb4e14bb71bd288c172a2032c751d9416e3231e85662aaf4b1c9b6342fe8b9b48d305c303804dce4db4098cdb382d0d6464db11013035f07979905484fa4316ef7b3a4695292798f680aa375b3f837c4c89eed1a8996be1c85284c601497ff22f4684e42350ddac7e48f02e0bfdb2e90abb4e37a5ab68938565c89c5443e8427061ded91d93fbeb4b4d95eb2a4c0b64961a87a018548139656a2e05f9a86487d042e01f4056a800d74a8b5d9b7f2dba6f8f7cf8fb40870312d92c1104eccc09052010d4e160cbed6f8914772dc2b27f613fd12cea552584d279297de89d4d7254b3777df7c872a47a3da8a7352329d9bbb6c05df5531efbb3df21c69e0071635ed41167442009d47690f8c5df284dbb03f558271b13c802c2afa4f807208ea89de49fe2f50c8ef6364dd2c02b835ba944dc5d150c1750359c6e5a49993110c00e69516cadba8f618c5d7e4542740482ab7eafdc2ab06e4744dd72dbae232ee90108c27fe54f4775ebafea9776e29243eb0d7125ceb55b49f1956c94ba2041aa2a7acc4eed84391d8eb443cffc32eef00eb810a6e45a70ee15f9aaaf7337cdbaa7e69f1ff59c0cb5251c132a43cc4b1dfcd90767135a7060fcb6052cc6c816c1740f187c670bc5216027bb5bf9da4e7d23a1c6c694cfde7cf5e841cf1d947c560c1c362daaa21a7a4fff70c6718cd54947e3cfa04b7f4d65138e3b36106983dec753a567380d2836e15c499bd73bd79d7bfc76bb10800253220cd1739a593d8a28dda7d09722cb8d1b758827c3b364d2c5b1ecf1fa781e2a587fef71970500249487dee427d399c77041360411a2abf0d87a51acca3b102cfdaabb190d270dc0a90e2e9e220d3743ec144ac6cfca5a62c196d88b2354b8967c5b1aa51772ea28e66426508bbb28f70d57a866ace5bd2c01854b9c1107073585d22b33a7ff7510e5172e0432ad597848f8260d7b6e77954110f81d05abce32f7f468c733fbdfa6433657d3a1416e21f1adcc069e27dbb94bbec8f14b3a1895281bf310d4f9d729972cb4c9c7b898f7323aca5c251fec619751a820257c177ca7a164773219b4809918f10308d3b2fb2313fdbe5a1078932c24c27b75b495ad1e786b8f25cb7c9768364657966b8b18d334b26ea9790d8ccf9817969413968ffdbfbee41a581e9011540e6b471d4d20b5b1d9879413709733b02d77cd8852086f4ff3c007d041f6c61c9034870507ed316080f261af532224027bd0f22179470084688cc6067ef9c96e2394c522c838b32cc853788351a8505bd5c4113dc2e346acda2a010ee34c13a6d3220cda03b44b6578119a8f3b1cd7c65ea3f106b4270500dd69d68dd28ec931c1c05351a4f70d363f3b47c9fc071d3c59b43c8bfb3f1870a941fcddeb1739068a4c11adf03a8afd1db85c815de1c41abee5db2dbdee213fa7ae57ac0633149ebae3dcc7ad9faa0e2b7a48be6a41347b1c35094d4131c8531bc370156662e2dbaf6e7dec5559fceada86d411886de7e73656437895a6bdb0db84872a230bb1edbbde453fafc421c002e3c113822c8f7c30674c4739c84244ab44f329ad32d5730d8bcce34bd84071db067ea9d92c0093f92288e44b69511a0c7591149a4da75144d7d81ffda7043efb4bdc9740c0a0f537df3aa491be702ecafe432ef400a1c7ec8f63e5a7bb307dac28d6a680d4591ad190559ba38516918b01fab7d32a7d3274a82aa88fc083162b60b7cc8c6369b7b7f6897553f41454ad487dc56e4092fb0b8a0f587e7be2e9a64b807118612446dc8352aa8eb81c808964329f1408a6a95c07d5b9323b54ed92d1ed88f0fabafa21e765b3d5bf5647d1db8c76d187704ba4ada1fbab103423d982393f936b8f5a263696878380d268732c55a5729c45eac6b809d66988ebd3bedb89eab49a7cda1fcc8bc3b1342e9da872e886a8553487e0ff7242a1f96f3c2a873c441e926b7e13f2429e96b7afab9f1f62033d4f55d848ddd32470646211da7a06387b49b2dd96503a6f37c392473523b3319872f3ddf5c4850922db8968781ba5f0198d6ecf907fe8345c3b6b3a6a75bb9ec8e8d08a925a7a9c3be3bbeeb36dbb12c7522f6f6a678749de1ecaa394e1ac1e531b67907af6ba0aacce16e64ad01f984a2151674246f9a06748945d962b7f8d500fabe947973c53fd8bbbd5b7b3ef8c2d6ab2555ba023c280b55f534a9f375cccdd5a80ff26a284a7eae486b4f10dd5133ed978c097598bf166673b56c351fbe12bc179b01ca618c89a0293693524ba3681e4d0cc650a8332983e9de978bbd2855a1c3cf480042b6812a14a6dc3fda2392f084453e3768146a75941fa697f5b36db4b1a594b1d2cad41372ac0f8e53957e09d0857fbcd713006ae5cfddf01fee067930a3a45eecc14778b78293f32e5cee5b1c2a8188ac7f196bee750d25f40377d250e370be44d449cf2aff55716eaa57b9c93d7f849766981b1aaea9d395ae0759c2350b57269c32c29d24781b15da233b24541898e3fc08de406694285561b7647d5e457ddc3e7eb1be89b63f29174c182c540a218e6c7775fc9acbe1db23327e5afe03e3fc5474b71087def89f85b77b3fd77fc9b8f4487c493d8103fe22680bbd78082fbc759e9a8dbeaf33ff27ae66d02c86e1b4393eb6e82b71750f58574e6047ba80c05d577dc5149366edf37ed353b6a147489f156b2dcc78f72f5fa8d000d8c4bd4aff32031201c81be51822752fb2d08e25727ba769f6adef6236a07aaa06ff7d82a41b1d25c7eb622e487009711b717b42d5ca15cd10fe7a0949ab6eb0bbf145c7446250012c85f12f2bf243165bea7d4d0fbc37e9e6fcd0b95eba1a13cbf124f6a2d660cee88490ab2ecd64dd483ca3c39fbfdb0cbf862c0e8d0348a03ddbba2b3aabde901bd51b8067b6b8fa2c1671ec38db9aae1695d77ada5e2e386c8504975c01163b5f863bd38ffa225fbd735ef1d0cc6720a801df2aab01dc1cc0453abb1e19f1f3e327468d7268ce7bcb39b05fd520b796d9accde3c715a9708ca01d829c69d0d7ff83b0a50a6e2e181fedf0fe65e778fb45289d1b1c9c8ecae8e3a901b27606fba98f77f71361813df7a1b061e520c024fc301e52b6f39a6834cb2a094706ffec482992e1bb20862ce46b0307717571c999904cfb82a0fb3d01747e39dea5bfa5feee81178601099e70cb6e90b8bae762f33059b1f3f712e4f645373cd0c226af6f8ddd1b9244910eef9c6b9211dba090acecea08ab5e66eda064453ca91af274e4637922adb7ea656ca525591f285eadbb90a1d2e3df2c6c3f98aab2f15a40c11b6d1a0046b45c1c8e68ddc0fe035d98ad1ff98302229f81f1483f988f73bd866e8925f1d6b228b311cf65057d66fe159fd27bb4f7b6512aa7519ffbfa9c5bee38f6f1ecb9f876436e19dfdabb0472e8f5af6587ab335cb92ea401b795cd0776ae043462874a66cdeafcf9f025d647cb5594af3380a38ccf74e839ddf6ec1ec650b1b902327af4c3e6342ae76991603a217dbf1ecc0538977d7fc5bba034a31f113497879ec689a0de5f34cc61ea93257baae23e38a557f05672356bc0c76c25a7e1fb88b04267b5d5a1b5c9f851acc66df4f52555a8094f9f4db17c2566b0104e91044f91ee1ea685c1263cd48e2d3855de6c4e4254e2d240fed193d544d51653cab33be8f8bc120c76b04e753e94a779aded6f1651676e0033408b26fd224e75c4d67d64ba2ef4bc9ff66a55ff9dd8fdd92d588f0bb8e1548aa2a0d0d04cdef98268d0455ffb39db98c61603ec1e438752dd1b32a0987c1196391b78ec235d3f43be2eed17cd50f8b7f4986c35e84b5e8894370fd20e1c092741a73abb8ea85b910fe01647c70abeb9af90d03f2949a12dec13a2ad5ce0be9554b784415b7d81ac897fad0a800d233ebf44dc8a2cc5cf5ca218fb13b2782fd42fea46693c3c5132bd9adafa3b0b7a5823c61243a749d58d44baea8cc94f737b4f9057e2906cf7ddbc2a165348ebbc924f5c187ed27c94b6582996a1eb069862b50077a9ded698aa38af95b6fad4e4d5370fd6311f0ce34a27ea913325752287bb23d2adcd6a297875357827b4ad96f18b7ee747627a7180509f865fddbf5908d496521359dbbb237aaaf119173f3a7033fcc83fb27c49113ed42174bb8d98ef8f5b279d07b4a0870f263d05808302b818f68c06b641535d7990d7ce76249880746374d1f8ee9f3d0b5503c219e8e93a569411c401eb6aa254c0076fe0f2a65a688b65babefe848f20465c3266438c62f8cdbd26398833c4ab7a9bfafffa561491741de5da5870b500f78e5ec0b4a68b283051d7c18f967eb002e2a9d40ffeac825208f8dcf1298b71003a006ca29ee8b652a4798292eba5c90d630d41fa88baa9ab8f2ab2416ff22639c677afa8086efbc5ad5b3d4d6ea2502c22347891c5e88a6a5402aa7c4d97751ce52212831f12b48e906a67c1f5cbbf008e094257ff9483123b77cb916768165bc0fdd3459bca91040acaed593da4d9b66f194434e3d2079fdb874b147f35cb09308a18bf19647f484607894df0c0f2bae2f560386da3c57363b1d9c56284b1beb87f0361efd80271c6d9862eb7aa4130af6e47e686e725b4334ab6a98285c0bd0f091e88a6fd8d386923cdcc81aa2f845cf8e25a531e73df8de5c12928471e3c1982234d5b01e469452744ae48af6a539a3a32946a7bcb7f776ce381538bcf1f589ef6ff87b5db9c2407f5f4fb491a4424f64d50b12ed88052cb20fca027091a8dfb3a595b6328e9190a611790ae7953e5c608f0e60e40bd6b1ffc84927946cac21904a95b7959972327f3b401ebebca7876489e7d0c8956585a346afc85474162e8fc2e051d2e17b5c0b71b85c48cc90ee7f6e43233049595a3dbd5363fa202165dc3f040b907842629f598218f9dba803521490cfe62afb7396a0d37ecee3078d8627d0291dba7aa216b4ced6ccdba68d44eb55b2b6df91eddfac49d997874f9cc89a14b5591b591c9492d1bacd02fbfe0f7b9b8fb80d18f2ac13cd9903430d258cd3920542c2d5724d4345e8132ea6113ab2330cfd2dae657f7f94556d5cf36e0567bfb0ac3707c750cd8641495e357de860abbb3e96b3adfa9af78a9aa4dfea8bfa8e8836601d0cbe9e11c84c70414daaa77a2f520714d41e8bde287f6969fb1d1a6eb154ea426bbc9e8fea8928d820e98ddc14eee36e6c92636aaf9e0bf80a47302333960bb2f83c0754956c517c4443a4d8f65143ece52e016f4b864b3bbe7d1c473406c7c5c051a66d9dd184a4203c51c6afc94b3abc571b6f8431cb0dc4007353dfe12cac570a5b5f3b5297bb6d762944f43478c994d67f984cc1dcc250a9d2dda78fe5a95061d90b78d811230d7f57879044c50457dda6a6984fe8c7a335f594e55239f485473c5111047e637e25eed297c25b8c05e2fb83f627f5540ceddfae4c46dc362dd4ce1b786b973618ac31ec1c1f56b2c0fa53134fea5bea65a5bd7b008dd48b602a20c134a5d517fa0d89c95466d78b7ea4aadecb91ac18109feebf586c373fa80eb4c870d7eae889381b1100b0fe1ce27b412af9c388bd70d5aeb9e9b74806cb5add250b58bd63602574b4694c766103dd87efd8e40b829e68da062e280c8f02fdfe3099e49f083e6ee595956e81fb8c6aaf8faa57861166927e1cb1a69c35fd37afc536f957d6932d90e574754a7702dfddfe77434419fe2cb9f7c82e848c29798b9ed93cc78d0a7dc360e7b371264b733ce2c6edbf31108f6c761d97cbb26718d9485d1c9479f4ea0d2f536a8bd171bd11b82b51816f1b5ad102c8539adb26dd1ca3c15d3deb06e1e0a5d590d120a81a25a487bc31a013cead96e1b7daed24eb8baa5907bcf1eae74894b5f0b260730e40cc7aee883127c0e193b9588214d2147fccf4f8f915a16beb1279b35300a8c66ef178e9af6b4d47bf14c1c4d09712a0438086c89d193b365f254260ad1c824979523252d0df3d3dbba7573f70f3474a5b712959f26882c219e5e45dfcc2a9014dd58eec34faae6bb5014211f0cffe84304edb452a59743cb0998192658e8fe0bbc83933f1b72a8991d68f1cb4ac9995d4f2e618524f7529f2c89296902fa79be6aa64d8c5ec0330ec1111f4e2e57d31109cc80972599fd5e65807014e168f12ae59c01f3d78d1e3a1144a7587ada6538d3d857e8178d3160fe561daec5956ca1aae296f229567692da8a0331fa9121c3c9335db9e84a4c6cc6e5b34fb879b2b00192aa59c8ddea61dd58056fc666ffe1734190dd7642562f1002a42cb9d79e823b799d265ea9329a3831b2a62ce72545d8b3c37f4f4d2f2a365f5850954818fd475f87b69cba0b7bbdf4cdc5a637a3be1aac4ad035ff7dfa3b705840351f448fb334b58153df6b72ea3d6804c15ed5d133ff80015e45930336ca5f6e478da9f5e91daa46e2b1562f9d85da4242bf052fd3a339972aea4267ecc71ee888aa30dc5fb48529637999ef19cde600e8f233172f2160e17ae8d1a9d633fd13d158fd93b967d344cf63f7f0ea56e041509be7fb1c56b7a5510e2d84778bc323adca901ea4b50deca86aae91b4f75ea78c9cad11cbd8762d161c07affbd218375506471b47c286dd362bd63ea0ae1699feac4e8fdf43a5196b56f929d1783d4bd71e5d7b820d8a2dcfdd0b459e525b0fc0b787036894906b91e2dc53f6c8046816662303d9da992d12963ebf3db9a385d0f1a5bca434883c6a38de547bcd6abe0a79a2018248a660c8751ee401bdf1c30c72e14bef6748d8e2ad0c9e8129471904439dad4dd005059d3c483e23f5b1fe60620a62dc3ab6cda4411cec52a4e24c0a73464e71aac41b68ceb393166f5cd2480781682f5aee5be115e0928adc0b68b8910bb65a2b878ed185b0dd4718335e6fa81157944c7445583115f76ec2c2d109309a9d9acb6b7c417d2e5004934657e013123eb2e3ab0fb526759f56d16c2fcc6867bee4d9858d35173b20708e5bfd181509ad62a8cb33dd543b8fa98be74bdeb432303a5a96567c91dcdedbb36682129858cc0225464ada5d946e01ad008296720ab8cf75a6a1026d06963cde60d87f19512413c8a790c2907cc58f2a6424c484d0fb923075f53e9e66367d17bee7be6c3d80e53eb5eab911380fe408a0e57e45a0cb6196acac178eb1d64f43ff3b647a377018035cc74f83570da9ff202fd43561b7f675304fc239f7eaf6d0303f56009277d17490dcd0bede02b35f7d79f1944c6f006a1245420bdfd5b35f75ce5c77e4cf1e8ac77e8fee75894ae18145d7a333fdbf6590b0896117828fb10e6042639c8770cd0175c78c10f2b2ad2ffe3f73aaf7594dbd0382e799735e195985b93da325cd1e95cd7cdb788c7f70502ce0141d79974338842820e329b6b9dad836f5bb2232bc672c412f2181273fcf1a52364c4e4de29937b6af58e1ed2a9ff7c041b42f2b3b9a6c6538026dc83ee09507b049fe00df96e7856a81936c3504eb560d151f053da7f85197fd784a98383cf976cee51c84ac74c6c7c84fde58158676de02e6f7ea295a1128784ccfccda25dacb44afeecb8e3e46f9a3a61aa740e3bf075124f1d20804fab88d9f91f5840b8234bc1a31bea924a641e69d17d53f7d7ba57c03cca2a7115acc7c624f7adb61c2b2f53dda8a7ccf52ce84580669345103bc37b3e2d2005ab52ab2ef8ec9fcf1ddac381c7a27b7bb936293dcb8c45fd9abd517f3b0bb31a3c1c64b0fb21cf74fb49e2d8920c3c156151b95261502018cec5d759acfa387db6fb813a05a7505358f3f9ab9b4172a06fb4572ef14a360a61f24ba5d99678d179ae28f3de465fe15aaf3a8ee1c878ade9806181f144155b62d977bdce8dd495fe3ccf61a0b07552dbdc8e0090aeb9d805c788d6c2eb658aa806b40a079a2ac33956b8f573c6cbdf2c01dc84aecbbb8f7d14eec377bc6dbdf20b9725c58535d6f454bc8293aeaab595999e742b71a8089eb05dfc8c4f9c9927b4ec85bab0e905d2c9c8ed9c786b2edddbdfb102d343d4fac28846b4cde76165aa5593ba3ea94ac14858758dd25a407337c2b2b1f79ca87fb9b0e5bc8d621bb73ab4b61daf8fd869c5c27e26051a6d3fec09c692643a854ef3a948a77a0c3519acd517de3b424a4fdbe72864787433a2416e31053f665127353af6f5131975fa04b449a16be919f63837fddb4fbc43ab9046f09cd87ee85f82921e59b8a33dbfcdaf09bd27236229b2d470f12608ad2142a302fe11aa70d033bc1e363c157898b992e771f0f56e6951f7b3bbfba6320581c90f4ff48bf9e9e5dc0c014f14e8b544403788839a9eacf7e6c8dbef3e785754e82047126b319b822a04fcb6592a09fbc6d460538708c6e468511e441783614bb3720e326a61e30999ebbc6bac41aabcf96e61e90ac09e680b3e199f5fd4ac0fba347de33fa24a78581011732861953393f18a4b08aebf3a3748ff6157484b6e74e2e589165e37c5601a2c3706745728c3074944c5a6ae14ce402fc5bce57357340dc915297d6464f8dbe44f768e80ffa52fe516b0a2abf836e28b549a9b48eaa2250bba745b1d093d30f5d90a8926e46b7c1ff7cb39291289337b957ec2b1aee3abc7824b9ed7d790fb5ec278ad1b42525cab632160f38451302a7c1c675d519811c0e97a54b85e1bac3aceef7f7b12d8195b830421711f3b320b0d0bc4d49ac8f05b4bc505278b9b7eedce312fb2a7182e24f669089ad128a73a13fffbbadf846200e03029fe776904e3705d9a5211055235452b7b36e2df84974d33befe918cc40abceac5e360251f529384e0fe0d77ea6ff9d719e2c9483c86b200587095924731a4c5ace86bb3a939cc612ed2a8545dea0eab34d935dc33961840ca0bbcf3ae9f6703f8dc03fb829356b2f6bff86a0a939e734ffd998dc910d15d4cf20532b8fda148fe7ddabced6c84f0e2bdd19049708d57029aeec817ea3477ac33f595c5954d12a34cc8a272acfff5cd1ed599dc9eb9db604fa919d4ebe0715ad439e5aff0ae597fd1e832cae8a0c68ee9ed947108b9dd7bc11bcde3ba0522a5f920327916d2c93ee7d04d3879e0ec21b6de19bf2b82f23c9b4b1ead0245f683c0e58e94f206589e481ed824e29f3ab4ed07e341a84739576d793c803d20f8d6cd34793e9fd739b9efd859909724ca25e5a73558971af4fc092756bf3d53d76bbbdbacf05afdbb1f44715c04ceda567ac9cc837c8404eac756d7d5e5880950d41db4e86a57179d664718749b2e1b76e5f45a8368f3d86ae5b0b06c9f4977ee4ae7e218631117d6de04befaf256755c9028256bc17e13eb5c72337f14c89fc2c1d709478834b008d1895dc4e58ada9b321a17704fc82ca90333a74c2c4f22099f721738fbe3c29b28e7c1e912b56a4c0c1c57e584389e18e47ff90f9955c93780abd447718ab08e3cb6e6107aee8c7ae0865e50d468a44d6ca9c76af76503a04ce1cc6a67e43e59352b0684c4214b06ccb8b74212ccc929405846f3a1c6ded5af5abc1da30356cdd361219488078a200a54991b338fc53840a71617d3a483d6aaf579726f21314d49e2b8c764ea48910fda73d12c09fe19d7c02c5a72c501a6ab5cc39eda6ba1ab3b75e821778d91b754523c01a0ac246f778db820500c235534db4050f92bd15ec79a013b796e86b3bd916ffa5378ff589e556e5badcc2581ce582d2cbe6dea46e4a9b3ece3cb32e8ac49a23c14a0d32fe0bc91f44141824c7adeb67d65930c2a088834ec77e3129f1eade2c8b61de9da6255e2da1e1b09ad3827e51a0f2e4b55eec7b8a751560849362176b6625e228911d0d4ed8125b56754a9653b49af59bd02bba330571b39e32c60c1557a1da515dac9799b920564e850dada0ca14d7cd9a002a854ff15ea8ff191df2812df375ac5984e1b2d749fa0c358ea83fbf87ea481d46a37ec2c97fde71cd5c5e2b574faa239381353b64b27d17d900bde0a2874114966aa2d62f8520271723946d242487269413dc0276fff6e490508e0323088685d15e60167d74de56d8f0de06364e5bba3156a5eb127b53d4f2f0abce1ec557a1d6b0b61379766dd1a413e8f955bcb0bc15abf9414f34c328c5b95a05d67c0ac3bc775daf0adb9f7680fb8034e5d66b2508f58985bc226e04c8459a73af9ea6d700e2db27ace0d07d109ba845965868d4aa6fdc1f53a793817b518b0baa0692c828daad2b5d0f97d3e2a6abfbabd5a965004bcc3bc05c967cc91f332ff830bc879c4bf847030299af6a8d083ce1aa2554cd558dd55451a850dbae585df395b3c9123058f40aa02e19693f3ee1067a4abb63965f9b83c78c84f449aa93b9944a5c0afd0843854787ecc11f08cbc778ae9a2745b032d3ee03d7db6ae5dcca132dbda94ffddadd070e7f8c6a80f8f34e00828a30018a49e3fc53401fc6a78175aa0356f7d17ded11239ba3eb1108a9c929e1730039e9b9cd167ba8a190e495861f5267c9d5a3842a1c7ce165201d3858a7768c5cb95f10dd42108c79d4dd59db7a6b12d2d90a3cb7333b92cffbe0dd19653cfc6d48d8bde1e533e90568b42f3ba1b122993c2d0a40a75a458eabe97515f51d20b785193e64a0f4811b88002da6318b1c87b79098e968e188d3bde145028711abf1c4775abb80191dcc8056ffc6192d5f1aec2b65eba9bf5aed1d16c462fa61ebd1c5956ff10337aa08be3544d6af95c96185ee4bbeb7eb57c5dda89304f56becc2813e0021bebb601bc2a22cf8910653d74037aff6897236d0a958520d8683bd9b9aa055d4ecc41aa28d4a97fd2ca4b281277de322cb6e3bae37b6887983bc6a2169a75aba48214747edd721cc81e93ddb07a9424fe10e4bbaa7ed8023d55a7284d981ef76e9c40e448155a60484646c61e2dba1aa4a2ba9b462b83c95b3ee28149d23836693ace2912d0c59a5042e571022841d76d5c4adf9d4679e4672b872cf737ca21a1e3a5a6496ad9920a91e463f21a8605d542653f2b0e52c0eb39b66dcea0b4c89ca5e9d4d14c340e7d77a50a225d5c77936cece0cbf4e99d0b7a4e9dcdb9b04693cf8694dc272147778ca89eb585728eeeb3beabc7f227a264d88fd15d366327898850ecc4e9ecbdc99a9bf7312c57ca7ad1179e1814a6ab98dc1d20ad4dda969bfdf9dd48ef22dbcba050bc699f61018102019abe2ad71da96069580016c480dce5d97225d1a7b44e9b248616e853c790ddd5d1213d9abae9319dbecbe2a003dde97a79724627d29b4cfd13e861c795cbbbed9083bd7069272bd0f354dfc7f38b4e79f5bf3b484fd9ec050c879e8abbe0ac20731e353d2c98f6da5c21ae49395b61cb4566c49f402f8454083a0ffc33de381d592082f5e830d5363450946285fb4444f190e0715f7a772a155ba2c12f7ab839351a23cf5fdd4d5278c8ddd1e14ee43ec45170ece5f8cb272e57bf93b9bd4715c8af176a3edac68f3858cae1cc22ffb53c4906bf787af1f7887898d0b872b05c8493901ac1a92d9af03b5613cf3caf012eb3c047679a89422e4c89a1107d88cda00b05473194af29b367f56ccdec63f96d684094b41e3091388d5b9a61c4084b7b3de5d51ca92ea20a1ad8badd2bcc5523dc815ded741b322ee8f539917c66c5dbd7e050ec6a12c05e271283c6447ec8405efde43c8aba13939ef0f4541368cba50ce400dca9fd96c628bfc5af660f7545bbf9b4c76fa7f1c3e1943d968c62952aa3988a8a5943226e08d861b5bf288a0c63397d7af866602787850d406e78adc13f11f021589b5d4ce77328cd798f4a598954ad30f8d8bf7e67c4dfa513c101c2d162745c8ab62abba098ce5b1708ae6689ebc9bed9366fab84fad9d5eb55a60db36603eebb281e11feacad87c4cf916649fdc4b896b586b896b7547601b3185f90d056647329356687111e38367a6e2eb9042f016abfbff1dc507191cec194f72535a357fae680d5e601d2cb3447fe921d348a2f859cccfb32c0a284885f29f0e915df53ab2f4f85e8477d796b95a96bc8ac8c334c71d1d50312891cfaf7fbf077311ba19fa446dfaca4152a5317c279a9ed7f04fb53ca7d0ea34df189738bca9e086e2a5a7eb84ac65784d18be70befd195ab005d8a8d8675a550af18ba3960d6181ddf1ad8dab7c65b9aba2b5c2deb40532e9eacaaab5a0ffc1e88523f01de525a1ca4c0f0f91e02c0e8d0f2f71f74d105eea2c829c6e55720f13bf4e3f0a83edb172285b94bb071d0f928c4da6e693f8b7993c0c0581cb678cd1d598e4045a3ad0d828a9f6e49b630f4f00d2db303875df98b6eae0f88a1b2244b5a2e9b3ec3fac3ff07f438ca46708b3b9b9b31b7ad4b9ca3ba6d21e964469ad2d32e4e73cab76169ef28825a3e24f203da69a74de1984157fba16ce65cb7be6e29152cb151ddb730d8d8043d5dd6ef1c53112f30f6dd1f57722ed29b9b41e83f07296f264e2e56507cc5e52a4250864fecc948ba9c0a4207095be183a011a8244139b0e84bfa9807d4b30c9704a98fad7f84ac17a49521bfed1d9e500c7f1759fc7b162f1ea019668b95e1396ca4a614bc0258af25b7881c2d4310b6630ccee260e3da43a89b128fa6e0a94b094a1907a9aecb347b4e757c282ace68debb750191ebaa3119f5dc9014f6e298cc8b6e03c80af79bde4839d2ec6768c5185a13919e7a4fb17fbbf2b379914677b836e9eb2c443b4a304d30d0ec5b93050be68ffe9152c4fab927c6c3558f6d7252ffb56c119c07478e4f2ad3d99c1546e43b4c874b5bda754ce5a95f42b706934b8ee94d87c8a3f529058edefbe2008711a8195052c75b58c150c24a9a84cc2cb0b4bc183c703e396172c91a6c7144cf4c9dce387fefaccea716f25d2046597109c82e32ea0d299def97a43c490b38fb9606f3cdd18818f34121036a0fa76ff13622833e00d5fc2cdd8d1d4a904c167ad2b9f5144b4e021f68626cbf0959681528c7289b6a0b546fb0885beff2781f066fd6903b56d0dab9f2b090fba3f046277ff8b6478f8e1d3d0053c65f5b54427e316998f1a59958f0d66bc491b1b87ec569d5d5ab0db5c26d2438977b432283e7ec19dcb2fd88b253d4f850c9ba7906f321a31b640dd2de3ba3120f6e2c4893a11f6a29c42c748b81b847df836fbedbf5b97dbe43e74de785b8c32debfa379e2c41aa169af46f28340a6661f3b76812407b8d720c677427e32aade984f3c6b11f2700a4cb28ee3c5ce5518ed849740c6c2633b35d492974af21460cf94b85c1527174c09fc0437885f582739041b514a6ccd94592626bef2f09d84a1cc979b0c91899e3d624a07d069902e59593aa46cc12515b896332ec2c9602060316f3ac9ac402e0f53057e18ad9cae16dd9900b18db2b5d8564739cd9b77e9b501457fd435729b5384b19f62ade5b7b3d7642dcf60b78167ccf074da7127287deda768aa9cab85e9128366d2db6af577b2028d1b9b3cdb5c4ca6bb24d32dc359c1fbfd789ca568414800ac05f450bdfb7cab50504d189d62cbe8957628f70fc2fbeacba8197a33152b66760c01aab8512b35b69baff993951ac631135e455ea1b2881c825b1317e70caf09de877c1a288659a716be16e168e6d5b9d44aa4d7e1df14e5300e8a537074fa89ac0f678eea06904be428d64ce9328834827a60ebea01cae190b38aaf13046c5bcfd0186365d5deb516d5ccdd5875d6d309676f02760693d9d88e994c84e2b05e2a04be7c920017db11e6d3794cdc74e0123d6ae2f38705717f1af9d246759cc48dc8f176e735b747f0cee7b45b8dd93a7a231a63e9ed3d3eaf0e4b55bad5d9c2f9aea1c74ecb6a025cd14f29384273e3476c4f3398e3af06389d0af35d140a855acc81671687d60b2cfcd41267f0600dc6d32b8524187d8260c61ace3d0f84b323e9e8006718a29fd13a4bcac08a1f3082011d71524310c488b90fe566b991637c4dfc6fc292e05ededadc3dbf187017a2d21c5980d9a42e09d92cfc6324b066011bc987486a7f92674025e413cc468b6bf2b140c49921b9116da70bd9acca19020780955e77a26c6be23f715f93e8d7a66397fa45e4a70083a5053a56b8e6c256e5331ccefaef3f578cae3497fdd604f470e5239f6da348429c8c1a4bff1e4d1cf2980b20917ed86c10ece26fe35931b2ddd1f52fa9bd6a8edb34286d593a15a38df53682c6b08001edf3783ec069f36f26d249996b34a65e94b0fe2caa043eabac83abdac3819bff664cc6bf21d123b4932f06627ea918390b5cebfe671b86f23df3ce367541296da8448e087a39527c20722dae7c32e0637700b229e6a34919a8286f86cb87dd3574b7188f7d14f0da7091c97797f585dadd1a746ceccc0d913896b0c5b91136ad47a1e2b8546bcaa582e9962e33a297c73ed3156605db410ef93696f187c71d0c1af4c780462c434c8163d0e096dd11c24319090fc7df325b2826b3ed29aeea483056aa694f55ded408e2c0a1c62d64b5e35e924a1b1448d135805c5856e2b77fa6b58ede2a8bfc1e385d7422ae95a38b3d5b59103ffbb03db396b4cb47f5222ce0682b1da026354a2f499dc1b9393fffc20ca41754695e51281113bbe9b25821a207b0be33c48fad97c3d26b8ed118bdcfac1cb89f3e6d22595f56c0f6ce2393fada88638e7a10de420c663554b19484c731c840d9390872421ec9f1d2a7831a4f8389d0f87729e533a586f7019878ca0e451f0fe147e0efe14be5c7eeab725d79319d8973a4c2c26804036d73f8908e74dec03dc732e5463e7f23af5855694c8e5dc5bef2175fcdf2f858efde8d1565a981687f47c978be6c60508f086ae4799f8efcd4cb7dce85e89df014b5ae9dee230649829506070e9a775fc1a88c094723b05014a883b97649abe073d889cc14091fa5995e4388b3f23adf031daf5e7587f2e95859f3e568349f9f3ae5b17b719c3e3d981b8531ac880a13201e3df3b44d04abbaa85af198ace3976342d3891f2f8d185ca541efd571f611946fa0879a57c4cc243687af1a29dfa95395505ff867d119a296306751684fad87efbb404139795484d73588fe74d9024f60b40efa12b5aefe42b82e43603a0e94f04684fd82d7378a67ca0b4d80115a01ea80d57b4cb8957624612b0a2378d0d28eb141a812d04f3836d6a41fca56fd448223529109586909b884e658f3154c8d108af45e38e1443f8262e8f03d6be57394b2a40e1c218666117f6fea0a9807432ef653739da419d094806cd060c82476f84a1c29c0f534487dc6db3c7dccf4a4839306eb31ece7a35a1b5a75cb582a820f04a8e0ccb80c4c107e047e79fb3808b162a4dba910a4cdd8ba95e25bfffd4daf93958b524ad54af4a6a4519d78a69e7545f75cfad9b00e8038887f286e78b6e9e6680788683306014f37995c7913384c872d3fcd4d891a1833c4609fb9ed275f5756cde9f92be28b3755e9d0c95c1b9dbe40e1b8404e322a1ef9fc2566ff9bde8fc11acb5534bb130664fdc64450bbd39d4cf992095c2cd841360830631a5aaa5ae2e9ca8aa3089da1ef6963eff740ea92fbd2d48417d133ce5ff006ac0da7349a4c4eac32753ba396d3df1a581000fdf9a99a83f86260492c2fa5bf2840f3388823701edc96e51b87d48067663ef7f975bb8c2176012bbfb27d96814e298fa53ba993811629018d3a1d25b49306aaf1b8c23830f422225832910f62fe1e979af8561dc6c345eac8913671afae297efab25ec555d57d81c74e21043e6652e2d290641dc8313faf430193d778396d36628ab248d315b16fcbb1764dbb1e8cf7f2dec3b5b76e72f7bee9848936eac75bbab7277a2c4478597d5cbd88637fd72cec034ff743ca604f57040dff05daa73ad819dcef79b23ebefdd3361ed12103d27391cafd2a417ef5a568d4fb8fcbf17248b22bce652dc7c7f4f05e748218c7e66346cb95a9ff0e792d54e3a025046968e9a89d18c80ec6dd3aa48210320e0b4a9ab47a8f9660c3589a5c065278d8b18448476ac337ffb14e2f8d77d7819dcd14ec809ec1649c2f99441413329ac75a4f127c39c914bb47f84f8f32be5a0d76d45cac79e6238e8c7ba8c7a0b6dc69930cf791d749c5d1b7718f99ad9d7ddafce596b9220775347b307e1f7dcb2e9a35c32efe4a1db8187b288491798a2d967b273baafb83230f364edac2b31b238972cf525ee297dd9bde329cdb6cf6dce00ae6c101c3338985eb12f406177e216eeccfa6841431253423d739c89a6f0ba121fa470e7a04693680d010b6a605fa445029bd00c1fd2db95b7bad7852e8e8638af6e3aeb4ab4d2d8017c1521bf43bdac65a3b3588cde752d9dc381c49b0b6dccc7e436843f6cc0ccdf66430907a79472d21f782bce6ea051111f8f641e27fc4ee00bcfb8d7a187636fc6d7487472d01acefd70bf7fcfb679468edf44a2dce5bb5bb1aa2c320807945da49a95e2a5d40d8d887e9f434eae768cd3a7af3b22fc28f18b15dbb64198723fde2654f7cbefea4b2c698d42bf7af4604e6e6072dc46c7ccb84aaf7f09cca646c338543296e9b15590b2cf2370ac5466f13d6b123e18954a373974e359e3ab8468e829a5b0b681287510be267daf833b2de0e0b3fdf475e2a84444a349fbe4a962c7f1b8ae09884c731ad6b2035ddff8883379da0ea9fb8137208d73820fae79b0d21e5f3fbf56e1bae8810962437598d336f292a4c9bf34decb8431cea8747c36403d90c2ce5cf23a676c90d0aa26afd819a3c177bed99d71d32b0ebd70c53155567d3b1a5483fd5fcaad2da997ace4879d6e1fe2c103393502711565dcf8bb145e1d571342758d473839f8eaa7e7731a9cd54e7d115194b55b223e104dd80d449713ab1af21d5b4c515c94b5050f658381077fedf9bdb63767fb870fe5d7664d7a24d6cb37df2197bb00b07c0e515f441255ca0a071c087790af94b0546da5f7907445dae603054045320f4a493c816a77b9afab6118348a08a0ff5d8df68a794bdf8cb57af440c29187f406ae07473904510e8750153fdcb3022bf65d8de603c9f6c6128e8b6572184a85653b6ee64c49138b9b6fff869fecf000c11148cc9ff412531e5e1b7154e307b2f2db5e07e515d8734b2a1e8dc398eb7514ee59bd3ccde96e92a46728942b7709bb78d3ed7ab9ca5dec65c7406ee55991d73264aa7b8b54eadab9cfcb664b55862503d4316b871f4b5e2d183c7858bd45e685466e40db4308f072502e310003f798404edc0c914ef2c51015ee4a2b056ba89ed5e416473e724a47d2e4c5c751d3bca7e08fa2b17e08e6149844ef3f2d748b94243a270309e8c8bc45dd7c195323a3e95e4753470008ab0dfb6b7dacd2504c0d9d0f40eae23875e76ef5d8d9cb744e4204488305043283ab0be2529b39dda1d3fb9a9984f14d4ce1d79dc6d995d6b776baf82aafd2e633093c3719db29a20ce8f78d42ffec41317efca9b6a801fff1e1c60f3478852e4ca5bb53f6e731c191e5a6df9fe5064ec30399fac33e3f717c60663f747b4e6de382b83068a8cb2a0f3d44e1984d68c671e485cfe6577fe2f3473ebe8b39bbda9afeeebd01f3b6d5f8bf9fc0e662844dc82c8901b089650259c656172750032f9104efdd094db9ab642aab1bffde972edd900eaf3b6d2655342acb7ea831eb7fbf5331cee55aeb392c9fe158adc9524ba17cbee8321cff7fbc6efec5763ff575aded5aef8bbcf6352e4ddb4b17e0be5e13ae3bde75686aa7a05a8e22340ebb002001719dd9bcb28ea84b6fbcda2fd5ea1557b1b0a6f65e46642238093ece25994a130ff0033b0358241c92133c627a8788ec8e55ed621e8f5a415614805c0c68d3ced08c6d87add92a944dc03d8e7060c767700fd93a1ceb816fda53fa40cd941d796507fc5da834bac87d53a586f0b2cf996e02ae99167a9236bdfad751bfbf0fb828b2fdb86a4a31693260bf42ecfd96d14731cde072d18056e79931cdaa56bd859ac679c1e7476f82952ffd64cf594402f3f7fcbb4af4e0055f7e9d52d0d781f22fec4a2b3c25888b4cd4b4296aacd5fb7884153f79ae2da9aed4e90ccb1c7c4c2e76279a4d2d4d9576946709f12e4dd6e0d9d1f20dfd2db81ebdc2c323cb05b4a9bec0e016cbec27425ee173a2f49c539dc11da8a8fa14aac607e1d638a3ec3bdb9b20c9dfb6240495e48c68562869d017d5cdc7fb2c4f88bbfd726f961f23e7e5f221e8fc38cfb95e9104bae612504afda2066187bc335d7a08d3aa4f0728b13d56c1d214a621f81a0de7c8f5753fdb253d947890ea2edd4a51dc53eb8ae24e62d7337eefa8bb0677e8cf8cb374adf454768abc4dddb70cc91a6805ac01017c99a40e6b18b50731da3b96bd0383fe6af0df966edff24a0c9feec3a30e634198deb950d4ddf1be34d51845be9e392e0f953d87899cc3a1ffcebd26f7029cc3d2c1e594dc8425cf671235dbd4a4b17a123246313b1b7c554219be1a524ba3bb8d0212862ad429ebaa008c3e29d55b2a9dd4a13283a14b679fab2297fca47fdfe039aea034e419b8f56ab3fee6c3298e99c20f7e3a2952780f59eb1632c1ed72c63b59c545a98b606c8d77049d11793b0dff95807da964dad43c33ef7f244f43b029b29fab2a8cf6762383d86b958b09773b29ba01bf6e40439f31b7c1a157bb4c49503235a486c2e5d5d41589878a917a8c1e2e9b760fb752b8752e811f22e95557faf2a6f5c1c843bc54a42642ed4dc969d5f0fb2a4ca5755e3c3b7fce0092224e7564c98f2d9440c61c7defebe683e08fba6800ac3fe4c674a04692d214fe6c0acf2b73556f29c0ff5cd2b29d909fcc1d19471b2d9101e718d4ba0c142b35a95de1f45013d94c574a3ce1769718fd2be552f117b2e960c89d5f637846da593328b390689d2a41d868575310912e943886f8fe82d2369763c32de36d1f963e7ac4ef411d15983866cf2bebc14cd38f05b695efbb1003fdc577f4e6e3995b5556ae15337595420d38baaeab9601ac1176b8609898d394a7ca0332696eca83b6e747b17e09344bfd329693a545c66d6a26ced725c3a9582f48f31e520369000515d5feb519996672978cec2398781b1fbb5904bd190e16e874fbc19466311e746e44947f781d0cfe9f03df854231638f66ca2e466aed10252e135338540c1e9d907c0c104a147793024b4e604f560a1ce00b90bcdf0fa178a69d86da17ab097a696466ce2b8f065497364e52854c965015aa57766aedec17ecd4cbbd939beb2fd1b5274b386f3ee42db4c1b2bc509262575fe7a0dc2da2088bff7d06a602723af3a8c7806a93d7c3657098fdfe5bc076fc468001d8a3844ad439bc763eec2b9e122aa65abd58445eb07641dd2b9d5a059da12b48db109bd7918110e254262ed85f12db330e235756489600c6a67aa3f56fcbf8940dded78be2cb23c3891fc7e9ac0bed436b09997708acf66099f5f5929f4a33b84bb3005665a559d26f56c4ee2f9d062bbd38176c07f92a3580b52257cc81990d0b90a2851682e3f61e79762703ecec4b6837ba0dc88cc72544d60bd630bfcca545968385aafe68f8f1c3c64f7ca8e6368b7c702af1bd079323dee39cbacc3d41e7dd6340f5d71fad3a0c8ad8ef6717058a70428786cb1a6d55cd49c49c3453a3618e2222a2c6cb284a682f8cfe4cfaec678aad20c15006d8ac1925182cd86c5d91cbac9379824f397d6693ddf1367abc97c7de8e3097b101a66b522d00f5783f0170283df0686faa84ea035ec76e3ca0dfc46d5ed2f0f396ebe13b257852a42d85b65f6f46b633516a5e3f2110f7100f9d2cda014d644377171b50fb15f9acf61715555f600823adfd2b7bece359d3ce3cb63c22246f6526914201ca77205982da0c3470000000000e183010000000000df3e975b4cefdba65152b71a66c40d41de744705f3a8c4b75dad6b3a81cda322df563b4d8a47caf4912f536e4d131ddaabc4dd64c22e219dcb948807e45d0b0d39c620a11d6139f6564973794837a6f63c5abeb9480958c70798dfb6bdbd9f391d3e0200000000000000000000000000000000000000000000000000000000008ea8e07de66daa7ff73b5b271f6b49e4750d7c776cab8dab49d7b3353a33e494609e49a4cf08c90265bc097c6602337aeb1fec2bad8094559a65f456afc8f5dc170283df0686faa84ea035ec76e3ca0dfc46d5ed2f0f396ebe13b257852a42d85b65f6f46b633516a5e3f2110f7100f9d2cda014d644377171b50fb15f9acf61715555f600823adfd2b7bece359d3ce3cb63c22246f6526914201ca77205982dffffffffffffffffffffffff7fffff7fffffffffffffffffffffffffffffffffffffffffefffffdffffffffffffffffffffffffffffffffffdfffffffff7ffffb1afdd83343d209ded369536b05bebf22915382bf0d50dd6d21d5e7e146a59a794cb4c0f8d7b92164e2c31a3b64e1a4314c1019c1dbe142bfbb1ef70c8c0f39399cf627eef2954607b7716aabd92854c1a8a2dca680a711c2bd1bbfb7664e892f7c3470000000000" +const lightClientUpdate1Hex = + "f6c34700000000006db605000000000032e35433b6bd8d415ed3df8dd54570e3e238ae6334e8899a54bf9804515d8d58293fcbbadbcdd05ed855e2b2286654030221f4177adc7cfa7d39ac414de06a9aa3eeee65a64fb83399522005b11170f228b21eef30be6f481d30695695064ac29484fced5c619a27d9d61af73af33ab3ac72b64b0928facaf8ebc967d657d9e0f3623e752709992bad1bdf9c3a3b253b8ab10cc8099a7e8d2ad1a4d0a537aa7bb3f04d57f877d3da9e2762f3d479cc18e154a9f4e7f5879ba029387f5916cc8bb74902e3aa238a69197b56f619216f2cc099eb2778eb3129e54913705613889720b33d18ba1e4e636dbc917e3d12552a8b80447b98a89ca3417b5d669cb5e45cd772619820b6071e80efd9cfe8c3898889c38f2a1be7513813b59493a43578e584370e937d211f8fa5ba772f1bb8aa43b4917f39846f1870cea8d2fcb2759643a6c8330ddfa5df4868ddc5e2458f7026aede0afd24a01fb8c7eec60cf2feec1eb19a364573d1dbb1d954a0a22498bac237db96ec3ed7e9c90e1cd1f0b59b9ba8966c02169af100668639cc15f518d7e45b3eb1286a64086dd1c0e68e099bd7689dbef697b17bbb01cf902dade9678a0eb6078f523423e9ff2b4ecda9ec00c58cf0f0aa7d8504585d2c379a404c8e4e7ceb6c82ed434838c70eeabfec212238a68210d2ddff385fddf235d9aa5d4be5cbc8953925ed17f66dcf1e6af1cc16533526000037f902d3b21cb9a5563b3816c0af8eb9496c74abfbb3ca2a98c02162d7715f846aa5051680e8f0bed3ab191a47d683d70b6fcaa5f9e82012948e68b501b20dfeeb9c8f121e30c4943390afe5445053be1d1535c13b35abb366e637cdc34f6691ceab31ecd561eb429a484ce7bfae2c7e23a996eaa1785474108c574d28ec1068df44d404de6302560acf7c3c62f06727f84b2f56814e23402ca6c4d2f38293d1f59db356c0020bc43bc6fcdf69a53c417a06c1a59f38e931796bc7318621aeb6ae4f6cebde9141055d25eb10e9a1211fdc56cb6d8ce08d9e2e8d488c17394730f062800345ef513ded3bf3ed27315aadde8bb01b80ec025097c547ad8f949ec33d116fadda6b5c39b84339279693a5d20179606df0659c30386c9e03fe133228c83a372fe2158aa19c3e621159ae5af7b06bf86f2a7aabb9f87c0a56d3707002f68cbe835f7045466a7dab4a718356bc8616b4dd23eb0ffad1c5dc33d9b07eb9a2aa665fd27941d2b045d85ae7537078a2065f19ebb362735ad313e850d80cc397c046ac630b7aa9098668744e8b9118c8f2aa24799d11c9ca0ee6f4abc31ae3072896ead170f1cc93ac4969f7fa4864f210debad59b6f494c85d11f3fab98ba26154f66a6f3fe4c0254b48af4293a32b0a3b9fb21d0065f3ad6bcda4709cc1178052e1d9b31f4b09a4e525861b416d89637d266b1a099b3fba6f7d60978a8e20e47eb246defc4e2d75863d2e357dac3398fb5020b88937f69e84311cd950e37096e24b3a18138cec0e0f17eff12109fe5df224cf86f8f18d38cb1fe66e0e19977a7cb6509eb9473b319f2e8daa3d84ec5f8b4482fd9205f043e37888d5c1c3788f6a24d1daaaa659f9325e0710d5e3a3898e5a14a7d08f7c8cc042ba7a216185a27afe610556e006a583f1ca53056b534873fbd1ecc4463108f3b1ee13c2ba260c7d354884796a22f1ecdbe779362dc25b24dfea81bd98bf945ba71e5bf86a82aef21816bc143de5c83353288672b1dc781a1714596225e88ada77e2db35e04b9bfe2d59b2ab60deb465f9284016ae28e5b82a39ae32fe74eaf1724cc13a5f361ff74c764afdb8c300d0475769735f38f616a14e876b06ef1c5bc7ee953679e64bfe6e9200b00789a632d9da537bcf58bdb157b6f39e224ea4209b22a99934c6b5dc996b7dc8b1c3ccecbf74688bc1d217fa4a76c3cdd709a61610c6a148bcbbe4f4de1e6c4f9496e0a1071aba36c3117980cc0d59a004d16f62c4889334be3ee652029d71b8020b317be7900dac2837f7a12422ecfe1c4830e7edcf7b349c221064799c9d410e1570747061c3f9ff97428aebeef4ac99cb3ac49d1fad4084458abe87b9f5ade10d1023575fa977025dd44e3ad1a925ea947350a93b742e2a16ada62a2c5311d8f931ce0e0829b405f1947a732a7cc201aa751e425bab1f6821539a72b9f2ff667763c57a82fd3f73bf0be6e9e458c74d0350c706fbe23c6a263333fc947e97ba1a68185ae56a7ec28e79d7b28b9f5e755e476f5156da4cdad44cc8235c3cdbe5e365b21bce38c98411bbdfd53d2bac0f210a159599091970832e8646b51029a89cc31e80d55eefa375c2f80a29ceb466bd39b0c7307719e3d46e2cfd5378eacd712b76aca5eab2f7a0027f02c433e07605355ecbf268f4393351e426f72751fd35869dde03a939e2d3c6edf48cc41992d9008973a1c877bfd105f584e9a8f9a80773ed04978bb4e7c75e40a17c832b97da60077309f9107dc4867c8a9846fe570f4199528d9854696627b9e38edfd106a21fe75b2b1acc0bdfadeac40b6a1f9049d4565a52072ef85df826be94f54da5c1307415b2b974fde09add587db05d351d8fb5ad0cc146551d4b4b4e9d6f50d5eacb8bf183075a8afddd728f3f64a587f0d1d22128994f2e7e59583177e8d9c35c28f7ea49a8cf9d6829baa7bb06206cc0d50ee0b9e7fa12d8206a6f8d85a58c3e321e35fb98711def57fcf0710348a5343d2f8668daf4cb9174df54fe82ee32e08208bd0a25949e55661f8615ceec260b5fe632996a7e0970d24e3eb7eade45fd4f7941a0474b12448cd20d4f74f9d9335f4f66d5900cd65d9237d0d84f8613d6782b3e9ec86f4dfc35ed35c1c527a882dadf739a9c2b2fe9bd2e06ffc48ae60e8848c40ec2c8247d1b20ac5fa3be82a9039bd60b4a2df1da0744320c958453c939e257895c6ba0fd071dc53f958b25a1624264842efca70ec9e76b7da5c8470407a60d7e5b5713890aec8117ce515c81738a54be557ae3d4a21757cbefe33ba4bae817c0e7f71cb9c1e148e7de0d8da05140e54abb778107227890e8d6fd86720cb82f9d291f8a548e8435dc5386b26989b6ff9216950b78c62b1fc431de15f7eee2778ea805f27a1e959d510453c7da837c74fa0dca389e8d13847450ea785243ae10404d416266a1bc932f083f92fe3e164a068b3e49df8be84e157d6356ea3ed35946b55ff0c99d1b823976d808cc773b42c307202251a3f8471c1c109df4c9aabed858924ac1436d5d8cc138ccfaa9e75c0690371aac2f857ee745e3c52d867456397a9cd0faf88784c2098ab03657d73e6229622180fdc6517b38a6aae550db6b452e4fc1c96a72fca3df2a4524139126170624f7ab45bf06b62ac9668c4984a5a7e99cb26a1e6d34b32711c364e51c302f7a595459eabdf7378ebd1aaff0dcc4f005139b6f87de4539f1e8afe920764b234945744d0c10cd3fa025cd2ab4234b243ee1230ace37531767a8e24b8e0c55e0378c2f0c766faa3458172d77d536475d186b8566bb49e13c333e2db0aaaef312f4e156c6ed74ca41b607bf34e23262bfbcc2ea6282179cae7e97a5cbdaf932621af355c50b2ec2f8fa0cc93694632e517e74edf1060dc138bf9f2c6c26d77d32decde9738235d7fc7f6a1cc0f5d07b0ef94175f9d39522ba04364bdbde6f39e1b411aa038ecc4c063a34157a31a23fca632dba59c403f8e416fac066c8fcf3924ac85853387585251477310b38d9b94432c7720713e5a312c373c19e5eec495979b1cf6ed387d9a6fc2492e278395346b924c271aa5253eed306f4c38456b347a22f1b29fc4e6c4f790487a89ce60a9539d358d2e8227fa265941748e1fab3d0ba0939bdac9d6474618e48b83efa3c8a2b5681060cf523f14dade122af7e506fe14288997c1bc9b39933582b63039b238aae2e1f9368b46e32e950bacbc1c3b4013143685b1db6abd1f01c1008fd523e23a7cca9d45d8f468dce35a55a9ceb160a2589d70f62b5f95c1d3c016796b41062093c44df5bfe7faa062e362ef3ea9f6f45b2a67481d7a5706071b27ef49fbbaa75410b26975dd33010014069b451d690bcbf52c3ff674238628d2543dfdea9fc787c893b83623bd7f4361592ee79120aa5b26fec714fa64177814a47c713fdaaaec749fe54baf97e06b47aef5017a25d23bf9938bfade298406394c9b93a63291852c2baeb0b37cc963b5b9c4f27851d2e270005a80749f758ba559cb6b49f87498fbd5d7cb73b0b7c80d1b090b8357af9d839fcedd639d8550964d27ee9515b1a8b41e5e0c043c6c17d1d720c1937135f001c9206246a596b5fd44171c08758410c5b4732d9f6cc1630579b042716d435c4a28d4357ac3ce6f3e82a8271bd0dd4c2a651f76e9f27e0296a02754d9e4821f9e097df6cb2bd1dbb71b3062cbfe6bff628230a77040ac1eb7de4366a8f36279bab06d2bd344c9bb90d6fcddf755922ccc7a3e349c0a560913ad49709e7efcf41943f5c8e4e922df14523d51e6b30db99d724fcfc7d7fc48a9613a9f5b3e8038d477ea53de48236c4d4cd771e6876ce995a75d9a6f3f6fc4ff7b881bdbb4e0fb8df6ca9b77470b0aa01460c224c980e35da171b24cede15c1751bced4eeaa8324c68e6695dfbf9e86357b593d6bff145d6361aaf310f9e82b29058534855b6b1f3bb09e90639c4eec1c7b8164b599222c1437a68f53f5d15627ec35b08b1bb0a1a6d6b23fb08dfda5d20b381396fab8b83b5dedb1179b39afc4e9c379214432f1fe394fb870a8af78fea7ea6349152ab9596fe060445963623cd14471de897a03b98a08374549c7e201fdc6c741191459c5e38dc8ac6692f117536df5d63537bcd49b1432a0730011e0de910010fb191a793be051c315be14efcf7ed8c8a94def8a42479f19dcbbe2e182ed24c22fa9e6027490215dd70f05b476b0a118998f6adf755cbb8ead017433182a4b12ccdae2802070b48c8cb10a2978dddad9660d352f703b760026d8fbc270348097682c682de1ab12f707bb6e4495613f1b935b5369a2468c7dcfa30ce9e10bc52b0249fc80d078075da3453fcf040d6b785b94cfc6c9105180101533436b352732f56d1fde3462bbe4a1ddedac24528543807a828fe1c79f67c3922dd1b5a37ed76b6a8889d47190a2b1afa3f35aabf67588577963485a1ba82c3aeeff7a3cc4507b8cd9aa3b8b02ca24321b8936b087d1ea3d5a0cb4ceaddb59845c7e9b654116c4ad6209b57eb53f7eb79b9abbc16864e34d3f184a61d1dbcd870a4c5d771b669b6ed51b915739a8223d60807575f5f1b85b659b4d9c608e9de4c6bad60b9a9c8f268cc13a53ebae4eba007dfe11f279696b598db9480ddd603540ff834a838447bd1226a8e42afed5fd0485ee6c92b53f0044ec7e4d01ba22d2f89b34d2a4573aa03d96d580699d7289c6438ce0ae04f35b721f04bf410c252afc0966a12d693050e6b408220b641bd4997de8df95a9eae45e2919c30ddfc079ae7aa809cc68fda05d7872fdbe1804c1a9fdd6d7c9c4eeeae74f65b1c1f9723e035644a337f0c846cf489e0f9abb4501486e6cabcb18a598489a6c6c7f4ad36c0dcf083acc5d83e858e03f25155b6d2552f9f6916bf81937a8e3c457a89b6418b125b56bbc7ca40c16a0247baa8b2126aa35048cb0f151f212abaef935f6dc199fbd55f575142acf9231989f1a7e64c73786aa8b4ea76ad171207b61d3f9cb2ddc6cf9dede907bf709828c845f1e11627ab8dbaf06946b30528d16a5a8b50d3a2d9946b8f37640fb734245d3bdde526dea2787d604d48ab9c5dd79af0df69914444899cc0aeaf93bda573f4aac640160086ffd4985e566b43c360a3258a5c80f04d305797ca9be2457a02279a988804d2ae15b38a3b66821b5d3fea6fd7deefd65b6f5fe7a97e5140fb36e035803a568aa7274b15a5fcb30e452a15372abb8257d7f7a91437aeb60612d341e62ea8fec26b447e8d2e02a49a3983c6d4a9a060662a4ed93d0ed874074083be5d712bf4051f37d3194e51a5f674573985c94e6867a601746b418d17e6fa28eebee53e670d26742ea7559e60b5afbd552b804cd94cfff0f48d8f91b34b3c63630326d266ca1ef364999907dfc51b8e59c590507a664d19fd0d188cea0d3c67b62a7e199995903acf0337ec8d5aa2dde760d3b30671d529a47a039d9233a93243c8ad3d6089d8d24ac41c3efb3b1f0e0c3b0bda3efe2baabb3938bdad08d0d73ff9842f154a0b1615d013959b2d400d7483c226897bc49b0571ad312fda3781f4e8607f93177f7b4112691680af579cccebe0270b727460432e0356b1383287a6e07235ac22188699db5978fbdb45f4710520662a4265f751f03e9b94b4b68df23528e91e3de52e73d96f5ca3e7b06d499cd06cad4ec84b17e4248865add0854361946a6e868799a30bcd4ea115b6dd14b6594d9f52a05c47370422af37eb131044a4a9cc510ba3d60849f086dba13e49f7238a3b6f27343ba4b7528d71c87f4ec5e2029ec4ee4503eacf71ca1ae03b61822bb1e3a78aa70e6d46ef51d65911da29342256767ec036a5c232b74e0083fa3086372b28cf318a59bffab3d10d3cc876815d96f132223d816a5bc2a68ac4714f15f01c9ca2d2e2fb0b25b824f1e5b2687ddab6427501e9f63a1a4ff472b444874f6f55b821d61aee035360fe7971e749c469c2492e1950ab62f5a6b87d135f701f5fe5fa878cd769f03774f420070f13837604b533d44f4ccffae1f5ad1c7b6624f6c0914bfdb9dcbe3389ca76205560567cfd2925608b89bb1cdc7df4aa6dac912db40d9746a31c6bb130e26aaff0d8feafc0c9d09e99eb8d03887a10fbbc9ee3bcef5e6b5b3889d4fa0b3e4be2cd1d60b273530cddd6eb1a7f8bc5b8facaa8d9de1189427b2fc55d81a48a7465f7d537a3e9b8c245893c52271d7e89f594e2947ff743bcefd27a467aa0ccb8fb6cb0657c520a02723f838b6ab49305f5526f6bfa44bc2c7d27a7e13d75808659c0818f818aa666d17f72bbf2b86f1bcbb8c045c61bb0d3a0507b5bd7935697dc861d27c3d35c256a4c7ae9bb1939dc565e2b3bb0b7a2adefe7c318897a767e456147ab2dd2aed00d27f32aea89c5443910c70fe742f32fe7e452a5bced9be5e76ce756155fb5d084c485afae9880576cf8c1af7596b0224a08ff954d99e6f9ee3fccb44e756e492f5ac80a87d21df3ab536193d5c6b1c19d48969825ec7719a53722858600b59850014003bdb4dc8f1c703380a2e229528a6e391212d7a051511f4b27d474259403e748dfc9e8b5a900820dc452dd4d2138ffc2ff9795bf904cd0da91b37c7f05ad7c066904d7d2c91ece42ea3d0b115529acd585c8dbb03cced93f5385e6918acbaca1dbe88f044c0b1069ab98ff34c0b0cb5c920346a8136318e0bb3f12df460c26d052be573a6cb0ed285d2e4925f7ac32c280a1b23a46e6509cf6fb58379f2f5c55a66c751bf53aade024a03611a6c3f2db3ab48e73b2538e8209eba66b79cfa8e42ecf842b90cf63b9e77c2acebd63916f325bc31de03bdff649111c67f774bdbf06805da37e4655089905f540771970218e0a933092c006b6bb1fae36cc863f28973b581cf00a8f744bed81215e35e56997326330b47457a0dfcfd7b50d35259fd920b8e911efdaea9b739f7f0e99b278b1c0247d63f35ae923b3e68c9054391542eca3201a6c1afc221e4d6fb2c9f1f2fe0da54c86a45aba97960b5605dc7cad330eb9432cd3c6be9427529323880ae704bf37da52f795beac0d8931d15b62e9b25db86d8dba2dc6cca755800c0ec8bddb39df325b8b15e195dd90d06d6b823b4ce0111e685957cfbabf7c9ae2fe1e7d1bc48234d30e6d8e2cc0382b7b5d03b689157722fc9ff9d0d7bcc12456455f172498a236044ae3e4cb55ddb2fac4259558f582e847d85c790620cd5e270df8d4f7edef99427525ddcdfc8854f56a142a3f3c94f6967df211f61ddb211be9780b626c955b30d911cd66780bbb176b4543e0ccb88ff36a1a2f468cba53253a5a69da3c30dc9103bb35959ead6e1f652de9c2d193f6dc3e8ce19268922105688011b41e54e4a74845bf0d3fb49a74b3d6a6184f96ed4b142f66ae948ec8975de37333f58bb4412da2bec0821e0f455cb05f4ca7e5b5217f756cd80a3b5a331093a38e15d7c78d2e632520476d0fa9f793da3307861c359ec1b91a0db6eb2c71c85768d87c85f1bf72aa25d8ffa833e006701993b24003759f7c20b39112535ed1493d78996ececcb57c2d0ccb5641239fd1847967f6a5e2655998ecab3f367ecfd6aa4437b413c7263d2fa2fd5228466d3e2b95a5f9edebe5e0d18dda6d3845f01852a777c03321121f1b685d9c65def15df64589f24cc1cbdc3ec663cf36d270877b46a00013ea3825571e62552903f5d770abf2d538af220a88fd5af7d4445bdb4259b2afd54409717e60338523df8c6ae1f8840abe9b3fa3389871df2eab6ebb99d9109195df1de925a2837741a3ad8fa5af0da51e3dabe0a8c7158ef753f1adee7baa9d926919c8d70ae202efbadb1665924dacd8bffb33d3099562801f7a68df2b83a6458a8d536dfa80c73d69b1a54788a506750792626105c389231848caa0d37c1890a9932cec256c32eadd663c90cc5e5500ef5a8cf33c03e490ad1d58fcf0a77969b51c4e1606ba59fe7473cf28427cf1e2b7684e53b2bc3a80f52e652357d834f076345fa2db454cb55fe7cadbdfa0fb1395f4a1321d16090afeba4f39b46327cb33abb6ead875cb80ed17d3de211d0a238632159dc9bd603ce7884185c1a2dc851707f9f6f9ba68592313a38ec485566dd206a2c2251eafe026583f166db15d6ad9fe38b4690be591f012656a2fb41217dbfe43ec0d8a9e0670fc160ed2867386e0e29d191880dd1e53d69ade05265d112e26588bc807f69343964144b9b8482f555a530d9f646c2b75f8587e3849f7fc15498f42c25fdf21074589c863ec75e6c1359ddf4330509b916e58b4beafbf6db8017993271072ae1d53fe51a36f845b110755541634f2d5324fa390506dc64a5548d57a32d65cfbacd66e7281a33fee60aee3a5c80e1190618dd96d350810bc88cd549ebea0728c3d9cdef033e6f498851e7c386158f5a725748e4b748e0795154fcac1bf7cb26acbba894b1c2252bad372b37591fd529dbb481a77b03e4a7fc11cb0c63ab5dba3007f3203ca8cc4b41ef5363dd634bc10e69186f4507c714e84c910b4df0dc1e494e31a5bda06701eaedd21717fbaf98804ef60d14ab072095bb63d73398cbc61c0a53f73aa03fabd3c2948cc2eb3c96bd18fcb9208d635a750abdb038c2a1c2e7da5b7b84aa50d04dfc4e7d3fdd93304d8c94f519dad2918a24ed45721f4d55af460e955e90b8927ab8f74ff5cb86208ba6fded969b7f9b688aa6f14bf353a4e1fc6fb9f6c3c2454b6b06311155161ee5dc52b1f7162c63002a89231887c2fb20d2138cecab157b91a00ad0f81562f2b269aab830ebdf9b774432ec92134e7e43a3070a6c9a878fc57e288550751d9bec7e4685d05ac2e8be7b564a96b6a9a2bf4c16cb91d0c79fa21f6b6b84b70c2265c1810346c47bd87fa1de178353620ee8a74e6d64294f7fe35e02c74d87db70a91250ecc411bd537870cfbbd187fa919e51d527279915ad435f3a8568e92d80e6519b97c09a5535ac594530b84dac45898f3b9c4d564c7e8e2acb421ce93cd6f0402cc1f017af6fc93754b75a7c7378a0bf9701895893f5bf2eca9291b27f7b86b84d33cf6d2df00db8147088c4146d9e1b33cfe3c2955456d856da07cf59de97487072fe1a446c8e22e6c5e78dfe2dc95ef7dcbb9520ceabba3495d9c6f56d671645536097db6a9b8e9aa9ac7daa7935f69a6f70992b091943ce6b8ca773e0aa43572577cb1490704099bc2328c37dc7936758182000e19ed5ecff66ca5d0c6ca5a91fe36aa6770421ef18841a1a99f5b29ebab323bdd47cf5fca51f5cb22763be647d6e05cbe9bd462fa71e2c19d05b3c0329ab7a5ac4fa458a30879aa8c1a3b5536af2eb5400715448928231964e4d2d8b64fdbc0c5eb2f231aa185501316fca6d07026821e85326a0feeedffce9bca1274a77ce682a9d9ab5a0e1daa93d5d08bc7291dbe15829b4eb76ca7ebc60d9d862c7dbd86e3a337a429b635cd765191d74a4d52fa9b4a12f0d65d782c235fadd74ef46e3441264a7d5290d91d6eece78a0cb38c83b18d425a96968eb4b8254648b15ae9699ca43bde24a90443c6e05b0842bbdaac6448fcdb3d0ba93667597493bee287a47b47ce9f6e792af3fb8c24e839ad35f9456dde606de28d418473d46311037d5c30358c7916f877e769a6a1519db4ec99fa2d2e289468bb766a910000fd71fd01676f8fe269ab9243f8b0d74611ec4a4ebafa18ee1603c32474da1e3eda9a57910b57d85b7f669226d47182cde2433b5b613961e4da1a6038767decad62303bb7a8996528c46caba5e360f3b97f5885a9410c86f687ff8fc2527250f9d0bd7042c5ec7f8f1f69f72b200cb9ab1d74af5c1605f19b18a0106dfc1c3d9c51888db12a9732d8a0ac4a18ddeaff383445fb63394d44cec0f1e3850dbc2687ccd11f932abf5c8086b96717bcc7dbb5338da6a22ed35f07dd9dc36f60acf4fc0ad69bcf95d350968e3d4a4487627a57eed5945faf94721303c9270ad79e3143a7649cb1724b358e697c8ac70a14b105d21d29af7ad7c445f0c9558b25aca616620d66637a896a21d2d86912a5bdeb692bd21f8dd6237c2eb70132ad6e97ddbb25bbe23d75fa848615f2fbf998a199e31519fbea2db7bd311e10ca26003bbc4df5c4bf84bfd3149370a6e0daef76c59e7a7730d4c891bf347e8cc35d864b30dd0dc72ae61b2a83a26280ab290559f7595c743e8a78b054313a5f2fc41da5fc60101267ee07bbaea42caf25b2553cc62278eccf9bf0b7b6e3f739db078cb78e6499619b8a5c71be54d0673845f2ca04331b5bcb8beab78a480299561b1f57611adf31a9edc0fe8ec7177b63e5828b8ab3735bc4ae781305b4203f53040c2f2d7cdb06706d6b1ab1aede9606b100ddf7b1a8314ddd200b67c51466a6cbd810566907d484a61c86efacd1f3abe1c8f92a9a6dd978af58135ec4cf85e7eb2d0f2edc745ab5be4eb7ef47257c4ef39bc5d333d719dd88a3c74eeb509a358900533de09eb4e8690806f362cb2f78ab9a792a65d3fa560934208ce4cf68da1aac757765157d0d9004804ddfb254b1ce76e0523b2fd3d83cdd004b15bb8261cf2d16875f3f65e7de583172a49039d5276560b793c0a71aae33978d56020abf02d66b70ee0a88a5d02deca6dbed53588f1f6922d9dcf874da1e1497a5ae9baffbb2602b45e0ed398595d397aafdfebe5806c0f596e203029b5de38b5c7a5ffe00847f04404fc725b20434dc2ed5b470d56db1c39dc80d56a951df5b7ba3aa497d5297eeec7a673f0d4904a348b902bec02480193bc278fd6e7ded2f07b9fe1f4dd95f1dfa63f669b53cbf385599d1aa2b9a53a823b00ae82d41c215de29c961f56db95498b03724b9fbc762d9aac7d961b8df7b9a1518efa169b060e7e41f990995e7682ba365f382816c8cf3368ecc0cf9f7b7fa49fc5845ad3e98c57cf1a19acf7fbe3dfeb34eb0b8ee8fbc1c2bb7ef3be1362b846171859d6113277c5933647fd6fd8b1bc8dc2c6d8e42dc68692390babaa3b09b6e3c8275949221453016d079a457e9855101d25cc259873f9334510f0f0169840c85d148a0fa2036cf3f04a38b13d860e214a429b92f73748cce24e56cffa91fcf2f4207be70e2d056fec9dc3f81d2cf2f6351d5ec3cd4c01ee3e8c58ac2dfdaf031992f21ffb514f995a7441accc2134ca29a0b0e721a964d1a302a7b5832b9698a92f83ac50087fa70ead562385cceae94965bf4bd1e40054c969fedee0c73fbda48f42f070e5bfa98dd9783c1ebd1aa9da6de2acf0a284fd691da5126bc77b26ab0ae956a31551cdc0ffffe9630a2cc0cca9dafc125604b607b33cf40db292369f8577a8679cad4b7c9d30152eeec7c00ac196e823985b8d27361ff1b0f2bd7c9f027834ec0dacad695abf774879b661fb77a65af20d6a87d386fafd64f218103a8d30f5905b4d2b386a2021802fbb8ce30f435aff6a19ba7ab541493793b071fa215df6da81567345f3d1d1b9f098c2fb63338975e37525797c6d2a7774c1ddd2da3d0730a80521077bd6edcd4670039d98e792f6d98e89224eff20e05864520801a1b1f2bf3eb4dc794eea899bee13ccc5d5a5328060d0b2bc53af654fabcf56a76b36e68a7c770c5558b7df0869ce78fc836a2cec7e67afed6162ff257a89f2eb12f36972a40e9ca0e88281179a861484094304c8034abb01d8cb0eaa633f59335f58da35be6100de84b8af34a9a7a80857f8c43f27ac19d8fa3b1e9c4c5a7cf68053aa4e87a6bd916d6301d290d038f0431c28ec38b78da5d9be2daa819ed2ea76079dcab9450045fc661fe036d9b63d8c31a9d6ca2b8cce56ccd136e81f781e24bc387dbdfa4c9cd5b632d4a0a3c6423b39800587eaeee3c8d23c5dffae1ccf7e89381c7b7af40170d29968ee68cb002d1866978edd21975c8e5a835ae7d1146f998b7d6c2bc9463b70caf4ff49c244c1173f9b9e950d627edd5cae3eb0092130fde91174dd36c423d96238c4b0af1477e00298bb1535a0273110eb0b9a2d4ec9cd4e42b9e86ed4c170c8e8edd2aa29536564028052e53f9acca43320815697f1fa8b5b9dc8116131ed31ebc54b84250174640329257329e0914229462b799fb09404b67932e95f8055a97fe60afcfbeaf164b7b38a0a79f0ac9218aec0e07942eec2e04f5f3e1c79eb4162d297fb43bf7bc854f8e3674dfa9edf173376e8ac50512eb506f465b3396fe3281f3fa92ee2aa6bed1b221a7baf4152764fb168751e216ce04d966bc04ffb494fe6008016349571a26cf2ec571ea37be58c98296ae442262ce6ac41b256f273a1310e9acea82bdeef3134169ee75321e8fc7e959682940eb6cab0caa646ee37ef63fb1846fcefb281ac5641e9ba10e70edae8b92eb66101064b5ddd0f828555273492a89a08c3db032f0686ac5e1c8d43abd777ee3a09b6dd93aa6215b7cea0653268f8b49c2f1b46cab6e0788b0eebc0aca0ba3f58f825b736bb5031fa4bd713c04f02bcf5cbcf439c75359188b6b1e2c9f1f99af10bb425d2d49f8af6cc9cdff844d3912aa59ed8d8b0a7abdb2e00a4400c48c140c15b60fceac620cc35f20f6ddc8891d99daa946a876aa485c3514241ff4397731445ec9ac67815c2d1a24a5e1de1d71ef641228de34831eb923b4bda27fb188a78b80c7cc6dda188dc5f25104fb1e66bdbd917d1971740f43cf99a80976d267617e2b984d674dd5d2f15199cb97b5c5d724f5e33827b8ec14a8e9283a5ccedf7fcef8b3384c74aa2c4e36d32036ce82478d9eff8e070b069497a80ce1079108321f8a4d273486f3f981b7d405824a8effbd4343c53e94058fcc57505f4cb428435d122015961d1d40d2df31ad73a65dea329b6a76e48240166b1d0e2c85d83f8eb4439e4446d8106e1e43d7ac5ddb3e034b4ce7db5c3c3e8f7a811a2171af0aa35295881310def98ea8b3ca0e98f2bd88b2a6f9528074737eb7f9e577e007a694b3b83e40a1a8d2b0fd1994ef91b9a67759e230c06da53b0c16d4749af6749fde0b8fe1d291e68d1eb185a84b83863418c0dffd173ffc49307b9121cd6a8eced3ce6f0d9d112dc8088cb8adcae6ad2dcc3205e86d081c8628a9d60ccdb97a9b8ce6ee2f1ed57680c92a903d2f578abd3391820c93aae86fb3a08a00516344692de11387cb5c7fb3d0d5a6a66e6e58fdd2aa7c5ea9aa6716d68685d2ea5b8e4b001d3f299e108e3dfba8e8ae7bd343cc2563b231334a2eadb44222c4b7fb238f1a70868ec721d077f08c7d5de6af915d44b3e55aa8e2a374997c2ba4dc703073afa0c7fbc48c69381ef048b8be35faac45ae2568390d5e12e05ce5ee687d8d8f3c90a52edd72df6af5976053a4c24f023995d02dea832fdd6a768cc9a73cc27e12fd3367b68af12fe8a7f43776829830df69d66fa84826b105fc84adf5afc6a0a4dbc508306d062583e7fb62c4cfdea1807f6819d40db7cbadc764253a71ae75e6e9ab4b377733534940af150f0649305779aff699c0cac23ad819b7927c4a009e03b2949c48885ba0d009380f278ad901fa2685f3349ddd39b4c22854ce0b3aa4eeef056979a0d019a88438b4e16ae6e4bd7a0f1dea7ea3b4df9625b0ffab7a7e65f1df3eeb713bbaf2649b36244d6ff4b1d185bc423a572f869531d0cda5720ed51feebaa3aee028b5f87a1c42b4f39e4d2b5c1747e95e2883cc8b894dc8ef69806fd808f95df787d023226c06ca657ce3b9fd17864c743e6d24a64f85b72aac2897971de5e66873fbfdbf1b0ec91209d6c1a546057ebbe768202cb159e51a288b7a18c8200280d472074d0e55aa5018aa443ebac85750c1cb74519c9973637a4641f379e44e4a04cb5930337bf4d6404210aad5db5af4b1056cb51ec8b9e5c188f9e4f3c3f33ddd6e052fd6c14934ea984c2c31066dd8275dfc4aa194d25222ea8cfa31e93793c2c034709019a930d623a43563537c2d38eb61a1861a59e90e4eefa4b78cee46b2f9ef3864c1e68f04b5aa16ab3fc2200e84bf0499fc8778f1ea931156267cbf89ed483c072703aa4fd7340cc53a3f1ccd42889b7ba7cacfbb82d220e62d75314e779312e2d693d6dbdca422c686941817793dd5e7a31842e0c111a5c351a671c65a062f056537e4e9def9f1a5b3cd63ed9ffd3b6875af53a1ba25cda9c673f38c2b02fc644cfffbf37b65e178a2b9aeb962961a796147c436d51317115877879ba374149278b6b10c31d0cca967818c6328f7f10ce26b28d1156a3e77c21e395812e22c9bdd28e1e4bb360c7b2cac99c4d8f60b7ea88ab1b34d91bc3461af2855dfcf886e1cca278d40ce604714d6734647c2fa280204fdb41c69c544f2e7c8be1f1aa10829b1afc13c9fff0f42b9f0693fdeab1530cd0947c2bcb9b0d0730fdef7c13f7824ba93df57f8be50b6637e4cd7dd0156bd9417568d5e5e8cf55118b00095ce6d05469aa8c8b3c86bd5a72c9896574366ecc197affb314dbedf636220f6d846cef5b0700f6341dff0fb99a9c9a575b4baa095dd123154f43bb3cb8e738ea2c3029681fd4e286a5d037043bfc2cee4c0eae8944685361daada9d9f7d227acc8a37e818f48a2f166b5bf323e65d04e1044ce6a70612478614646818b6fda13140e6eb98b2933cb21fdf69c2522f11c766ce234b52ac713df0fb4e14bb71bd288c172a2032c751d9416e3231e85662aaf4b1c9b6342fe8b9b48d305c303804dce4db4098cdb382d0d6464db11013035f07979905484fa4316ef7b3a4695292798f680aa375b3f837c4c89eed1a8996be1c85284c601497ff22f4684e42350ddac7e48f02e0bfdb2e90abb4e37a5ab68938565c89c5443e8427061ded91d93fbeb4b4d95eb2a4c0b64961a87a018548139656a2e05f9a86487d042e01f4056a800d74a8b5d9b7f2dba6f8f7cf8fb40870312d92c1104eccc09052010d4e160cbed6f8914772dc2b27f613fd12cea552584d279297de89d4d7254b3777df7c872a47a3da8a7352329d9bbb6c05df5531efbb3df21c69e0071635ed41167442009d47690f8c5df284dbb03f558271b13c802c2afa4f807208ea89de49fe2f50c8ef6364dd2c02b835ba944dc5d150c1750359c6e5a49993110c00e69516cadba8f618c5d7e4542740482ab7eafdc2ab06e4744dd72dbae232ee90108c27fe54f4775ebafea9776e29243eb0d7125ceb55b49f1956c94ba2041aa2a7acc4eed84391d8eb443cffc32eef00eb810a6e45a70ee15f9aaaf7337cdbaa7e69f1ff59c0cb5251c132a43cc4b1dfcd90767135a7060fcb6052cc6c816c1740f187c670bc5216027bb5bf9da4e7d23a1c6c694cfde7cf5e841cf1d947c560c1c362daaa21a7a4fff70c6718cd54947e3cfa04b7f4d65138e3b36106983dec753a567380d2836e15c499bd73bd79d7bfc76bb10800253220cd1739a593d8a28dda7d09722cb8d1b758827c3b364d2c5b1ecf1fa781e2a587fef71970500249487dee427d399c77041360411a2abf0d87a51acca3b102cfdaabb190d270dc0a90e2e9e220d3743ec144ac6cfca5a62c196d88b2354b8967c5b1aa51772ea28e66426508bbb28f70d57a866ace5bd2c01854b9c1107073585d22b33a7ff7510e5172e0432ad597848f8260d7b6e77954110f81d05abce32f7f468c733fbdfa6433657d3a1416e21f1adcc069e27dbb94bbec8f14b3a1895281bf310d4f9d729972cb4c9c7b898f7323aca5c251fec619751a820257c177ca7a164773219b4809918f10308d3b2fb2313fdbe5a1078932c24c27b75b495ad1e786b8f25cb7c9768364657966b8b18d334b26ea9790d8ccf9817969413968ffdbfbee41a581e9011540e6b471d4d20b5b1d9879413709733b02d77cd8852086f4ff3c007d041f6c61c9034870507ed316080f261af532224027bd0f22179470084688cc6067ef9c96e2394c522c838b32cc853788351a8505bd5c4113dc2e346acda2a010ee34c13a6d3220cda03b44b6578119a8f3b1cd7c65ea3f106b4270500dd69d68dd28ec931c1c05351a4f70d363f3b47c9fc071d3c59b43c8bfb3f1870a941fcddeb1739068a4c11adf03a8afd1db85c815de1c41abee5db2dbdee213fa7ae57ac0633149ebae3dcc7ad9faa0e2b7a48be6a41347b1c35094d4131c8531bc370156662e2dbaf6e7dec5559fceada86d411886de7e73656437895a6bdb0db84872a230bb1edbbde453fafc421c002e3c113822c8f7c30674c4739c84244ab44f329ad32d5730d8bcce34bd84071db067ea9d92c0093f92288e44b69511a0c7591149a4da75144d7d81ffda7043efb4bdc9740c0a0f537df3aa491be702ecafe432ef400a1c7ec8f63e5a7bb307dac28d6a680d4591ad190559ba38516918b01fab7d32a7d3274a82aa88fc083162b60b7cc8c6369b7b7f6897553f41454ad487dc56e4092fb0b8a0f587e7be2e9a64b807118612446dc8352aa8eb81c808964329f1408a6a95c07d5b9323b54ed92d1ed88f0fabafa21e765b3d5bf5647d1db8c76d187704ba4ada1fbab103423d982393f936b8f5a263696878380d268732c55a5729c45eac6b809d66988ebd3bedb89eab49a7cda1fcc8bc3b1342e9da872e886a8553487e0ff7242a1f96f3c2a873c441e926b7e13f2429e96b7afab9f1f62033d4f55d848ddd32470646211da7a06387b49b2dd96503a6f37c392473523b3319872f3ddf5c4850922db8968781ba5f0198d6ecf907fe8345c3b6b3a6a75bb9ec8e8d08a925a7a9c3be3bbeeb36dbb12c7522f6f6a678749de1ecaa394e1ac1e531b67907af6ba0aacce16e64ad01f984a2151674246f9a06748945d962b7f8d500fabe947973c53fd8bbbd5b7b3ef8c2d6ab2555ba023c280b55f534a9f375cccdd5a80ff26a284a7eae486b4f10dd5133ed978c097598bf166673b56c351fbe12bc179b01ca618c89a0293693524ba3681e4d0cc650a8332983e9de978bbd2855a1c3cf480042b6812a14a6dc3fda2392f084453e3768146a75941fa697f5b36db4b1a594b1d2cad41372ac0f8e53957e09d0857fbcd713006ae5cfddf01fee067930a3a45eecc14778b78293f32e5cee5b1c2a8188ac7f196bee750d25f40377d250e370be44d449cf2aff55716eaa57b9c93d7f849766981b1aaea9d395ae0759c2350b57269c32c29d24781b15da233b24541898e3fc08de406694285561b7647d5e457ddc3e7eb1be89b63f29174c182c540a218e6c7775fc9acbe1db23327e5afe03e3fc5474b71087def89f85b77b3fd77fc9b8f4487c493d8103fe22680bbd78082fbc759e9a8dbeaf33ff27ae66d02c86e1b4393eb6e82b71750f58574e6047ba80c05d577dc5149366edf37ed353b6a147489f156b2dcc78f72f5fa8d000d8c4bd4aff32031201c81be51822752fb2d08e25727ba769f6adef6236a07aaa06ff7d82a41b1d25c7eb622e487009711b717b42d5ca15cd10fe7a0949ab6eb0bbf145c7446250012c85f12f2bf243165bea7d4d0fbc37e9e6fcd0b95eba1a13cbf124f6a2d660cee88490ab2ecd64dd483ca3c39fbfdb0cbf862c0e8d0348a03ddbba2b3aabde901bd51b8067b6b8fa2c1671ec38db9aae1695d77ada5e2e386c8504975c01163b5f863bd38ffa225fbd735ef1d0cc6720a801df2aab01dc1cc0453abb1e19f1f3e327468d7268ce7bcb39b05fd520b796d9accde3c715a9708ca01d829c69d0d7ff83b0a50a6e2e181fedf0fe65e778fb45289d1b1c9c8ecae8e3a901b27606fba98f77f71361813df7a1b061e520c024fc301e52b6f39a6834cb2a094706ffec482992e1bb20862ce46b0307717571c999904cfb82a0fb3d01747e39dea5bfa5feee81178601099e70cb6e90b8bae762f33059b1f3f712e4f645373cd0c226af6f8ddd1b9244910eef9c6b9211dba090acecea08ab5e66eda064453ca91af274e4637922adb7ea656ca525591f285eadbb90a1d2e3df2c6c3f98aab2f15a40c11b6d1a0046b45c1c8e68ddc0fe035d98ad1ff98302229f81f1483f988f73bd866e8925f1d6b228b311cf65057d66fe159fd27bb4f7b6512aa7519ffbfa9c5bee38f6f1ecb9f876436e19dfdabb0472e8f5af6587ab335cb92ea401b795cd0776ae043462874a66cdeafcf9f025d647cb5594af3380a38ccf74e839ddf6ec1ec650b1b902327af4c3e6342ae76991603a217dbf1ecc0538977d7fc5bba034a31f113497879ec689a0de5f34cc61ea93257baae23e38a557f05672356bc0c76c25a7e1fb88b04267b5d5a1b5c9f851acc66df4f52555a8094f9f4db17c2566b0104e91044f91ee1ea685c1263cd48e2d3855de6c4e4254e2d240fed193d544d51653cab33be8f8bc120c76b04e753e94a779aded6f1651676e0033408b26fd224e75c4d67d64ba2ef4bc9ff66a55ff9dd8fdd92d588f0bb8e1548aa2a0d0d04cdef98268d0455ffb39db98c61603ec1e438752dd1b32a0987c1196391b78ec235d3f43be2eed17cd50f8b7f4986c35e84b5e8894370fd20e1c092741a73abb8ea85b910fe01647c70abeb9af90d03f2949a12dec13a2ad5ce0be9554b784415b7d81ac897fad0a800d233ebf44dc8a2cc5cf5ca218fb13b2782fd42fea46693c3c5132bd9adafa3b0b7a5823c61243a749d58d44baea8cc94f737b4f9057e2906cf7ddbc2a165348ebbc924f5c187ed27c94b6582996a1eb069862b50077a9ded698aa38af95b6fad4e4d5370fd6311f0ce34a27ea913325752287bb23d2adcd6a297875357827b4ad96f18b7ee747627a7180509f865fddbf5908d496521359dbbb237aaaf119173f3a7033fcc83fb27c49113ed42174bb8d98ef8f5b279d07b4a0870f263d05808302b818f68c06b641535d7990d7ce76249880746374d1f8ee9f3d0b5503c219e8e93a569411c401eb6aa254c0076fe0f2a65a688b65babefe848f20465c3266438c62f8cdbd26398833c4ab7a9bfafffa561491741de5da5870b500f78e5ec0b4a68b283051d7c18f967eb002e2a9d40ffeac825208f8dcf1298b71003a006ca29ee8b652a4798292eba5c90d630d41fa88baa9ab8f2ab2416ff22639c677afa8086efbc5ad5b3d4d6ea2502c22347891c5e88a6a5402aa7c4d97751ce52212831f12b48e906a67c1f5cbbf008e094257ff9483123b77cb916768165bc0fdd3459bca91040acaed593da4d9b66f194434e3d2079fdb874b147f35cb09308a18bf19647f484607894df0c0f2bae2f560386da3c57363b1d9c56284b1beb87f0361efd80271c6d9862eb7aa4130af6e47e686e725b4334ab6a98285c0bd0f091e88a6fd8d386923cdcc81aa2f845cf8e25a531e73df8de5c12928471e3c1982234d5b01e469452744ae48af6a539a3a32946a7bcb7f776ce381538bcf1f589ef6ff87b5db9c2407f5f4fb491a4424f64d50b12ed88052cb20fca027091a8dfb3a595b6328e9190a611790ae7953e5c608f0e60e40bd6b1ffc84927946cac21904a95b7959972327f3b401ebebca7876489e7d0c8956585a346afc85474162e8fc2e051d2e17b5c0b71b85c48cc90ee7f6e43233049595a3dbd5363fa202165dc3f040b907842629f598218f9dba803521490cfe62afb7396a0d37ecee3078d8627d0291dba7aa216b4ced6ccdba68d44eb55b2b6df91eddfac49d997874f9cc89a14b5591b591c9492d1bacd02fbfe0f7b9b8fb80d18f2ac13cd9903430d258cd3920542c2d5724d4345e8132ea6113ab2330cfd2dae657f7f94556d5cf36e0567bfb0ac3707c750cd8641495e357de860abbb3e96b3adfa9af78a9aa4dfea8bfa8e8836601d0cbe9e11c84c70414daaa77a2f520714d41e8bde287f6969fb1d1a6eb154ea426bbc9e8fea8928d820e98ddc14eee36e6c92636aaf9e0bf80a47302333960bb2f83c0754956c517c4443a4d8f65143ece52e016f4b864b3bbe7d1c473406c7c5c051a66d9dd184a4203c51c6afc94b3abc571b6f8431cb0dc4007353dfe12cac570a5b5f3b5297bb6d762944f43478c994d67f984cc1dcc250a9d2dda78fe5a95061d90b78d811230d7f57879044c50457dda6a6984fe8c7a335f594e55239f485473c5111047e637e25eed297c25b8c05e2fb83f627f5540ceddfae4c46dc362dd4ce1b786b973618ac31ec1c1f56b2c0fa53134fea5bea65a5bd7b008dd48b602a20c134a5d517fa0d89c95466d78b7ea4aadecb91ac18109feebf586c373fa80eb4c870d7eae889381b1100b0fe1ce27b412af9c388bd70d5aeb9e9b74806cb5add250b58bd63602574b4694c766103dd87efd8e40b829e68da062e280c8f02fdfe3099e49f083e6ee595956e81fb8c6aaf8faa57861166927e1cb1a69c35fd37afc536f957d6932d90e574754a7702dfddfe77434419fe2cb9f7c82e848c29798b9ed93cc78d0a7dc360e7b371264b733ce2c6edbf31108f6c761d97cbb26718d9485d1c9479f4ea0d2f536a8bd171bd11b82b51816f1b5ad102c8539adb26dd1ca3c15d3deb06e1e0a5d590d120a81a25a487bc31a013cead96e1b7daed24eb8baa5907bcf1eae74894b5f0b260730e40cc7aee883127c0e193b9588214d2147fccf4f8f915a16beb1279b35300a8c66ef178e9af6b4d47bf14c1c4d09712a0438086c89d193b365f254260ad1c824979523252d0df3d3dbba7573f70f3474a5b712959f26882c219e5e45dfcc2a9014dd58eec34faae6bb5014211f0cffe84304edb452a59743cb0998192658e8fe0bbc83933f1b72a8991d68f1cb4ac9995d4f2e618524f7529f2c89296902fa79be6aa64d8c5ec0330ec1111f4e2e57d31109cc80972599fd5e65807014e168f12ae59c01f3d78d1e3a1144a7587ada6538d3d857e8178d3160fe561daec5956ca1aae296f229567692da8a0331fa9121c3c9335db9e84a4c6cc6e5b34fb879b2b00192aa59c8ddea61dd58056fc666ffe1734190dd7642562f1002a42cb9d79e823b799d265ea9329a3831b2a62ce72545d8b3c37f4f4d2f2a365f5850954818fd475f87b69cba0b7bbdf4cdc5a637a3be1aac4ad035ff7dfa3b705840351f448fb334b58153df6b72ea3d6804c15ed5d133ff80015e45930336ca5f6e478da9f5e91daa46e2b1562f9d85da4242bf052fd3a339972aea4267ecc71ee888aa30dc5fb48529637999ef19cde600e8f233172f2160e17ae8d1a9d633fd13d158fd93b967d344cf63f7f0ea56e041509be7fb1c56b7a5510e2d84778bc323adca901ea4b50deca86aae91b4f75ea78c9cad11cbd8762d161c07affbd218375506471b47c286dd362bd63ea0ae1699feac4e8fdf43a5196b56f929d1783d4bd71e5d7b820d8a2dcfdd0b459e525b0fc0b787036894906b91e2dc53f6c8046816662303d9da992d12963ebf3db9a385d0f1a5bca434883c6a38de547bcd6abe0a79a2018248a660c8751ee401bdf1c30c72e14bef6748d8e2ad0c9e8129471904439dad4dd005059d3c483e23f5b1fe60620a62dc3ab6cda4411cec52a4e24c0a73464e71aac41b68ceb393166f5cd2480781682f5aee5be115e0928adc0b68b8910bb65a2b878ed185b0dd4718335e6fa81157944c7445583115f76ec2c2d109309a9d9acb6b7c417d2e5004934657e013123eb2e3ab0fb526759f56d16c2fcc6867bee4d9858d35173b20708e5bfd181509ad62a8cb33dd543b8fa98be74bdeb432303a5a96567c91dcdedbb36682129858cc0225464ada5d946e01ad008296720ab8cf75a6a1026d06963cde60d87f19512413c8a790c2907cc58f2a6424c484d0fb923075f53e9e66367d17bee7be6c3d80e53eb5eab911380fe408a0e57e45a0cb6196acac178eb1d64f43ff3b647a377018035cc74f83570da9ff202fd43561b7f675304fc239f7eaf6d0303f56009277d17490dcd0bede02b35f7d79f1944c6f006a1245420bdfd5b35f75ce5c77e4cf1e8ac77e8fee75894ae18145d7a333fdbf6590b0896117828fb10e6042639c8770cd0175c78c10f2b2ad2ffe3f73aaf7594dbd0382e799735e195985b93da325cd1e95cd7cdb788c7f70502ce0141d79974338842820e329b6b9dad836f5bb2232bc672c412f2181273fcf1a52364c4e4de29937b6af58e1ed2a9ff7c041b42f2b3b9a6c6538026dc83ee09507b049fe00df96e7856a81936c3504eb560d151f053da7f85197fd784a98383cf976cee51c84ac74c6c7c84fde58158676de02e6f7ea295a1128784ccfccda25dacb44afeecb8e3e46f9a3a61aa740e3bf075124f1d20804fab88d9f91f5840b8234bc1a31bea924a641e69d17d53f7d7ba57c03cca2a7115acc7c624f7adb61c2b2f53dda8a7ccf52ce84580669345103bc37b3e2d2005ab52ab2ef8ec9fcf1ddac381c7a27b7bb936293dcb8c45fd9abd517f3b0bb31a3c1c64b0fb21cf74fb49e2d8920c3c156151b95261502018cec5d759acfa387db6fb813a05a7505358f3f9ab9b4172a06fb4572ef14a360a61f24ba5d99678d179ae28f3de465fe15aaf3a8ee1c878ade9806181f144155b62d977bdce8dd495fe3ccf61a0b07552dbdc8e0090aeb9d805c788d6c2eb658aa806b40a079a2ac33956b8f573c6cbdf2c01dc84aecbbb8f7d14eec377bc6dbdf20b9725c58535d6f454bc8293aeaab595999e742b71a8089eb05dfc8c4f9c9927b4ec85bab0e905d2c9c8ed9c786b2edddbdfb102d343d4fac28846b4cde76165aa5593ba3ea94ac14858758dd25a407337c2b2b1f79ca87fb9b0e5bc8d621bb73ab4b61daf8fd869c5c27e26051a6d3fec09c692643a854ef3a948a77a0c3519acd517de3b424a4fdbe72864787433a2416e31053f665127353af6f5131975fa04b449a16be919f63837fddb4fbc43ab9046f09cd87ee85f82921e59b8a33dbfcdaf09bd27236229b2d470f12608ad2142a302fe11aa70d033bc1e363c157898b992e771f0f56e6951f7b3bbfba6320581c90f4ff48bf9e9e5dc0c014f14e8b544403788839a9eacf7e6c8dbef3e785754e82047126b319b822a04fcb6592a09fbc6d460538708c6e468511e441783614bb3720e326a61e30999ebbc6bac41aabcf96e61e90ac09e680b3e199f5fd4ac0fba347de33fa24a78581011732861953393f18a4b08aebf3a3748ff6157484b6e74e2e589165e37c5601a2c3706745728c3074944c5a6ae14ce402fc5bce57357340dc915297d6464f8dbe44f768e80ffa52fe516b0a2abf836e28b549a9b48eaa2250bba745b1d093d30f5d90a8926e46b7c1ff7cb39291289337b957ec2b1aee3abc7824b9ed7d790fb5ec278ad1b42525cab632160f38451302a7c1c675d519811c0e97a54b85e1bac3aceef7f7b12d8195b830421711f3b320b0d0bc4d49ac8f05b4bc505278b9b7eedce312fb2a7182e24f669089ad128a73a13fffbbadf846200e03029fe776904e3705d9a5211055235452b7b36e2df84974d33befe918cc40abceac5e360251f529384e0fe0d77ea6ff9d719e2c9483c86b200587095924731a4c5ace86bb3a939cc612ed2a8545dea0eab34d935dc33961840ca0bbcf3ae9f6703f8dc03fb829356b2f6bff86a0a939e734ffd998dc910d15d4cf20532b8fda148fe7ddabced6c84f0e2bdd19049708d57029aeec817ea3477ac33f595c5954d12a34cc8a272acfff5cd1ed599dc9eb9db604fa919d4ebe0715ad439e5aff0ae597fd1e832cae8a0c68ee9ed947108b9dd7bc11bcde3ba0522a5f920327916d2c93ee7d04d3879e0ec21b6de19bf2b82f23c9b4b1ead0245f683c0e58e94f206589e481ed824e29f3ab4ed07e341a84739576d793c803d20f8d6cd34793e9fd739b9efd859909724ca25e5a73558971af4fc092756bf3d53d76bbbdbacf05afdbb1f44715c04ceda567ac9cc837c8404eac756d7d5e5880950d41db4e86a57179d664718749b2e1b76e5f45a8368f3d86ae5b0b06c9f4977ee4ae7e218631117d6de04befaf256755c9028256bc17e13eb5c72337f14c89fc2c1d709478834b008d1895dc4e58ada9b321a17704fc82ca90333a74c2c4f22099f721738fbe3c29b28e7c1e912b56a4c0c1c57e584389e18e47ff90f9955c93780abd447718ab08e3cb6e6107aee8c7ae0865e50d468a44d6ca9c76af76503a04ce1cc6a67e43e59352b0684c4214b06ccb8b74212ccc929405846f3a1c6ded5af5abc1da30356cdd361219488078a200a54991b338fc53840a71617d3a483d6aaf579726f21314d49e2b8c764ea48910fda73d12c09fe19d7c02c5a72c501a6ab5cc39eda6ba1ab3b75e821778d91b754523c01a0ac246f778db820500c235534db4050f92bd15ec79a013b796e86b3bd916ffa5378ff589e556e5badcc2581ce582d2cbe6dea46e4a9b3ece3cb32e8ac49a23c14a0d32fe0bc91f44141824c7adeb67d65930c2a088834ec77e3129f1eade2c8b61de9da6255e2da1e1b09ad3827e51a0f2e4b55eec7b8a751560849362176b6625e228911d0d4ed8125b56754a9653b49af59bd02bba330571b39e32c60c1557a1da515dac9799b920564e850dada0ca14d7cd9a002a854ff15ea8ff191df2812df375ac5984e1b2d749fa0c358ea83fbf87ea481d46a37ec2c97fde71cd5c5e2b574faa239381353b64b27d17d900bde0a2874114966aa2d62f8520271723946d242487269413dc0276fff6e490508e0323088685d15e60167d74de56d8f0de06364e5bba3156a5eb127b53d4f2f0abce1ec557a1d6b0b61379766dd1a413e8f955bcb0bc15abf9414f34c328c5b95a05d67c0ac3bc775daf0adb9f7680fb8034e5d66b2508f58985bc226e04c8459a73af9ea6d700e2db27ace0d07d109ba845965868d4aa6fdc1f53a793817b518b0baa0692c828daad2b5d0f97d3e2a6abfbabd5a965004bcc3bc05c967cc91f332ff830bc879c4bf847030299af6a8d083ce1aa2554cd558dd55451a850dbae585df395b3c9123058f40aa02e19693f3ee1067a4abb63965f9b83c78c84f449aa93b9944a5c0afd0843854787ecc11f08cbc778ae9a2745b032d3ee03d7db6ae5dcca132dbda94ffddadd070e7f8c6a80f8f34e00828a30018a49e3fc53401fc6a78175aa0356f7d17ded11239ba3eb1108a9c929e1730039e9b9cd167ba8a190e495861f5267c9d5a3842a1c7ce165201d3858a7768c5cb95f10dd42108c79d4dd59db7a6b12d2d90a3cb7333b92cffbe0dd19653cfc6d48d8bde1e533e90568b42f3ba1b122993c2d0a40a75a458eabe97515f51d20b785193e64a0f4811b88002da6318b1c87b79098e968e188d3bde145028711abf1c4775abb80191dcc8056ffc6192d5f1aec2b65eba9bf5aed1d16c462fa61ebd1c5956ff10337aa08be3544d6af95c96185ee4bbeb7eb57c5dda89304f56becc2813e0021bebb601bc2a22cf8910653d74037aff6897236d0a958520d8683bd9b9aa055d4ecc41aa28d4a97fd2ca4b281277de322cb6e3bae37b6887983bc6a2169a75aba48214747edd721cc81e93ddb07a9424fe10e4bbaa7ed8023d55a7284d981ef76e9c40e448155a60484646c61e2dba1aa4a2ba9b462b83c95b3ee28149d23836693ace2912d0c59a5042e571022841d76d5c4adf9d4679e4672b872cf737ca21a1e3a5a6496ad9920a91e463f21a8605d542653f2b0e52c0eb39b66dcea0b4c89ca5e9d4d14c340e7d77a50a225d5c77936cece0cbf4e99d0b7a4e9dcdb9b04693cf8694dc272147778ca89eb585728eeeb3beabc7f227a264d88fd15d366327898850ecc4e9ecbdc99a9bf7312c57ca7ad1179e1814a6ab98dc1d20ad4dda969bfdf9dd48ef22dbcba050bc699f61018102019abe2ad71da96069580016c480dce5d97225d1a7b44e9b248616e853c790ddd5d1213d9abae9319dbecbe2a003dde97a79724627d29b4cfd13e861c795cbbbed9083bd7069272bd0f354dfc7f38b4e79f5bf3b484fd9ec050c879e8abbe0ac20731e353d2c98f6da5c21ae49395b61cb4566c49f402f8454083a0ffc33de381d592082f5e830d5363450946285fb4444f190e0715f7a772a155ba2c12f7ab839351a23cf5fdd4d5278c8ddd1e14ee43ec45170ece5f8cb272e57bf93b9bd4715c8af176a3edac68f3858cae1cc22ffb53c4906bf787af1f7887898d0b872b05c8493901ac1a92d9af03b5613cf3caf012eb3c047679a89422e4c89a1107d88cda00b05473194af29b367f56ccdec63f96d684094b41e3091388d5b9a61c4084b7b3de5d51ca92ea20a1ad8badd2bcc5523dc815ded741b322ee8f539917c66c5dbd7e050ec6a12c05e271283c6447ec8405efde43c8aba13939ef0f4541368cba50ce400dca9fd96c628bfc5af660f7545bbf9b4c76fa7f1c3e1943d968c62952aa3988a8a5943226e08d861b5bf288a0c63397d7af866602787850d406e78adc13f11f021589b5d4ce77328cd798f4a598954ad30f8d8bf7e67c4dfa513c101c2d162745c8ab62abba098ce5b1708ae6689ebc9bed9366fab84fad9d5eb55a60db36603eebb281e11feacad87c4cf916649fdc4b896b586b896b7547601b3185f90d056647329356687111e38367a6e2eb9042f016abfbff1dc507191cec194f72535a357fae680d5e601d2cb3447fe921d348a2f859cccfb32c0a284885f29f0e915df53ab2f4f85e8477d796b95a96bc8ac8c334c71d1d50312891cfaf7fbf077311ba19fa446dfaca4152a5317c279a9ed7f04fb53ca7d0ea34df189738bca9e086e2a5a7eb84ac65784d18be70befd195ab005d8a8d8675a550af18ba3960d6181ddf1ad8dab7c65b9aba2b5c2deb40532e9eacaaab5a0ffc1e88523f01de525a1ca4c0f0f91e02c0e8d0f2f71f74d105eea2c829c6e55720f13bf4e3f0a83edb172285b94bb071d0f928c4da6e693f8b7993c0c0581cb678cd1d598e4045a3ad0d828a9f6e49b630f4f00d2db303875df98b6eae0f88a1b2244b5a2e9b3ec3fac3ff07f438ca46708b3b9b9b31b7ad4b9ca3ba6d21e964469ad2d32e4e73cab76169ef28825a3e24f203da69a74de1984157fba16ce65cb7be6e29152cb151ddb730d8d8043d5dd6ef1c53112f30f6dd1f57722ed29b9b41e83f07296f264e2e56507cc5e52a4250864fecc948ba9c0a4207095be183a011a8244139b0e84bfa9807d4b30c9704a98fad7f84ac17a49521bfed1d9e500c7f1759fc7b162f1ea019668b95e1396ca4a614bc0258af25b7881c2d4310b6630ccee260e3da43a89b128fa6e0a94b094a1907a9aecb347b4e757c282ace68debb750191ebaa3119f5dc9014f6e298cc8b6e03c80af79bde4839d2ec6768c5185a13919e7a4fb17fbbf2b379914677b836e9eb2c443b4a304d30d0ec5b93050be68ffe9152c4fab927c6c3558f6d7252ffb56c119c07478e4f2ad3d99c1546e43b4c874b5bda754ce5a95f42b706934b8ee94d87c8a3f529058edefbe2008711a8195052c75b58c150c24a9a84cc2cb0b4bc183c703e396172c91a6c7144cf4c9dce387fefaccea716f25d2046597109c82e32ea0d299def97a43c490b38fb9606f3cdd18818f34121036a0fa76ff13622833e00d5fc2cdd8d1d4a904c167ad2b9f5144b4e021f68626cbf0959681528c7289b6a0b546fb0885beff2781f066fd6903b56d0dab9f2b090fba3f046277ff8b6478f8e1d3d0053c65f5b54427e316998f1a59958f0d66bc491b1b87ec569d5d5ab0db5c26d2438977b432283e7ec19dcb2fd88b253d4f850c9ba7906f321a31b640dd2de3ba3120f6e2c4893a11f6a29c42c748b81b847df836fbedbf5b97dbe43e74de785b8c32debfa379e2c41aa169af46f28340a6661f3b76812407b8d720c677427e32aade984f3c6b11f2700a4cb28ee3c5ce5518ed849740c6c2633b35d492974af21460cf94b85c1527174c09fc0437885f582739041b514a6ccd94592626bef2f09d84a1cc979b0c91899e3d624a07d069902e59593aa46cc12515b896332ec2c9602060316f3ac9ac402e0f53057e18ad9cae16dd9900b18db2b5d8564739cd9b77e9b501457fd435729b5384b19f62ade5b7b3d7642dcf60b78167ccf074da7127287deda768aa9cab85e9128366d2db6af577b2028d1b9b3cdb5c4ca6bb24d32dc359c1fbfd789ca568414800ac05f450bdfb7cab50504d189d62cbe8957628f70fc2fbeacba8197a33152b66760c01aab8512b35b69baff993951ac631135e455ea1b2881c825b1317e70caf09de877c1a288659a716be16e168e6d5b9d44aa4d7e1df14e5300e8a537074fa89ac0f678eea06904be428d64ce9328834827a60ebea01cae190b38aaf13046c5bcfd0186365d5deb516d5ccdd5875d6d309676f02760693d9d88e994c84e2b05e2a04be7c920017db11e6d3794cdc74e0123d6ae2f38705717f1af9d246759cc48dc8f176e735b747f0cee7b45b8dd93a7a231a63e9ed3d3eaf0e4b55bad5d9c2f9aea1c74ecb6a025cd14f29384273e3476c4f3398e3af06389d0af35d140a855acc81671687d60b2cfcd41267f0600dc6d32b8524187d8260c61ace3d0f84b323e9e8006718a29fd13a4bcac08a1f3082011d71524310c488b90fe566b991637c4dfc6fc292e05ededadc3dbf187017a2d21c5980d9a42e09d92cfc6324b066011bc987486a7f92674025e413cc468b6bf2b140c49921b9116da70bd9acca19020780955e77a26c6be23f715f93e8d7a66397fa45e4a70083a5053a56b8e6c256e5331ccefaef3f578cae3497fdd604f470e5239f6da348429c8c1a4bff1e4d1cf2980b20917ed86c10ece26fe35931b2ddd1f52fa9bd6a8edb34286d593a15a38df53682c6b08001edf3783ec069f36f26d249996b34a65e94b0fe2caa043eabac83abdac3819bff664cc6bf21d123b4932f06627ea918390b5cebfe671b86f23df3ce367541296da8448e087a39527c20722dae7c32e0637700b229e6a34919a8286f86cb87dd3574b7188f7d14f0da7091c97797f585dadd1a746ceccc0d913896b0c5b91136ad47a1e2b8546bcaa582e9962e33a297c73ed3156605db410ef93696f187c71d0c1af4c780462c434c8163d0e096dd11c24319090fc7df325b2826b3ed29aeea483056aa694f55ded408e2c0a1c62d64b5e35e924a1b1448d135805c5856e2b77fa6b58ede2a8bfc1e385d7422ae95a38b3d5b59103ffbb03db396b4cb47f5222ce0682b1da026354a2f499dc1b9393fffc20ca41754695e51281113bbe9b25821a207b0be33c48fad97c3d26b8ed118bdcfac1cb89f3e6d22595f56c0f6ce2393fada88638e7a10de420c663554b19484c731c840d9390872421ec9f1d2a7831a4f8389d0f87729e533a586f7019878ca0e451f0fe147e0efe14be5c7eeab725d79319d8973a4c2c26804036d73f8908e74dec03dc732e5463e7f23af5855694c8e5dc5bef2175fcdf2f858efde8d1565a981687f47c978be6c60508f086ae4799f8efcd4cb7dce85e89df014b5ae9dee230649829506070e9a775fc1a88c094723b05014a883b97649abe073d889cc14091fa5995e4388b3f23adf031daf5e7587f2e95859f3e568349f9f3ae5b17b719c3e3d981b8531ac880a13201e3df3b44d04abbaa85af198ace3976342d3891f2f8d185ca541efd571f611946fa0879a57c4cc243687af1a29dfa95395505ff867d119a296306751684fad87efbb404139795484d73588fe74d9024f60b40efa12b5aefe42b82e43603a0e94f04684fd82d7378a67ca0b4d80115a01ea80d57b4cb8957624612b0a2378d0d28eb141a812d04f3836d6a41fca56fd448223529109586909b884e658f3154c8d108af45e38e1443f8262e8f03d6be57394b2a40e1c218666117f6fea0a9807432ef653739da419d094806cd060c82476f84a1c29c0f534487dc6db3c7dccf4a4839306eb31ece7a35a1b5a75cb582a820f04a8e0ccb80c4c107e047e79fb3808b162a4dba910a4cdd8ba95e25bfffd4daf93958b524ad54af4a6a4519d78a69e7545f75cfad9b00e8038887f286e78b6e9e6680788683306014f37995c7913384c872d3fcd4d891a1833c4609fb9ed275f5756cde9f92be28b3755e9d0c95c1b9dbe40e1b8404e322a1ef9fc2566ff9bde8fc11acb5534bb130664fdc64450bbd39d4cf992095c2cd841360830631a5aaa5ae2e9ca8aa3089da1ef6963eff740ea92fbd2d48417d133ce5ff006ac0da7349a4c4eac32753ba396d3df1a581000fdf9a99a83f86260492c2fa5bf2840f3388823701edc96e51b87d48067663ef7f975bb8c2176012bbfb27d96814e298fa53ba993811629018d3a1d25b49306aaf1b8c23830f422225832910f62fe1e979af8561dc6c345eac8913671afae297efab25ec555d57d81c74e21043e6652e2d290641dc8313faf430193d778396d36628ab248d315b16fcbb1764dbb1e8cf7f2dec3b5b76e72f7bee9848936eac75bbab7277a2c4478597d5cbd88637fd72cec034ff743ca604f57040dff05daa73ad819dcef79b23ebefdd3361ed12103d27391cafd2a417ef5a568d4fb8fcbf17248b22bce652dc7c7f4f05e748218c7e66346cb95a9ff0e792d54e3a025046968e9a89d18c80ec6dd3aa48210320e0b4a9ab47a8f9660c3589a5c065278d8b18448476ac337ffb14e2f8d77d7819dcd14ec809ec1649c2f99441413329ac75a4f127c39c914bb47f84f8f32be5a0d76d45cac79e6238e8c7ba8c7a0b6dc69930cf791d749c5d1b7718f99ad9d7ddafce596b9220775347b307e1f7dcb2e9a35c32efe4a1db8187b288491798a2d967b273baafb83230f364edac2b31b238972cf525ee297dd9bde329cdb6cf6dce00ae6c101c3338985eb12f406177e216eeccfa6841431253423d739c89a6f0ba121fa470e7a04693680d010b6a605fa445029bd00c1fd2db95b7bad7852e8e8638af6e3aeb4ab4d2d8017c1521bf43bdac65a3b3588cde752d9dc381c49b0b6dccc7e436843f6cc0ccdf66430907a79472d21f782bce6ea051111f8f641e27fc4ee00bcfb8d7a187636fc6d7487472d01acefd70bf7fcfb679468edf44a2dce5bb5bb1aa2c320807945da49a95e2a5d40d8d887e9f434eae768cd3a7af3b22fc28f18b15dbb64198723fde2654f7cbefea4b2c698d42bf7af4604e6e6072dc46c7ccb84aaf7f09cca646c338543296e9b15590b2cf2370ac5466f13d6b123e18954a373974e359e3ab8468e829a5b0b681287510be267daf833b2de0e0b3fdf475e2a84444a349fbe4a962c7f1b8ae09884c731ad6b2035ddff8883379da0ea9fb8137208d73820fae79b0d21e5f3fbf56e1bae8810962437598d336f292a4c9bf34decb8431cea8747c36403d90c2ce5cf23a676c90d0aa26afd819a3c177bed99d71d32b0ebd70c53155567d3b1a5483fd5fcaad2da997ace4879d6e1fe2c103393502711565dcf8bb145e1d571342758d473839f8eaa7e7731a9cd54e7d115194b55b223e104dd80d449713ab1af21d5b4c515c94b5050f658381077fedf9bdb63767fb870fe5d7664d7a24d6cb37df2197bb00b07c0e515f441255ca0a071c087790af94b0546da5f7907445dae603054045320f4a493c816a77b9afab6118348a08a0ff5d8df68a794bdf8cb57af440c29187f406ae07473904510e8750153fdcb3022bf65d8de603c9f6c6128e8b6572184a85653b6ee64c49138b9b6fff869fecf000c11148cc9ff412531e5e1b7154e307b2f2db5e07e515d8734b2a1e8dc398eb7514ee59bd3ccde96e92a46728942b7709bb78d3ed7ab9ca5dec65c7406ee55991d73264aa7b8b54eadab9cfcb664b55862503d4316b871f4b5e2d183c7858bd45e685466e40db4308f072502e310003f798404edc0c914ef2c51015ee4a2b056ba89ed5e416473e724a47d2e4c5c751d3bca7e08fa2b17e08e6149844ef3f2d748b94243a270309e8c8bc45dd7c195323a3e95e4753470008ab0dfb6b7dacd2504c0d9d0f40eae23875e76ef5d8d9cb744e4204488305043283ab0be2529b39dda1d3fb9a9984f14d4ce1d79dc6d995d6b776baf82aafd2e633093c3719db29a20ce8f78d42ffec41317efca9b6a801fff1e1c60f3478852e4ca5bb53f6e731c191e5a6df9fe5064ec30399fac33e3f717c60663f747b4e6de382b83068a8cb2a0f3d44e1984d68c671e485cfe6577fe2f3473ebe8b39bbda9afeeebd01f3b6d5f8bf9fc0e662844dc82c8901b089650259c656172750032f9104efdd094db9ab642aab1bffde972edd900eaf3b6d2655342acb7ea831eb7fbf5331cee55aeb392c9fe158adc9524ba17cbee8321cff7fbc6efec5763ff575aded5aef8bbcf6352e4ddb4b17e0be5e13ae3bde75686aa7a05a8e22340ebb002001719dd9bcb28ea84b6fbcda2fd5ea1557b1b0a6f65e46642238093ece25994a130ff0033b0358241c92133c627a8788ec8e55ed621e8f5a415614805c0c68d3ced08c6d87add92a944dc03d8e7060c767700fd93a1ceb816fda53fa40cd941d796507fc5da834bac87d53a586f0b2cf996e02ae99167a9236bdfad751bfbf0fb828b2fdb86a4a31693260bf42ecfd96d14731cde072d18056e79931cdaa56bd859ac679c1e7476f82952ffd64cf594402f3f7fcbb4af4e0055f7e9d52d0d781f22fec4a2b3c25888b4cd4b4296aacd5fb7884153f79ae2da9aed4e90ccb1c7c4c2e76279a4d2d4d9576946709f12e4dd6e0d9d1f20dfd2db81ebdc2c323cb05b4a9bec0e016cbec27425ee173a2f49c539dc11da8a8fa14aac607e1d638a3ec3bdb9b20c9dfb6240495e48c68562869d017d5cdc7fb2c4f88bbfd726f961f23e7e5f221e8fc38cfb95e9104bae612504afda2066187bc335d7a08d3aa4f0728b13d56c1d214a621f81a0de7c8f5753fdb253d947890ea2edd4a51dc53eb8ae24e62d7337eefa8bb0677e8cf8cb374adf454768abc4dddb70cc91a6805ac01017c99a40e6b18b50731da3b96bd0383fe6af0df966edff24a0c9feec3a30e634198deb950d4ddf1be34d51845be9e392e0f953d87899cc3a1ffcebd26f7029cc3d2c1e594dc8425cf671235dbd4a4b17a123246313b1b7c554219be1a524ba3bb8d0212862ad429ebaa008c3e29d55b2a9dd4a13283a14b679fab2297fca47fdfe039aea034e419b8f56ab3fee6c3298e99c20f7e3a2952780f59eb1632c1ed72c63b59c545a98b606c8d77049d11793b0dff95807da964dad43c33ef7f244f43b029b29fab2a8cf6762383d86b958b09773b29ba01bf6e40439f31b7c1a157bb4c49503235a486c2e5d5d41589878a917a8c1e2e9b760fb752b8752e811f22e95557faf2a6f5c1c843bc54a42642ed4dc969d5f0fb2a4ca5755e3c3b7fce0092224e7564c98f2d9440c61c7defebe683e08fba6800ac3fe4c674a04692d214fe6c0acf2b73556f29c0ff5cd2b29d909fcc1d19471b2d9101e718d4ba0c142b35a95de1f45013d94c574a3ce1769718fd2be552f117b2e960c89d5f637846da593328b390689d2a41d868575310912e943886f8fe82d2369763c32de36d1f963e7ac4ef411d15983866cf2bebc14cd38f05b695efbb1003fdc577f4e6e3995b5556ae15337595420d38baaeab9601ac1176b8609898d394a7ca0332696eca83b6e747b17e09344bfd329693a545c66d6a26ced725c3a9582f48f31e520369000515d5feb519996672978cec2398781b1fbb5904bd190e16e874fbc19466311e746e44947f781d0cfe9f03df854231638f66ca2e466aed10252e135338540c1e9d907c0c104a147793024b4e604f560a1ce00b90bcdf0fa178a69d86da17ab097a696466ce2b8f065497364e52854c965015aa57766aedec17ecd4cbbd939beb2fd1b5274b386f3ee42db4c1b2bc509262575fe7a0dc2da2088bff7d06a602723af3a8c7806a93d7c3657098fdfe5bc076fc468001d8a3844ad439bc763eec2b9e122aa65abd58445eb07641dd2b9d5a059da12b48db109bd7918110e254262ed85f12db330e235756489600c6a67aa3f56fcbf8940dded78be2cb23c3891fc7e9ac0bed436b09997708acf66099f5f5929f4a33b84bb3005665a559d26f56c4ee2f9d062bbd38176c07f92a3580b52257cc81990d0b90a2851682e3f61e79762703ecec4b6837ba0dc88cc72544d60bd630bfcca545968385aafe68f8f1c3c64f7ca8e6368b7c702af1bd079323dee39cbacc3d41e7dd6340f5d71fad3a0c8ad8ef6717058a70428786cb1a6d55cd49c49c3453a3618e2222a2c6cb284a682f8cfe4cfaec678aad20c15006d8ac1925182cd86c5d91cbac9379824f397d6693ddf1367abc97c7de8e3097b101a66b522d00f5783f0170283df0686faa84ea035ec76e3ca0dfc46d5ed2f0f396ebe13b257852a42d85b65f6f46b633516a5e3f2110f7100f9d2cda014d644377171b50fb15f9acf61715555f600823adfd2b7bece359d3ce3cb63c22246f6526914201ca77205982da0c3470000000000e183010000000000df3e975b4cefdba65152b71a66c40d41de744705f3a8c4b75dad6b3a81cda322df563b4d8a47caf4912f536e4d131ddaabc4dd64c22e219dcb948807e45d0b0d39c620a11d6139f6564973794837a6f63c5abeb9480958c70798dfb6bdbd9f391d3e0200000000000000000000000000000000000000000000000000000000008ea8e07de66daa7ff73b5b271f6b49e4750d7c776cab8dab49d7b3353a33e494609e49a4cf08c90265bc097c6602337aeb1fec2bad8094559a65f456afc8f5dc170283df0686faa84ea035ec76e3ca0dfc46d5ed2f0f396ebe13b257852a42d85b65f6f46b633516a5e3f2110f7100f9d2cda014d644377171b50fb15f9acf61715555f600823adfd2b7bece359d3ce3cb63c22246f6526914201ca77205982dffffffffffffffffffffffff7fffff7fffffffffffffffffffffffffffffffffffffffffefffffdffffffffffffffffffffffffffffffffffdfffffffff7ffffb1afdd83343d209ded369536b05bebf22915382bf0d50dd6d21d5e7e146a59a794cb4c0f8d7b92164e2c31a3b64e1a4314c1019c1dbe142bfbb1ef70c8c0f39399cf627eef2954607b7716aabd92854c1a8a2dca680a711c2bd1bbfb7664e892f7c3470000000000" -const lightClientFinalityUpdateHex = "c0944b0000000000c9a90300000000006592efc86bcbec40089236714969b722d8d7959143352343a8ececf2249301076dc06b2dd0db664b650d2ddc4eb93e66e3e04cbe48792e8a548c140aa6ce9b48782f61f2f22379f48496fe46f985fc847ae1037431065c266e5e35bde9c2d96e80944b0000000000181d04000000000067c56b943c2f675d14eda966f49e0770c14b787c1a110d8afde5685802c2cd72c6a6ac803b52db1b54f546df2b6fcbc1a183fd1fe0bb140aa8ddfe7f1273866de52f0093645d011468823506cb04899f6b502f64cc4fdb7096c34193e61ab747a45c02000000000000000000000000000000000000000000000000000000000066643d84b06888be939498f352d0d74c2f8271578a34f315c6387dac995a84348d38d1863bc3e3009228f49eb91a69b72a8400f3ece13ed0ac56902f2ba8be8e408162be20793635f30d56017fb0e820ce9dbfe8b1d4532a339a33a6bddc7c99b8e16b33456f81799718d165f5bf75861f8df8f0e99201d9fbfb7c288597eac604cf9c45403e10e0e043ef0c5734eefa2a71014bce222cd8f036bfc8fe2927e4fffffff7ffbdfbf7ffff7ffdfffefffffffffffffffffffffff7ffbffefffffffffffffffffffffffbfffffffffffffffffffffffeffeffffffffff7fbffffff930b286947d7e3d1b7c117ca09e6566c592dda54f714c43f039e61b475403c88cbe889c1df0b06f182776e2613cf6bbf083a6f2424ea1696c6500942010970740a0a334c5b1887f0f25fd76a87d1b8e61da540c212e8acf612dc369bb572d5b8c1944b0000000000" +const lightClientFinalityUpdateHex = + "c0944b0000000000c9a90300000000006592efc86bcbec40089236714969b722d8d7959143352343a8ececf2249301076dc06b2dd0db664b650d2ddc4eb93e66e3e04cbe48792e8a548c140aa6ce9b48782f61f2f22379f48496fe46f985fc847ae1037431065c266e5e35bde9c2d96e80944b0000000000181d04000000000067c56b943c2f675d14eda966f49e0770c14b787c1a110d8afde5685802c2cd72c6a6ac803b52db1b54f546df2b6fcbc1a183fd1fe0bb140aa8ddfe7f1273866de52f0093645d011468823506cb04899f6b502f64cc4fdb7096c34193e61ab747a45c02000000000000000000000000000000000000000000000000000000000066643d84b06888be939498f352d0d74c2f8271578a34f315c6387dac995a84348d38d1863bc3e3009228f49eb91a69b72a8400f3ece13ed0ac56902f2ba8be8e408162be20793635f30d56017fb0e820ce9dbfe8b1d4532a339a33a6bddc7c99b8e16b33456f81799718d165f5bf75861f8df8f0e99201d9fbfb7c288597eac604cf9c45403e10e0e043ef0c5734eefa2a71014bce222cd8f036bfc8fe2927e4fffffff7ffbdfbf7ffff7ffdfffefffffffffffffffffffffff7ffbffefffffffffffffffffffffffbfffffffffffffffffffffffeffeffffffffff7fbffffff930b286947d7e3d1b7c117ca09e6566c592dda54f714c43f039e61b475403c88cbe889c1df0b06f182776e2613cf6bbf083a6f2424ea1696c6500942010970740a0a334c5b1887f0f25fd76a87d1b8e61da540c212e8acf612dc369bb572d5b8c1944b0000000000" -const lightClientOptimisticUpdateHex = "b2944b000000000059d20600000000002fcb02f3de6192458bed0eff1992aaa98d5590a32a01fe96ef449e6bce803ab99e3b54169d85bb91798a1a621ef158766efccd61116697e98e06cc0535adbbd7c4826fcc8533a06f13848c375fab80695c4d99a5d1fb1f05118d095be39aaa4ffffffffffffffff7ffff7ffdeffffffffffffffffffffffffff7ffbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7ffffffffa10f053de563e0e40da3df39038dcd57ecf0bd58c796fbb76d8f0d1fe8741aa18ce762a82eca147fcdb9b49af16a75470e9ec1474f3e978a20f0dfb97bbe8c9bade890d7d62fc3122129c6abb7b691e2787ebe5f51fae7060f4cb04d364d468ab3944b0000000000" +const lightClientOptimisticUpdateHex = + "b2944b000000000059d20600000000002fcb02f3de6192458bed0eff1992aaa98d5590a32a01fe96ef449e6bce803ab99e3b54169d85bb91798a1a621ef158766efccd61116697e98e06cc0535adbbd7c4826fcc8533a06f13848c375fab80695c4d99a5d1fb1f05118d095be39aaa4ffffffffffffffff7ffff7ffdeffffffffffffffffffffffffff7ffbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7ffffffffa10f053de563e0e40da3df39038dcd57ecf0bd58c796fbb76d8f0d1fe8741aa18ce762a82eca147fcdb9b49af16a75470e9ec1474f3e978a20f0dfb97bbe8c9bade890d7d62fc3122129c6abb7b691e2787ebe5f51fae7060f4cb04d364d468ab3944b0000000000" const bootstrapBytes* = byteutils.hexToSeqByte(bootstrapHex) lightClientUpdateBytes* = byteutils.hexToSeqByte(lightClientUpdateHex) lightClientUpdateBytes1* = byteutils.hexToSeqByte(lightClientUpdate1Hex) - lightClientFinalityUpdateBytes* = - byteutils.hexToSeqByte(lightClientFinalityUpdateHex) + lightClientFinalityUpdateBytes* = byteutils.hexToSeqByte(lightClientFinalityUpdateHex) lightClientOptimisticUpdateBytes* = byteutils.hexToSeqByte(lightClientOptimisticUpdateHex) diff --git a/fluffy/tests/beacon_network_tests/test_beacon_content.nim b/fluffy/tests/beacon_network_tests/test_beacon_content.nim index 821de7f500..3c1d49449b 100644 --- a/fluffy/tests/beacon_network_tests/test_beacon_content.nim +++ b/fluffy/tests/beacon_network_tests/test_beacon_content.nim @@ -1,5 +1,5 @@ # Nimbus - Portal Network -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -8,7 +8,10 @@ {.used.} import - unittest2, stew/byteutils, stew/io2, stew/results, + unittest2, + stew/byteutils, + stew/io2, + stew/results, beacon_chain/networking/network_metadata, beacon_chain/spec/forks, beacon_chain/spec/datatypes/altair, @@ -27,14 +30,17 @@ suite "Beacon Content Encodings - Mainnet": metadata = getMetadataForNetwork("mainnet") genesisState = try: - template genesisData(): auto = metadata.genesis.bakedBytes - newClone(readSszForkedHashedBeaconState( - metadata.cfg, - genesisData.toOpenArray(genesisData.low, genesisData.high))) + template genesisData(): auto = + metadata.genesis.bakedBytes + + newClone( + readSszForkedHashedBeaconState( + metadata.cfg, genesisData.toOpenArray(genesisData.low, genesisData.high) + ) + ) except CatchableError as err: raiseAssert "Invalid baked-in state: " & err.msg - genesis_validators_root = - getStateField(genesisState[], genesis_validators_root) + genesis_validators_root = getStateField(genesisState[], genesis_validators_root) forkDigests = newClone ForkDigests.init(metadata.cfg, genesis_validators_root) test "LightClientBootstrap": @@ -49,10 +55,9 @@ suite "Beacon Content Encodings - Mainnet": # Decode content and content key let - contentKey = decodeSsz( - contentKeyEncoded, ContentKey) - contentValue = decodeLightClientBootstrapForked( - forkDigests[], contentValueEncoded) + contentKey = decodeSsz(contentKeyEncoded, ContentKey) + contentValue = + decodeLightClientBootstrapForked(forkDigests[], contentValueEncoded) check: contentKey.isOk() contentValue.isOk() @@ -66,8 +71,7 @@ suite "Beacon Content Encodings - Mainnet": check blockRoot == key.lightClientBootstrapKey.blockHash # re-encode content and content key - let encoded = encodeForkedLightClientObject( - bootstrap, forkDigests.capella) + let encoded = encodeForkedLightClientObject(bootstrap, forkDigests.capella) check encoded == contentValueEncoded check encode(key).asSeq() == contentKeyEncoded @@ -84,10 +88,9 @@ suite "Beacon Content Encodings - Mainnet": # Decode content and content key let - contentKey = decodeSsz( - contentKeyEncoded, ContentKey) - contentValue = decodeLightClientUpdatesByRange( - forkDigests[], contentValueEncoded) + contentKey = decodeSsz(contentKeyEncoded, ContentKey) + contentValue = + decodeLightClientUpdatesByRange(forkDigests[], contentValueEncoded) check: contentKey.isOk() contentValue.isOk() @@ -102,11 +105,10 @@ suite "Beacon Content Encodings - Mainnet": when lcDataFork > LightClientDataFork.None: check forkyObject.finalized_header.beacon.slot div (SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD) == - key.lightClientUpdateKey.startPeriod + uint64(i) + key.lightClientUpdateKey.startPeriod + uint64(i) # re-encode content and content key - let encoded = encodeLightClientUpdatesForked( - forkDigests.capella, updates.asSeq()) + let encoded = encodeLightClientUpdatesForked(forkDigests.capella, updates.asSeq()) check encoded == contentValueEncoded check encode(key).asSeq() == contentKeyEncoded @@ -123,10 +125,9 @@ suite "Beacon Content Encodings - Mainnet": # Decode content and content key let - contentKey = decodeSsz( - contentKeyEncoded, ContentKey) - contentValue = decodeLightClientFinalityUpdateForked( - forkDigests[], contentValueEncoded) + contentKey = decodeSsz(contentKeyEncoded, ContentKey) + contentValue = + decodeLightClientFinalityUpdateForked(forkDigests[], contentValueEncoded) check: contentKey.isOk() @@ -157,10 +158,9 @@ suite "Beacon Content Encodings - Mainnet": # Decode content and content key let - contentKey = decodeSsz( - contentKeyEncoded, ContentKey) - contentValue = decodeLightClientOptimisticUpdateForked( - forkDigests[], contentValueEncoded) + contentKey = decodeSsz(contentKeyEncoded, ContentKey) + contentValue = + decodeLightClientOptimisticUpdateForked(forkDigests[], contentValueEncoded) check: contentKey.isOk() @@ -183,20 +183,20 @@ suite "Beacon Content Encodings": # TODO: These tests are less useful now and should instead be altered to # use the consensus test vectors to simply test if encoding / decoding works # fine for the different forks. - const forkDigests = - ForkDigests( - phase0: ForkDigest([0'u8, 0, 0, 1]), - altair: ForkDigest([0'u8, 0, 0, 2]), - bellatrix: ForkDigest([0'u8, 0, 0, 3]), - capella: ForkDigest([0'u8, 0, 0, 4]), - deneb: ForkDigest([0'u8, 0, 0, 5]) - ) + const forkDigests = ForkDigests( + phase0: ForkDigest([0'u8, 0, 0, 1]), + altair: ForkDigest([0'u8, 0, 0, 2]), + bellatrix: ForkDigest([0'u8, 0, 0, 3]), + capella: ForkDigest([0'u8, 0, 0, 4]), + deneb: ForkDigest([0'u8, 0, 0, 5]), + ) test "LightClientBootstrap": let altairData = SSZ.decode(bootstrapBytes, altair.LightClientBootstrap) bootstrap = ForkedLightClientBootstrap( - kind: LightClientDataFork.Altair, altairData: altairData) + kind: LightClientDataFork.Altair, altairData: altairData + ) encoded = encodeForkedLightClientObject(bootstrap, forkDigests.altair) decoded = decodeLightClientBootstrapForked(forkDigests, encoded) @@ -210,7 +210,8 @@ suite "Beacon Content Encodings": let altairData = SSZ.decode(lightClientUpdateBytes, altair.LightClientUpdate) update = ForkedLightClientUpdate( - kind: LightClientDataFork.Altair, altairData: altairData) + kind: LightClientDataFork.Altair, altairData: altairData + ) encoded = encodeForkedLightClientObject(update, forkDigests.altair) decoded = decodeLightClientUpdateForked(forkDigests, encoded) @@ -224,7 +225,8 @@ suite "Beacon Content Encodings": let altairData = SSZ.decode(lightClientUpdateBytes, altair.LightClientUpdate) update = ForkedLightClientUpdate( - kind: LightClientDataFork.Altair, altairData: altairData) + kind: LightClientDataFork.Altair, altairData: altairData + ) updateList = @[update, update] encoded = encodeLightClientUpdatesForked(forkDigests.altair, updateList) @@ -237,10 +239,11 @@ suite "Beacon Content Encodings": test "LightClientFinalityUpdate": let - altairData = SSZ.decode( - lightClientFinalityUpdateBytes, altair.LightClientFinalityUpdate) + altairData = + SSZ.decode(lightClientFinalityUpdateBytes, altair.LightClientFinalityUpdate) update = ForkedLightClientFinalityUpdate( - kind: LightClientDataFork.Altair, altairData: altairData) + kind: LightClientDataFork.Altair, altairData: altairData + ) encoded = encodeForkedLightClientObject(update, forkDigests.altair) decoded = decodeLightClientFinalityUpdateForked(forkDigests, encoded) @@ -252,10 +255,11 @@ suite "Beacon Content Encodings": test "LightClientOptimisticUpdate": let - altairData = SSZ.decode( - lightClientOptimisticUpdateBytes, altair.LightClientOptimisticUpdate) + altairData = + SSZ.decode(lightClientOptimisticUpdateBytes, altair.LightClientOptimisticUpdate) update = ForkedLightClientOptimisticUpdate( - kind: LightClientDataFork.Altair, altairData: altairData) + kind: LightClientDataFork.Altair, altairData: altairData + ) encoded = encodeForkedLightClientObject(update, forkDigests.altair) decoded = decodeLightClientOptimisticUpdateForked(forkDigests, encoded) @@ -270,12 +274,12 @@ suite "Beacon Content Encodings": altairData = SSZ.decode(bootstrapBytes, altair.LightClientBootstrap) # TODO: This doesn't make much sense with current API bootstrap = ForkedLightClientBootstrap( - kind: LightClientDataFork.Altair, altairData: altairData) + kind: LightClientDataFork.Altair, altairData: altairData + ) - encodedTooEarlyFork = encodeForkedLightClientObject( - bootstrap, forkDigests.phase0) - encodedUnknownFork = encodeForkedLightClientObject( - bootstrap, ForkDigest([0'u8, 0, 0, 6])) + encodedTooEarlyFork = encodeForkedLightClientObject(bootstrap, forkDigests.phase0) + encodedUnknownFork = + encodeForkedLightClientObject(bootstrap, ForkDigest([0'u8, 0, 0, 6])) check: decodeLightClientBootstrapForked(forkDigests, @[]).isErr() @@ -284,7 +288,7 @@ suite "Beacon Content Encodings": suite "Beacon ContentKey Encodings ": test "Invalid prefix - 0 value": - let encoded = ByteList.init(@[byte 0x00]) + let encoded = ByteList.init(@[byte 0x00]) let decoded = decode(encoded) check decoded.isNone() diff --git a/fluffy/tests/beacon_network_tests/test_beacon_light_client.nim b/fluffy/tests/beacon_network_tests/test_beacon_light_client.nim index 1552b392ed..0db4eb9d36 100644 --- a/fluffy/tests/beacon_network_tests/test_beacon_light_client.nim +++ b/fluffy/tests/beacon_network_tests/test_beacon_light_client.nim @@ -1,5 +1,5 @@ # Nimbus - Portal Network -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -8,7 +8,8 @@ {.push raises: [].} import - testutils/unittests, chronos, + testutils/unittests, + chronos, eth/p2p/discoveryv5/protocol as discv5_protocol, beacon_chain/spec/forks, beacon_chain/spec/datatypes/altair, @@ -21,10 +22,12 @@ procSuite "Portal Beacon Light Client": let rng = newRng() proc headerCallback( - q: AsyncQueue[ForkedLightClientHeader]): LightClientHeaderCallback = + q: AsyncQueue[ForkedLightClientHeader] + ): LightClientHeaderCallback = return ( - proc (lightClient: LightClient, finalizedHeader: ForkedLightClientHeader) - {.gcsafe, raises: [].} = + proc( + lightClient: LightClient, finalizedHeader: ForkedLightClientHeader + ) {.gcsafe, raises: [].} = try: q.putNoWait(finalizedHeader) except AsyncQueueFullError as exc: @@ -41,7 +44,8 @@ procSuite "Portal Beacon Light Client": lcNode2 = newLCNode(rng, 20303, networkData) altairData = SSZ.decode(bootstrapBytes, altair.LightClientBootstrap) bootstrap = ForkedLightClientBootstrap( - kind: LightClientDataFork.Altair, altairData: altairData) + kind: LightClientDataFork.Altair, altairData: altairData + ) bootstrapHeaderHash = hash_tree_root(altairData.header) check: @@ -52,12 +56,9 @@ procSuite "Portal Beacon Light Client": (await lcNode2.portalProtocol().ping(lcNode1.localNode())).isOk() let - bootstrapKey = LightClientBootstrapKey( - blockHash: bootstrapHeaderHash - ) + bootstrapKey = LightClientBootstrapKey(blockHash: bootstrapHeaderHash) bootstrapContentKey = ContentKey( - contentType: lightClientBootstrap, - lightClientBootstrapKey: bootstrapKey + contentType: lightClientBootstrap, lightClientBootstrapKey: bootstrapKey ) bootstrapContentKeyEncoded = encode(bootstrapContentKey) @@ -66,12 +67,12 @@ procSuite "Portal Beacon Light Client": lcNode2.portalProtocol().storeContent( bootstrapContentKeyEncoded, bootstrapContentId, - encodeForkedLightClientObject(bootstrap, networkData.forks.altair) + encodeForkedLightClientObject(bootstrap, networkData.forks.altair), ) let lc = LightClient.new( - lcNode1.beaconNetwork, rng, networkData, - LightClientFinalizationMode.Optimistic) + lcNode1.beaconNetwork, rng, networkData, LightClientFinalizationMode.Optimistic + ) lc.onFinalizedHeader = headerCallback(finalizedHeaders) lc.onOptimisticHeader = headerCallback(optimisticHeaders) @@ -91,4 +92,3 @@ procSuite "Portal Beacon Light Client": check: hash_tree_root(receivedFinalHeader.altairData) == bootstrapHeaderHash hash_tree_root(receivedOptimisticHeader.altairData) == bootstrapHeaderHash - diff --git a/fluffy/tests/beacon_network_tests/test_beacon_network.nim b/fluffy/tests/beacon_network_tests/test_beacon_network.nim index d05a1318b8..aafeb914ee 100644 --- a/fluffy/tests/beacon_network_tests/test_beacon_network.nim +++ b/fluffy/tests/beacon_network_tests/test_beacon_network.nim @@ -6,18 +6,18 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - testutils/unittests, chronos, + testutils/unittests, + chronos, eth/p2p/discoveryv5/protocol as discv5_protocol, beacon_chain/spec/forks, beacon_chain/spec/datatypes/altair, # Test helpers - beacon_chain/../tests/testblockutil, - beacon_chain/../tests/mocking/mock_genesis, - beacon_chain/../tests/consensus_spec/fixtures_utils, - + beacon_chain /../ tests/testblockutil, + beacon_chain /../ tests/mocking/mock_genesis, + beacon_chain /../ tests/consensus_spec/fixtures_utils, ../../network/wire/portal_protocol, - ../../network/beacon/[beacon_network, beacon_init_loader, - beacon_chain_historical_summaries], + ../../network/beacon/ + [beacon_network, beacon_init_loader, beacon_chain_historical_summaries], "."/[light_client_test_data, beacon_test_helpers] procSuite "Beacon Content Network": @@ -40,14 +40,12 @@ procSuite "Beacon Content Network": let altairData = SSZ.decode(bootstrapBytes, altair.LightClientBootstrap) bootstrap = ForkedLightClientBootstrap( - kind: LightClientDataFork.Altair, altairData: altairData) - bootstrapHeaderHash = hash_tree_root(altairData.header) - bootstrapKey = LightClientBootstrapKey( - blockHash: bootstrapHeaderHash + kind: LightClientDataFork.Altair, altairData: altairData ) + bootstrapHeaderHash = hash_tree_root(altairData.header) + bootstrapKey = LightClientBootstrapKey(blockHash: bootstrapHeaderHash) bootstrapContentKey = ContentKey( - contentType: lightClientBootstrap, - lightClientBootstrapKey: bootstrapKey + contentType: lightClientBootstrap, lightClientBootstrapKey: bootstrapKey ) bootstrapContentKeyEncoded = encode(bootstrapContentKey) @@ -56,13 +54,11 @@ procSuite "Beacon Content Network": lcNode2.portalProtocol().storeContent( bootstrapContentKeyEncoded, bootstrapContentId, - encodeForkedLightClientObject(bootstrap, forkDigests.altair) + encodeForkedLightClientObject(bootstrap, forkDigests.altair), ) let bootstrapFromNetworkResult = - await lcNode1.beaconNetwork.getLightClientBootstrap( - bootstrapHeaderHash - ) + await lcNode1.beaconNetwork.getLightClientBootstrap(bootstrapHeaderHash) check: bootstrapFromNetworkResult.isOk() @@ -86,55 +82,51 @@ procSuite "Beacon Content Network": (await lcNode2.portalProtocol().ping(lcNode1.localNode())).isOk() let - finalityUpdateData = SSZ.decode( - lightClientFinalityUpdateBytes, altair.LightClientFinalityUpdate) + finalityUpdateData = + SSZ.decode(lightClientFinalityUpdateBytes, altair.LightClientFinalityUpdate) finalityUpdate = ForkedLightClientFinalityUpdate( - kind: LightClientDataFork.Altair, altairData: finalityUpdateData) + kind: LightClientDataFork.Altair, altairData: finalityUpdateData + ) finalizedHeaderSlot = finalityUpdateData.finalized_header.beacon.slot - finalizedOptimisticHeaderSlot = - finalityUpdateData.attested_header.beacon.slot + finalizedOptimisticHeaderSlot = finalityUpdateData.attested_header.beacon.slot - optimisticUpdateData = SSZ.decode( - lightClientOptimisticUpdateBytes, altair.LightClientOptimisticUpdate) + optimisticUpdateData = + SSZ.decode(lightClientOptimisticUpdateBytes, altair.LightClientOptimisticUpdate) optimisticUpdate = ForkedLightClientOptimisticUpdate( - kind: LightClientDataFork.Altair, altairData: optimisticUpdateData) + kind: LightClientDataFork.Altair, altairData: optimisticUpdateData + ) optimisticHeaderSlot = optimisticUpdateData.signature_slot - finalityUpdateKey = finalityUpdateContentKey( - distinctBase(finalizedHeaderSlot) - ) + finalityUpdateKey = finalityUpdateContentKey(distinctBase(finalizedHeaderSlot)) finalityKeyEnc = encode(finalityUpdateKey) finalityUpdateId = toContentId(finalityKeyEnc) - optimisticUpdateKey = optimisticUpdateContentKey( - distinctBase(optimisticHeaderSlot)) + optimisticUpdateKey = + optimisticUpdateContentKey(distinctBase(optimisticHeaderSlot)) optimisticKeyEnc = encode(optimisticUpdateKey) optimisticUpdateId = toContentId(optimisticKeyEnc) - # This silently assumes that peer stores only one latest update, under # the contentId coresponding to latest update content key lcNode2.portalProtocol().storeContent( finalityKeyEnc, finalityUpdateId, - encodeForkedLightClientObject(finalityUpdate, forkDigests.altair) + encodeForkedLightClientObject(finalityUpdate, forkDigests.altair), ) lcNode2.portalProtocol().storeContent( optimisticKeyEnc, optimisticUpdateId, - encodeForkedLightClientObject(optimisticUpdate, forkDigests.altair) + encodeForkedLightClientObject(optimisticUpdate, forkDigests.altair), ) let - finalityResult = - await lcNode1.beaconNetwork.getLightClientFinalityUpdate( - distinctBase(finalizedHeaderSlot), - ) - optimisticResult = - await lcNode1.beaconNetwork.getLightClientOptimisticUpdate( - distinctBase(optimisticHeaderSlot) - ) + finalityResult = await lcNode1.beaconNetwork.getLightClientFinalityUpdate( + distinctBase(finalizedHeaderSlot) + ) + optimisticResult = await lcNode1.beaconNetwork.getLightClientOptimisticUpdate( + distinctBase(optimisticHeaderSlot) + ) check: finalityResult.isOk() @@ -163,34 +155,26 @@ procSuite "Beacon Content Network": altairData1 = SSZ.decode(lightClientUpdateBytes, altair.LightClientUpdate) altairData2 = SSZ.decode(lightClientUpdateBytes1, altair.LightClientUpdate) update1 = ForkedLightClientUpdate( - kind: LightClientDataFork.Altair, altairData: altairData1) + kind: LightClientDataFork.Altair, altairData: altairData1 + ) update2 = ForkedLightClientUpdate( - kind: LightClientDataFork.Altair, altairData: altairData2) + kind: LightClientDataFork.Altair, altairData: altairData2 + ) updates = @[update1, update2] content = encodeLightClientUpdatesForked(forkDigests.altair, updates) - startPeriod = - altairData1.attested_header.beacon.slot.sync_committee_period + startPeriod = altairData1.attested_header.beacon.slot.sync_committee_period contentKey = ContentKey( contentType: lightClientUpdate, - lightClientUpdateKey: LightClientUpdateKey( - startPeriod: startPeriod.uint64, - count: uint64(2) - ) + lightClientUpdateKey: + LightClientUpdateKey(startPeriod: startPeriod.uint64, count: uint64(2)), ) contentKeyEncoded = encode(contentKey) contentId = toContentId(contentKey) - lcNode2.portalProtocol().storeContent( - contentKeyEncoded, - contentId, - content - ) + lcNode2.portalProtocol().storeContent(contentKeyEncoded, contentId, content) let updatesResult = - await lcNode1.beaconNetwork.getLightClientUpdatesByRange( - startPeriod, - uint64(2) - ) + await lcNode1.beaconNetwork.getLightClientUpdatesByRange(startPeriod, uint64(2)) check: updatesResult.isOk() @@ -218,29 +202,28 @@ procSuite "Beacon Content Network": # index i = 0 is second block. # index i = 8190 is 8192th block and last one that is part of the first # historical root - for i in 0..= ConsensusFork.Capella: - let historical_summaries = forkyState.data.historical_summaries - let res = buildProof(state[]) - check res.isOk() - let - proof = res.get() - - historicalSummariesWithProof = HistoricalSummariesWithProof( - finalized_slot: forkyState.data.slot, - historical_summaries: historical_summaries, - proof: proof - ) - - content = SSZ.encode(historicalSummariesWithProof) - - (content, forkyState.data.slot, forkyState.root) - else: - raiseAssert("Not implemented pre-Capella") + let (content, slot, root) = withState(state[]): + when consensusFork >= ConsensusFork.Capella: + let historical_summaries = forkyState.data.historical_summaries + let res = buildProof(state[]) + check res.isOk() + let + proof = res.get() + + historicalSummariesWithProof = HistoricalSummariesWithProof( + finalized_slot: forkyState.data.slot, + historical_summaries: historical_summaries, + proof: proof, + ) + + content = SSZ.encode(historicalSummariesWithProof) + + (content, forkyState.data.slot, forkyState.root) + else: + raiseAssert("Not implemented pre-Capella") let networkData = loadNetworkData("mainnet") lcNode1 = newLCNode(rng, 20302, networkData) @@ -258,11 +241,7 @@ procSuite "Beacon Content Network": contentKeyEncoded = historicalSummariesContentKey().encode() contentId = toContentId(contentKeyEncoded) - lcNode2.portalProtocol().storeContent( - contentKeyEncoded, - contentId, - content - ) + lcNode2.portalProtocol().storeContent(contentKeyEncoded, contentId, content) block: let res = await lcNode1.beaconNetwork.getHistoricalSummaries() @@ -276,22 +255,18 @@ procSuite "Beacon Content Network": dummyFinalityUpdate = capella.LightClientFinalityUpdate( finalized_header: capella.LightClientHeader( beacon: BeaconBlockHeader(slot: slot, state_root: root) - )) + ) + ) finalityUpdateForked = ForkedLightClientFinalityUpdate( - kind: LightClientDataFork.Capella, capellaData: dummyFinalityUpdate) - forkDigest = forkDigestAtEpoch( - forkDigests, epoch(slot), cfg) - content = encodeFinalityUpdateForked( - forkDigest,finalityUpdateForked) + kind: LightClientDataFork.Capella, capellaData: dummyFinalityUpdate + ) + forkDigest = forkDigestAtEpoch(forkDigests, epoch(slot), cfg) + content = encodeFinalityUpdateForked(forkDigest, finalityUpdateForked) contentKey = finalityUpdateContentKey(slot.distinctBase()) contentKeyEncoded = encode(contentKey) contentId = toContentId(contentKeyEncoded) - lcNode1.portalProtocol().storeContent( - contentKeyEncoded, - contentId, - content - ) + lcNode1.portalProtocol().storeContent(contentKeyEncoded, contentId, content) block: let res = await lcNode1.beaconNetwork.getHistoricalSummaries() diff --git a/fluffy/tests/portal_spec_tests/mainnet/all_fluffy_portal_spec_tests.nim b/fluffy/tests/portal_spec_tests/mainnet/all_fluffy_portal_spec_tests.nim index 665bae68e3..8e019a5712 100644 --- a/fluffy/tests/portal_spec_tests/mainnet/all_fluffy_portal_spec_tests.nim +++ b/fluffy/tests/portal_spec_tests/mainnet/all_fluffy_portal_spec_tests.nim @@ -1,11 +1,11 @@ # Nimbus -# Copyright (c) 2022 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) # * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) # at your option. This file may not be copied, modified, or distributed except according to those terms. -{. warning[UnusedImport]:off .} +{.warning[UnusedImport]: off.} import ./test_portal_wire_encoding, diff --git a/fluffy/tests/portal_spec_tests/mainnet/test_accumulator_root.nim b/fluffy/tests/portal_spec_tests/mainnet/test_accumulator_root.nim index 56d1198078..702cc045fe 100644 --- a/fluffy/tests/portal_spec_tests/mainnet/test_accumulator_root.nim +++ b/fluffy/tests/portal_spec_tests/mainnet/test_accumulator_root.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -10,7 +10,9 @@ {.push raises: [].} import - unittest2, stint, stew/byteutils, + unittest2, + stint, + stew/byteutils, eth/common/eth_types_rlp, ../../../eth_data/history_data_json_store, ../../../network/history/[history_content, accumulator] @@ -21,7 +23,8 @@ suite "Header Accumulator Root": hashTreeRoots = [ "53521984da4bbdbb011fe8a1473bf71bdf1040b14214a05cd1ce6932775bc7fa", "ae48c6d4e1b0a68324f346755645ba7e5d99da3dd1c38a9acd10e2fe4f43cfb4", - "52f7bd6204be2d98cb9d09aa375b4355140e0d65744ce7b2f3ea34d8e6453572"] + "52f7bd6204be2d98cb9d09aa375b4355140e0d65744ce7b2f3ea34d8e6453572", + ] dataFile = "./fluffy/tests/blocks/mainnet_blocks_1-2.json" diff --git a/fluffy/tests/portal_spec_tests/mainnet/test_header_content.nim b/fluffy/tests/portal_spec_tests/mainnet/test_header_content.nim index 495f1334eb..f39d9491d5 100644 --- a/fluffy/tests/portal_spec_tests/mainnet/test_header_content.nim +++ b/fluffy/tests/portal_spec_tests/mainnet/test_header_content.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -9,43 +9,43 @@ {.push raises: [].} -import - unittest2, stew/byteutils, - ../../../network/header/header_content +import unittest2, stew/byteutils, ../../../network/header/header_content suite "Header Gossip ContentKey Encodings": test "BlockHeader": - # Input - const - blockHash = BlockHash.fromHex( - "0xd1c390624d3bd4e409a61a858e5dcc5517729a9170d014a6c96530d64dd8621d") - blockNumber = 2.stuint(256) - - # Output - const - contentKeyHex = - "00d1c390624d3bd4e409a61a858e5dcc5517729a9170d014a6c96530d64dd8621d0200000000000000000000000000000000000000000000000000000000000000" - contentId = - "93053813395975896824800219097617621670658136800980011170166846009189305194644" - # or - contentIdHexBE = - "cdba9789eec7a1994ec7c033c46c2c94242da2c016051bf09240fd9a81589894" - - let contentKey = ContentKey( - contentType: newBlockHeader, - newBlockHeaderKey: - NewBlockHeaderKey(blockHash: blockHash, blockNumber: blockNumber)) - - let encoded = encode(contentKey) - check encoded.asSeq.toHex == contentKeyHex - let decoded = decode(encoded) - check decoded.isSome() - - let contentKeyDecoded = decoded.get() - check: - contentKeyDecoded.contentType == contentKey.contentType - contentKeyDecoded.newBlockHeaderKey == contentKey.newBlockHeaderKey - - toContentId(contentKey) == parse(contentId, StUint[256], 10) - # In stint this does BE hex string - toContentId(contentKey).toHex() == contentIdHexBE + # Input + const + blockHash = BlockHash.fromHex( + "0xd1c390624d3bd4e409a61a858e5dcc5517729a9170d014a6c96530d64dd8621d" + ) + blockNumber = 2.stuint(256) + + # Output + const + contentKeyHex = + "00d1c390624d3bd4e409a61a858e5dcc5517729a9170d014a6c96530d64dd8621d0200000000000000000000000000000000000000000000000000000000000000" + contentId = + "93053813395975896824800219097617621670658136800980011170166846009189305194644" + # or + contentIdHexBE = + "cdba9789eec7a1994ec7c033c46c2c94242da2c016051bf09240fd9a81589894" + + let contentKey = ContentKey( + contentType: newBlockHeader, + newBlockHeaderKey: + NewBlockHeaderKey(blockHash: blockHash, blockNumber: blockNumber), + ) + + let encoded = encode(contentKey) + check encoded.asSeq.toHex == contentKeyHex + let decoded = decode(encoded) + check decoded.isSome() + + let contentKeyDecoded = decoded.get() + check: + contentKeyDecoded.contentType == contentKey.contentType + contentKeyDecoded.newBlockHeaderKey == contentKey.newBlockHeaderKey + + toContentId(contentKey) == parse(contentId, StUint[256], 10) + # In stint this does BE hex string + toContentId(contentKey).toHex() == contentIdHexBE diff --git a/fluffy/tests/portal_spec_tests/mainnet/test_history_content.nim b/fluffy/tests/portal_spec_tests/mainnet/test_history_content.nim index 82467e45e7..a847b31637 100644 --- a/fluffy/tests/portal_spec_tests/mainnet/test_history_content.nim +++ b/fluffy/tests/portal_spec_tests/mainnet/test_history_content.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2023 Status Research & Development GmbH +# Copyright (c) 2023-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -10,7 +10,8 @@ {.push raises: [].} import - unittest2, stew/byteutils, + unittest2, + stew/byteutils, eth/common/eth_types_rlp, ../../../network_metadata, ../../../eth_data/[history_data_json_store, history_data_ssz_e2s], @@ -20,25 +21,26 @@ import suite "History Content Encodings": test "HeaderWithProof Building and Encoding": const - headerFile = "./vendor/portal-spec-tests/tests/mainnet/history/headers/1000001-1000010.e2s" - accumulatorFile = "./vendor/portal-spec-tests/tests/mainnet/history/accumulator/epoch-accumulator-00122.ssz" - headersWithProofFile = "./vendor/portal-spec-tests/tests/mainnet/history/headers_with_proof/1000001-1000010.json" + headerFile = + "./vendor/portal-spec-tests/tests/mainnet/history/headers/1000001-1000010.e2s" + accumulatorFile = + "./vendor/portal-spec-tests/tests/mainnet/history/accumulator/epoch-accumulator-00122.ssz" + headersWithProofFile = + "./vendor/portal-spec-tests/tests/mainnet/history/headers_with_proof/1000001-1000010.json" let blockHeaders = readBlockHeaders(headerFile).valueOr: raiseAssert "Invalid header file: " & headerFile epochAccumulator = readEpochAccumulatorCached(accumulatorFile).valueOr: raiseAssert "Invalid epoch accumulator file: " & accumulatorFile - blockHeadersWithProof = - buildHeadersWithProof(blockHeaders, epochAccumulator).valueOr: - raiseAssert "Could not build headers with proof" + blockHeadersWithProof = buildHeadersWithProof(blockHeaders, epochAccumulator).valueOr: + raiseAssert "Could not build headers with proof" accumulator = try: SSZ.decode(finishedAccumulator, FinishedAccumulator) except SszError as err: raiseAssert "Invalid baked-in accumulator: " & err.msg - let res = readJsonType(headersWithProofFile, JsonPortalContentTable) check res.isOk() let content = res.get() @@ -48,8 +50,7 @@ suite "History Content Encodings": # them with the ones from the test vectors. let blockNumber = blockHeaders[i].blockNumber - contentKeyEncoded = - content[blockNumber.toString()].content_key.hexToSeqByte() + contentKeyEncoded = content[blockNumber.toString()].content_key.hexToSeqByte() contentValueEncoded = content[blockNumber.toString()].content_value.hexToSeqByte() @@ -60,10 +61,8 @@ suite "History Content Encodings": # Also run the encode/decode loopback and verification of the header # proofs. let - contentKey = decodeSsz( - contentKeyEncoded, ContentKey) - contentValue = decodeSsz( - contentValueEncoded, BlockHeaderWithProof) + contentKey = decodeSsz(contentKeyEncoded, ContentKey) + contentValue = decodeSsz(contentValueEncoded, BlockHeaderWithProof) check: contentKey.isOk() @@ -105,10 +104,8 @@ suite "History Content Encodings": # Decode content let - contentKey = decodeSsz( - contentKeyEncoded, ContentKey) - contentValue = decodeSsz( - contentValueEncoded, BlockHeaderWithProof) + contentKey = decodeSsz(contentKeyEncoded, ContentKey) + contentValue = decodeSsz(contentValueEncoded, BlockHeaderWithProof) check: contentKey.isOk() @@ -129,8 +126,7 @@ suite "History Content Encodings": test "PortalBlockBody (Legacy) Encoding/Decoding and Verification": const - dataFile = - "./vendor/portal-spec-tests/tests/mainnet/history/bodies/14764013.json" + dataFile = "./vendor/portal-spec-tests/tests/mainnet/history/bodies/14764013.json" headersWithProofFile = "./vendor/portal-spec-tests/tests/mainnet/history/headers_with_proof/14764013.json" @@ -150,11 +146,9 @@ suite "History Content Encodings": # Get the header for validation of body let headerEncoded = headers[k].content_value.hexToSeqByte() - headerWithProofRes = decodeSsz( - headerEncoded, BlockHeaderWithProof) + headerWithProofRes = decodeSsz(headerEncoded, BlockHeaderWithProof) check headerWithProofRes.isOk() - let headerRes = decodeRlp( - headerWithProofRes.get().header.asSeq(), BlockHeader) + let headerRes = decodeRlp(headerWithProofRes.get().header.asSeq(), BlockHeader) check headerRes.isOk() let header = headerRes.get() @@ -163,8 +157,7 @@ suite "History Content Encodings": check contentKey.isOk() # Decode (SSZ + RLP decode step) and validate block body - let contentValue = validateBlockBodyBytes( - contentValueEncoded, header) + let contentValue = validateBlockBodyBytes(contentValueEncoded, header) check contentValue.isOk() # Encode content and content key @@ -175,9 +168,8 @@ suite "History Content Encodings": test "PortalBlockBody (Shanghai) Encoding/Decoding": # TODO: We don't have the header (without proof) ready here so cannot do # full validation for now. Add this header and then we can do like above. - const - dataFile = - "./vendor/portal-spec-tests/tests/mainnet/history/bodies/17139055.json" + const dataFile = + "./vendor/portal-spec-tests/tests/mainnet/history/bodies/17139055.json" let res = readJsonType(dataFile, JsonPortalContentTable) check res.isOk() @@ -193,8 +185,7 @@ suite "History Content Encodings": check contentKey.isOk() # Decode (SSZ + RLP decode step) and validate block body - let contentValue = decodeBlockBodyBytes( - contentValueEncoded) + let contentValue = decodeBlockBodyBytes(contentValueEncoded) check contentValue.isOk() # Encode content and content key @@ -225,11 +216,9 @@ suite "History Content Encodings": # Get the header for validation of receipts let headerEncoded = headers[k].content_value.hexToSeqByte() - headerWithProofRes = decodeSsz( - headerEncoded, BlockHeaderWithProof) + headerWithProofRes = decodeSsz(headerEncoded, BlockHeaderWithProof) check headerWithProofRes.isOk() - let headerRes = decodeRlp( - headerWithProofRes.get().header.asSeq(), BlockHeader) + let headerRes = decodeRlp(headerWithProofRes.get().header.asSeq(), BlockHeader) check headerRes.isOk() let header = headerRes.get() @@ -238,8 +227,7 @@ suite "History Content Encodings": check contentKey.isOk() # Decode (SSZ + RLP decode step) and validate receipts - let contentValue = validateReceiptsBytes( - contentValueEncoded, header.receiptRoot) + let contentValue = validateReceiptsBytes(contentValueEncoded, header.receiptRoot) check contentValue.isOk() # Encode content diff --git a/fluffy/tests/portal_spec_tests/mainnet/test_history_content_keys.nim b/fluffy/tests/portal_spec_tests/mainnet/test_history_content_keys.nim index a698c757b2..c281082432 100644 --- a/fluffy/tests/portal_spec_tests/mainnet/test_history_content_keys.nim +++ b/fluffy/tests/portal_spec_tests/mainnet/test_history_content_keys.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2021-2022 Status Research & Development GmbH +# Copyright (c) 2021-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -8,8 +8,11 @@ {.used.} import - unittest2, stew/byteutils, stint, - ssz_serialization, ssz_serialization/[proofs, merkleization], + unittest2, + stew/byteutils, + stint, + ssz_serialization, + ssz_serialization/[proofs, merkleization], ../../../network/history/[history_content, accumulator] # According to test vectors: @@ -19,7 +22,8 @@ suite "History ContentKey Encodings": test "BlockHeader": # Input const blockHash = BlockHash.fromHex( - "0xd1c390624d3bd4e409a61a858e5dcc5517729a9170d014a6c96530d64dd8621d") + "0xd1c390624d3bd4e409a61a858e5dcc5517729a9170d014a6c96530d64dd8621d" + ) # Output const @@ -32,8 +36,8 @@ suite "History ContentKey Encodings": "3e86b3767b57402ea72e369ae0496ce47cc15be685bec3b4726b9f316e3895fe" let contentKey = ContentKey( - contentType: blockHeader, - blockHeaderKey: BlockKey(blockHash: blockHash)) + contentType: blockHeader, blockHeaderKey: BlockKey(blockHash: blockHash) + ) let encoded = encode(contentKey) check encoded.asSeq.toHex == contentKeyHex @@ -52,7 +56,8 @@ suite "History ContentKey Encodings": test "BlockBody": # Input const blockHash = BlockHash.fromHex( - "0xd1c390624d3bd4e409a61a858e5dcc5517729a9170d014a6c96530d64dd8621d") + "0xd1c390624d3bd4e409a61a858e5dcc5517729a9170d014a6c96530d64dd8621d" + ) # Output const @@ -64,9 +69,8 @@ suite "History ContentKey Encodings": contentIdHexBE = "ebe414854629d60c58ddd5bf60fd72e41760a5f7a463fdcb169f13ee4a26786b" - let contentKey = ContentKey( - contentType: blockBody, - blockBodyKey: BlockKey(blockHash: blockHash)) + let contentKey = + ContentKey(contentType: blockBody, blockBodyKey: BlockKey(blockHash: blockHash)) let encoded = encode(contentKey) check encoded.asSeq.toHex == contentKeyHex @@ -85,7 +89,8 @@ suite "History ContentKey Encodings": test "Receipts": # Input const blockHash = BlockHash.fromHex( - "0xd1c390624d3bd4e409a61a858e5dcc5517729a9170d014a6c96530d64dd8621d") + "0xd1c390624d3bd4e409a61a858e5dcc5517729a9170d014a6c96530d64dd8621d" + ) # Output const @@ -97,9 +102,8 @@ suite "History ContentKey Encodings": contentIdHexBE = "a888f4aafe9109d495ac4d4774a6277c1ada42035e3da5e10a04cc93247c04a4" - let contentKey = ContentKey( - contentType: receipts, - receiptsKey: BlockKey(blockHash: blockHash)) + let contentKey = + ContentKey(contentType: receipts, receiptsKey: BlockKey(blockHash: blockHash)) let encoded = encode(contentKey) check encoded.asSeq.toHex == contentKeyHex @@ -118,7 +122,8 @@ suite "History ContentKey Encodings": test "Epoch Accumulator": # Input const epochHash = Digest.fromHex( - "0xe242814b90ed3950e13aac7e56ce116540c71b41d1516605aada26c6c07cc491") + "0xe242814b90ed3950e13aac7e56ce116540c71b41d1516605aada26c6c07cc491" + ) # Output const @@ -132,7 +137,8 @@ suite "History ContentKey Encodings": let contentKey = ContentKey( contentType: epochAccumulator, - epochAccumulatorKey: EpochAccumulatorKey(epochHash: epochHash)) + epochAccumulatorKey: EpochAccumulatorKey(epochHash: epochHash), + ) let encoded = encode(contentKey) check encoded.asSeq.toHex == contentKeyHex diff --git a/fluffy/tests/portal_spec_tests/mainnet/test_history_content_validation.nim b/fluffy/tests/portal_spec_tests/mainnet/test_history_content_validation.nim index bf273bcade..143113a6a5 100644 --- a/fluffy/tests/portal_spec_tests/mainnet/test_history_content_validation.nim +++ b/fluffy/tests/portal_spec_tests/mainnet/test_history_content_validation.nim @@ -1,5 +1,5 @@ # Nimbus - Portal Network -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -10,7 +10,8 @@ {.push raises: [].} import - unittest2, stint, + unittest2, + stint, stew/[byteutils, results], eth/[common/eth_types, rlp], ../../../common/common_types, @@ -20,12 +21,11 @@ import const dataFile = "./fluffy/tests/blocks/mainnet_blocks_selected.json" # Block that will be validated - blockHashStr = - "0xce8f770a56203e10afe19c7dd7e2deafc356e6cce0a560a30a85add03da56137" + blockHashStr = "0xce8f770a56203e10afe19c7dd7e2deafc356e6cce0a560a30a85add03da56137" suite "History Network Content Validation": - let blockDataTable = readJsonType(dataFile, BlockDataTable).expect( - "Valid data file should parse") + let blockDataTable = + readJsonType(dataFile, BlockDataTable).expect("Valid data file should parse") let blockData = try: @@ -40,20 +40,20 @@ suite "History Network Content Validation": blockHash = BlockHash.fromHex(blockHashStr) - blockHeader = decodeRlp(blockHeaderBytes, BlockHeader).expect( - "Valid header should decode") - blockBody = validateBlockBodyBytes( - blockBodyBytes, blockHeader).expect( - "Should be Valid decoded block body") - receipts = validateReceiptsBytes( - receiptsBytes, blockHeader.receiptRoot).expect( - "Should be Valid decoded receipts") + blockHeader = + decodeRlp(blockHeaderBytes, BlockHeader).expect("Valid header should decode") + blockBody = validateBlockBodyBytes(blockBodyBytes, blockHeader).expect( + "Should be Valid decoded block body" + ) + receipts = validateReceiptsBytes(receiptsBytes, blockHeader.receiptRoot).expect( + "Should be Valid decoded receipts" + ) test "Valid Header": check validateBlockHeaderBytes(blockHeaderBytes, blockHash).isOk() test "Malformed Header": - let malformedBytes = blockHeaderBytes[10..blockHeaderBytes.high] + let malformedBytes = blockHeaderBytes[10 .. blockHeaderBytes.high] check validateBlockHeaderBytes(malformedBytes, blockHash).isErr() @@ -67,28 +67,25 @@ suite "History Network Content Validation": check validateBlockHeaderBytes(modifiedHeaderBytes, blockHash).isErr() test "Valid Block Body": - check validateBlockBodyBytes( - blockBodyBytes, blockHeader).isOk() + check validateBlockBodyBytes(blockBodyBytes, blockHeader).isOk() test "Malformed Block Body": - let malformedBytes = blockBodyBytes[10..blockBodyBytes.high] + let malformedBytes = blockBodyBytes[10 .. blockBodyBytes.high] - check validateBlockBodyBytes( - malformedBytes, blockHeader).isErr() + check validateBlockBodyBytes(malformedBytes, blockHeader).isErr() test "Invalid Block Body - Modified Transaction List": var modifiedBody = blockBody # drop first transaction let modifiedTransactionList = - blockBody.transactions[1..blockBody.transactions.high] + blockBody.transactions[1 .. blockBody.transactions.high] modifiedBody.transactions = modifiedTransactionList let modifiedBodyBytes = encode(modifiedBody) - check validateBlockBodyBytes( - modifiedBodyBytes, blockHeader).isErr() + check validateBlockBodyBytes(modifiedBodyBytes, blockHeader).isErr() test "Invalid Block Body - Modified Uncles List": var modifiedBody = blockBody @@ -97,21 +94,19 @@ suite "History Network Content Validation": let modifiedBodyBytes = encode(modifiedBody) - check validateBlockBodyBytes( - modifiedBodyBytes, blockHeader).isErr() + check validateBlockBodyBytes(modifiedBodyBytes, blockHeader).isErr() test "Valid Receipts": check validateReceiptsBytes(receiptsBytes, blockHeader.receiptRoot).isOk() test "Malformed Receipts": - let malformedBytes = receiptsBytes[10..receiptsBytes.high] + let malformedBytes = receiptsBytes[10 .. receiptsBytes.high] check validateReceiptsBytes(malformedBytes, blockHeader.receiptRoot).isErr() test "Invalid Receipts - Modified Receipts List": - var modifiedReceipts = receipts[1..receipts.high] + var modifiedReceipts = receipts[1 .. receipts.high] let modifiedReceiptsBytes = encode(modifiedReceipts) - check validateReceiptsBytes( - modifiedReceiptsBytes, blockHeader.receiptRoot).isErr() + check validateReceiptsBytes(modifiedReceiptsBytes, blockHeader.receiptRoot).isErr() diff --git a/fluffy/tests/portal_spec_tests/mainnet/test_portal_wire_encoding.nim b/fluffy/tests/portal_spec_tests/mainnet/test_portal_wire_encoding.nim index 6e006f9c5f..9154778a8f 100644 --- a/fluffy/tests/portal_spec_tests/mainnet/test_portal_wire_encoding.nim +++ b/fluffy/tests/portal_spec_tests/mainnet/test_portal_wire_encoding.nim @@ -1,5 +1,5 @@ # Fluffy -# Copyright (c) 2021-2023 Status Research & Development GmbH +# Copyright (c) 2021-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -8,7 +8,10 @@ {.used.} import - unittest2, stint, stew/[byteutils, results], eth/p2p/discoveryv5/enr, + unittest2, + stint, + stew/[byteutils, results], + eth/p2p/discoveryv5/enr, ../../../network/wire/messages # According to test vectors: @@ -91,15 +94,22 @@ suite "Portal Wire Protocol Message Encodings": test "Nodes Response - enrs": var e1, e2: Record check: - e1.fromURI("enr:-HW4QBzimRxkmT18hMKaAL3IcZF1UcfTMPyi3Q1pxwZZbcZVRI8DC5infUAB_UauARLOJtYTxaagKoGmIjzQxO2qUygBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAPMljNMTg") - e2.fromURI("enr:-HW4QNfxw543Ypf4HXKXdYxkyzfcxcO-6p9X986WldfVpnVTQX1xlTnWrktEWUbeTZnmgOuAY_KUhbVV1Ft98WoYUBMBgmlkgnY0iXNlY3AyNTZrMaEDDiy3QkHAxPyOgWbxp5oF1bDdlYE6dLCUUp8xfVw50jU") + e1.fromURI( + "enr:-HW4QBzimRxkmT18hMKaAL3IcZF1UcfTMPyi3Q1pxwZZbcZVRI8DC5infUAB_UauARLOJtYTxaagKoGmIjzQxO2qUygBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAPMljNMTg" + ) + e2.fromURI( + "enr:-HW4QNfxw543Ypf4HXKXdYxkyzfcxcO-6p9X986WldfVpnVTQX1xlTnWrktEWUbeTZnmgOuAY_KUhbVV1Ft98WoYUBMBgmlkgnY0iXNlY3AyNTZrMaEDDiy3QkHAxPyOgWbxp5oF1bDdlYE6dLCUUp8xfVw50jU" + ) let total = 0x1'u8 - n = NodesMessage(total: total, enrs: List[ByteList, 32](@[ByteList(e1.raw), ByteList(e2.raw)])) + n = NodesMessage( + total: total, enrs: List[ByteList, 32](@[ByteList(e1.raw), ByteList(e2.raw)]) + ) let encoded = encodeMessage(n) - check encoded.toHex == "030105000000080000007f000000f875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235" + check encoded.toHex == + "030105000000080000007f000000f875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235" let decoded = decodeMessage(encoded) check decoded.isOk() @@ -132,8 +142,8 @@ suite "Portal Wire Protocol Message Encodings": test "Content Response - connection id": let connectionId = Bytes2([byte 0x01, 0x02]) - c = ContentMessage( - contentMessageType: connectionIdType, connectionId: connectionId) + c = + ContentMessage(contentMessageType: connectionIdType, connectionId: connectionId) let encoded = encodeMessage(c) check encoded.toHex == "05000102" @@ -168,15 +178,20 @@ suite "Portal Wire Protocol Message Encodings": test "Content Response - enrs": var e1, e2: Record check: - e1.fromURI("enr:-HW4QBzimRxkmT18hMKaAL3IcZF1UcfTMPyi3Q1pxwZZbcZVRI8DC5infUAB_UauARLOJtYTxaagKoGmIjzQxO2qUygBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAPMljNMTg") - e2.fromURI("enr:-HW4QNfxw543Ypf4HXKXdYxkyzfcxcO-6p9X986WldfVpnVTQX1xlTnWrktEWUbeTZnmgOuAY_KUhbVV1Ft98WoYUBMBgmlkgnY0iXNlY3AyNTZrMaEDDiy3QkHAxPyOgWbxp5oF1bDdlYE6dLCUUp8xfVw50jU") + e1.fromURI( + "enr:-HW4QBzimRxkmT18hMKaAL3IcZF1UcfTMPyi3Q1pxwZZbcZVRI8DC5infUAB_UauARLOJtYTxaagKoGmIjzQxO2qUygBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAPMljNMTg" + ) + e2.fromURI( + "enr:-HW4QNfxw543Ypf4HXKXdYxkyzfcxcO-6p9X986WldfVpnVTQX1xlTnWrktEWUbeTZnmgOuAY_KUhbVV1Ft98WoYUBMBgmlkgnY0iXNlY3AyNTZrMaEDDiy3QkHAxPyOgWbxp5oF1bDdlYE6dLCUUp8xfVw50jU" + ) let enrs = List[ByteList, 32](@[ByteList(e1.raw), ByteList(e2.raw)]) c = ContentMessage(contentMessageType: enrsType, enrs: enrs) let encoded = encodeMessage(c) - check encoded.toHex == "0502080000007f000000f875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235" + check encoded.toHex == + "0502080000007f000000f875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235" let decoded = decodeMessage(encoded) check decoded.isOk() diff --git a/fluffy/tests/state_network_tests/test_state_content_keys.nim b/fluffy/tests/state_network_tests/test_state_content_keys.nim index 5fd6144ebe..c46e82a117 100644 --- a/fluffy/tests/state_network_tests/test_state_content_keys.nim +++ b/fluffy/tests/state_network_tests/test_state_content_keys.nim @@ -11,7 +11,6 @@ import eth/keys, ../../network/state/state_content - suite "State Content Keys": const evenNibles = "008679e8ed" test "Encode/decode even nibbles": @@ -20,8 +19,7 @@ suite "State Content Keys": packedNibbles = packNibbles(nibbles) unpackedNibbles = unpackNibbles(packedNibbles) - let - encoded = SSZ.encode(packedNibbles) + let encoded = SSZ.encode(packedNibbles) check encoded.toHex() == evenNibles check unpackedNibbles == nibbles @@ -33,22 +31,25 @@ suite "State Content Keys": packedNibbles = packNibbles(nibbles) unpackedNibbles = unpackNibbles(packedNibbles) - let - encoded = SSZ.encode(packedNibbles) - + let encoded = SSZ.encode(packedNibbles) + check encoded.toHex() == oddNibbles check unpackedNibbles == nibbles - const accountTrieNodeKeyEncoded = "20240000006225fcc63b22b80301d9f2582014e450e91f9b329b7cc87ad16894722fff5296008679e8ed" + const accountTrieNodeKeyEncoded = + "20240000006225fcc63b22b80301d9f2582014e450e91f9b329b7cc87ad16894722fff5296008679e8ed" test "Encode/decode AccountTrieNodeKey": const nibbles: seq[byte] = @[8, 6, 7, 9, 14, 8, 14, 13] packedNibbles = packNibbles(nibbles) - nodeHash = NodeHash.fromHex("6225fcc63b22b80301d9f2582014e450e91f9b329b7cc87ad16894722fff5296") + nodeHash = NodeHash.fromHex( + "6225fcc63b22b80301d9f2582014e450e91f9b329b7cc87ad16894722fff5296" + ) let accountTrieNodeKey = AccountTrieNodeKey(path: packedNibbles, nodeHash: nodeHash) - contentKey = ContentKey(contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey) + contentKey = + ContentKey(contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey) encoded = contentKey.encode() check $encoded == accountTrieNodeKeyEncoded @@ -56,19 +57,26 @@ suite "State Content Keys": raiseAssert "Cannot decode AccountTrieNodeKey" check: decoded.contentType == accountTrieNode - decoded.accountTrieNodeKey == AccountTrieNodeKey(path: packedNibbles, nodeHash: nodeHash) + decoded.accountTrieNodeKey == + AccountTrieNodeKey(path: packedNibbles, nodeHash: nodeHash) - const contractTrieNodeKeyEncoded = "21c02aaa39b223fe8d0a0e5c4f27ead9083c756cc238000000eb43d68008d216e753fef198cf51077f5a89f406d9c244119d1643f0f2b1901100405787" + const contractTrieNodeKeyEncoded = + "21c02aaa39b223fe8d0a0e5c4f27ead9083c756cc238000000eb43d68008d216e753fef198cf51077f5a89f406d9c244119d1643f0f2b1901100405787" test "Encode/decode ContractTrieNodeKey": const nibbles: seq[byte] = @[4, 0, 5, 7, 8, 7] packedNibbles = packNibbles(nibbles) address = Address.fromHex("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") - nodeHash = NodeHash.fromHex("eb43d68008d216e753fef198cf51077f5a89f406d9c244119d1643f0f2b19011") + nodeHash = NodeHash.fromHex( + "eb43d68008d216e753fef198cf51077f5a89f406d9c244119d1643f0f2b19011" + ) let - contractTrieNodeKey = ContractTrieNodeKey(address: address, path: packedNibbles, nodeHash: nodeHash) - contentKey = ContentKey(contentType: contractTrieNode, contractTrieNodeKey: contractTrieNodeKey) + contractTrieNodeKey = + ContractTrieNodeKey(address: address, path: packedNibbles, nodeHash: nodeHash) + contentKey = ContentKey( + contentType: contractTrieNode, contractTrieNodeKey: contractTrieNodeKey + ) encoded = contentKey.encode() check $encoded == contractTrieNodeKeyEncoded @@ -76,17 +84,22 @@ suite "State Content Keys": raiseAssert "Cannot decode ContractTrieNodeKey" check: decoded.contentType == contractTrieNode - decoded.contractTrieNodeKey == ContractTrieNodeKey(address: address, path: packedNibbles, nodeHash: nodeHash) + decoded.contractTrieNodeKey == + ContractTrieNodeKey(address: address, path: packedNibbles, nodeHash: nodeHash) - const contractCodeKeyEncoded = "22c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2d0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23" + const contractCodeKeyEncoded = + "22c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2d0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23" test "Encode/decode ContractCodeKey": const address = Address.fromHex("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") - codeHash = CodeHash.fromHex("d0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23") + codeHash = CodeHash.fromHex( + "d0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23" + ) let contractCodeKey = ContractCodeKey(address: address, codeHash: codeHash) - contentKey = ContentKey(contentType: contractCode, contractCodeKey: contractCodeKey) + contentKey = + ContentKey(contentType: contractCode, contractCodeKey: contractCodeKey) encoded = contentKey.encode() check $encoded == contractCodeKeyEncoded @@ -98,7 +111,7 @@ suite "State Content Keys": decoded.contractCodeKey.codeHash == codeHash test "Invalid prefix - 0 value": - let encoded = ByteList.init(@[byte 0x00]) + let encoded = ByteList.init(@[byte 0x00]) let decoded = decode(encoded) check decoded.isNone() diff --git a/fluffy/tests/state_network_tests/test_state_content_values.nim b/fluffy/tests/state_network_tests/test_state_content_values.nim index bd0c6bbbdd..1c03cb8307 100644 --- a/fluffy/tests/state_network_tests/test_state_content_values.nim +++ b/fluffy/tests/state_network_tests/test_state_content_values.nim @@ -21,10 +21,14 @@ suite "State Content Values": let blockContent = readJsonType(testVectorDir & "block.json", JsonBlock).valueOr: raiseAssert "Cannot read test vector: " & error - accountTrieNode = readJsonType(testVectorDir & "account_trie_node.json", JsonAccountTrieNode).valueOr: + accountTrieNode = readJsonType( + testVectorDir & "account_trie_node.json", JsonAccountTrieNode + ).valueOr: raiseAssert "Cannot read test vector: " & error blockHash = BlockHash.fromHex(blockContent.`block`.block_hash) - proof = TrieProof.init(blockContent.account_proof.map((hex) => TrieNode.init(hex.hexToSeqByte()))) + proof = TrieProof.init( + blockContent.account_proof.map((hex) => TrieNode.init(hex.hexToSeqByte())) + ) accountTrieNodeOffer = AccountTrieNodeOffer(blockHash: blockHash, proof: proof) encoded = SSZ.encode(accountTrieNodeOffer) @@ -38,7 +42,9 @@ suite "State Content Values": let blockContent = readJsonType(testVectorDir & "block.json", JsonBlock).valueOr: raiseAssert "Cannot read test vector: " & error - accountTrieNode = readJsonType(testVectorDir & "account_trie_node.json", JsonAccountTrieNode).valueOr: + accountTrieNode = readJsonType( + testVectorDir & "account_trie_node.json", JsonAccountTrieNode + ).valueOr: raiseAssert "Cannot read test vector: " & error node = TrieNode.init(blockContent.account_proof[^1].hexToSeqByte()) @@ -55,16 +61,21 @@ suite "State Content Values": let blockContent = readJsonType(testVectorDir & "block.json", JsonBlock).valueOr: raiseAssert "Cannot read test vector: " & error - contractStorageTrieNode = readJsonType(testVectorDir & "contract_storage_trie_node.json", JsonContractStorageTtrieNode).valueOr: + contractStorageTrieNode = readJsonType( + testVectorDir & "contract_storage_trie_node.json", JsonContractStorageTtrieNode + ).valueOr: raiseAssert "Cannot read test vector: " & error blockHash = BlockHash.fromHex(blockContent.`block`.block_hash) - storageProof = TrieProof.init(blockContent.storage_proof.map((hex) => TrieNode.init(hex.hexToSeqByte()))) - accountProof = TrieProof.init(blockContent.account_proof.map((hex) => TrieNode.init(hex.hexToSeqByte()))) + storageProof = TrieProof.init( + blockContent.storage_proof.map((hex) => TrieNode.init(hex.hexToSeqByte())) + ) + accountProof = TrieProof.init( + blockContent.account_proof.map((hex) => TrieNode.init(hex.hexToSeqByte())) + ) contractTrieNodeOffer = ContractTrieNodeOffer( - blockHash: blockHash, - storage_proof: storageProof, - account_proof: accountProof) + blockHash: blockHash, storage_proof: storageProof, account_proof: accountProof + ) encoded = SSZ.encode(contractTrieNodeOffer) expected = contractStorageTrieNode.content_value_offer.hexToSeqByte() @@ -77,7 +88,9 @@ suite "State Content Values": let blockContent = readJsonType(testVectorDir & "block.json", JsonBlock).valueOr: raiseAssert "Cannot read test vector: " & error - contractStorageTrieNode = readJsonType(testVectorDir & "contract_storage_trie_node.json", JsonContractStorageTtrieNode).valueOr: + contractStorageTrieNode = readJsonType( + testVectorDir & "contract_storage_trie_node.json", JsonContractStorageTtrieNode + ).valueOr: raiseAssert "Cannot read test vector: " & error node = TrieNode.init(blockContent.storage_proof[^1].hexToSeqByte()) @@ -94,16 +107,18 @@ suite "State Content Values": let blockContent = readJsonType(testVectorDir & "block.json", JsonBlock).valueOr: raiseAssert "Cannot read test vector: " & error - contractBytecode = readJsonType(testVectorDir & "contract_bytecode.json", JsonContractBytecode).valueOr: + contractBytecode = readJsonType( + testVectorDir & "contract_bytecode.json", JsonContractBytecode + ).valueOr: raiseAssert "Cannot read test vector: " & error code = Bytecode.init(blockContent.bytecode.hexToSeqByte()) blockHash = BlockHash.fromHex(blockContent.`block`.block_hash) - accountProof = TrieProof.init(blockContent.account_proof.map((hex) => TrieNode.init(hex.hexToSeqByte()))) - contractCodeOffer = ContractCodeOffer( - code: code, - blockHash: blockHash, - accountProof: accountProof) + accountProof = TrieProof.init( + blockContent.account_proof.map((hex) => TrieNode.init(hex.hexToSeqByte())) + ) + contractCodeOffer = + ContractCodeOffer(code: code, blockHash: blockHash, accountProof: accountProof) encoded = SSZ.encode(contractCodeOffer) expected = contractBytecode.content_value_offer.hexToSeqByte() @@ -116,7 +131,9 @@ suite "State Content Values": let blockContent = readJsonType(testVectorDir & "block.json", JsonBlock).valueOr: raiseAssert "Cannot read test vector: " & error - contractBytecode = readJsonType(testVectorDir & "contract_bytecode.json", JsonContractBytecode).valueOr: + contractBytecode = readJsonType( + testVectorDir & "contract_bytecode.json", JsonContractBytecode + ).valueOr: raiseAssert "Cannot read test vector: " & error code = Bytecode.init(blockContent.bytecode.hexToSeqByte()) diff --git a/fluffy/tests/state_network_tests/test_state_network.nim b/fluffy/tests/state_network_tests/test_state_network.nim index 0658015484..f0cf659bcc 100644 --- a/fluffy/tests/state_network_tests/test_state_network.nim +++ b/fluffy/tests/state_network_tests/test_state_network.nim @@ -1,5 +1,5 @@ # Fluffy -# Copyright (c) 2021-2023 Status Research & Development GmbH +# Copyright (c) 2021-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -9,11 +9,13 @@ import std/[os, json, sequtils, strutils, sugar], stew/[byteutils, io2], nimcrypto/hash, - testutils/unittests, chronos, + testutils/unittests, + chronos, eth/trie/hexary_proof_verification, eth/keys, eth/common/[eth_types, eth_hash], - eth/p2p/discoveryv5/protocol as discv5_protocol, eth/p2p/discoveryv5/routing_table, + eth/p2p/discoveryv5/protocol as discv5_protocol, + eth/p2p/discoveryv5/routing_table, ../../../nimbus/[config, db/core_db, db/state_db], ../../../nimbus/common/[chain_config, genesis], ../../network/wire/[portal_protocol, portal_stream], @@ -21,8 +23,7 @@ import ../../database/content_db, .././test_helpers -const testVectorDir = - "./vendor/portal-spec-tests/tests/mainnet/state/" +const testVectorDir = "./vendor/portal-spec-tests/tests/mainnet/state/" proc genesisToTrie(filePath: string): CoreDbMptRef = # TODO: Doing our best here with API that exists, to be improved. @@ -30,9 +31,10 @@ proc genesisToTrie(filePath: string): CoreDbMptRef = if not loadNetworkParams(filePath, cn): quit(1) - let sdb = newStateDB(newCoreDbRef LegacyDbMemory, false) - let map = toForkTransitionTable(cn.config) - let fork = map.toHardFork(forkDeterminationInfo(0.toBlockNumber, cn.genesis.timestamp)) + let sdb = newStateDB(newCoreDbRef LegacyDbMemory, false) + let map = toForkTransitionTable(cn.config) + let fork = + map.toHardFork(forkDeterminationInfo(0.toBlockNumber, cn.genesis.timestamp)) discard toGenesisHeader(cn.genesis, sdb, fork) sdb.getTrie @@ -44,15 +46,15 @@ procSuite "State Network": let trie = genesisToTrie("fluffy" / "tests" / "custom_genesis" / "chainid7.json") - node1 = initDiscoveryNode( - rng, PrivateKey.random(rng[]), localAddress(20302)) + node1 = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20302)) sm1 = StreamManager.new(node1) - node2 = initDiscoveryNode( - rng, PrivateKey.random(rng[]), localAddress(20303)) + node2 = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20303)) sm2 = StreamManager.new(node2) - proto1 = StateNetwork.new(node1, ContentDB.new("", uint32.high, inMemory = true), sm1) - proto2 = StateNetwork.new(node2, ContentDB.new("", uint32.high, inMemory = true), sm2) + proto1 = + StateNetwork.new(node1, ContentDB.new("", uint32.high, inMemory = true), sm1) + proto2 = + StateNetwork.new(node2, ContentDB.new("", uint32.high, inMemory = true), sm2) check proto2.portalProtocol.addNode(node1.localNode) == Added @@ -67,7 +69,8 @@ procSuite "State Network": # TODO: add stateRoot, and path eventually accountTrieNodeKey = AccountTrieNodeKey(nodeHash: nodeHash) contentKey = ContentKey( - contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey) + contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey + ) contentId = toContentId(contentKey) discard proto1.contentDB.put(contentId, v, proto1.portalProtocol.localNode.id) @@ -79,7 +82,8 @@ procSuite "State Network": let accountTrieNodeKey = AccountTrieNodeKey(nodeHash: nodeHash) contentKey = ContentKey( - contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey) + contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey + ) contentId = toContentId(contentKey) # Note: GetContent and thus the lookup here is not really needed, as we @@ -100,19 +104,19 @@ procSuite "State Network": # findNodes request, to properly test the lookup call. let trie = genesisToTrie("fluffy" / "tests" / "custom_genesis" / "chainid7.json") - node1 = initDiscoveryNode( - rng, PrivateKey.random(rng[]), localAddress(20302)) + node1 = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20302)) sm1 = StreamManager.new(node1) - node2 = initDiscoveryNode( - rng, PrivateKey.random(rng[]), localAddress(20303)) + node2 = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20303)) sm2 = StreamManager.new(node2) - node3 = initDiscoveryNode( - rng, PrivateKey.random(rng[]), localAddress(20304)) + node3 = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20304)) sm3 = StreamManager.new(node3) - proto1 = StateNetwork.new(node1, ContentDB.new("", uint32.high, inMemory = true), sm1) - proto2 = StateNetwork.new(node2, ContentDB.new("", uint32.high, inMemory = true), sm2) - proto3 = StateNetwork.new(node3, ContentDB.new("", uint32.high, inMemory = true), sm3) + proto1 = + StateNetwork.new(node1, ContentDB.new("", uint32.high, inMemory = true), sm1) + proto2 = + StateNetwork.new(node2, ContentDB.new("", uint32.high, inMemory = true), sm2) + proto3 = + StateNetwork.new(node3, ContentDB.new("", uint32.high, inMemory = true), sm3) # Node1 knows about Node2, and Node2 knows about Node3 which hold all content check proto1.portalProtocol.addNode(node2.localNode) == Added @@ -130,7 +134,8 @@ procSuite "State Network": let accountTrieNodeKey = AccountTrieNodeKey(nodeHash: nodeHash) contentKey = ContentKey( - contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey) + contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey + ) contentId = toContentId(contentKey) discard proto2.contentDB.put(contentId, v, proto2.portalProtocol.localNode.id) @@ -145,8 +150,8 @@ procSuite "State Network": let accountTrieNodeKey = AccountTrieNodeKey(nodeHash: nodeHash) - contentKey = ContentKey( - contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey) + contentKey = + ContentKey(contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey) let foundContent = await proto1.getContent(contentKey) diff --git a/fluffy/tests/state_network_tests/test_state_network_gossip.nim b/fluffy/tests/state_network_tests/test_state_network_gossip.nim index b4075d2166..5551feca53 100644 --- a/fluffy/tests/state_network_tests/test_state_network_gossip.nim +++ b/fluffy/tests/state_network_tests/test_state_network_gossip.nim @@ -25,33 +25,36 @@ procSuite "State Network Gossip": asyncTest "Test Gossip of Account Trie Node Offer": let - recursiveGossipSteps = readJsonType(testVectorDir & "recursive_gossip.json", JsonRecursiveGossip).valueOr: + recursiveGossipSteps = readJsonType( + testVectorDir & "recursive_gossip.json", JsonRecursiveGossip + ).valueOr: raiseAssert "Cannot read test vector: " & error numOfClients = recursiveGossipSteps.len() - 1 var clients: seq[StateNetwork] - for i in 0..numOfClients: + for i in 0 .. numOfClients: let node = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20400 + i)) sm = StreamManager.new(node) - proto = StateNetwork.new(node, ContentDB.new("", uint32.high, inMemory = true), sm) + proto = + StateNetwork.new(node, ContentDB.new("", uint32.high, inMemory = true), sm) proto.start() clients.add(proto) - for i in 0..numOfClients-1: + for i in 0 .. numOfClients - 1: let currentNode = clients[i] - nextNode = clients[i+1] + nextNode = clients[i + 1] check: currentNode.portalProtocol.addNode(nextNode.portalProtocol.localNode) == Added (await currentNode.portalProtocol.ping(nextNode.portalProtocol.localNode)).isOk() - for i in 0..numOfClients-1: + for i in 0 .. numOfClients - 1: let pair = recursiveGossipSteps[i] currentNode = clients[i] - nextNode = clients[i+1] + nextNode = clients[i + 1] key = ByteList.init(pair.content_key.hexToSeqByte()) decodedKey = key.decode().valueOr: @@ -63,27 +66,26 @@ procSuite "State Network Gossip": value = pair.content_value.hexToSeqByte() decodedValue = SSZ.decode(value, AccountTrieNodeOffer) - offerValue = OfferContentValue(contentType: accountTrieNode, accountTrieNode: decodedValue) + offerValue = + OfferContentValue(contentType: accountTrieNode, accountTrieNode: decodedValue) nextValue = recursiveGossipSteps[1].content_value.hexToSeqByte() nextDecodedValue = SSZ.decode(nextValue, AccountTrieNodeOffer) - nextOfferValue = OfferContentValue(contentType: accountTrieNode, accountTrieNode: nextDecodedValue) + nextOfferValue = OfferContentValue( + contentType: accountTrieNode, accountTrieNode: nextDecodedValue + ) nextRetrievalValue = nextOfferValue.offerContentToRetrievalContent().encode() if i == 0: await currentNode.portalProtocol.gossipContent( - Opt.none(NodeId), - key, - decodedKey, - value, - offerValue - ) + Opt.none(NodeId), key, decodedKey, value, offerValue + ) await sleepAsync(100.milliseconds) #TODO figure out how to get rid of this sleep check (await nextNode.getContent(decodedNextKey)) == Opt.some(nextRetrievalValue) - for i in 0..numOfClients: + for i in 0 .. numOfClients: await clients[i].portalProtocol.baseProtocol.closeWait() # TODO Add tests for Contract Trie Node Offer & Contract Code Offer diff --git a/fluffy/tests/test_accumulator.nim b/fluffy/tests/test_accumulator.nim index 94049242cf..94bae73952 100644 --- a/fluffy/tests/test_accumulator.nim +++ b/fluffy/tests/test_accumulator.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -10,7 +10,8 @@ {.push raises: [].} import - unittest2, stint, + unittest2, + stint, eth/common/eth_types_rlp, ../eth_data/history_data_json_store, ../network/history/[history_content, accumulator], @@ -27,20 +28,20 @@ suite "Header Accumulator": 0, epochSize - 1, epochSize, - epochSize*2 - 1, - epochSize*2, - epochSize*3 - 1, - epochSize*3, - epochSize*3 + 1, - int(amount) - 1] + epochSize * 2 - 1, + epochSize * 2, + epochSize * 3 - 1, + epochSize * 3, + epochSize * 3 + 1, + int(amount) - 1, + ] var headers: seq[BlockHeader] - for i in 0.. 16383 - for i in 0..= ConsensusFork.Capella: @@ -102,8 +103,11 @@ suite "Beacon Chain Block Proofs": let proof = res.get() check verifyProof( - blocks[i].root, proof, - historical_summaries[historicalRootsIndex].block_summary_root, blockRootIndex) + blocks[i].root, + proof, + historical_summaries[historicalRootsIndex].block_summary_root, + blockRootIndex, + ) test "BeaconBlockHeaderProof for BeaconBlockBody": # for i in 0..<(SLOTS_PER_HISTORICAL_ROOT - 1): # Test all blocks @@ -115,7 +119,7 @@ suite "Beacon Chain Block Proofs": proposer_index: beaconBlock.proposer_index, parent_root: beaconBlock.parent_root, state_root: beaconBlock.state_root, - body_root: hash_tree_root(beaconBlock.body) + body_root: hash_tree_root(beaconBlock.body), ) beaconBlockBody = beaconBlock.body @@ -140,8 +144,7 @@ suite "Beacon Chain Block Proofs": check verifyProof(leave, proof, root) test "BeaconChainBlockProof for Execution BlockHeader": - let - blockRoots = getStateField(state[], block_roots).data + let blockRoots = getStateField(state[], block_roots).data withState(state[]): when consensusFork >= ConsensusFork.Capella: @@ -156,7 +159,7 @@ suite "Beacon Chain Block Proofs": proposer_index: beaconBlock.proposer_index, parent_root: beaconBlock.parent_root, state_root: beaconBlock.state_root, - body_root: hash_tree_root(beaconBlock.body) + body_root: hash_tree_root(beaconBlock.body), ) beaconBlockBody = beaconBlock.body diff --git a/fluffy/tests/test_beacon_chain_historical_roots.nim b/fluffy/tests/test_beacon_chain_historical_roots.nim index 2be08f76d0..70c0410fb1 100644 --- a/fluffy/tests/test_beacon_chain_historical_roots.nim +++ b/fluffy/tests/test_beacon_chain_historical_roots.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2023 Status Research & Development GmbH +# Copyright (c) 2023-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -14,10 +14,9 @@ import beacon_chain/spec/forks, beacon_chain/spec/datatypes/bellatrix, # Test helpers - beacon_chain/../tests/testblockutil, - beacon_chain/../tests/mocking/mock_genesis, - beacon_chain/../tests/consensus_spec/fixtures_utils, - + beacon_chain /../ tests/testblockutil, + beacon_chain /../ tests/mocking/mock_genesis, + beacon_chain /../ tests/consensus_spec/fixtures_utils, ../network/history/experimental/beacon_chain_historical_roots suite "Beacon Chain Historical Roots": @@ -34,7 +33,7 @@ suite "Beacon Chain Historical Roots": # index i = 0 is second block. # index i = 8190 is 8192th block and last one that is part of the first # historical root - for i in 0..= ConsensusFork.Bellatrix: if forkyBlck.message.is_execution_block: - template payload(): auto = forkyBlck.message.body.execution_payload + template payload(): auto = + forkyBlck.message.body.execution_payload # TODO: Get rid of the asEngineExecutionPayload step? let executionPayload = payload.asEngineExecutionPayload() - let (hash, headerWithProof, body) = - asPortalBlockData(executionPayload) + let (hash, headerWithProof, body) = asPortalBlockData(executionPayload) logScope: - blockhash = history_content.`$`hash + blockhash = history_content.`$` hash block: # gossip header let contentKey = history_content.ContentKey.init(blockHeader, hash) @@ -433,10 +432,10 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = try: let peers = await portalRpcClient.portal_historyGossip( - toHex(encodedContentKey), - SSZ.encode(headerWithProof).toHex()) - info "Block header gossiped", peers, - contentKey = encodedContentKey.toHex() + toHex(encodedContentKey), SSZ.encode(headerWithProof).toHex() + ) + info "Block header gossiped", + peers, contentKey = encodedContentKey.toHex() except CatchableError as e: error "JSON-RPC error", error = $e.msg # TODO: clean-up when json-rpc gets async raises annotations @@ -456,10 +455,10 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = try: let peers = await portalRpcClient.portal_historyGossip( - encodedContentKey.toHex(), - SSZ.encode(body).toHex()) - info "Block body gossiped", peers, - contentKey = encodedContentKey.toHex() + encodedContentKey.toHex(), SSZ.encode(body).toHex() + ) + info "Block body gossiped", + peers, contentKey = encodedContentKey.toHex() except CatchableError as e: error "JSON-RPC error", error = $e.msg @@ -472,18 +471,18 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = if web3Client.isSome(): let client = web3Client.get() # get receipts - let receipts = - (await client.getBlockReceipts( - executionPayload.transactions, hash)).valueOr: + let receipts = ( + await client.getBlockReceipts(executionPayload.transactions, hash) + ).valueOr: # (await web3Client.get().getBlockReceipts( # executionPayload.transactions)).valueOr: - error "Error getting block receipts", error - # TODO: clean-up when json-rpc gets async raises annotations - try: - await client.close() - except CatchableError: - discard - return + error "Error getting block receipts", error + # TODO: clean-up when json-rpc gets async raises annotations + try: + await client.close() + except CatchableError: + discard + return # TODO: clean-up when json-rpc gets async raises annotations try: await client.close() @@ -497,15 +496,15 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = # gossip receipts let contentKey = history_content.ContentKey.init( - history_content.ContentType.receipts, hash) + history_content.ContentType.receipts, hash + ) let encodedContentKeyHex = contentKey.encode.asSeq().toHex() try: let peers = await portalRpcClient.portal_historyGossip( - encodedContentKeyHex, - SSZ.encode(portalReceipts).toHex()) - info "Block receipts gossiped", peers, - contentKey = encodedContentKeyHex + encodedContentKeyHex, SSZ.encode(portalReceipts).toHex() + ) + info "Block receipts gossiped", peers, contentKey = encodedContentKeyHex except CatchableError as e: error "JSON-RPC error for portal_historyGossip", error = $e.msg @@ -517,17 +516,15 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = return - optimisticProcessor = initOptimisticProcessor( - getBeaconTime, optimisticHandler) + optimisticProcessor = initOptimisticProcessor(getBeaconTime, optimisticHandler) lightClient = createLightClient( - network, rng, lcConfig, cfg, forkDigests, getBeaconTime, - genesis_validators_root, LightClientFinalizationMode.Optimistic) + network, rng, lcConfig, cfg, forkDigests, getBeaconTime, genesis_validators_root, + LightClientFinalizationMode.Optimistic, + ) ### Beacon Light Client content bridging specific callbacks - proc onBootstrap( - lightClient: LightClient, - bootstrap: ForkedLightClientBootstrap) = + proc onBootstrap(lightClient: LightClient, bootstrap: ForkedLightClientBootstrap) = withForkyObject(bootstrap): when lcDataFork > LightClientDataFork.None: info "New Beacon LC bootstrap", @@ -536,22 +533,18 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = let root = hash_tree_root(forkyObject.header) contentKey = encode(bootstrapContentKey(root)) - forkDigest = forkDigestAtEpoch( - forkDigests[], epoch(forkyObject.header.beacon.slot), cfg) - content = encodeBootstrapForked( - forkDigest, - bootstrap - ) + forkDigest = + forkDigestAtEpoch(forkDigests[], epoch(forkyObject.header.beacon.slot), cfg) + content = encodeBootstrapForked(forkDigest, bootstrap) proc GossipRpcAndClose() {.async.} = try: let contentKeyHex = contentKey.asSeq().toHex() peers = await portalRpcClient.portal_beaconGossip( - contentKeyHex, - content.toHex()) - info "Beacon LC bootstrap gossiped", peers, - contentKey = contentKeyHex + contentKeyHex, content.toHex() + ) + info "Beacon LC bootstrap gossiped", peers, contentKey = contentKeyHex except CatchableError as e: error "JSON-RPC error", error = $e.msg @@ -569,21 +562,18 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = period = forkyObject.attested_header.beacon.slot.sync_committee_period contentKey = encode(updateContentKey(period.uint64, uint64(1))) forkDigest = forkDigestAtEpoch( - forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg) - content = encodeLightClientUpdatesForked( - forkDigest, - @[update] + forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg ) + content = encodeLightClientUpdatesForked(forkDigest, @[update]) proc GossipRpcAndClose() {.async.} = try: let contentKeyHex = contentKey.asSeq().toHex() peers = await portalRpcClient.portal_beaconGossip( - contentKeyHex, - content.toHex()) - info "Beacon LC bootstrap gossiped", peers, - contentKey = contentKeyHex + contentKeyHex, content.toHex() + ) + info "Beacon LC bootstrap gossiped", peers, contentKey = contentKeyHex except CatchableError as e: error "JSON-RPC error", error = $e.msg @@ -592,8 +582,8 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = asyncSpawn(GossipRpcAndClose()) proc onOptimisticUpdate( - lightClient: LightClient, - update: ForkedLightClientOptimisticUpdate) = + lightClient: LightClient, update: ForkedLightClientOptimisticUpdate + ) = withForkyObject(update): when lcDataFork > LightClientDataFork.None: info "New Beacon LC optimistic update", @@ -603,21 +593,18 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = slot = forkyObject.signature_slot contentKey = encode(optimisticUpdateContentKey(slot.uint64)) forkDigest = forkDigestAtEpoch( - forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg) - content = encodeOptimisticUpdateForked( - forkDigest, - update + forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg ) + content = encodeOptimisticUpdateForked(forkDigest, update) proc GossipRpcAndClose() {.async.} = try: let contentKeyHex = contentKey.asSeq().toHex() peers = await portalRpcClient.portal_beaconGossip( - contentKeyHex, - content.toHex()) - info "Beacon LC bootstrap gossiped", peers, - contentKey = contentKeyHex + contentKeyHex, content.toHex() + ) + info "Beacon LC bootstrap gossiped", peers, contentKey = contentKeyHex except CatchableError as e: error "JSON-RPC error", error = $e.msg @@ -626,8 +613,8 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = asyncSpawn(GossipRpcAndClose()) proc onFinalityUpdate( - lightClient: LightClient, - update: ForkedLightClientFinalityUpdate) = + lightClient: LightClient, update: ForkedLightClientFinalityUpdate + ) = withForkyObject(update): when lcDataFork > LightClientDataFork.None: info "New Beacon LC finality update", @@ -636,21 +623,18 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = finalizedSlot = forkyObject.finalized_header.beacon.slot contentKey = encode(finalityUpdateContentKey(finalizedSlot.uint64)) forkDigest = forkDigestAtEpoch( - forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg) - content = encodeFinalityUpdateForked( - forkDigest, - update + forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg ) + content = encodeFinalityUpdateForked(forkDigest, update) proc GossipRpcAndClose() {.async.} = try: let contentKeyHex = contentKey.asSeq().toHex() peers = await portalRpcClient.portal_beaconGossip( - contentKeyHex, - content.toHex()) - info "Beacon LC bootstrap gossiped", peers, - contentKey = contentKeyHex + contentKeyHex, content.toHex() + ) + info "Beacon LC bootstrap gossiped", peers, contentKey = contentKeyHex except CatchableError as e: error "JSON-RPC error", error = $e.msg @@ -668,51 +652,57 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = info "Listening to incoming network requests" network.registerProtocol( - PeerSync, PeerSync.NetworkState.init( - cfg, forkDigests, genesisBlockRoot, getBeaconTime)) + PeerSync, + PeerSync.NetworkState.init(cfg, forkDigests, genesisBlockRoot, getBeaconTime), + ) network.addValidator( getBeaconBlocksTopic(forkDigests.phase0), - proc (signedBlock: phase0.SignedBeaconBlock): errors.ValidationResult = - toValidationResult( - optimisticProcessor.processSignedBeaconBlock(signedBlock))) + proc(signedBlock: phase0.SignedBeaconBlock): errors.ValidationResult = + toValidationResult(optimisticProcessor.processSignedBeaconBlock(signedBlock)) + , + ) network.addValidator( getBeaconBlocksTopic(forkDigests.altair), - proc (signedBlock: altair.SignedBeaconBlock): errors.ValidationResult = - toValidationResult( - optimisticProcessor.processSignedBeaconBlock(signedBlock))) + proc(signedBlock: altair.SignedBeaconBlock): errors.ValidationResult = + toValidationResult(optimisticProcessor.processSignedBeaconBlock(signedBlock)) + , + ) network.addValidator( getBeaconBlocksTopic(forkDigests.bellatrix), - proc (signedBlock: bellatrix.SignedBeaconBlock): errors.ValidationResult = - toValidationResult( - optimisticProcessor.processSignedBeaconBlock(signedBlock))) + proc(signedBlock: bellatrix.SignedBeaconBlock): errors.ValidationResult = + toValidationResult(optimisticProcessor.processSignedBeaconBlock(signedBlock)) + , + ) network.addValidator( getBeaconBlocksTopic(forkDigests.capella), - proc (signedBlock: capella.SignedBeaconBlock): errors.ValidationResult = - toValidationResult( - optimisticProcessor.processSignedBeaconBlock(signedBlock))) + proc(signedBlock: capella.SignedBeaconBlock): errors.ValidationResult = + toValidationResult(optimisticProcessor.processSignedBeaconBlock(signedBlock)) + , + ) network.addValidator( getBeaconBlocksTopic(forkDigests.deneb), - proc (signedBlock: deneb.SignedBeaconBlock): errors.ValidationResult = - toValidationResult( - optimisticProcessor.processSignedBeaconBlock(signedBlock))) + proc(signedBlock: deneb.SignedBeaconBlock): errors.ValidationResult = + toValidationResult(optimisticProcessor.processSignedBeaconBlock(signedBlock)) + , + ) lightClient.installMessageValidators() waitFor network.startListening() waitFor network.start() proc onFinalizedHeader( - lightClient: LightClient, finalizedHeader: ForkedLightClientHeader) = + lightClient: LightClient, finalizedHeader: ForkedLightClientHeader + ) = withForkyHeader(finalizedHeader): when lcDataFork > LightClientDataFork.None: - info "New LC finalized header", - finalized_header = shortLog(forkyHeader) + info "New LC finalized header", finalized_header = shortLog(forkyHeader) proc onOptimisticHeader( - lightClient: LightClient, optimisticHeader: ForkedLightClientHeader) = + lightClient: LightClient, optimisticHeader: ForkedLightClientHeader + ) = withForkyHeader(optimisticHeader): when lcDataFork > LightClientDataFork.None: - info "New LC optimistic header", - optimistic_header = shortLog(forkyHeader) + info "New LC optimistic header", optimistic_header = shortLog(forkyHeader) optimisticProcessor.setOptimisticHeader(forkyHeader.beacon) lightClient.onFinalizedHeader = onFinalizedHeader @@ -742,18 +732,19 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = targetGossipState = getTargetGossipState( slot.epoch, cfg.ALTAIR_FORK_EPOCH, cfg.BELLATRIX_FORK_EPOCH, - cfg.CAPELLA_FORK_EPOCH, cfg.DENEB_FORK_EPOCH, isBehind) + cfg.CAPELLA_FORK_EPOCH, cfg.DENEB_FORK_EPOCH, isBehind, + ) + + template currentGossipState(): auto = + blocksGossipState - template currentGossipState(): auto = blocksGossipState if currentGossipState == targetGossipState: return if currentGossipState.card == 0 and targetGossipState.card > 0: - debug "Enabling blocks topic subscriptions", - wallSlot = slot, targetGossipState + debug "Enabling blocks topic subscriptions", wallSlot = slot, targetGossipState elif currentGossipState.card > 0 and targetGossipState.card == 0: - debug "Disabling blocks topic subscriptions", - wallSlot = slot + debug "Disabling blocks topic subscriptions", wallSlot = slot else: # Individual forks added / removed discard @@ -769,8 +760,8 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = for gossipFork in newGossipForks: let forkDigest = forkDigests[].atConsensusFork(gossipFork) network.subscribe( - getBeaconBlocksTopic(forkDigest), blocksTopicParams, - enableTopicMetrics = true) + getBeaconBlocksTopic(forkDigest), blocksTopicParams, enableTopicMetrics = true + ) blocksGossipState = targetGossipState @@ -800,8 +791,7 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = when isMainModule: {.pop.} - var config = makeBannerAndConfig( - "Nimbus beacon chain bridge", BeaconBridgeConf) + var config = makeBannerAndConfig("Nimbus beacon chain bridge", BeaconBridgeConf) {.push raises: [].} run(config) diff --git a/fluffy/tools/beacon_lc_bridge/beacon_lc_bridge_conf.nim b/fluffy/tools/beacon_lc_bridge/beacon_lc_bridge_conf.nim index d6c3823360..b6becbb195 100644 --- a/fluffy/tools/beacon_lc_bridge/beacon_lc_bridge_conf.nim +++ b/fluffy/tools/beacon_lc_bridge/beacon_lc_bridge_conf.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2023 Status Research & Development GmbH +# Copyright (c) 2023-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -7,170 +7,179 @@ {.push raises: [].} -import - std/os, - json_serialization/std/net, - beacon_chain/light_client, - beacon_chain/conf +import std/os, json_serialization/std/net, beacon_chain/light_client, beacon_chain/conf export net, conf proc defaultDataDir*(): string = - let dataDir = when defined(windows): - "AppData" / "Roaming" / "FluffyBeaconLCBridge" - elif defined(macosx): - "Library" / "Application Support" / "FluffyBeaconLCBridge" - else: - ".cache" / "fluffy-beacon-lc-bridge" + let dataDir = + when defined(windows): + "AppData" / "Roaming" / "FluffyBeaconLCBridge" + elif defined(macosx): + "Library" / "Application Support" / "FluffyBeaconLCBridge" + else: + ".cache" / "fluffy-beacon-lc-bridge" getHomeDir() / dataDir -const - defaultDataDirDesc* = defaultDataDir() +const defaultDataDirDesc* = defaultDataDir() type Web3UrlKind* = enum - HttpUrl, WsUrl + HttpUrl + WsUrl Web3Url* = object kind*: Web3UrlKind web3Url*: string -type BeaconBridgeConf* = object - # Config - configFile* {. - desc: "Loads the configuration from a TOML file" - name: "config-file" .}: Option[InputFile] +type BeaconBridgeConf* = object # Config + configFile* {.desc: "Loads the configuration from a TOML file", name: "config-file".}: + Option[InputFile] # Logging - logLevel* {. - desc: "Sets the log level" - defaultValue: "INFO" - name: "log-level" .}: string + logLevel* {.desc: "Sets the log level", defaultValue: "INFO", name: "log-level".}: + string logStdout* {. - hidden - desc: "Specifies what kind of logs should be written to stdout (auto, colors, nocolors, json)" - defaultValueDesc: "auto" - defaultValue: StdoutLogKind.Auto - name: "log-format" .}: StdoutLogKind + hidden, + desc: + "Specifies what kind of logs should be written to stdout (auto, colors, nocolors, json)", + defaultValueDesc: "auto", + defaultValue: StdoutLogKind.Auto, + name: "log-format" + .}: StdoutLogKind # Storage dataDir* {. - desc: "The directory where beacon_lc_bridge will store all data" - defaultValue: defaultDataDir() - defaultValueDesc: $defaultDataDirDesc - abbr: "d" - name: "data-dir" .}: OutDir + desc: "The directory where beacon_lc_bridge will store all data", + defaultValue: defaultDataDir(), + defaultValueDesc: $defaultDataDirDesc, + abbr: "d", + name: "data-dir" + .}: OutDir # Portal JSON-RPC API server to connect to rpcAddress* {. - desc: "Listening address of the Portal JSON-RPC server" - defaultValue: "127.0.0.1" - name: "rpc-address" .}: string + desc: "Listening address of the Portal JSON-RPC server", + defaultValue: "127.0.0.1", + name: "rpc-address" + .}: string rpcPort* {. - desc: "Listening port of the Portal JSON-RPC server" - defaultValue: 8545 - name: "rpc-port" .}: Port + desc: "Listening port of the Portal JSON-RPC server", + defaultValue: 8545, + name: "rpc-port" + .}: Port ## Bridge options - beaconLightClient* {. - desc: "Enable beacon light client content bridging" - defaultValue: false - name: "beacon-light-client" .}: bool + desc: "Enable beacon light client content bridging", + defaultValue: false, + name: "beacon-light-client" + .}: bool - web3Url* {. - desc: "Execution layer JSON-RPC API URL" - name: "web3-url" .}: Option[Web3Url] + web3Url* {.desc: "Execution layer JSON-RPC API URL", name: "web3-url".}: + Option[Web3Url] ## Beacon chain light client specific options # For Consensus light sync - No default - Needs to be provided by the user trustedBlockRoot* {. - desc: "Recent trusted finalized block root to initialize the consensus light client from" - name: "trusted-block-root" .}: Eth2Digest + desc: + "Recent trusted finalized block root to initialize the consensus light client from", + name: "trusted-block-root" + .}: Eth2Digest # Network eth2Network* {. - desc: "The Eth2 network to join" - defaultValueDesc: "mainnet" - name: "network" .}: Option[string] + desc: "The Eth2 network to join", defaultValueDesc: "mainnet", name: "network" + .}: Option[string] # Libp2p bootstrapNodes* {. - desc: "Specifies one or more bootstrap nodes to use when connecting to the network" - abbr: "b" - name: "bootstrap-node" .}: seq[string] + desc: "Specifies one or more bootstrap nodes to use when connecting to the network", + abbr: "b", + name: "bootstrap-node" + .}: seq[string] bootstrapNodesFile* {. - desc: "Specifies a line-delimited file of bootstrap Ethereum network addresses" - defaultValue: "" - name: "bootstrap-file" .}: InputFile + desc: "Specifies a line-delimited file of bootstrap Ethereum network addresses", + defaultValue: "", + name: "bootstrap-file" + .}: InputFile listenAddress* {. - desc: "Listening address for the Ethereum LibP2P and Discovery v5 traffic" - defaultValue: defaultListenAddress - defaultValueDesc: $defaultListenAddressDesc - name: "listen-address" .}: IpAddress + desc: "Listening address for the Ethereum LibP2P and Discovery v5 traffic", + defaultValue: defaultListenAddress, + defaultValueDesc: $defaultListenAddressDesc, + name: "listen-address" + .}: IpAddress tcpPort* {. - desc: "Listening TCP port for Ethereum LibP2P traffic" - defaultValue: defaultEth2TcpPort - defaultValueDesc: $defaultEth2TcpPortDesc - name: "tcp-port" .}: Port + desc: "Listening TCP port for Ethereum LibP2P traffic", + defaultValue: defaultEth2TcpPort, + defaultValueDesc: $defaultEth2TcpPortDesc, + name: "tcp-port" + .}: Port udpPort* {. - desc: "Listening UDP port for node discovery" - defaultValue: defaultEth2TcpPort - defaultValueDesc: $defaultEth2TcpPortDesc - name: "udp-port" .}: Port + desc: "Listening UDP port for node discovery", + defaultValue: defaultEth2TcpPort, + defaultValueDesc: $defaultEth2TcpPortDesc, + name: "udp-port" + .}: Port # TODO: Select a lower amount of peers. maxPeers* {. - desc: "The target number of peers to connect to" - defaultValue: 160 # 5 (fanout) * 64 (subnets) / 2 (subs) for a healthy mesh - name: "max-peers" .}: int + desc: "The target number of peers to connect to", + defaultValue: 160, # 5 (fanout) * 64 (subnets) / 2 (subs) for a healthy mesh + name: "max-peers" + .}: int hardMaxPeers* {. - desc: "The maximum number of peers to connect to. Defaults to maxPeers * 1.5" - name: "hard-max-peers" .}: Option[int] + desc: "The maximum number of peers to connect to. Defaults to maxPeers * 1.5", + name: "hard-max-peers" + .}: Option[int] nat* {. - desc: "Specify method to use for determining public address. " & - "Must be one of: any, none, upnp, pmp, extip:" - defaultValue: NatConfig(hasExtIp: false, nat: NatAny) - defaultValueDesc: "any" - name: "nat" .}: NatConfig + desc: + "Specify method to use for determining public address. " & + "Must be one of: any, none, upnp, pmp, extip:", + defaultValue: NatConfig(hasExtIp: false, nat: NatAny), + defaultValueDesc: "any", + name: "nat" + .}: NatConfig enrAutoUpdate* {. - desc: "Discovery can automatically update its ENR with the IP address " & - "and UDP port as seen by other nodes it communicates with. " & - "This option allows to enable/disable this functionality" - defaultValue: false - name: "enr-auto-update" .}: bool + desc: + "Discovery can automatically update its ENR with the IP address " & + "and UDP port as seen by other nodes it communicates with. " & + "This option allows to enable/disable this functionality", + defaultValue: false, + name: "enr-auto-update" + .}: bool agentString* {. defaultValue: "nimbus", - desc: "Node agent string which is used as identifier in the LibP2P network" - name: "agent-string" .}: string + desc: "Node agent string which is used as identifier in the LibP2P network", + name: "agent-string" + .}: string - discv5Enabled* {. - desc: "Enable Discovery v5" - defaultValue: true - name: "discv5" .}: bool + discv5Enabled* {.desc: "Enable Discovery v5", defaultValue: true, name: "discv5".}: + bool directPeers* {. - desc: "The list of priviledged, secure and known peers to connect and" & - "maintain the connection to, this requires a not random netkey-file." & - "In the complete multiaddress format like:" & - "/ip4/
/tcp//p2p/." & - "Peering agreements are established out of band and must be reciprocal" - name: "direct-peer" .}: seq[string] - -proc parseCmdArg*( - T: type Web3Url, p: string): T {.raises: [ValueError].} = + desc: + "The list of priviledged, secure and known peers to connect and" & + "maintain the connection to, this requires a not random netkey-file." & + "In the complete multiaddress format like:" & + "/ip4/
/tcp//p2p/." & + "Peering agreements are established out of band and must be reciprocal", + name: "direct-peer" + .}: seq[string] + +proc parseCmdArg*(T: type Web3Url, p: string): T {.raises: [ValueError].} = let url = parseUri(p) normalizedScheme = url.scheme.toLowerAscii() @@ -182,7 +191,7 @@ proc parseCmdArg*( else: raise newException( ValueError, - "The Web3 URL must specify one of following protocols: http/https/ws/wss" + "The Web3 URL must specify one of following protocols: http/https/ws/wss", ) proc completeCmdArg*(T: type Web3Url, val: string): seq[string] = @@ -211,5 +220,5 @@ func asLightClientConf*(pc: BeaconBridgeConf): LightClientConf = trustedBlockRoot: pc.trustedBlockRoot, web3Urls: @[], jwtSecret: none(InputFile), - stopAtEpoch: 0 + stopAtEpoch: 0, ) diff --git a/fluffy/tools/benchmark.nim b/fluffy/tools/benchmark.nim index d397042a62..f29b9a5ce4 100644 --- a/fluffy/tools/benchmark.nim +++ b/fluffy/tools/benchmark.nim @@ -1,5 +1,5 @@ # Fluffy -# Copyright (c) 2023 Status Research & Development GmbH +# Copyright (c) 2023-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -20,16 +20,18 @@ template withTimer*(stats: var RunningStat, body: untyped) = let stop = cpuTime() stats.push stop - start -proc printTimers*[Timers: enum]( - timers: array[Timers, RunningStat] -) = - func fmtTime(t: float): string = &"{t * 1000 :>12.3f}, " +proc printTimers*[Timers: enum](timers: array[Timers, RunningStat]) = + func fmtTime(t: float): string = + &"{t * 1000 :>12.3f}, " echo "All timings are in ms and are cpu time." echo &"{\"Average\" :>12}, {\"StdDev\" :>12}, {\"Min\" :>12}, " & &"{\"Max\" :>12}, {\"Samples\" :>12}, {\"Test\" :>12} " for t in Timers: - echo fmtTime(timers[t].mean), fmtTime(timers[t].standardDeviationS), - fmtTime(timers[t].min), fmtTime(timers[t].max), &"{timers[t].n :>12}, ", + echo fmtTime(timers[t].mean), + fmtTime(timers[t].standardDeviationS), + fmtTime(timers[t].min), + fmtTime(timers[t].max), + &"{timers[t].n :>12}, ", $t diff --git a/fluffy/tools/blockwalk.nim b/fluffy/tools/blockwalk.nim index ce776b4286..5e581f7e67 100644 --- a/fluffy/tools/blockwalk.nim +++ b/fluffy/tools/blockwalk.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -12,9 +12,13 @@ import std/strutils, - confutils, chronicles, chronicles/topics_registry, stew/byteutils, + confutils, + chronicles, + chronicles/topics_registry, + stew/byteutils, eth/common/eth_types, - ../../nimbus/rpc/[rpc_types], ../../nimbus/errors, + ../../nimbus/rpc/[rpc_types], + ../../nimbus/errors, ../rpc/eth_rpc_client type @@ -22,27 +26,28 @@ type BlockWalkConf* = object logLevel* {. - defaultValue: LogLevel.INFO - defaultValueDesc: $LogLevel.INFO - desc: "Sets the log level" - name: "log-level" .}: LogLevel + defaultValue: LogLevel.INFO, + defaultValueDesc: $LogLevel.INFO, + desc: "Sets the log level", + name: "log-level" + .}: LogLevel rpcAddress* {. - desc: "Address of the JSON-RPC service" - defaultValue: "127.0.0.1" - name: "rpc-address" .}: string + desc: "Address of the JSON-RPC service", + defaultValue: "127.0.0.1", + name: "rpc-address" + .}: string rpcPort* {. - defaultValue: 8545 - desc: "Port of the JSON-RPC service" - name: "rpc-port" .}: uint16 + defaultValue: 8545, desc: "Port of the JSON-RPC service", name: "rpc-port" + .}: uint16 blockHash* {. - desc: "The block hash from where to start walking the blocks backwards" - name: "block-hash" .}: Hash256 + desc: "The block hash from where to start walking the blocks backwards", + name: "block-hash" + .}: Hash256 -proc parseCmdArg*(T: type Hash256, p: string): T - {.raises: [ValueError].} = +proc parseCmdArg*(T: type Hash256, p: string): T {.raises: [ValueError].} = var hash: Hash256 try: hexToByteArray(p, hash.data) diff --git a/fluffy/tools/content_verifier.nim b/fluffy/tools/content_verifier.nim index 74af7b44c5..e3e5a42c9f 100644 --- a/fluffy/tools/content_verifier.nim +++ b/fluffy/tools/content_verifier.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2022-2023 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -11,28 +11,31 @@ {.push raises: [].} import - confutils, chronicles, chronicles/topics_registry, stew/byteutils, + confutils, + chronicles, + chronicles/topics_registry, + stew/byteutils, ../network_metadata, ../network/history/[accumulator, history_content, history_network], ../rpc/portal_rpc_client -type - ContentVerifierConf* = object - logLevel* {. - defaultValue: LogLevel.INFO - defaultValueDesc: $LogLevel.INFO - desc: "Sets the log level" - name: "log-level" .}: LogLevel +type ContentVerifierConf* = object + logLevel* {. + defaultValue: LogLevel.INFO, + defaultValueDesc: $LogLevel.INFO, + desc: "Sets the log level", + name: "log-level" + .}: LogLevel - rpcAddress* {. - desc: "Address of the JSON-RPC service" - defaultValue: "127.0.0.1" - name: "rpc-address" .}: string + rpcAddress* {. + desc: "Address of the JSON-RPC service", + defaultValue: "127.0.0.1", + name: "rpc-address" + .}: string - rpcPort* {. - defaultValue: 8545 - desc: "Port of the JSON-RPC service" - name: "rpc-port" .}: uint16 + rpcPort* {. + defaultValue: 8545, desc: "Port of the JSON-RPC service", name: "rpc-port" + .}: uint16 proc checkAccumulators(client: RpcClient) {.async.} = let accumulator = @@ -48,18 +51,21 @@ proc checkAccumulators(client: RpcClient) {.async.} = try: let content = await client.portal_historyRecursiveFindContent( - contentKey.encode.asSeq().toHex()) + contentKey.encode.asSeq().toHex() + ) let res = decodeSsz(hexToSeqByte(content), EpochAccumulator) if res.isErr(): - echo "[Invalid] EpochAccumulator number " & $i & ": " & $root & " error: " & res.error + echo "[Invalid] EpochAccumulator number " & $i & ": " & $root & " error: " & + res.error else: let epochAccumulator = res.get() let resultingRoot = hash_tree_root(epochAccumulator) if resultingRoot == root: echo "[Available] EpochAccumulator number " & $i & ": " & $root else: - echo "[Invalid] EpochAccumulator number " & $i & ": " & $root & " error: Invalid root" + echo "[Invalid] EpochAccumulator number " & $i & ": " & $root & + " error: Invalid root" except RpcPostError as e: # RpcPostError when for example timing out on the request. Could retry # in this case. @@ -68,7 +74,8 @@ proc checkAccumulators(client: RpcClient) {.async.} = except ValueError as e: # Either an error with the provided content key or the content was # simply not available in the network - echo "[Not Available] EpochAccumulator number " & $i & ": " & $root & " error: " & e.msg + echo "[Not Available] EpochAccumulator number " & $i & ": " & $root & " error: " & + e.msg # Using the http connection re-use seems to slow down these sequentual # requests considerably. Force a new connection setup by doing a close after diff --git a/fluffy/tools/eth_data_exporter.nim b/fluffy/tools/eth_data_exporter.nim index fedc87fa52..c440e4368b 100644 --- a/fluffy/tools/eth_data_exporter.nim +++ b/fluffy/tools/eth_data_exporter.nim @@ -41,8 +41,10 @@ import confutils, stew/[byteutils, io2], json_serialization, - faststreams, chronicles, - eth/[common, rlp], chronos, + faststreams, + chronicles, + eth/[common, rlp], + chronos, eth/common/eth_types_json_serialization, json_rpc/rpcclient, snappy, @@ -57,7 +59,8 @@ import from ../network/history/history_network import encode from ../../nimbus/utils/utils import calcTxRoot, calcReceiptRoot -chronicles.formatIt(IoErrorCode): $it +chronicles.formatIt(IoErrorCode): + $it proc downloadHeader(client: RpcClient, i: uint64): BlockHeader = let blockNumber = u256(i) @@ -82,7 +85,7 @@ proc writeHeadersToJson(config: ExporterConf, client: RpcClient) = try: var writer = JsonWriter[DefaultFlavor].init(fh.s, pretty = true) writer.beginRecord() - for i in config.startBlock..config.endBlock: + for i in config.startBlock .. config.endBlock: let blck = client.downloadHeader(i) writer.writeHeaderRecord(blck) if ((i - config.startBlock) mod 8192) == 0 and i != config.startBlock: @@ -105,7 +108,7 @@ proc writeBlocksToJson(config: ExporterConf, client: RpcClient) = try: var writer = JsonWriter[DefaultFlavor].init(fh.s, pretty = true) writer.beginRecord() - for i in config.startBlock..config.endBlock: + for i in config.startBlock .. config.endBlock: let blck = downloadBlock(i, client) writer.writeBlockRecord(blck.header, blck.body, blck.receipts) if ((i - config.startBlock) mod 8192) == 0 and i != config.startBlock: @@ -128,17 +131,16 @@ proc writeBlocksToDb(config: ExporterConf, client: RpcClient) = defer: db.close() - for i in config.startBlock..config.endBlock: + for i in config.startBlock .. config.endBlock: let blck = downloadBlock(i, client) blockHash = blck.header.blockHash() contentKeyType = BlockKey(blockHash: blockHash) - headerKey = encode(ContentKey( - contentType: blockHeader, blockHeaderKey: contentKeyType)) - bodyKey = encode(ContentKey( - contentType: blockBody, blockBodyKey: contentKeyType)) - receiptsKey = encode( - ContentKey(contentType: receipts, receiptsKey: contentKeyType)) + headerKey = + encode(ContentKey(contentType: blockHeader, blockHeaderKey: contentKeyType)) + bodyKey = encode(ContentKey(contentType: blockBody, blockBodyKey: contentKeyType)) + receiptsKey = + encode(ContentKey(contentType: receipts, receiptsKey: contentKeyType)) db.put(headerKey.toContentId(), headerKey.asSeq(), rlp.encode(blck.header)) @@ -181,8 +183,8 @@ proc newRpcClient(web3Url: Web3Url): RpcClient = client proc connectRpcClient( - client: RpcClient, web3Url: Web3Url): - Future[Result[void, string]] {.async.} = + client: RpcClient, web3Url: Web3Url +): Future[Result[void, string]] {.async.} = case web3Url.kind of HttpUrl: try: @@ -211,7 +213,8 @@ proc cmdExportEra1(config: ExporterConf) = var era = Era1(config.era) while config.eraCount == 0 or era < Era1(config.era) + config.eraCount: - defer: era += 1 + defer: + era += 1 let startNumber = era.startNumber() @@ -228,8 +231,10 @@ proc cmdExportEra1(config: ExporterConf) = var completed = false block writeFileBlock: - let e2 = openFile(tmpName, {OpenFlags.Write, OpenFlags.Create, OpenFlags.Truncate}).get() - defer: discard closeFile(e2) + let e2 = + openFile(tmpName, {OpenFlags.Write, OpenFlags.Create, OpenFlags.Truncate}).get() + defer: + discard closeFile(e2) # TODO: Not checking the result of init, update or finish here, as all # error cases are fatal. But maybe we could throw proper errors still. @@ -237,15 +242,18 @@ proc cmdExportEra1(config: ExporterConf) = # Header records to build the accumulator root var headerRecords: seq[accumulator.HeaderRecord] - for blockNumber in startNumber..endNumber: + for blockNumber in startNumber .. endNumber: let blck = try: # TODO: Not sure about the errors that can occur here. But the whole # block requests over json-rpc should be reworked here (and can be # used in the bridge also then) - requestBlock(blockNumber.u256, flags = {DownloadReceipts}, client = some(client)) + requestBlock( + blockNumber.u256, flags = {DownloadReceipts}, client = some(client) + ) except CatchableError as e: - error "Failed retrieving block, skip creation of era1 file", blockNumber, era, error = e.msg + error "Failed retrieving block, skip creation of era1 file", + blockNumber, era, error = e.msg break writeFileBlock var ttd: UInt256 @@ -254,12 +262,13 @@ proc cmdExportEra1(config: ExporterConf) = except ValueError: break writeFileBlock - headerRecords.add(accumulator.HeaderRecord( - blockHash: blck.header.blockHash(), - totalDifficulty: ttd)) + headerRecords.add( + accumulator.HeaderRecord( + blockHash: blck.header.blockHash(), totalDifficulty: ttd + ) + ) - group.update( - e2, blockNumber, blck.header, blck.body, blck.receipts, ttd).get() + group.update(e2, blockNumber, blck.header, blck.body, blck.receipts, ttd).get() accumulatorRoot = getEpochAccumulatorRoot(headerRecords) @@ -292,15 +301,15 @@ proc cmdVerifyEra1(config: ExporterConf) = let f = Era1File.open(config.era1FileName).valueOr: warn "Failed to open era file", error = error quit 1 - defer: close(f) + defer: + close(f) let root = f.verify.valueOr: warn "Verification of era file failed", error = error quit 1 notice "Era1 file succesfully verified", - accumulatorRoot = root.data.to0xHex(), - file = config.era1FileName + accumulatorRoot = root.data.to0xHex(), file = config.era1FileName when isMainModule: {.pop.} @@ -329,15 +338,13 @@ when isMainModule: if (config.endBlock < config.startBlock): fatal "Initial block number should be smaller than end block number", - startBlock = config.startBlock, - endBlock = config.endBlock + startBlock = config.startBlock, endBlock = config.endBlock quit 1 try: exportBlocks(config, client) finally: waitFor client.close() - of HistoryCmd.exportEpochHeaders: let client = newRpcClient(config.web3Url) let connectRes = waitFor client.connectRpcClient(config.web3Url) @@ -349,24 +356,25 @@ when isMainModule: # Downloading headers from JSON RPC endpoint info "Requesting epoch headers", epoch var headers: seq[BlockHeader] - for j in 0.. lastOptimisticUpdateSlot + 1: # TODO: If this turns out to be too tricky to not gossip old updates, @@ -158,34 +166,33 @@ proc runBeacon(config: PortalBridgeConf) {.raises: [CatchableError].} = # Or basically `lightClientOptimisticUpdateSlotOffset` await sleepAsync((SECONDS_PER_SLOT div INTERVALS_PER_SLOT).int.seconds) - await portalRpcClient.connect( - config.rpcAddress, Port(config.rpcPort), false) + await portalRpcClient.connect(config.rpcAddress, Port(config.rpcPort), false) - let res = await gossipLCOptimisticUpdate( - restClient, portalRpcClient, - cfg, forkDigests) + let res = + await gossipLCOptimisticUpdate(restClient, portalRpcClient, cfg, forkDigests) if res.isErr(): warn "Error gossiping LC optimistic update", error = res.error else: - if wallEpoch > lastFinalityUpdateEpoch + 2 and - wallSlot > start_slot(wallEpoch): - let res = await gossipLCFinalityUpdate( - restClient, portalRpcClient, - cfg, forkDigests) + if wallEpoch > lastFinalityUpdateEpoch + 2 and wallSlot > start_slot(wallEpoch): + let res = + await gossipLCFinalityUpdate(restClient, portalRpcClient, cfg, forkDigests) if res.isErr(): warn "Error gossiping LC finality update", error = res.error else: lastFinalityUpdateEpoch = epoch(res.get()) - if wallPeriod > lastUpdatePeriod and - wallSlot > start_slot(wallEpoch): + if wallPeriod > lastUpdatePeriod and wallSlot > start_slot(wallEpoch): # TODO: Need to delay timing here also with one slot? let res = await gossipLCUpdates( - restClient, portalRpcClient, - sync_committee_period(wallSlot).uint64, 1, - cfg, forkDigests) + restClient, + portalRpcClient, + sync_committee_period(wallSlot).uint64, + 1, + cfg, + forkDigests, + ) if res.isErr(): warn "Error gossiping LC update", error = res.error @@ -213,8 +220,9 @@ proc runBeacon(config: PortalBridgeConf) {.raises: [CatchableError].} = timeToNextSlot = nextSlot.start_beacon_time() - getBeaconTime() waitFor backfill( - restClient, config.rpcAddress, config.rpcPort, - config.backfillAmount, config.trustedBlockRoot) + restClient, config.rpcAddress, config.rpcPort, config.backfillAmount, + config.trustedBlockRoot, + ) asyncSpawn runOnSlotLoop() diff --git a/fluffy/tools/portal_bridge/portal_bridge_beacon.nim b/fluffy/tools/portal_bridge/portal_bridge_beacon.nim index 215323ca79..b81137936b 100644 --- a/fluffy/tools/portal_bridge/portal_bridge_beacon.nim +++ b/fluffy/tools/portal_bridge/portal_bridge_beacon.nim @@ -9,7 +9,8 @@ import chronos, - chronicles, chronicles/topics_registry, + chronicles, + chronicles/topics_registry, stew/byteutils, eth/async_utils, json_rpc/clients/httpclient, @@ -18,27 +19,25 @@ import ../../rpc/portal_rpc_client, ../eth_data_exporter/cl_data_exporter -const - restRequestsTimeout = 30.seconds +const restRequestsTimeout = 30.seconds # TODO: From nimbus_binary_common, but we don't want to import that. proc sleepAsync*(t: TimeDiff): Future[void] = - sleepAsync(nanoseconds( - if t.nanoseconds < 0: 0'i64 else: t.nanoseconds)) + sleepAsync(nanoseconds(if t.nanoseconds < 0: 0'i64 else: t.nanoseconds)) proc gossipLCBootstrapUpdate*( - restClient: RestClientRef, portalRpcClient: RpcHttpClient, + restClient: RestClientRef, + portalRpcClient: RpcHttpClient, trustedBlockRoot: Eth2Digest, - cfg: RuntimeConfig, forkDigests: ref ForkDigests): - Future[Result[void, string]] {.async.} = + cfg: RuntimeConfig, + forkDigests: ref ForkDigests, +): Future[Result[void, string]] {.async.} = var bootstrap = try: info "Downloading LC bootstrap" awaitWithTimeout( - restClient.getLightClientBootstrap( - trustedBlockRoot, - cfg, forkDigests), - restRequestsTimeout + restClient.getLightClientBootstrap(trustedBlockRoot, cfg, forkDigests), + restRequestsTimeout, ): return err("Attempt to download LC bootstrap timed out") except CatchableError as exc: @@ -49,22 +48,17 @@ proc gossipLCBootstrapUpdate*( let slot = forkyObject.header.beacon.slot contentKey = encode(bootstrapContentKey(trustedBlockRoot)) - forkDigest = forkDigestAtEpoch( - forkDigests[], epoch(slot), cfg) - content = encodeBootstrapForked( - forkDigest, - bootstrap - ) + forkDigest = forkDigestAtEpoch(forkDigests[], epoch(slot), cfg) + content = encodeBootstrapForked(forkDigest, bootstrap) proc GossipRpcAndClose(): Future[Result[void, string]] {.async.} = try: let contentKeyHex = contentKey.asSeq().toHex() peers = await portalRpcClient.portal_beaconRandomGossip( - contentKeyHex, - content.toHex()) - info "Beacon LC bootstrap gossiped", peers, - contentKey = contentKeyHex + contentKeyHex, content.toHex() + ) + info "Beacon LC bootstrap gossiped", peers, contentKey = contentKeyHex return ok() except CatchableError as e: return err("JSON-RPC error: " & $e.msg) @@ -74,22 +68,25 @@ proc gossipLCBootstrapUpdate*( return ok() else: return err(res.error) - else: return err("No LC bootstraps pre Altair") proc gossipLCUpdates*( - restClient: RestClientRef, portalRpcClient: RpcHttpClient, - startPeriod: uint64, count: uint64, - cfg: RuntimeConfig, forkDigests: ref ForkDigests): - Future[Result[void, string]] {.async.} = + restClient: RestClientRef, + portalRpcClient: RpcHttpClient, + startPeriod: uint64, + count: uint64, + cfg: RuntimeConfig, + forkDigests: ref ForkDigests, +): Future[Result[void, string]] {.async.} = var updates = try: info "Downloading LC updates", count awaitWithTimeout( restClient.getLightClientUpdatesByRange( - SyncCommitteePeriod(startPeriod), count, cfg, forkDigests), - restRequestsTimeout + SyncCommitteePeriod(startPeriod), count, cfg, forkDigests + ), + restRequestsTimeout, ): return err("Attempt to download LC updates timed out") except CatchableError as exc: @@ -104,20 +101,17 @@ proc gossipLCUpdates*( contentKey = encode(updateContentKey(period.uint64, count)) forkDigest = forkDigestAtEpoch(forkDigests[], epoch(slot), cfg) - content = encodeLightClientUpdatesForked( - forkDigest, - updates - ) + content = encodeLightClientUpdatesForked(forkDigest, updates) proc GossipRpcAndClose(): Future[Result[void, string]] {.async.} = try: let contentKeyHex = contentKey.asSeq().toHex() peers = await portalRpcClient.portal_beaconRandomGossip( - contentKeyHex, - content.toHex()) - info "Beacon LC update gossiped", peers, - contentKey = contentKeyHex, period, count + contentKeyHex, content.toHex() + ) + info "Beacon LC update gossiped", + peers, contentKey = contentKeyHex, period, count return ok() except CatchableError as e: return err("JSON-RPC error: " & $e.msg) @@ -138,16 +132,16 @@ proc gossipLCUpdates*( return err("No updates downloaded") proc gossipLCFinalityUpdate*( - restClient: RestClientRef, portalRpcClient: RpcHttpClient, - cfg: RuntimeConfig, forkDigests: ref ForkDigests): - Future[Result[Slot, string]] {.async.} = + restClient: RestClientRef, + portalRpcClient: RpcHttpClient, + cfg: RuntimeConfig, + forkDigests: ref ForkDigests, +): Future[Result[Slot, string]] {.async.} = var update = try: info "Downloading LC finality update" awaitWithTimeout( - restClient.getLightClientFinalityUpdate( - cfg, forkDigests), - restRequestsTimeout + restClient.getLightClientFinalityUpdate(cfg, forkDigests), restRequestsTimeout ): return err("Attempt to download LC finality update timed out") except CatchableError as exc: @@ -159,21 +153,19 @@ proc gossipLCFinalityUpdate*( finalizedSlot = forkyObject.finalized_header.beacon.slot contentKey = encode(finalityUpdateContentKey(finalizedSlot.uint64)) forkDigest = forkDigestAtEpoch( - forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg) - content = encodeFinalityUpdateForked( - forkDigest, - update + forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg ) + content = encodeFinalityUpdateForked(forkDigest, update) proc GossipRpcAndClose(): Future[Result[void, string]] {.async.} = try: let contentKeyHex = contentKey.asSeq().toHex() peers = await portalRpcClient.portal_beaconRandomGossip( - contentKeyHex, - content.toHex()) - info "Beacon LC finality update gossiped", peers, - contentKey = contentKeyHex, finalizedSlot + contentKeyHex, content.toHex() + ) + info "Beacon LC finality update gossiped", + peers, contentKey = contentKeyHex, finalizedSlot return ok() except CatchableError as e: return err("JSON-RPC error: " & $e.msg) @@ -183,21 +175,20 @@ proc gossipLCFinalityUpdate*( return ok(finalizedSlot) else: return err(res.error) - else: return err("No LC updates pre Altair") proc gossipLCOptimisticUpdate*( - restClient: RestClientRef, portalRpcClient: RpcHttpClient, - cfg: RuntimeConfig, forkDigests: ref ForkDigests): - Future[Result[Slot, string]] {.async.} = + restClient: RestClientRef, + portalRpcClient: RpcHttpClient, + cfg: RuntimeConfig, + forkDigests: ref ForkDigests, +): Future[Result[Slot, string]] {.async.} = var update = try: info "Downloading LC optimistic update" awaitWithTimeout( - restClient.getLightClientOptimisticUpdate( - cfg, forkDigests), - restRequestsTimeout + restClient.getLightClientOptimisticUpdate(cfg, forkDigests), restRequestsTimeout ): return err("Attempt to download LC optimistic update timed out") except CatchableError as exc: @@ -209,21 +200,19 @@ proc gossipLCOptimisticUpdate*( slot = forkyObject.signature_slot contentKey = encode(optimisticUpdateContentKey(slot.uint64)) forkDigest = forkDigestAtEpoch( - forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg) - content = encodeOptimisticUpdateForked( - forkDigest, - update + forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg ) + content = encodeOptimisticUpdateForked(forkDigest, update) proc GossipRpcAndClose(): Future[Result[void, string]] {.async.} = try: let contentKeyHex = contentKey.asSeq().toHex() peers = await portalRpcClient.portal_beaconRandomGossip( - contentKeyHex, - content.toHex()) - info "Beacon LC optimistic update gossiped", peers, - contentKey = contentKeyHex, slot + contentKeyHex, content.toHex() + ) + info "Beacon LC optimistic update gossiped", + peers, contentKey = contentKeyHex, slot return ok() except CatchableError as e: @@ -234,6 +223,5 @@ proc gossipLCOptimisticUpdate*( return ok(slot) else: return err(res.error) - else: return err("No LC updates pre Altair") diff --git a/fluffy/tools/portal_bridge/portal_bridge_conf.nim b/fluffy/tools/portal_bridge/portal_bridge_conf.nim index 945d18e198..0c0c7ff4d5 100644 --- a/fluffy/tools/portal_bridge/portal_bridge_conf.nim +++ b/fluffy/tools/portal_bridge/portal_bridge_conf.nim @@ -7,10 +7,7 @@ {.push raises: [].} -import - confutils, confutils/std/net, - nimcrypto/hash, - ../../logging +import confutils, confutils/std/net, nimcrypto/hash, ../../logging export net @@ -22,62 +19,60 @@ type history = "Run a Portal bridge for the history network" state = "Run a Portal bridge for the state network" - PortalBridgeConf* = object - # Logging - logLevel* {. - desc: "Sets the log level" - defaultValue: "INFO" - name: "log-level" .}: string + PortalBridgeConf* = object # Logging + logLevel* {.desc: "Sets the log level", defaultValue: "INFO", name: "log-level".}: + string logStdout* {. - hidden - desc: "Specifies what kind of logs should be written to stdout (auto, colors, nocolors, json)" - defaultValueDesc: "auto" - defaultValue: StdoutLogKind.Auto - name: "log-format" .}: StdoutLogKind + hidden, + desc: + "Specifies what kind of logs should be written to stdout (auto, colors, nocolors, json)", + defaultValueDesc: "auto", + defaultValue: StdoutLogKind.Auto, + name: "log-format" + .}: StdoutLogKind # Portal JSON-RPC API server to connect to rpcAddress* {. - desc: "Listening address of the Portal JSON-RPC server" - defaultValue: "127.0.0.1" - name: "rpc-address" .}: string + desc: "Listening address of the Portal JSON-RPC server", + defaultValue: "127.0.0.1", + name: "rpc-address" + .}: string rpcPort* {. - desc: "Listening port of the Portal JSON-RPC server" - defaultValue: 8545 - name: "rpc-port" .}: Port - - case cmd* {. - command - desc: "" - .}: PortalBridgeCmd + desc: "Listening port of the Portal JSON-RPC server", + defaultValue: 8545, + name: "rpc-port" + .}: Port + case cmd* {.command, desc: "".}: PortalBridgeCmd of PortalBridgeCmd.beacon: # Beacon node REST API URL restUrl* {. - desc: "URL of the beacon node REST service" - defaultValue: "http://127.0.0.1:5052" - name: "rest-url" .}: string + desc: "URL of the beacon node REST service", + defaultValue: "http://127.0.0.1:5052", + name: "rest-url" + .}: string # Backfill options backfillAmount* {. - desc: "Amount of beacon LC updates to backfill gossip into the network" - defaultValue: 64 - name: "backfill-amount" .}: uint64 + desc: "Amount of beacon LC updates to backfill gossip into the network", + defaultValue: 64, + name: "backfill-amount" + .}: uint64 trustedBlockRoot* {. - desc: "Trusted finalized block root for which to gossip a LC bootstrap into the network" - defaultValue: none(TrustedDigest) - name: "trusted-block-root" .}: Option[TrustedDigest] - + desc: + "Trusted finalized block root for which to gossip a LC bootstrap into the network", + defaultValue: none(TrustedDigest), + name: "trusted-block-root" + .}: Option[TrustedDigest] of PortalBridgeCmd.history: discard - of PortalBridgeCmd.state: discard -func parseCmdArg*(T: type TrustedDigest, input: string): T - {.raises: [ValueError].} = +func parseCmdArg*(T: type TrustedDigest, input: string): T {.raises: [ValueError].} = TrustedDigest.fromHex(input) func completeCmdArg*(T: type TrustedDigest, input: string): seq[string] = diff --git a/fluffy/tools/portalcli.nim b/fluffy/tools/portalcli.nim index a30690607c..1d678f7266 100644 --- a/fluffy/tools/portalcli.nim +++ b/fluffy/tools/portalcli.nim @@ -7,8 +7,14 @@ import std/[options, strutils, tables], - confutils, confutils/std/net, chronicles, chronicles/topics_registry, - chronos, metrics, metrics/chronos_httpserver, stew/[byteutils, results], + confutils, + confutils/std/net, + chronicles, + chronicles/topics_registry, + chronos, + metrics, + metrics/chronos_httpserver, + stew/[byteutils, results], nimcrypto/[hash, sha2], eth/[keys, net/nat], eth/p2p/discoveryv5/[enr, node], @@ -37,109 +43,120 @@ type PortalCliConf* = object logLevel* {. - defaultValue: LogLevel.DEBUG - defaultValueDesc: $LogLevel.DEBUG - desc: "Sets the log level" - name: "log-level" .}: LogLevel + defaultValue: LogLevel.DEBUG, + defaultValueDesc: $LogLevel.DEBUG, + desc: "Sets the log level", + name: "log-level" + .}: LogLevel - udpPort* {. - defaultValue: 9009 - desc: "UDP listening port" - name: "udp-port" .}: uint16 + udpPort* {.defaultValue: 9009, desc: "UDP listening port", name: "udp-port".}: + uint16 listenAddress* {. - defaultValue: defaultListenAddress - defaultValueDesc: $defaultListenAddressDesc - desc: "Listening address for the Discovery v5 traffic" - name: "listen-address" }: IpAddress + defaultValue: defaultListenAddress, + defaultValueDesc: $defaultListenAddressDesc, + desc: "Listening address for the Discovery v5 traffic", + name: "listen-address" + .}: IpAddress # Note: This will add bootstrap nodes for both Discovery v5 network and each # enabled Portal network. No distinction is made on bootstrap nodes per # specific network. bootstrapNodes* {. - desc: "ENR URI of node to bootstrap Discovery v5 and the Portal networks from. Argument may be repeated" - name: "bootstrap-node" .}: seq[Record] + desc: + "ENR URI of node to bootstrap Discovery v5 and the Portal networks from. Argument may be repeated", + name: "bootstrap-node" + .}: seq[Record] bootstrapNodesFile* {. - desc: "Specifies a line-delimited file of ENR URIs to bootstrap Discovery v5 and Portal networks from" - defaultValue: "" - name: "bootstrap-file" }: InputFile + desc: + "Specifies a line-delimited file of ENR URIs to bootstrap Discovery v5 and Portal networks from", + defaultValue: "", + name: "bootstrap-file" + .}: InputFile nat* {. - desc: "Specify method to use for determining public address. " & - "Must be one of: any, none, upnp, pmp, extip:" - defaultValue: NatConfig(hasExtIp: false, nat: NatAny) - defaultValueDesc: "any" - name: "nat" .}: NatConfig + desc: + "Specify method to use for determining public address. " & + "Must be one of: any, none, upnp, pmp, extip:", + defaultValue: NatConfig(hasExtIp: false, nat: NatAny), + defaultValueDesc: "any", + name: "nat" + .}: NatConfig enrAutoUpdate* {. - defaultValue: false - desc: "Discovery can automatically update its ENR with the IP address " & - "and UDP port as seen by other nodes it communicates with. " & - "This option allows to enable/disable this functionality" - name: "enr-auto-update" .}: bool + defaultValue: false, + desc: + "Discovery can automatically update its ENR with the IP address " & + "and UDP port as seen by other nodes it communicates with. " & + "This option allows to enable/disable this functionality", + name: "enr-auto-update" + .}: bool networkKey* {. desc: "Private key (secp256k1) for the p2p network, hex encoded.", - defaultValue: PrivateKey.random(keys.newRng()[]) - defaultValueDesc: "random" - name: "network-key" .}: PrivateKey + defaultValue: PrivateKey.random(keys.newRng()[]), + defaultValueDesc: "random", + name: "network-key" + .}: PrivateKey metricsEnabled* {. - defaultValue: false - desc: "Enable the metrics server" - name: "metrics" .}: bool + defaultValue: false, desc: "Enable the metrics server", name: "metrics" + .}: bool metricsAddress* {. - defaultValue: defaultAdminListenAddress - defaultValueDesc: $defaultAdminListenAddressDesc - desc: "Listening address of the metrics server" - name: "metrics-address" .}: IpAddress + defaultValue: defaultAdminListenAddress, + defaultValueDesc: $defaultAdminListenAddressDesc, + desc: "Listening address of the metrics server", + name: "metrics-address" + .}: IpAddress metricsPort* {. - defaultValue: 8008 - desc: "Listening HTTP port of the metrics server" - name: "metrics-port" .}: Port + defaultValue: 8008, + desc: "Listening HTTP port of the metrics server", + name: "metrics-port" + .}: Port protocolId* {. - defaultValue: historyProtocolId - desc: "Portal wire protocol id for the network to connect to" - name: "protocol-id" .}: PortalProtocolId + defaultValue: historyProtocolId, + desc: "Portal wire protocol id for the network to connect to", + name: "protocol-id" + .}: PortalProtocolId # TODO maybe it is worth defining minimal storage size and throw error if # value provided is smaller than minimum storageSize* {. - desc: "Maximum amount (in bytes) of content which will be stored " & - "in local database." - defaultValue: defaultStorageSize - name: "storage-size" .}: uint32 - - case cmd* {. - command - defaultValue: noCommand }: PortalCmd + desc: + "Maximum amount (in bytes) of content which will be stored " & + "in local database.", + defaultValue: defaultStorageSize, + name: "storage-size" + .}: uint32 + + case cmd* {.command, defaultValue: noCommand.}: PortalCmd of noCommand: discard of ping: pingTarget* {. - argument - desc: "ENR URI of the node to a send ping message" - name: "node" .}: Node + argument, desc: "ENR URI of the node to a send ping message", name: "node" + .}: Node of findNodes: distance* {. - defaultValue: 255 - desc: "Distance parameter for the findNodes message" - name: "distance" .}: uint16 + defaultValue: 255, + desc: "Distance parameter for the findNodes message", + name: "distance" + .}: uint16 # TODO: Order here matters as else the help message does not show all the # information, see: https://github.com/status-im/nim-confutils/issues/15 findNodesTarget* {. - argument - desc: "ENR URI of the node to send a findNodes message" - name: "node" .}: Node + argument, desc: "ENR URI of the node to send a findNodes message", name: "node" + .}: Node of findContent: findContentTarget* {. - argument - desc: "ENR URI of the node to send a findContent message" - name: "node" .}: Node + argument, + desc: "ENR URI of the node to send a findContent message", + name: "node" + .}: Node proc parseCmdArg*(T: type enr.Record, p: string): T = if not fromURI(result, p): @@ -178,8 +195,7 @@ proc parseCmdArg*(T: type PortalProtocolId, p: string): T = try: result = byteutils.hexToByteArray(p, 2) except ValueError: - raise newException(ValueError, - "Invalid protocol id, not a valid hex value") + raise newException(ValueError, "Invalid protocol id, not a valid hex value") proc completeCmdArg*(T: type PortalProtocolId, val: string): seq[string] = return @[] @@ -204,8 +220,8 @@ proc run(config: PortalCliConf) = bindIp = config.listenAddress udpPort = Port(config.udpPort) # TODO: allow for no TCP port mapping! - (extIp, _, extUdpPort) = setupAddress(config.nat, - config.listenAddress, udpPort, udpPort, "portalcli") + (extIp, _, extUdpPort) = + setupAddress(config.nat, config.listenAddress, udpPort, udpPort, "portalcli") var bootstrapRecords: seq[Record] loadBootstrapFile(string config.bootstrapNodesFile, bootstrapRecords) @@ -213,11 +229,15 @@ proc run(config: PortalCliConf) = let d = newProtocol( config.networkKey, - extIp, none(Port), extUdpPort, + extIp, + none(Port), + extUdpPort, bootstrapRecords = bootstrapRecords, - bindIp = bindIp, bindPort = udpPort, + bindIp = bindIp, + bindPort = udpPort, enrAutoUpdate = config.enrAutoUpdate, - rng = rng) + rng = rng, + ) d.open() @@ -226,9 +246,14 @@ proc run(config: PortalCliConf) = sm = StreamManager.new(d) cq = newAsyncQueue[(Opt[NodeId], ContentKeysList, seq[seq[byte]])](50) stream = sm.registerNewStream(cq) - portal = PortalProtocol.new(d, config.protocolId, - testContentIdHandler, createGetHandler(db), stream, - bootstrapRecords = bootstrapRecords) + portal = PortalProtocol.new( + d, + config.protocolId, + testContentIdHandler, + createGetHandler(db), + stream, + bootstrapRecords = bootstrapRecords, + ) portal.dbPut = createStoreHandler(db, defaultRadiusConfig, portal) @@ -240,8 +265,11 @@ proc run(config: PortalCliConf) = url = "http://" & $address & ":" & $port & "/metrics" try: chronos_httpserver.startMetricsHttpServer($address, port) - except CatchableError as exc: raise exc - except Exception as exc: raiseAssert exc.msg # TODO fix metrics + except CatchableError as exc: + raise exc + except Exception as exc: + raiseAssert exc.msg + # TODO fix metrics case config.cmd of ping: @@ -264,14 +292,12 @@ proc run(config: PortalCliConf) = # For now just some bogus bytes let contentKey = ByteList.init(@[1'u8]) - let foundContent = waitFor portal.findContent(config.findContentTarget, - contentKey) + let foundContent = waitFor portal.findContent(config.findContentTarget, contentKey) if foundContent.isOk(): echo foundContent.get() else: echo foundContent.error - of noCommand: d.start() portal.start() diff --git a/fluffy/tools/utp_testing/utp_rpc_types.nim b/fluffy/tools/utp_testing/utp_rpc_types.nim index 2af836cbbb..e9e7ccfd63 100644 --- a/fluffy/tools/utp_testing/utp_rpc_types.nim +++ b/fluffy/tools/utp_testing/utp_rpc_types.nim @@ -6,7 +6,6 @@ {.push raises: [].} - import std/[hashes, json], json_rpc/jsonmarshal, @@ -20,15 +19,15 @@ type SKey* = object id*: uint16 nodeId*: NodeId -proc writeValue*(w: var JsonWriter[JrpcConv], v: SKey) - {.gcsafe, raises: [IOError].} = +proc writeValue*(w: var JsonWriter[JrpcConv], v: SKey) {.gcsafe, raises: [IOError].} = let hex = v.nodeId.toBytesBE().toHex() let numId = v.id.toBytesBE().toHex() let finalStr = hex & numId w.writeValue(finalStr) -proc readValue*(r: var JsonReader[JrpcConv], val: var SKey) - {.gcsafe, raises: [IOError, JsonReaderError].} = +proc readValue*( + r: var JsonReader[JrpcConv], val: var SKey +) {.gcsafe, raises: [IOError, JsonReaderError].} = let str = r.parseString() if str.len < 64: r.raiseUnexpectedValue("SKey: too short string") diff --git a/fluffy/tools/utp_testing/utp_test.nim b/fluffy/tools/utp_testing/utp_test.nim index 90c2422928..18a3755b0c 100644 --- a/fluffy/tools/utp_testing/utp_test.nim +++ b/fluffy/tools/utp_testing/utp_test.nim @@ -7,8 +7,11 @@ import std/[options, sequtils, sugar, strutils], - unittest2, testutils, chronos, - json_rpc/rpcclient, stew/byteutils, + unittest2, + testutils, + chronos, + json_rpc/rpcclient, + stew/byteutils, eth/keys, ./utp_test_rpc_client @@ -34,13 +37,13 @@ procSuite "uTP network simulator tests": let rng = newRng() - type - FutureCallback[A] = proc (): Future[A] {.gcsafe, raises: [].} + type FutureCallback[A] = proc(): Future[A] {.gcsafe, raises: [].} # combinator which repeatedly calls passed closure until returned future is # successfull # TODO: currently works only for non void types proc repeatTillSuccess[A]( - f: FutureCallback[A], maxTries: int = 20): Future[A] {.async.} = + f: FutureCallback[A], maxTries: int = 20 + ): Future[A] {.async.} = var i = 0 while true: try: @@ -58,19 +61,17 @@ procSuite "uTP network simulator tests": raise canc proc findServerConnection( - connections: openArray[SKey], - clientId: NodeId, - clientConnectionId: uint16): Option[Skey] = - let conns: seq[SKey] = - connections.filter((key:Skey) => key.id == (clientConnectionId + 1) and - key.nodeId == clientId) + connections: openArray[SKey], clientId: NodeId, clientConnectionId: uint16 + ): Option[Skey] = + let conns: seq[SKey] = connections.filter( + (key: Skey) => key.id == (clientConnectionId + 1) and key.nodeId == clientId + ) if len(conns) == 0: none[Skey]() else: some[Skey](conns[0]) - proc setupTest(): - Future[(RpcHttpClient, NodeInfo, RpcHttpClient, NodeInfo)] {.async.} = + proc setupTest(): Future[(RpcHttpClient, NodeInfo, RpcHttpClient, NodeInfo)] {.async.} = let client = newRpcHttpClient() let server = newRpcHttpClient() @@ -91,12 +92,12 @@ procSuite "uTP network simulator tests": let (client, clientInfo, server, serverInfo) = await setupTest() - clientConnectionKey = await repeatTillSuccess(() => - client.utp_connect(serverInfo.enr)) - serverConnections = await repeatTillSuccess(() => - server.utp_get_connections()) + clientConnectionKey = + await repeatTillSuccess(() => client.utp_connect(serverInfo.enr)) + serverConnections = await repeatTillSuccess(() => server.utp_get_connections()) maybeServerConnectionKey = serverConnections.findServerConnection( - clientInfo.nodeId, clientConnectionKey.id) + clientInfo.nodeId, clientConnectionKey.id + ) check: maybeServerConnectionKey.isSome() @@ -119,12 +120,12 @@ procSuite "uTP network simulator tests": let (client, clientInfo, server, serverInfo) = await setupTest() - clientConnectionKey = await repeatTillSuccess(() => - client.utp_connect(serverInfo.enr)) - serverConnections = await repeatTillSuccess(() => - server.utp_get_connections()) + clientConnectionKey = + await repeatTillSuccess(() => client.utp_connect(serverInfo.enr)) + serverConnections = await repeatTillSuccess(() => server.utp_get_connections()) maybeServerConnectionKey = serverConnections.findServerConnection( - clientInfo.nodeId, clientConnectionKey.id) + clientInfo.nodeId, clientConnectionKey.id + ) check: maybeServerConnectionKey.isSome() @@ -146,12 +147,12 @@ procSuite "uTP network simulator tests": let (client, clientInfo, server, serverInfo) = await setupTest() - clientConnectionKey = await repeatTillSuccess(() => - client.utp_connect(serverInfo.enr)) - serverConnections = await repeatTillSuccess(() => - server.utp_get_connections()) + clientConnectionKey = + await repeatTillSuccess(() => client.utp_connect(serverInfo.enr)) + serverConnections = await repeatTillSuccess(() => server.utp_get_connections()) maybeServerConnectionKey = serverConnections.findServerConnection( - clientInfo.nodeId, clientConnectionKey.id) + clientInfo.nodeId, clientConnectionKey.id + ) check: maybeServerConnectionKey.isSome() @@ -159,7 +160,7 @@ procSuite "uTP network simulator tests": let serverConnectionKey = maybeServerConnectionKey.unsafeGet() var totalBytesToWrite: string - for i in 0.. - client.utp_connect(serverInfo.enr)) - serverConnections = await repeatTillSuccess(() => - server.utp_get_connections()) + clientConnectionKey = + await repeatTillSuccess(() => client.utp_connect(serverInfo.enr)) + serverConnections = await repeatTillSuccess(() => server.utp_get_connections()) serverConnectionKeyRes = serverConnections.findServerConnection( - clientInfo.nodeId, clientConnectionKey.id) + clientInfo.nodeId, clientConnectionKey.id + ) check serverConnectionKeyRes.isSome() diff --git a/fluffy/tools/utp_testing/utp_test_app.nim b/fluffy/tools/utp_testing/utp_test_app.nim index 0a35c90c98..8d6a02ea66 100644 --- a/fluffy/tools/utp_testing/utp_test_app.nim +++ b/fluffy/tools/utp_testing/utp_test_app.nim @@ -8,7 +8,9 @@ import std/[hashes, tables, net], - chronos, chronicles, confutils, + chronos, + chronicles, + confutils, confutils/std/net as confNet, stew/[byteutils, endians2], json_rpc/servers/httpserver, @@ -19,48 +21,42 @@ import ../../rpc/rpc_discovery_api, ./utp_rpc_types -const - defaultListenAddress* = (static parseIpAddress("127.0.0.1")) +const defaultListenAddress* = (static parseIpAddress("127.0.0.1")) type AppConf* = object - rpcPort* {. - defaultValue: 7041 - desc: "Json rpc port" - name: "rpc-port" .}: Port + rpcPort* {.defaultValue: 7041, desc: "Json rpc port", name: "rpc-port".}: Port - udpPort* {. - defaultValue: 7042 - desc: "UDP listening port" - name: "udp-port" .}: Port + udpPort* {.defaultValue: 7042, desc: "UDP listening port", name: "udp-port".}: Port udpListenAddress* {. - defaultValue: defaultListenAddress - desc: "UDP listening address" - name: "udp-listen-address" .}: IpAddress + defaultValue: defaultListenAddress, + desc: "UDP listening address", + name: "udp-listen-address" + .}: IpAddress rpcListenAddress* {. - defaultValue: defaultListenAddress - desc: "RPC listening address" - name: "rpc-listen-address" .}: IpAddress + defaultValue: defaultListenAddress, + desc: "RPC listening address", + name: "rpc-listen-address" + .}: IpAddress -proc writeValue*(w: var JsonWriter[JrpcConv], v: Record) - {.gcsafe, raises: [IOError].} = +proc writeValue*(w: var JsonWriter[JrpcConv], v: Record) {.gcsafe, raises: [IOError].} = w.writeValue(v.toURI()) -proc readValue*(r: var JsonReader[JrpcConv], val: var Record) - {.gcsafe, raises: [IOError, JsonReaderError].} = +proc readValue*( + r: var JsonReader[JrpcConv], val: var Record +) {.gcsafe, raises: [IOError, JsonReaderError].} = if not fromURI(val, r.parseString()): r.raiseUnexpectedValue("Invalid ENR") proc installUtpHandlers( - srv: RpcHttpServer, - d: protocol.Protocol, - s: UtpDiscv5Protocol, - t: ref Table[SKey, UtpSocket[NodeAddress]]) {.raises: [CatchableError].} = - + srv: RpcHttpServer, + d: protocol.Protocol, + s: UtpDiscv5Protocol, + t: ref Table[SKey, UtpSocket[NodeAddress]], +) {.raises: [CatchableError].} = srv.rpc("utp_connect") do(r: enr.Record) -> SKey: - let - nodeRes = newNode(r) + let nodeRes = newNode(r) if nodeRes.isOk(): let node = nodeRes.get() @@ -116,9 +112,11 @@ proc installUtpHandlers( else: raise newException(ValueError, "Socket with provided key is missing") -proc buildAcceptConnection(t: ref Table[SKey, UtpSocket[NodeAddress]]): AcceptConnectionCallback[NodeAddress] = +proc buildAcceptConnection( + t: ref Table[SKey, UtpSocket[NodeAddress]] +): AcceptConnectionCallback[NodeAddress] = return ( - proc (server: UtpRouter[NodeAddress], client: UtpSocket[NodeAddress]): Future[void] = + proc(server: UtpRouter[NodeAddress], client: UtpSocket[NodeAddress]): Future[void] = let fut = newFuture[void]() let key = client.socketKey.toSKey() t[key] = client @@ -146,17 +144,23 @@ when isMainModule: let d = newProtocol( key, - some(discAddress), none(Port), some(conf.udpPort), + some(discAddress), + none(Port), + some(conf.udpPort), bootstrapRecords = @[], - bindIp = discAddress, bindPort = conf.udpPort, + bindIp = discAddress, + bindPort = conf.udpPort, enrAutoUpdate = true, - rng = rng) + rng = rng, + ) d.open() let cfg = SocketConfig.init(incomingSocketReceiveTimeout = none[Duration]()) - utp = UtpDiscv5Protocol.new(d, protName, buildAcceptConnection(table), socketConfig = cfg) + utp = UtpDiscv5Protocol.new( + d, protName, buildAcceptConnection(table), socketConfig = cfg + ) # needed for some of the discovery api: nodeInfo, setEnr, ping srv.installDiscoveryApiHandlers(d) diff --git a/fluffy/tools/utp_testing/utp_test_rpc_calls.nim b/fluffy/tools/utp_testing/utp_test_rpc_calls.nim index 5cce0e4dab..af3e462906 100644 --- a/fluffy/tools/utp_testing/utp_test_rpc_calls.nim +++ b/fluffy/tools/utp_testing/utp_test_rpc_calls.nim @@ -1,3 +1,8 @@ +# Copyright (c) 2022-2024 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. proc utp_connect(enr: Record): SKey proc utp_write(k: SKey, b: string): bool diff --git a/fluffy/tools/utp_testing/utp_test_rpc_client.nim b/fluffy/tools/utp_testing/utp_test_rpc_client.nim index 2e78ef7d79..f3de9b9e08 100644 --- a/fluffy/tools/utp_testing/utp_test_rpc_client.nim +++ b/fluffy/tools/utp_testing/utp_test_rpc_client.nim @@ -1,17 +1,18 @@ # Nimbus -# Copyright (c) 2022 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. import - std/os, - json_rpc/rpcclient, - ./utp_rpc_types, - ../../rpc/[rpc_types, rpc_discovery_api] + std/os, json_rpc/rpcclient, ./utp_rpc_types, ../../rpc/[rpc_types, rpc_discovery_api] export utp_rpc_types, rpc_types createRpcSigs(RpcClient, currentSourcePath.parentDir / "utp_test_rpc_calls.nim") -createRpcSigs(RpcClient, currentSourcePath.parentDir /../ "" /../ "rpc" / "rpc_calls" / "rpc_discovery_calls.nim") +createRpcSigs( + RpcClient, + currentSourcePath.parentDir /../ "" /../ "rpc" / "rpc_calls" / + "rpc_discovery_calls.nim", +) diff --git a/fluffy/version.nim b/fluffy/version.nim index e220d71770..63e044f487 100644 --- a/fluffy/version.nim +++ b/fluffy/version.nim @@ -1,5 +1,5 @@ # Nimbus fluffy -# Copyright (c) 2023 Status Research & Development GmbH +# Copyright (c) 2023-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -7,20 +7,16 @@ {.push raises: [].} -import - std/strutils, - stew/byteutils, - metrics +import std/strutils, stew/byteutils, metrics const versionMajor* = 0 versionMinor* = 1 versionBuild* = 0 - gitRevision* = strip(staticExec("git rev-parse --short HEAD"))[0..5] + gitRevision* = strip(staticExec("git rev-parse --short HEAD"))[0 .. 5] - versionAsStr* = - $versionMajor & "." & $versionMinor & "." & $versionBuild + versionAsStr* = $versionMajor & "." & $versionMinor & "." & $versionBuild fullVersionStr* = "v" & versionAsStr & "-" & gitRevision @@ -30,12 +26,11 @@ const nimBanner* = staticExec("nim --version | grep Version") # The web3_clientVersion - clientVersion* = clientName & "/" & - fullVersionStr & "/" & - hostOS & "-" & hostCPU & "/" & - "Nim" & NimVersion + clientVersion* = + clientName & "/" & fullVersionStr & "/" & hostOS & "-" & hostCPU & "/" & "Nim" & + NimVersion - compileYear = CompileDate[0 ..< 4] # YYYY-MM-DD (UTC) + compileYear = CompileDate[0 ..< 4] # YYYY-MM-DD (UTC) copyrightBanner* = "Copyright (c) 2021-" & compileYear & " Status Research & Development GmbH" @@ -49,17 +44,19 @@ func getNimGitHash*(): string = return for line in tmp: if line.startsWith(gitPrefix) and line.len > 8 + gitPrefix.len: - result = line[gitPrefix.len..