From ba996a8d2bbd5bb56e9ab4ce1ad3120d7a5af2db Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Sat, 18 Apr 2020 22:26:30 -0400 Subject: [PATCH] solution: initial implementation for Bitcoin upstreams --- build.gradle | 3 + .../dshackle/config/UpstreamsConfig.kt | 5 + .../dshackle/config/UpstreamsConfigReader.kt | 26 +- .../io/emeraldpay/dshackle/data/BlockId.kt | 12 +- .../io/emeraldpay/dshackle/data/HashId.kt | 6 +- .../io/emeraldpay/dshackle/data/TxId.kt | 10 +- .../io/emeraldpay/dshackle/rpc/StreamHead.kt | 2 +- .../dshackle/startup/ConfiguredUpstreams.kt | 51 +- .../dshackle/upstream/AbstractHead.kt | 52 + .../dshackle/upstream/AggregatedUpstream.kt | 3 +- .../dshackle/upstream/CachingEthereumApi.kt | 6 +- .../dshackle/upstream/ChainUpstreams.kt | 22 +- .../dshackle/upstream/CurrentUpstreams.kt | 75 +- .../dshackle/upstream/DefaultUpstream.kt | 25 +- .../EmptyEthereumHead.kt => EmptyHead.kt} | 5 +- .../EthereumHeadMerge.kt => MergedHead.kt} | 8 +- .../dshackle/upstream/bitcoin/BitcoinApi.kt | 44 + .../upstream/bitcoin/BitcoinChainUpstreams.kt | 79 + .../upstream/bitcoin/BitcoinRpcClient.kt | 54 + .../upstream/bitcoin/BitcoinRpcHead.kt | 63 + .../upstream/bitcoin/BitcoinUpstream.kt | 91 + .../bitcoin/BitcoinUpstreamValidator.kt | 47 + .../upstream/bitcoin/DefaultBitcoinMethods.kt | 10 + .../dshackle/upstream/bitcoin/ExtractBlock.kt | 40 + .../upstream/calls/DirectCallMethods.kt | 6 +- .../upstream/ethereum/DefaultEthereumHead.kt | 47 +- .../ethereum/EthereumChainUpstreams.kt | 34 +- .../upstream/ethereum/EthereumHead.kt | 24 - .../ethereum/EthereumHeadLagObserver.kt | 3 +- .../upstream/ethereum/EthereumUpstream.kt | 40 +- .../EthereumUpstreamValidator.kt} | 11 +- .../upstream/grpc/EthereumGrpcUpstream.kt | 21 +- .../dshackle/cache/BlocksMemCacheSpec.groovy | 6 +- .../dshackle/cache/HeightCacheSpec.groovy | 14 +- .../dshackle/cache/TxMemCacheSpec.groovy | 14 +- .../dshackle/data/BlockIdSpec.groovy | 30 + .../emeraldpay/dshackle/data/TxIdSpec.groovy | 29 + .../dshackle/test/EthereumHeadMock.groovy | 4 +- .../dshackle/test/EthereumUpstreamMock.groovy | 6 +- ...ockServer.groovy => MockGrpcServer.groovy} | 6 +- .../upstream/CachingEthereumApiSpec.groovy | 17 +- .../upstream/bitcoin/BitcoinApiSpec.groovy | 108 ++ .../bitcoin/BitcoinRpcClientSpec.groovy | 86 + .../bitcoin/BitcoinRpcHeadSpec.groovy | 95 ++ .../upstream/bitcoin/ExtractBlockSpec.groovy | 24 + .../EthereumHeadLagObserverSpec.groovy | 11 +- .../grpc/EthereumGrpcUpstreamSpec.groovy | 4 +- src/test/resources/bitcoin/block-626472.json | 1509 +++++++++++++++++ 48 files changed, 2640 insertions(+), 248 deletions(-) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt rename src/main/kotlin/io/emeraldpay/dshackle/upstream/{ethereum/EmptyEthereumHead.kt => EmptyHead.kt} (87%) rename src/main/kotlin/io/emeraldpay/dshackle/upstream/{ethereum/EthereumHeadMerge.kt => MergedHead.kt} (87%) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt delete mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHead.kt rename src/main/kotlin/io/emeraldpay/dshackle/upstream/{UpstreamValidator.kt => ethereum/EthereumUpstreamValidator.kt} (89%) create mode 100644 src/test/groovy/io/emeraldpay/dshackle/data/BlockIdSpec.groovy create mode 100644 src/test/groovy/io/emeraldpay/dshackle/data/TxIdSpec.groovy rename src/test/groovy/io/emeraldpay/dshackle/test/{MockServer.groovy => MockGrpcServer.groovy} (90%) create mode 100644 src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy create mode 100644 src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClientSpec.groovy create mode 100644 src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy create mode 100644 src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlockSpec.groovy create mode 100644 src/test/resources/bitcoin/block-626472.json diff --git a/build.gradle b/build.gradle index 19582c0d..a216236a 100644 --- a/build.gradle +++ b/build.gradle @@ -84,6 +84,7 @@ dependencies { compile "io.infinitape:etherjar-rpc-ws:$etherjarVersion" compile "io.infinitape:etherjar-rpc-emerald:$etherjarVersion" compile "io.infinitape:etherjar-tx:$etherjarVersion" + compile 'org.bitcoinj:bitcoinj-core:0.15.8' compile 'org.apache.httpcomponents:httpmime:4.5.8' compile 'org.apache.httpcomponents:httpclient:4.5.8' @@ -108,9 +109,11 @@ dependencies { testCompile "org.codehaus.groovy:groovy:$groovyVersion" testCompile 'cglib:cglib-nodep:3.2.12' testCompile "org.spockframework:spock-core:$spockVersion" + testCompile 'de.jodamob.kotlin:kotlin-runner-spock:0.3.1' testCompile "io.grpc:grpc-testing:${grpcVersion}" testCompile "io.projectreactor:reactor-test:$reactorVersion" testCompile 'org.objenesis:objenesis:3.0.1' + testCompile 'org.mock-server:mockserver-netty:5.10' } compileKotlin { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt index 40db6ea4..f9957d8b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt @@ -96,6 +96,10 @@ class UpstreamsConfig { var ws: WsEndpoint? = null } + class BitcoinConnection : UpstreamConnection() { + var rpc: HttpEndpoint? = null + } + class HttpEndpoint(val url: URI) { var basicAuth: AuthConfig.ClientBasicAuth? = null var tls: AuthConfig.ClientTlsAuth? = null @@ -124,6 +128,7 @@ class UpstreamsConfig { enum class UpstreamType private constructor(vararg code: String) { ETHEREUM_JSON_RPC("ethereum"), + BITCOIN_JSON_RPC("bitcoin"), DSHACKLE("dshackle", "grpc"), UNKNOWN("unknown"); diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt index a8d8a2a7..08f9df5b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt @@ -87,7 +87,7 @@ class UpstreamsConfigReader( val connConfigNode = getMapping(connNode, "ethereum")!! val upstream = UpstreamsConfig.Upstream() readUpstreamCommon(upNode, upstream) - readUpstreamEthereum(upNode, upstream) + readUpstreamStandard(upNode, upstream) if (isValid(upstream)) { config.upstreams.add(upstream) val connection = UpstreamsConfig.EthereumConnection() @@ -113,6 +113,26 @@ class UpstreamsConfigReader( } else { log.error("Upstream at #0 has invalid configuration") } + } else if (hasAny(connNode, "bitcoin")) { + val connConfigNode = getMapping(connNode, "bitcoin")!! + val upstream = UpstreamsConfig.Upstream() + readUpstreamCommon(upNode, upstream) + readUpstreamStandard(upNode, upstream) + if (isValid(upstream)) { + config.upstreams.add(upstream) + val connection = UpstreamsConfig.BitcoinConnection() + upstream.connection = connection + getMapping(connConfigNode, "rpc")?.let { node -> + getValueAsString(node, "url")?.let { url -> + val http = UpstreamsConfig.HttpEndpoint(URI(url)) + connection.rpc = http + http.basicAuth = authConfigReader.readClientBasicAuth(node) + http.tls = authConfigReader.readClientTls(node) + } + } + } else { + log.error("Upstream at #0 has invalid configuration") + } } else if (hasAny(connNode, "grpc")) { val connConfigNode = getMapping(connNode, "grpc")!! val upstream = UpstreamsConfig.Upstream() @@ -162,13 +182,13 @@ class UpstreamsConfigReader( } } - internal fun readUpstreamEthereum(upNode: MappingNode, upstream: UpstreamsConfig.Upstream) { + internal fun readUpstreamStandard(upNode: MappingNode, upstream: UpstreamsConfig.Upstream<*>) { upstream.chain = getValueAsString(upNode, "chain") if (hasAny(upNode, "labels")) { getMapping(upNode, "labels")?.let { labels -> labels.value.stream() .filter { n -> n.keyNode is ScalarNode && n.valueNode is ScalarNode } - .map { n -> Tuples.of((n.keyNode as ScalarNode).value, (n.valueNode as ScalarNode).value)} + .map { n -> Tuples.of((n.keyNode as ScalarNode).value, (n.valueNode as ScalarNode).value) } .map { kv -> Tuples.of(kv.t1.trim(), kv.t2.trim()) } .filter { kv -> StringUtils.isNotEmpty(kv.t1) && StringUtils.isNotEmpty(kv.t2) } .forEach { kv -> diff --git a/src/main/kotlin/io/emeraldpay/dshackle/data/BlockId.kt b/src/main/kotlin/io/emeraldpay/dshackle/data/BlockId.kt index 671730b7..3a5ae533 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/data/BlockId.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/data/BlockId.kt @@ -15,7 +15,9 @@ */ package io.emeraldpay.dshackle.data +import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.rpc.json.BlockJson +import org.bouncycastle.util.encoders.Hex class BlockId( value: ByteArray @@ -23,7 +25,7 @@ class BlockId( companion object { @JvmStatic - fun from(hash: io.infinitape.etherjar.domain.BlockHash): BlockId { + fun from(hash: BlockHash): BlockId { return BlockId(hash.bytes) } @@ -34,7 +36,13 @@ class BlockId( @JvmStatic fun from(id: String): BlockId { - return from(io.infinitape.etherjar.domain.BlockHash.from(id)) + val clean = if (id.startsWith("0x")) { + id.substring(2) + } else { + id + } + val bytes = Hex.decode(clean) + return BlockId(bytes) } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/data/HashId.kt b/src/main/kotlin/io/emeraldpay/dshackle/data/HashId.kt index 206b45dc..ab4d2eb9 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/data/HashId.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/data/HashId.kt @@ -28,11 +28,9 @@ open class HashId( } fun toHex(): String { - val hex = CharArray(value.size * 2 + 2) - hex[0] = '0' - hex[1] = 'x' + val hex = CharArray(value.size * 2) var i = 0 - var j = 2 + var j = 0 while (i < value.size) { hex[j++] = HEX_DIGITS[0xF0 and value[i].toInt() ushr 4] hex[j++] = HEX_DIGITS[0x0F and value[i].toInt()] diff --git a/src/main/kotlin/io/emeraldpay/dshackle/data/TxId.kt b/src/main/kotlin/io/emeraldpay/dshackle/data/TxId.kt index 1dd1b976..e4f3d061 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/data/TxId.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/data/TxId.kt @@ -17,6 +17,8 @@ package io.emeraldpay.dshackle.data import io.infinitape.etherjar.domain.TransactionId import io.infinitape.etherjar.rpc.json.TransactionJson +import org.bouncycastle.util.encoders.Hex +import java.math.BigInteger class TxId( value: ByteArray @@ -35,7 +37,13 @@ class TxId( @JvmStatic fun from(id: String): TxId { - return from(TransactionId.from(id)) + val clean = if (id.startsWith("0x")) { + id.substring(2) + } else { + id + } + val bytes = Hex.decode(clean) + return TxId(bytes) } } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt index a10c07ea..cf533b86 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt @@ -65,7 +65,7 @@ class StreamHead( .setHeight(block.height) .setTimestamp(block.timestamp!!.toEpochMilli()) .setWeight(ByteString.copyFrom(block.difficulty.toByteArray())) - .setBlockId(block.hash.toHex().substring(2)) + .setBlockId(block.hash.toHex()) .build() } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt index de3ade53..5d1ac9fb 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt @@ -20,6 +20,9 @@ import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.upstream.CurrentUpstreams +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcClient +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream @@ -32,6 +35,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Repository import java.net.URI import java.util.* +import java.util.concurrent.atomic.AtomicInteger import javax.annotation.PostConstruct import kotlin.collections.HashMap @@ -44,6 +48,7 @@ open class ConfiguredUpstreams( ) { private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java) + private var seq = AtomicInteger(0) private val chainNames = mapOf( "ethereum" to Chain.ETHEREUM, @@ -51,7 +56,10 @@ open class ConfiguredUpstreams( "eth" to Chain.ETHEREUM, "etc" to Chain.ETHEREUM_CLASSIC, "morden" to Chain.TESTNET_MORDEN, - "kovan" to Chain.TESTNET_KOVAN + "kovan" to Chain.TESTNET_KOVAN, + "kovan-testnet" to Chain.TESTNET_KOVAN, + "bitcoin" to Chain.BITCOIN, + "bitcoin-testnet" to Chain.TESTNET_BITCOIN ) @PostConstruct @@ -69,13 +77,20 @@ open class ConfiguredUpstreams( log.error("Chain is unknown: ${up.chain}") return@forEach } - if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) { - log.error("Chain is unsupported: ${up.chain}") - return@forEach - } val options = (up.options ?: UpstreamsConfig.Options()) .merge(defaultOptions[chain] ?: UpstreamsConfig.Options.getDefaults()) - buildEthereumUpstream(up.cast(UpstreamsConfig.EthereumConnection::class.java), chain, options) + when (BlockchainType.fromBlockchain(chain)) { + BlockchainType.ETHEREUM -> { + buildEthereumUpstream(up.cast(UpstreamsConfig.EthereumConnection::class.java), chain, options) + } + BlockchainType.BITCOIN -> { + buildBitcoinUpstream(up.cast(UpstreamsConfig.BitcoinConnection::class.java), chain, options) + } + else -> { + log.error("Chain is unsupported: ${up.chain}") + return@forEach + } + } } } } @@ -101,10 +116,30 @@ open class ConfiguredUpstreams( return defaultOptions } + private fun buildBitcoinUpstream(config: UpstreamsConfig.Upstream, + chain: Chain, + options: UpstreamsConfig.Options) { + + val conn = config.connection!! + var rpcApi: BitcoinApi? = null + + conn.rpc?.let { endpoint -> + val rpcClient = BitcoinRpcClient(endpoint.url.toString(), endpoint.basicAuth!!) + rpcApi = BitcoinApi(rpcClient, objectMapper) + } + rpcApi?.let { api -> + val upstream = BitcoinUpstream(config.id + ?: "bitcoin-${seq.getAndIncrement()}", chain, api, options, objectMapper) + + upstream.start() + currentUpstreams.update(UpstreamChange(chain, upstream, UpstreamChange.ChangeType.ADDED)) + } + + } + private fun buildEthereumUpstream(config: UpstreamsConfig.Upstream, chain: Chain, - options: UpstreamsConfig.Options - ) { + options: UpstreamsConfig.Options) { val conn = config.connection!! var rpcApi: DirectEthereumApi? = null val urls = ArrayList() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt new file mode 100644 index 00000000..b2eb7ec0 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt @@ -0,0 +1,52 @@ +package io.emeraldpay.dshackle.upstream + +import io.emeraldpay.dshackle.data.BlockContainer +import org.slf4j.LoggerFactory +import reactor.core.Disposable +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.publisher.TopicProcessor +import java.util.concurrent.atomic.AtomicReference + +abstract class AbstractHead : Head { + + companion object { + private val log = LoggerFactory.getLogger(AbstractHead::class.java) + } + + private val head = AtomicReference(null) + private val stream: TopicProcessor = TopicProcessor.create() + + fun follow(source: Flux): Disposable { + return source.distinctUntilChanged { + it.hash + }.filter { block -> + val curr = head.get() + curr == null || curr.difficulty < block.difficulty + } + .subscribe { block -> + val prev = head.getAndUpdate { curr -> + if (curr == null || curr.difficulty < block.difficulty) { + block + } else { + curr + } + } + if (prev == null || prev.hash != block.hash) { + log.debug("New block ${block.height} ${block.hash}") + stream.onNext(block) + } + } + } + + override fun getFlux(): Flux { + return Flux.merge( + Mono.justOrEmpty(head.get()), + Flux.from(stream) + ).onBackpressureLatest() + } + + fun getCurrent(): BlockContainer? { + return head.get() + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt index 30d803ac..a461b95d 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt @@ -20,7 +20,6 @@ import io.emeraldpay.dshackle.cache.* import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import org.springframework.context.Lifecycle import reactor.core.Disposable import reactor.core.publisher.Flux @@ -89,7 +88,7 @@ abstract class AggregatedUpstream( cacheSubscription = null } - fun onHeadUpdated(head: EthereumHead) { + fun onHeadUpdated(head: Head) { reconfigLock.withLock { cacheSubscription?.dispose() cacheSubscription = head.getFlux().subscribe { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CachingEthereumApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CachingEthereumApi.kt index d60daf40..659088de 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CachingEthereumApi.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CachingEthereumApi.kt @@ -18,9 +18,7 @@ package io.emeraldpay.dshackle.upstream import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.data.* -import io.emeraldpay.dshackle.upstream.ethereum.EmptyEthereumHead import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import io.infinitape.etherjar.hex.HexQuantity import org.slf4j.LoggerFactory import reactor.core.publisher.Mono @@ -30,7 +28,7 @@ import java.util.function.Function open class CachingEthereumApi( private val objectMapper: ObjectMapper, private val caches: Caches, - private val head: EthereumHead + private val head: Head ): EthereumApi(objectMapper) { companion object { @@ -41,7 +39,7 @@ open class CachingEthereumApi( */ @JvmStatic fun empty(objectMapper: ObjectMapper): CachingEthereumApi { - return CachingEthereumApi(objectMapper, Caches.default(objectMapper), EmptyEthereumHead()) + return CachingEthereumApi(objectMapper, Caches.default(objectMapper), EmptyHead()) } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt index aedfdfc2..b2c5ee1e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt @@ -17,11 +17,14 @@ package io.emeraldpay.dshackle.upstream import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.cache.Caches +import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle import reactor.core.Disposable import reactor.core.publisher.Mono +import java.lang.IllegalStateException +import java.time.Duration /** * General interface to upstream(s) to a single chain @@ -111,5 +114,22 @@ abstract class ChainUpstreams( return 0 } - abstract fun printStatus() + fun printStatus() { + var height: Long? = null + try { + height = getHead().getFlux().next().block(Duration.ofSeconds(1))?.height + } catch (e: IllegalStateException) { + //timout + } catch (e: Exception) { + log.warn("Head processing error: ${e.javaClass} ${e.message}") + } + val statuses = upstreams.map { it.getStatus() } + .groupBy { it } + .map { "${it.key.name}/${it.value.size}" } + .joinToString(",") + val lag = upstreams.map { it.getLag() } + .joinToString(", ") + + log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]") + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt index 0ad5cc24..666a1e73 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt @@ -16,17 +16,19 @@ package io.emeraldpay.dshackle.upstream import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.cache.CachesEnabled import io.emeraldpay.dshackle.cache.CachesFactory import io.emeraldpay.dshackle.startup.UpstreamChange +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinChainUpstreams +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.grpc.Chain -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.scheduling.annotation.Scheduled @@ -34,6 +36,7 @@ import org.springframework.stereotype.Repository import reactor.core.publisher.Flux import reactor.core.publisher.TopicProcessor import java.util.* +import java.util.concurrent.Callable import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -54,36 +57,60 @@ class CurrentUpstreams( fun update(change: UpstreamChange) { updateLock.withLock { val chain = change.chain - val up = change.upstream - .cast(EthereumUpstream::class.java, EthereumApi::class.java) as Upstream - val current = chainMapping[chain] as ChainUpstreams? - if (change.type == UpstreamChange.ChangeType.REMOVED) { - current?.removeUpstream(up.getId()) - log.info("Upstream ${change.upstream.getId()} with chain $chain has been removed") - } else { - if (current == null) { - val created = EthereumChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) - if (up is CachesEnabled) { - up.setCaches(created.caches) + when (BlockchainType.fromBlockchain(chain)) { + BlockchainType.ETHEREUM -> { + val up = change.upstream + .cast(EthereumUpstream::class.java, EthereumApi::class.java) as Upstream + val current = chainMapping[chain] as ChainUpstreams? + val factory = Callable { + EthereumChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams } - created.addUpstream(up) - created.start() - chainMapping[chain] = created - chainsBus.onNext(chain) - } else { - if (up is CachesEnabled) { - up.setCaches(current.caches) + processUpdate(change, up, current, factory) + } + BlockchainType.BITCOIN -> { + val up = change.upstream + .cast(BitcoinUpstream::class.java, BitcoinApi::class.java) + val current = chainMapping[chain] as ChainUpstreams? + val factory = Callable { + BitcoinChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams } - current.addUpstream(up) + processUpdate(change, up, current, factory) } - if (!callTargets.containsKey(chain)) { - setupDefaultMethods(chain) + else -> { + log.error("Update for unsupported chain: $chain") } - log.info("Upstream ${change.upstream.getId()} with chain $chain has been added") } } } + fun processUpdate(change: UpstreamChange, up: Upstream, current: ChainUpstreams?, factory: Callable>) { + val chain = change.chain + if (change.type == UpstreamChange.ChangeType.REMOVED) { + current?.removeUpstream(up.getId()) + log.info("Upstream ${change.upstream.getId()} with chain $chain has been removed") + } else { + if (current == null) { + val created = factory.call() + if (up is CachesEnabled) { + up.setCaches(created.caches) + } + created.addUpstream(up) + created.start() + chainMapping[chain] = created + chainsBus.onNext(chain) + } else { + if (up is CachesEnabled) { + up.setCaches(current.caches) + } + current.addUpstream(up) + } + if (!callTargets.containsKey(chain)) { + setupDefaultMethods(chain) + } + log.info("Upstream ${change.upstream.getId()} with chain $chain has been added") + } + } + override fun getUpstream(chain: Chain): AggregatedUpstream<*>? { return chainMapping[chain] } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt index 95a196cc..a8be0591 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt @@ -15,20 +15,29 @@ */ package io.emeraldpay.dshackle.upstream +import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.upstream.calls.CallMethods import reactor.core.publisher.Flux import reactor.core.publisher.TopicProcessor import java.util.concurrent.atomic.AtomicReference abstract class DefaultUpstream( + private val id: String, defaultLag: Long, - defaultAvail: UpstreamAvailability + defaultAvail: UpstreamAvailability, + private val options: UpstreamsConfig.Options, + private val targets: CallMethods? ) : Upstream { - constructor() : this(Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE) + constructor(id: String, options: UpstreamsConfig.Options, targets: CallMethods?) : this(id, Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE, options, targets) private val status = AtomicReference(Status(defaultLag, defaultAvail, statusByLag(defaultLag, defaultAvail))) private val statusStream: TopicProcessor = TopicProcessor.create() + override fun isAvailable(): Boolean { + return getStatus() == UpstreamAvailability.OK + } + override fun getStatus(): UpstreamAvailability { return status.get().status } @@ -67,5 +76,17 @@ abstract class DefaultUpstream( return this.status.get().lag } + override fun getId(): String { + return id + } + + override fun getOptions(): UpstreamsConfig.Options { + return options + } + + override fun getMethods(): CallMethods { + return targets ?: throw IllegalStateException("Methods are not set") + } + class Status(val lag: Long, val avail: UpstreamAvailability, val status: UpstreamAvailability) } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EmptyEthereumHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt similarity index 87% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EmptyEthereumHead.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt index d715abd4..65f18dec 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EmptyEthereumHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.emeraldpay.dshackle.upstream.ethereum +package io.emeraldpay.dshackle.upstream import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.upstream.Head import reactor.core.publisher.Flux -class EmptyEthereumHead : EthereumHead { +class EmptyHead : Head { override fun getFlux(): Flux { return Flux.empty() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadMerge.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/MergedHead.kt similarity index 87% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadMerge.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/MergedHead.kt index d2484732..6cef3cdf 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadMerge.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/MergedHead.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.emeraldpay.dshackle.upstream.ethereum +package io.emeraldpay.dshackle.upstream import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.CachesEnabled @@ -21,9 +21,9 @@ import org.springframework.context.Lifecycle import reactor.core.Disposable import reactor.core.publisher.Flux -class EthereumHeadMerge( - private val sources: Iterable -): DefaultEthereumHead(), Lifecycle, CachesEnabled { +class MergedHead( + private val sources: Iterable +) : AbstractHead(), Lifecycle, CachesEnabled { private var subscription: Disposable? = null diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt new file mode 100644 index 00000000..beb8dcab --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt @@ -0,0 +1,44 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.upstream.UpstreamApi +import io.infinitape.etherjar.rpc.RpcException +import io.infinitape.etherjar.rpc.json.FullResponseJson +import io.infinitape.etherjar.rpc.json.RequestJson +import io.infinitape.etherjar.rpc.json.ResponseJson +import org.slf4j.LoggerFactory +import reactor.core.publisher.Mono + +open class BitcoinApi( + val bitcoinRpcClient: BitcoinRpcClient, + val objectMapper: ObjectMapper +) : UpstreamApi { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinApi::class.java) + } + + open override fun execute(id: Int, method: String, params: List): Mono { + //TODO optimize extraction + return executeAndResult(id, method, params, Object::class.java).map { + objectMapper.writeValueAsBytes(it) + } + } + + open fun executeAndResult(id: Int, method: String, params: List, resultType: Class): Mono { + val rpc = RequestJson(method, params, id) + return Mono.just(rpc) + .map(objectMapper::writeValueAsBytes) + .flatMap(bitcoinRpcClient::execute) + .flatMap { json -> + val type: JavaType = objectMapper.typeFactory.constructParametricType(FullResponseJson::class.java, resultType, Int::class.java) + val resp = objectMapper.readerFor(type).readValue>(json) + if (resp.hasError()) { + Mono.error(resp.error.asException()) + } else { + Mono.just(resp.result) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt new file mode 100644 index 00000000..d34649b6 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt @@ -0,0 +1,79 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.cache.Caches +import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.upstream.* +import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi +import io.emeraldpay.dshackle.upstream.ethereum.EthereumHeadLagObserver +import io.emeraldpay.grpc.Chain +import org.slf4j.LoggerFactory +import org.springframework.context.Lifecycle + +class BitcoinChainUpstreams( + chain: Chain, + val upstreams: MutableList, + caches: Caches, + objectMapper: ObjectMapper +) : ChainUpstreams(chain, upstreams as MutableList>, caches, objectMapper) { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinChainUpstreams::class.java) + } + + private var head: Head? = null + + override fun init() { + if (upstreams.size > 0) { + head = updateHead() + } + super.init() + } + + override fun updateHead(): Head { + head?.let { + if (it is Lifecycle) { + it.stop() + } + } + lagObserver?.stop() + lagObserver = null + val head = if (upstreams.size == 1) { + val upstream = upstreams.first() + upstream.setLag(0) + upstream.getHead() + } else { + val newHead = MergedHead(upstreams.map { it.getHead() }).apply { + this.start() + } +// val lagObserver = TODO +// this.lagObserver = lagObserver + newHead + } + onHeadUpdated(head) + return head + } + + override fun setHead(head: Head) { + this.head = head + } + + override fun getHead(): Head { + return head!! + } + + override fun getLabels(): Collection { + return upstreams.flatMap { it.getLabels() } + } + + override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { + if (!selfType.isAssignableFrom(this.javaClass)) { + throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") + } + if (!upstreamType.isAssignableFrom(BitcoinApi::class.java)) { + throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $upstreamType") + } + return this as T + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt new file mode 100644 index 00000000..fa58efb3 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt @@ -0,0 +1,54 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.config.AuthConfig +import io.netty.buffer.Unpooled +import io.netty.handler.codec.http.HttpHeaderNames +import io.netty.handler.codec.http.HttpHeaders +import org.slf4j.LoggerFactory +import reactor.core.publisher.Mono +import reactor.netty.http.client.HttpClient +import java.util.* +import java.util.function.Consumer + +class BitcoinRpcClient( + private val target: String, + basicAuth: AuthConfig.ClientBasicAuth? +) { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinRpcClient::class.java) + } + + private val httpClient: HttpClient + + init { + var build = HttpClient.create() + + build = build.headers { h -> + h.add(HttpHeaderNames.CONTENT_TYPE, "application/json") + } + + basicAuth?.let { basicAuth -> + val authString: String = basicAuth.username + ":" + basicAuth.password + val authBase64 = Base64.getEncoder().encodeToString(authString.toByteArray()) + val auth = "Basic $authBase64" + val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, auth) } + build = build.headers(headers) + } + + this.httpClient = build + } + + fun execute(request: ByteArray): Mono { + val response = httpClient + .post() + .uri(target) + .send(Mono.just(request).map { Unpooled.wrappedBuffer(it) }) + + return response.responseContent() + .aggregate() + .asByteArray() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt new file mode 100644 index 00000000..837adc94 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt @@ -0,0 +1,63 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.Defaults +import io.emeraldpay.dshackle.upstream.AbstractHead +import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.ethereum.EthereumRpcHead +import org.slf4j.LoggerFactory +import org.springframework.context.Lifecycle +import org.springframework.scheduling.concurrent.CustomizableThreadFactory +import reactor.core.Disposable +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers +import java.time.Duration +import java.util.concurrent.Executors + +class BitcoinRpcHead( + private val api: BitcoinApi, + private val extractBlock: ExtractBlock, + private val interval: Duration = Duration.ofSeconds(15) +) : Head, AbstractHead(), Lifecycle { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinRpcHead::class.java) + val scheduler = Schedulers.fromExecutor(Executors.newCachedThreadPool(CustomizableThreadFactory("bitcoin-rpc-head"))) + } + + private var refreshSubscription: Disposable? = null + + override fun isRunning(): Boolean { + return refreshSubscription != null + } + + override fun start() { + if (refreshSubscription != null) { + log.warn("Called to start when running") + return + } + val base = Flux.interval(interval) + .publishOn(scheduler) + .flatMap { + api.executeAndResult(0, "getbestblockhash", emptyList(), String::class.java) + .timeout(Defaults.timeout, Mono.error(Exception("Best block hash is not received"))) + } + .distinctUntilChanged() + .flatMap { hash -> + api.execute(0, "getblock", listOf(hash)) + .map(extractBlock::extract) + .timeout(Defaults.timeout, Mono.error(Exception("Block data is not received"))) + } + .onErrorContinue { err, _ -> + log.debug("RPC error ${err.message}") + } + refreshSubscription = super.follow(base) + } + + override fun stop() { + val copy = refreshSubscription + refreshSubscription = null + copy?.dispose() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt new file mode 100644 index 00000000..b5b0bbfa --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt @@ -0,0 +1,91 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.upstream.* +import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator +import io.emeraldpay.grpc.Chain +import org.slf4j.LoggerFactory +import org.springframework.context.Lifecycle +import reactor.core.Disposable +import reactor.core.publisher.Mono + +class BitcoinUpstream( + id: String, + val chain: Chain, + private val api: BitcoinApi, + options: UpstreamsConfig.Options, + private val objectMapper: ObjectMapper +) : DefaultUpstream(id, options, DefaultBitcoinMethods()), Lifecycle { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java) + } + + private val head: Head = createHead() + private var validatorSubscription: Disposable? = null + + private fun createHead(): Head { + return BitcoinRpcHead( + api, + ExtractBlock(objectMapper) + ) + } + + override fun getHead(): Head { + return head + } + + override fun getApi(matcher: Selector.Matcher): Mono { + return Mono.just(api) + } + + override fun getLabels(): Collection { + return listOf(UpstreamsConfig.Labels()) + } + + override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { + if (!selfType.isAssignableFrom(this.javaClass)) { + throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") + } + if (!upstreamType.isAssignableFrom(BitcoinApi::class.java)) { + throw ClassCastException("Cannot cast ${BitcoinApi::class.java} to $upstreamType") + } + return this as T + } + + override fun isRunning(): Boolean { + var runningAny = validatorSubscription != null + if (head is Lifecycle) { + runningAny = runningAny || head.isRunning + } + return runningAny + } + + override fun start() { + log.info("Configured for ${chain.chainName}") + if (head is Lifecycle) { + if (!head.isRunning) { + head.start() + } + } + validatorSubscription?.dispose() + + if (getOptions().disableValidation != null && getOptions().disableValidation!!) { + this.setLag(0) + this.setStatus(UpstreamAvailability.OK) + } else { + val validator = BitcoinUpstreamValidator(api, getOptions()) + validatorSubscription = validator.start() + .subscribe(this::setStatus) + } + } + + override fun stop() { + if (head is Lifecycle) { + head.stop() + } + validatorSubscription?.dispose() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt new file mode 100644 index 00000000..be7f3ef0 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt @@ -0,0 +1,47 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.Defaults +import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.upstream.Selector +import io.emeraldpay.dshackle.upstream.UpstreamAvailability +import io.infinitape.etherjar.rpc.Commands +import org.slf4j.LoggerFactory +import org.springframework.scheduling.concurrent.CustomizableThreadFactory +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers +import java.time.Duration +import java.util.concurrent.Executors + +class BitcoinUpstreamValidator( + private val api: BitcoinApi, + private val options: UpstreamsConfig.Options +) { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinUpstreamValidator::class.java) + val scheduler = Schedulers.fromExecutor(Executors.newCachedThreadPool(CustomizableThreadFactory("bitcoin-validator"))) + } + + fun validate(): Mono { + return api.executeAndResult(0, "getconnectioncount", emptyList(), Int::class.java) + .map { count -> + val minPeers = options.minPeers ?: 1 + if (count < minPeers) { + UpstreamAvailability.IMMATURE + } else { + UpstreamAvailability.OK + } + } + .onErrorReturn(UpstreamAvailability.UNAVAILABLE) + } + + fun start(): Flux { + return Flux.interval(Duration.ofSeconds(15)) + .subscribeOn(scheduler) + .flatMap { + validate() + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt new file mode 100644 index 00000000..370a464f --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt @@ -0,0 +1,10 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods + +class DefaultBitcoinMethods : DirectCallMethods( + listOf("getbestblockhash", "getblock", "gettransaction") +) { + + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt new file mode 100644 index 00000000..25e94a01 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt @@ -0,0 +1,40 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.data.BlockId +import io.emeraldpay.dshackle.data.TxId +import org.apache.commons.codec.binary.Hex +import org.slf4j.LoggerFactory +import java.math.BigInteger +import java.time.Instant + +class ExtractBlock( + private val objectMapper: ObjectMapper +) { + + companion object { + private val log = LoggerFactory.getLogger(ExtractBlock::class.java) + } + + fun extract(json: ByteArray): BlockContainer { + val data = objectMapper.readValue(json, Map::class.java) as Map + + val height = data["height"] as Number? ?: throw IllegalArgumentException("Block JSON has no height") + val time = data["time"] as Number? ?: throw IllegalArgumentException("Block JSON has no time") + val hash = data["hash"] as String? ?: throw IllegalArgumentException("Block JSON has no hash") + val chainwork = data["chainwork"] as String? ?: throw IllegalArgumentException("Block JSON has no chainwork") + val transactions = (data["tx"] as List?)?.map(TxId.Companion::from) ?: emptyList() + + return BlockContainer( + height.toLong(), + BlockId.from(hash), + BigInteger(1, Hex.decodeHex(chainwork)), + Instant.ofEpochMilli(time.toLong() * 1000), + false, + json, + transactions + ) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DirectCallMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DirectCallMethods.kt index eea7d106..53d8b622 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DirectCallMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DirectCallMethods.kt @@ -22,10 +22,10 @@ import io.emeraldpay.dshackle.quorum.CallQuorum * Configuration that uses [AlwaysQuorum] for all available methods. The methods list itself * is provided in constructor (or empty otherwise) */ -class DirectCallMethods(private val methods: Set) : CallMethods { +open class DirectCallMethods(private val methods: Set) : CallMethods { - constructor(): this(emptySet()) - constructor(methods: Collection): this(methods.toSet()) + constructor() : this(emptySet()) + constructor(methods: Collection) : this(methods.toSet()) override fun getQuorumFor(method: String): CallQuorum { return AlwaysQuorum() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DefaultEthereumHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DefaultEthereumHead.kt index 487119d3..ab23c3ad 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DefaultEthereumHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DefaultEthereumHead.kt @@ -1,49 +1,8 @@ package io.emeraldpay.dshackle.upstream.ethereum -import io.emeraldpay.dshackle.data.BlockContainer -import org.slf4j.LoggerFactory -import reactor.core.Disposable -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import reactor.core.publisher.TopicProcessor -import java.util.concurrent.atomic.AtomicReference +import io.emeraldpay.dshackle.upstream.AbstractHead +import io.emeraldpay.dshackle.upstream.Head -open class DefaultEthereumHead: EthereumHead { +open class DefaultEthereumHead : Head, AbstractHead() { - private val log = LoggerFactory.getLogger(DefaultEthereumHead::class.java) - private val head = AtomicReference(null) - private val stream: TopicProcessor = TopicProcessor.create() - - fun follow(source: Flux): Disposable { - return source.distinctUntilChanged { - it.hash - }.filter { block -> - val curr = head.get() - curr == null || curr.difficulty < block.difficulty - } - .subscribe { block -> - val prev = head.getAndUpdate { curr -> - if (curr == null || curr.difficulty < block.difficulty) { - block - } else { - curr - } - } - if (prev == null || prev.hash != block.hash) { - log.debug("New block ${block.height} ${block.hash}") - stream.onNext(block) - } - } - } - - override fun getFlux(): Flux { - return Flux.merge( - Mono.justOrEmpty(head.get()), - Flux.from(stream) - ).onBackpressureLatest() - } - - fun getCurrent(): BlockContainer? { - return head.get() - } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainUpstreams.kt index 239b790e..ae389a0e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainUpstreams.kt @@ -20,13 +20,8 @@ import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.grpc.Chain -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle -import java.lang.IllegalStateException -import java.time.Duration class EthereumChainUpstreams( chain: Chain, @@ -39,7 +34,7 @@ class EthereumChainUpstreams( private val log = LoggerFactory.getLogger(EthereumChainUpstreams::class.java) } - private var head: EthereumHead? = null + private var head: Head? = null init { this.init() @@ -52,15 +47,15 @@ class EthereumChainUpstreams( super.init() } - override fun getHead(): EthereumHead { + override fun getHead(): Head { return head!! } override fun setHead(head: Head) { - this.head = head as EthereumHead + this.head = head } - override fun updateHead(): EthereumHead { + override fun updateHead(): Head { head?.let { if (it is Lifecycle) { it.stop() @@ -73,7 +68,7 @@ class EthereumChainUpstreams( upstream.setLag(0) upstream.getHead() } else { - val newHead = EthereumHeadMerge(upstreams.map { it.getHead() }).apply { + val newHead = MergedHead(upstreams.map { it.getHead() }).apply { this.start() } val lagObserver = EthereumHeadLagObserver(newHead, upstreams as Collection>).apply { @@ -90,25 +85,6 @@ class EthereumChainUpstreams( return upstreams.flatMap { it.getLabels() } } - override fun printStatus() { - var height: Long? = null - try { - height = getHead().getFlux().next().block(Duration.ofSeconds(1))?.height - } catch (e: IllegalStateException) { - //timout - } catch (e: Exception) { - log.warn("Head processing error: ${e.javaClass} ${e.message}") - } - val statuses = upstreams.map { it.getStatus() } - .groupBy { it } - .map { "${it.key.name}/${it.value.size}" } - .joinToString(",") - val lag = upstreams.map { it.getLag() } - .joinToString(", ") - - log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]") - } - @SuppressWarnings("unchecked") override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHead.kt deleted file mode 100644 index 0f5719c7..00000000 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHead.kt +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2019 ETCDEV GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.emeraldpay.dshackle.upstream.ethereum - -import io.emeraldpay.dshackle.upstream.Head -import io.infinitape.etherjar.domain.TransactionId -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson - -interface EthereumHead : Head { -} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserver.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserver.kt index 47c88390..17c6327e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserver.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserver.kt @@ -18,12 +18,13 @@ package io.emeraldpay.dshackle.upstream.ethereum import io.emeraldpay.dshackle.upstream.HeadLagObserver import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.upstream.Head import org.slf4j.LoggerFactory import reactor.core.publisher.Flux import java.time.Duration class EthereumHeadLagObserver( - master: EthereumHead, + master: Head, followers: Collection> ) : HeadLagObserver(master, followers) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt index b405a039..1b4abd00 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt @@ -24,8 +24,6 @@ import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods import io.emeraldpay.grpc.Chain -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle import reactor.core.Disposable @@ -33,15 +31,15 @@ import reactor.core.publisher.Mono import java.time.Duration open class EthereumUpstream( - private val id: String, + id: String, val chain: Chain, private val api: DirectEthereumApi, private val ethereumWs: EthereumWs? = null, - private val options: UpstreamsConfig.Options, + options: UpstreamsConfig.Options, val node: QuorumForLabels.QuorumItem, - private val targets: CallMethods, + targets: CallMethods, private val objectMapper: ObjectMapper -) : DefaultUpstream(), Upstream, CachesEnabled, Lifecycle { +) : DefaultUpstream(id, options, targets), Upstream, CachesEnabled, Lifecycle { constructor(id: String, chain: Chain, api: DirectEthereumApi, objectMapper: ObjectMapper) : this(id, chain, api, null, UpstreamsConfig.Options.getDefaults(), QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()), @@ -50,7 +48,7 @@ open class EthereumUpstream( private val log = LoggerFactory.getLogger(EthereumUpstream::class.java) - private val head: EthereumHead = this.createHead() + private val head: Head = this.createHead() private var validatorSubscription: Disposable? = null init { @@ -64,18 +62,14 @@ open class EthereumUpstream( } } - override fun getId(): String { - return id - } - override fun start() { log.info("Configured for ${chain.chainName}") - if (options.disableValidation != null && options.disableValidation!!) { + if (getOptions().disableValidation != null && getOptions().disableValidation!!) { this.setLag(0) this.setStatus(UpstreamAvailability.OK) } else { - val validator = UpstreamValidator(this, options) + val validator = EthereumUpstreamValidator(this, getOptions()) validatorSubscription = validator.start() .subscribe(this::setStatus) } @@ -93,7 +87,7 @@ open class EthereumUpstream( } } - open fun createHead(): EthereumHead { + open fun createHead(): Head { return if (ethereumWs != null) { val ws = EthereumWsHead(ethereumWs).apply { this.start() @@ -102,22 +96,18 @@ open class EthereumUpstream( val rpc = EthereumRpcHead(api, objectMapper, Duration.ofSeconds(30)).apply { this.start() } - EthereumHeadMerge(listOf(rpc, ws)).apply { + MergedHead(listOf(rpc, ws)).apply { this.start() } } else { - log.warn("Setting up upstream $id with RPC-only access, less effective than WS+RPC") + log.warn("Setting up upstream ${this.getId()} with RPC-only access, less effective than WS+RPC") EthereumRpcHead(api, objectMapper).apply { this.start() } } } - override fun isAvailable(): Boolean { - return getStatus() == UpstreamAvailability.OK - } - - override fun getHead(): EthereumHead { + override fun getHead(): Head { return head } @@ -125,18 +115,10 @@ open class EthereumUpstream( return Mono.just(api) } - override fun getOptions(): UpstreamsConfig.Options { - return options - } - override fun getLabels(): Collection { return listOf(node.labels) } - override fun getMethods(): CallMethods { - return targets - } - @Suppress("unchecked") override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/UpstreamValidator.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt similarity index 89% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/UpstreamValidator.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt index c6c5bf12..8adf1840 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/UpstreamValidator.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.emeraldpay.dshackle.upstream +package io.emeraldpay.dshackle.upstream.ethereum import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.config.UpstreamsConfig -import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream +import io.emeraldpay.dshackle.upstream.Selector +import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.infinitape.etherjar.rpc.* import org.slf4j.LoggerFactory import org.springframework.scheduling.concurrent.CustomizableThreadFactory @@ -27,13 +28,13 @@ import reactor.core.scheduler.Schedulers import java.time.Duration import java.util.concurrent.Executors -class UpstreamValidator( +class EthereumUpstreamValidator( private val ethereumUpstream: EthereumUpstream, private val options: UpstreamsConfig.Options ) { companion object { - private val log = LoggerFactory.getLogger(UpstreamValidator::class.java) - val scheduler = Schedulers.fromExecutor(Executors.newCachedThreadPool(CustomizableThreadFactory("validator"))) + private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java) + val scheduler = Schedulers.fromExecutor(Executors.newCachedThreadPool(CustomizableThreadFactory("ethereum-validator"))) } fun validate(): Mono { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt index 99ef6ee1..1e786ac6 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt @@ -33,7 +33,6 @@ import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import io.emeraldpay.grpc.Chain import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.rpc.* @@ -59,13 +58,16 @@ open class EthereumGrpcUpstream( private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub, private val objectMapper: ObjectMapper, private val rpcClient: ReactorEmeraldClient -) : DefaultUpstream(), CachesEnabled, Lifecycle { +) : DefaultUpstream( + "$parentId/${chain.chainCode}", + UpstreamsConfig.Options.getDefaults(), + null +), CachesEnabled, Lifecycle { private var allLabels: Collection = ArrayList() private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java) private var caches: Caches? = null - private val options = UpstreamsConfig.Options.getDefaults() private val nodes = AtomicReference(QuorumForLabels()) private val head = DefaultEthereumHead() private var targets: CallMethods? = null @@ -84,10 +86,6 @@ open class EthereumGrpcUpstream( } } - override fun getId(): String { - return "$parentId/${chain.chainCode}" - } - override fun start() { if (this.isRunning) return val chainRef = Common.Chain.newBuilder() @@ -200,12 +198,12 @@ open class EthereumGrpcUpstream( } override fun isAvailable(): Boolean { - return getStatus() == UpstreamAvailability.OK && head.getCurrent() != null && nodes.get().getAll().any { + return super.isAvailable() && head.getCurrent() != null && nodes.get().getAll().any { it.quorum > 0 } } - override fun getHead(): EthereumHead { + override fun getHead(): Head { return head } @@ -213,10 +211,6 @@ open class EthereumGrpcUpstream( return Mono.just(createApi(matcher)) } - override fun getOptions(): UpstreamsConfig.Options { - return options - } - override fun setCaches(caches: Caches) { this.caches = caches } @@ -231,5 +225,4 @@ open class EthereumGrpcUpstream( } return this as T } - } \ No newline at end of file diff --git a/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksMemCacheSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksMemCacheSpec.groovy index 5bbe6db4..b676248e 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksMemCacheSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksMemCacheSpec.groovy @@ -77,9 +77,9 @@ class BlocksMemCacheSpec extends Specification { def act3 = cache.read(BlockId.from(hash3)).block() def act4 = cache.read(BlockId.from(hash4)).block() then: - act2.hash.toHex() == hash2 - act3.hash.toHex() == hash3 - act4.hash.toHex() == hash4 + act2.hash.toHex() == hash2.substring(2) + act3.hash.toHex() == hash3.substring(2) + act4.hash.toHex() == hash4.substring(2) act1 == null } diff --git a/src/test/groovy/io/emeraldpay/dshackle/cache/HeightCacheSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/cache/HeightCacheSpec.groovy index e711438e..a1cb2abc 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/cache/HeightCacheSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/cache/HeightCacheSpec.groovy @@ -38,10 +38,10 @@ class HeightCacheSpec extends Specification { def act3 = cache.read(102).block() def act4 = cache.read(103).block() then: - act1.toHex() == hash1 - act2.toHex() == hash2 - act3.toHex() == hash3 - act4.toHex() == hash4 + act1.toHex() == hash1.substring(2) + act2.toHex() == hash2.substring(2) + act3.toHex() == hash3.substring(2) + act4.toHex() == hash4.substring(2) } def "Keeps only configured amount"() { @@ -65,8 +65,8 @@ class HeightCacheSpec extends Specification { def act4 = cache.read(103).block() then: act1 == null - act2.toHex() == hash2 - act3.toHex() == hash3 - act4.toHex() == hash4 + act2.toHex() == hash2.substring(2) + act3.toHex() == hash3.substring(2) + act4.toHex() == hash4.substring(2) } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/cache/TxMemCacheSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/cache/TxMemCacheSpec.groovy index 5b310d7e..c576ae49 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/cache/TxMemCacheSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/cache/TxMemCacheSpec.groovy @@ -57,9 +57,9 @@ class TxMemCacheSpec extends Specification { def act3 = cache.read(TxId.from(hash3)).block() def act4 = cache.read(TxId.from(hash4)).block() then: - act2.hash.toHex() == hash2 - act3.hash.toHex() == hash3 - act4.hash.toHex() == hash4 + act2.hash.toHex() == hash2.substring(2) + act3.hash.toHex() == hash3.substring(2) + act4.hash.toHex() == hash4.substring(2) act1 == null } @@ -93,8 +93,8 @@ class TxMemCacheSpec extends Specification { then: act1 == null act2 == null - act3.hash.toHex() == hash3 - act4.hash.toHex() == hash4 + act3.hash.toHex() == hash3.substring(2) + act4.hash.toHex() == hash4.substring(2) } def "Evict all by block data"() { @@ -137,7 +137,7 @@ class TxMemCacheSpec extends Specification { then: act1 == null act2 == null - act3.hash.toHex() == hash3 - act4.hash.toHex() == hash4 + act3.hash.toHex() == hash3.substring(2) + act4.hash.toHex() == hash4.substring(2) } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/data/BlockIdSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/data/BlockIdSpec.groovy new file mode 100644 index 00000000..73a16e04 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/data/BlockIdSpec.groovy @@ -0,0 +1,30 @@ +package io.emeraldpay.dshackle.data + +import io.infinitape.etherjar.domain.BlockHash +import spock.lang.Specification + +class BlockIdSpec extends Specification { + + def "Create from string"() { + expect: + BlockId.from(source).toHex() == exp + where: + exp | source + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "A0E65CBC1B52A8CA60562112C6060552D882F16F34A9DBA2CCDC05C0A6A27100" + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0xA0E65CBC1B52A8CA60562112C6060552D882F16F34A9DBA2CCDC05C0A6A27100" + "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0x00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + } + + def "Create from block hash"() { + expect: + BlockId.from(BlockHash.from(source)).toHex() == exp + where: + exp | source + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0x00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + } + +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/data/TxIdSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/data/TxIdSpec.groovy new file mode 100644 index 00000000..d333eb90 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/data/TxIdSpec.groovy @@ -0,0 +1,29 @@ +package io.emeraldpay.dshackle.data + +import io.infinitape.etherjar.domain.TransactionId +import spock.lang.Specification + +class TxIdSpec extends Specification { + + def "Create from string"() { + expect: + TxId.from(source).toHex() == exp + where: + exp | source + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "A0E65CBC1B52A8CA60562112C6060552D882F16F34A9DBA2CCDC05C0A6A27100" + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0xA0E65CBC1B52A8CA60562112C6060552D882F16F34A9DBA2CCDC05C0A6A27100" + "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0x00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + } + + def "Create from block hash"() { + expect: + TxId.from(TransactionId.from(source)).toHex() == exp + where: + exp | source + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0x00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy index fa737f51..aca59f3c 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy @@ -16,12 +16,12 @@ package io.emeraldpay.dshackle.test import io.emeraldpay.dshackle.data.BlockContainer -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead +import io.emeraldpay.dshackle.upstream.Head import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.core.publisher.TopicProcessor -class EthereumHeadMock implements EthereumHead { +class EthereumHeadMock implements Head { private TopicProcessor bus = TopicProcessor.create() private BlockContainer latest diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy index 22fdd3e5..7c87bbc2 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy @@ -17,11 +17,11 @@ package io.emeraldpay.dshackle.test import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.grpc.Chain @@ -58,12 +58,12 @@ class EthereumUpstreamMock extends EthereumUpstream { } @Override - EthereumHead createHead() { + Head createHead() { return ethereumHeadMock } @Override - EthereumHead getHead() { + Head getHead() { return ethereumHeadMock } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/MockServer.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/MockGrpcServer.groovy similarity index 90% rename from src/test/groovy/io/emeraldpay/dshackle/test/MockServer.groovy rename to src/test/groovy/io/emeraldpay/dshackle/test/MockGrpcServer.groovy index 2940b2ce..35bb1bfe 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/MockServer.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/MockGrpcServer.groovy @@ -21,15 +21,15 @@ import io.grpc.inprocess.InProcessChannelBuilder import io.grpc.inprocess.InProcessServerBuilder import io.grpc.testing.GrpcCleanupRule -class MockServer { +class MockGrpcServer { GrpcCleanupRule grpcCleanup = new GrpcCleanupRule() - ReactorBlockchainGrpc.ReactorBlockchainStub clientForServer(ReactorBlockchainGrpc.BlockchainImplBase impl){ + ReactorBlockchainGrpc.ReactorBlockchainStub clientForServer(ReactorBlockchainGrpc.BlockchainImplBase impl) { String serverName = InProcessServerBuilder.generateName() grpcCleanup.register(InProcessServerBuilder .forName(serverName).directExecutor().addService(impl).build().start()); - def channel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()) + def channel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()) return ReactorBlockchainGrpc.newReactorStub(channel) } diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/CachingEthereumApiSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/CachingEthereumApiSpec.groovy index 659d8a85..34212d27 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/CachingEthereumApiSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/CachingEthereumApiSpec.groovy @@ -10,7 +10,6 @@ import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.TxId import io.emeraldpay.dshackle.test.TestingCommons -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.TransactionId import io.infinitape.etherjar.rpc.json.BlockJson @@ -30,7 +29,7 @@ class CachingEthereumApiSpec extends Specification { def "Get blockNumber from head"() { setup: - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.default(objectMapper), @@ -58,7 +57,7 @@ class CachingEthereumApiSpec extends Specification { def "Return empty if block is not cached"() { setup: - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.default(objectMapper), @@ -76,7 +75,7 @@ class CachingEthereumApiSpec extends Specification { def "Return block by hash when cached"() { setup: def cache = new BlocksMemCache(); - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(cache).build(), @@ -106,7 +105,7 @@ class CachingEthereumApiSpec extends Specification { setup: def blocksCache = new BlocksMemCache() def heightCache = new HeightCache() - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setBlockByHeight(heightCache).build(), @@ -135,7 +134,7 @@ class CachingEthereumApiSpec extends Specification { setup: def blocksCache = Mock(BlocksMemCache) def txCache = Mock(TxMemCache) - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setTxByHash(txCache).build(), @@ -161,7 +160,7 @@ class CachingEthereumApiSpec extends Specification { setup: def blocksCache = Mock(BlocksMemCache) def txCache = Mock(TxMemCache) - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setTxByHash(txCache).build(), @@ -191,7 +190,7 @@ class CachingEthereumApiSpec extends Specification { def blocksCache = Mock(BlocksMemCache) def txCache = Mock(TxMemCache) def heightCache = Mock(HeightCache) - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setTxByHash(txCache).setBlockByHeight(heightCache).build(), @@ -219,7 +218,7 @@ class CachingEthereumApiSpec extends Specification { def blocksCache = Mock(BlocksMemCache) def txCache = Mock(TxMemCache) def heightCache = Mock(HeightCache) - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setTxByHash(txCache).setBlockByHeight(heightCache).build(), diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy new file mode 100644 index 00000000..78e784bc --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy @@ -0,0 +1,108 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.test.TestingCommons +import io.infinitape.etherjar.rpc.RpcException +import org.mockserver.integration.ClientAndServer +import org.mockserver.model.HttpRequest +import org.mockserver.model.HttpResponse +import reactor.test.StepVerifier +import spock.lang.Specification + +import java.time.Duration + +class BitcoinApiSpec extends Specification { + + ClientAndServer mockServer + BitcoinApi api + + def setup() { + mockServer = ClientAndServer.startClientAndServer(18332); + api = new BitcoinApi( + new BitcoinRpcClient("localhost:18332", null), + TestingCommons.objectMapper() + ) + } + + def cleanup() { + mockServer.stop() + } + + def "Request simple"() { + setup: + def resp = '{' + + ' "result": "0000000000000000000889c2e52ca5e1cecac60bce9a3754201a7a9a67791e90",' + + ' "error": null,' + + ' "id": 15' + + '}' + mockServer.when( + HttpRequest.request() + ).respond( + HttpResponse.response(resp) + ) + when: + def act = api.executeAndResult(15, "getbestblockhash", [], String) + then: + StepVerifier.create(act) + .expectNext("0000000000000000000889c2e52ca5e1cecac60bce9a3754201a7a9a67791e90") + .expectComplete() + .verify(Duration.ofSeconds(1)) + mockServer.verify( + HttpRequest.request() + .withMethod("POST") + .withBody('{"jsonrpc":"2.0","method":"getbestblockhash","params":[],"id":15}') + ) + } + + def "Request with params"() { + setup: + def resp = '{' + + ' "result": "something",' + + ' "id": 1' + + '}' + mockServer.when( + HttpRequest.request() + ).respond( + HttpResponse.response(resp) + ) + when: + def act = api.executeAndResult(1, "getsomething", ["something", false], String) + then: + StepVerifier.create(act) + .expectNext("something") + .expectComplete() + .verify(Duration.ofSeconds(1)) + mockServer.verify( + HttpRequest.request() + .withMethod("POST") + .withBody('{"jsonrpc":"2.0","method":"getsomething","params":["something",false],"id":1}') + ) + } + + def "Returns error"() { + setup: + def resp = '{' + + ' "result": null,' + + ' "error": {' + + ' "code": -32601,' + + ' "message": "Method not found"' + + ' },' + + ' "id": 1' + + '}' + mockServer.when( + HttpRequest.request() + ).respond( + HttpResponse.response(resp) + ) + when: + def act = api.executeAndResult(1, "geterror", [], String) + then: + StepVerifier.create(act) + .expectError(RpcException) + .verify(Duration.ofSeconds(1)) + mockServer.verify( + HttpRequest.request() + .withMethod("POST") + .withBody('{"jsonrpc":"2.0","method":"geterror","params":[],"id":1}') + ) + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClientSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClientSpec.groovy new file mode 100644 index 00000000..a0ea6d1f --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClientSpec.groovy @@ -0,0 +1,86 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.config.AuthConfig +import org.mockserver.integration.ClientAndServer +import org.mockserver.matchers.Times +import org.mockserver.model.HttpRequest +import org.mockserver.model.HttpResponse +import org.mockserver.model.MediaType +import org.mockserver.verify.VerificationTimes +import reactor.test.StepVerifier +import spock.lang.Shared +import spock.lang.Specification + +import java.time.Duration + +class BitcoinRpcClientSpec extends Specification { + + ClientAndServer mockServer + + def setup() { + mockServer = ClientAndServer.startClientAndServer(18332); + } + + def cleanup() { + mockServer.stop() + } + + def "Make request"() { + setup: + def client = new BitcoinRpcClient("localhost:18332", null) + + mockServer.when( + HttpRequest.request() + .withMethod("POST") + .withBody("ping"), + Times.exactly(1) + ).respond( + HttpResponse.response() + .withBody("pong") + ) + when: + def act = client.execute("ping".bytes).map { new String(it) } + then: + StepVerifier.create(act) + .expectNext("pong") + .expectComplete() + .verify(Duration.ofSeconds(1)) + mockServer.verify( + HttpRequest.request() + .withMethod("POST") + .withBody("ping") + .withContentType(MediaType.APPLICATION_JSON) + ) + + } + + def "Make request with basic auth"() { + setup: + def auth = new AuthConfig.ClientBasicAuth("user", "passwd") + def client = new BitcoinRpcClient("localhost:18332", auth) + + mockServer.when( + HttpRequest.request() + .withMethod("POST") + .withBody("ping") + ).respond( + HttpResponse.response() + .withBody("pong") + ) + when: + def act = client.execute("ping".bytes).map { new String(it) } + then: + StepVerifier.create(act) + .expectNext("pong") + .expectComplete() + .verify(Duration.ofSeconds(1)) + mockServer.verify( + HttpRequest.request() + .withMethod("POST") + .withBody("ping") + .withContentType(MediaType.APPLICATION_JSON) + .withHeader("authorization", "Basic dXNlcjpwYXNzd2Q=") + ) + } + +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy new file mode 100644 index 00000000..a8739cfe --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy @@ -0,0 +1,95 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import de.jodamob.kotlin.testrunner.OpenedClasses +import de.jodamob.kotlin.testrunner.SpotlinTestRunner +import io.emeraldpay.dshackle.test.TestingCommons +import org.junit.runner.RunWith +import org.mockserver.integration.ClientAndServer +import reactor.core.publisher.Mono +import reactor.test.StepVerifier +import spock.lang.Specification + +import java.time.Duration + +@RunWith(SpotlinTestRunner) +@OpenedClasses(BitcoinApi) +class BitcoinRpcHeadSpec extends Specification { + + ClientAndServer mockServer + + def "Follow 2 blocks created over 3 requests"() { + setup: + String hash1 = "0000000000000000000cf5a5d4dfc4347c0c1a863ec5fdb429b02b2162e50001" + String hash2 = "0000000000000000000cf5a5d4dfc4347c0c1a863ec5fdb429b02b2162e50002" + + def block1 = """ + { + "hash": "${hash1}", + "confirmations": 2, + "strippedsize": 800464, + "size": 1591897, + "weight": 3993289, + "height": 626472, + "version": 549453824, + "versionHex": "20c00000", + "merkleroot": "7835a26b6c9316232f8194c6b64b0a366e26a9a14e6a7e7409c34594d98a83c2", + "tx": [], + "time": 1587171526, + "mediantime": 1587168930, + "nonce": 4111305375, + "bits": "171320bc", + "difficulty": 14715214060656.53, + "chainwork": "00000000000000000000000000000000000000000e9baec5f63190a2b0bbcf6b", + "nTx": 0, + "previousblockhash": "000000000000000000106b8cb739dd3c412613aa5d459a739794ec2572a74c10" + } + """ + def block2 = """ + { + "hash": "${hash2}", + "confirmations": 2, + "strippedsize": 800464, + "size": 1591897, + "weight": 3993289, + "height": 626473, + "version": 549453824, + "versionHex": "20c00000", + "merkleroot": "7835a26b6c9316232f8194c6b64b0a366e26a9a14e6a7e7409c34594d98a83c2", + "tx": [], + "time": 1587172526, + "mediantime": 1587168930, + "nonce": 4111305375, + "bits": "171320bc", + "difficulty": 14715214060656.53, + "chainwork": "00000000000000000000000000000000000000000e9baec5f63190a2b0bbcf6c", + "nTx": 0, + "previousblockhash": "${hash1}" + } + """ + + BitcoinApi api = Mock(BitcoinApi) { + _ * executeAndResult(_, "getbestblockhash", _, String) >>> [ + Mono.just(hash1), Mono.just(hash1), Mono.just(hash2) + ] + _ * execute(_, "getblock", [hash1]) >> Mono.just(block1.bytes) + _ * execute(_, "getblock", [hash2]) >> Mono.just(block2.bytes) + } + BitcoinRpcHead head = new BitcoinRpcHead(api, new ExtractBlock(TestingCommons.objectMapper()), Duration.ofMillis(200)) + + when: + def act = head.flux.take(2) + head.start() + + then: + StepVerifier.create(act) + .expectNextMatches { block -> + block.hash.toHex() == hash1 + }.as("Block 1") + .expectNextMatches { block -> + block.hash.toHex() == hash2 + }.as("Block 2") + .expectComplete() + .verify(Duration.ofSeconds(3)) + + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlockSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlockSpec.groovy new file mode 100644 index 00000000..7acd5a18 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlockSpec.groovy @@ -0,0 +1,24 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.test.TestingCommons +import spock.lang.Specification + +class ExtractBlockSpec extends Specification { + + ExtractBlock extractBlock = new ExtractBlock(TestingCommons.objectMapper()) + + def "Extract standard block"() { + setup: + def json = this.class.getClassLoader().getResourceAsStream("bitcoin/block-626472.json").bytes + when: + def act = extractBlock.extract(json) + then: + act.hash.toHex() == "0000000000000000000889c2e52ca5e1cecac60bce9a3754201a7a9a67791e90" + act.height == 626472 + act.timestamp.toString() == "2020-04-18T00:58:46Z" + act.difficulty.toString(16) == "e9baec5f63190a2b0bbcf6b" + !act.full + act.transactions.size() == 1487 + act.json == json + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserverSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserverSpec.groovy index c25b0e2e..d1653a6d 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserverSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserverSpec.groovy @@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.upstream.ethereum import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.test.TestingCommons +import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.HeadLagObserver import io.emeraldpay.dshackle.upstream.Upstream import io.infinitape.etherjar.domain.BlockHash @@ -37,10 +38,10 @@ class EthereumHeadLagObserverSpec extends Specification { def "Updates lag distance"() { setup: - EthereumHead master = Mock() + Head master = Mock() - EthereumHead head1 = Mock() - EthereumHead head2 = Mock() + Head head1 = Mock() + Head head2 = Mock() Upstream up1 = Mock { _ * getHead() >> head1 @@ -89,7 +90,7 @@ class EthereumHeadLagObserverSpec extends Specification { def "Probes until there is no difference"() { setup: - EthereumHead master = Mock() + Head master = Mock() HeadLagObserver observer = new EthereumHeadLagObserver(master, []) Upstream up = Mock() @@ -118,7 +119,7 @@ class EthereumHeadLagObserverSpec extends Specification { def "Correct distance"() { setup: - EthereumHead master = Mock() + Head master = Mock() HeadLagObserver observer = new EthereumHeadLagObserver(master, []) expect: def top = new BlockJson().with { diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstreamSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstreamSpec.groovy index 0775bdbe..fe3ed37c 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstreamSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstreamSpec.groovy @@ -21,7 +21,7 @@ import io.emeraldpay.api.proto.BlockchainGrpc import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.Common import io.emeraldpay.dshackle.data.BlockId -import io.emeraldpay.dshackle.test.MockServer +import io.emeraldpay.dshackle.test.MockGrpcServer import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.grpc.Chain @@ -38,7 +38,7 @@ import java.util.concurrent.CompletableFuture class EthereumGrpcUpstreamSpec extends Specification { - MockServer mockServer = new MockServer() + MockGrpcServer mockServer = new MockGrpcServer() ObjectMapper objectMapper = TestingCommons.objectMapper() def "Subscribe to head"() { diff --git a/src/test/resources/bitcoin/block-626472.json b/src/test/resources/bitcoin/block-626472.json new file mode 100644 index 00000000..1bbf4181 --- /dev/null +++ b/src/test/resources/bitcoin/block-626472.json @@ -0,0 +1,1509 @@ +{ + "hash": "0000000000000000000889c2e52ca5e1cecac60bce9a3754201a7a9a67791e90", + "confirmations": 2, + "strippedsize": 800464, + "size": 1591897, + "weight": 3993289, + "height": 626472, + "version": 549453824, + "versionHex": "20c00000", + "merkleroot": "7835a26b6c9316232f8194c6b64b0a366e26a9a14e6a7e7409c34594d98a83c2", + "tx": [ + "02a920f009f7cf6d029dbb091d43a9a856fb048a0240d3361594129229786432", + "a18629f88032454c86b6271f5863732da2f567b8c61dd78ab638c9432e5f4b1c", + "3413673b45dbb11aff3905fcfd56cdeb06215b93626a026e1822b7b324799a55", + "07832210310dc67ee3c3345ba0ad233ab359191d73bb1b9b0402fccaa6fcfaa8", + "7af0927ff75c10972f68a7a8fdb0055e3a693e16b5499e22cb33417c34c72cc9", + "383cc7febd7187a9257c5aca1033f59137d91fe17b4f4c4dc5c7e05e49ac99fc", + "c25a3ee7946583c1a10fe16826027e65a566292b0441e49e9df848e15ad16687", + "a52986ffdc8a2e6fe387a82196490c14c17264042e4f946a7d4de2e0276bd6b3", + "8bd7dd502a681e318a2fad8cce504c083c441cc8bcd552499f568ef0477a05c3", + "2a8c16889b55dff181c90891b3830120f586df386cbb40446a58b476cb9447d0", + "47a7cf89f1878f9dfb473e83c28748ee8e3d99502109085b4a1ca08db36d70e8", + "8c0be59ee38e0a31df1fdacd91b61e633a92c61669706c2384d5060911ec33f2", + "0c1eb0039a1257762eed60d6ca54138c029176fee1fee8b4de255f7a5865daf7", + "1a0431dddbb0fb202827b825e6eed389acfe2739b58d244abe145862047d1934", + "a56f59ae16ce8edf52dc8532a00f9b5b385aebcee525bb217a6094fb22a72701", + "d4eb304b24c52c41f106dddec72c0e9b224aec1da9da66e17a471a60e8561ef6", + "fc7b4ed728050ee1fad5e3fd99fbe9feb67895177027dce3f82d8348a6c8a80c", + "7a16852effc2bbdc629ecef201f4b4f742c8268afa5f812994fc9e213e481948", + "834c0f500bee28a31a7a9b55969dd4cfb1e8a2c7ee15c0c013b1f606c11e1837", + "4c2142d011a77e303dab6a525e9aee38e33ce3756c34eaeb2a482a4c802b455a", + "2b2d445e951936a91bf3b74b7ebfe3ba36967ca55032a8805e5e7dcf2ed2e490", + "982f171358ff1c8d2c044d5a37368ec23557a4db61a755940c7651646a064755", + "ae93dda12f99c93621b75b1147e1233a7ed1e1caa41a3d565ed07eabbdaa8565", + "4effe153f21f84472a79772cc4d788411b5ec7c09b55a3a4f2abd471e0f4e45b", + "9458e9f4d095dcf76ab12358af95758d4e27ac9cdd2d6597967e1a0f8f9cb7d1", + "ba9aa701ea10d4d766d1b1d1b1cbc7a38e799a6529194c74a7a8b086a5286477", + "d0c1f6f4d4a2c737b8de6925675eede86f3f4e4259f51c3cd32ec980f143c412", + "37f5301eef170ad547a797439bc456d9d3f39054d1b48af8d182365026c1608f", + "8e24a44cfb796c13f3159df8d53243dedfe8abebff44a7119648b4c71bdb18f0", + "4b2aafa57b56a3dff0c261f989be6d7ceb7f969b82cbbf0c9e82f72fb8f60cec", + "4f3f6c72ca4b17849a09f2b56e94b7d0fcd70ca51ae65ef7d9761b3d624b3087", + "3c64109774ae9023dd20b688683583ee59f5d3967f5aaada3ef83b8ffb9d1e45", + "381487155a1cb39688e832b1603f015fec9f7339ac68925a586c17f9285b3542", + "ab22a95ebd1a8cc4569b83f46630451071d8d9121cab6d9c3a712eebc1faf89e", + "783fb6fd58f28b68fb4f40ed9c13ec0c1e6c43c625751f68b18c799e0fc0c854", + "e017a20991d20b1854d006bfd17d6d1f7f8d8790fe4608d528174fac360ed363", + "4f1cea1b0545269298293190d0d6980ea228616f52504986c976385600b61b83", + "051d5877902319ff31a5b8e1d7c12b0d0a0d606392acc2f72a84eaf268e052fb", + "5b07d8bc3982e143441eb4b69004acc750917884e38cae459368d70b1aaafe32", + "fafe5de736267255206bf2eab27a3dc3f6911a94300c63736621c4c18303c6fa", + "23e9de507203a7b40126b88f373043f541fcf56de449c10f395f89d0f31d7a64", + "9fdc988b5a3413070757135bbe9e0d39d44519624248839373d7770c2ab7d86f", + "35fdb0c645970207d023c390bf1bf568cd5336a00f518f29069e7bdd5a3803e1", + "fdea27dab177a54da06c83825e0247d5a9e964c5b78cb65428a12f340c479683", + "7b277ee3c3bc107552b469537f7bcdd9a62bf5e66b8aad9eeb9a86bc6510e194", + "4719c2716a141213884e46cec871cfa9ee149e3a2678eba57dfa3cefbf0ae82e", + "50c6c7fec8a63021f0e115809a3a3c6f8a32191f610bfb8a5ea0f17f6e5d8770", + "514e239c74a60765e299f5fdf9a2499f0b0b8e46dad9f3e92d53e7090c7b463c", + "c5c1ceb288a50024cd7f347be2821eb43ec157a61f9ff286440d8615d77e3e04", + "d11d61276e23161180b600666d1c776607dd24b9f8755042cf6c95b2512dbc69", + "a53fbdb36c9b8ced6ed653fbe950c62658ac7a6e95f59cb21fcf5339b75c9e48", + "f9fb12f196670fdf6cc8d4b75df3e41c126ac96e11e4cbf9136de572b0fa0ab9", + "87e0b25352aba5c588be26c2f8d45344427c0362b011d299ac239111b48eabdd", + "7a3c80eccd7451e70293ef3c61e40ffb5c7bf07b68d4be4d73de5569120bc7bf", + "35ab541484fbe5488c6387687b45f069869f1a568f40b3b68026a7a7e3402b95", + "ff65c703ad43a8a4f7885274d3969308ce6c13ddac7a7316c88b8b176eda191e", + "a92ace1345ef64507ae186349ac3c7d2d6c994e57fe19e0ddab49fbf13a12928", + "96aba1806bbaffdd3d6b742ed34a832543e9e3d386410591ff3d71835c010ada", + "58a82a4d55abb6ea7677e0d2c1718a8df7dd1d4fe540415179ecf313a0cba1fe", + "58d2296063140bf2935c055be7ed0ce809d90be414e00c344bbc5172ddaad852", + "d4ffd634bedca4513b98b7afef827569e09bb772872859f30cceb2acf276c66b", + "366eaa1795fe7fd522ebf649696f835f8c57d4437eca1dabac2e64084fb04bc7", + "cc94ba7c6ab0f1bce010b8b366e5aa22baad176ae087e57a391be2cadaa9f1eb", + "f56f607bd1eff5741c23310af1928f6702ebe30a51a8da377c6b40d174caa836", + "16bf2421c10b41cf73426146f2d7a7c1c5e9a916f281579bca5d44681d45dca8", + "1d65757dafe6245868f76ceb48d14ab9f4d7d252fb974b501d638d537cbf2012", + "0cd8d7ebb323503716f8c72ab35b546b38696f764a38a454c03cbdef0f450964", + "391e4d2b88f9393f01d6eeaa3618f7aca816f8a7c463fcaac8c74bef3e932637", + "89a131044b15f314d3bd5e645ad945edc55319bdf8b13519d3f8724d5e3a4324", + "1ee2dbe775700359a617c5b6380ef256c22d2b80308ac87bac25769e3f6f0af8", + "26e7796190265500182b0c21f5e2f322cf727e30c6abb5603afb80374cdb9f30", + "2b98978c207b68233e02ca0c5f7d0c6e54d4833977203b8a409c05e4428682a1", + "ebece119f96aede36133a88b935df181012ed8c8825f7ef788b1cbb681dbb2e7", + "3341116a1c6e5e6e4851d4397325dd0d3dd50f4cad0a7f7ceb807bef1f190985", + "4f813a4a6ca66af738da7295475aa922d60bc167cdb37dd6424e912f9ed47fe9", + "f096008c127a0565fc17ec51b4717188ac2528343281cd9a438ba3f282ae5ab1", + "0a4b711ee4c7ffe224712d73aa0585af4248eb71addf4a72c72503a2d4463fe7", + "8012c7a95ae65ce9c6ba1250e961f8ef4af314b5d6b8e127c90745e046ae0c26", + "f06a61c4f0552f4dc5edf232f882cb607f5ecae12ed329561f79b446f5a55f87", + "290b1685a53c60e6350682b38035ea8c027e5dff1db3249f2d3dd5c4794697c6", + "e12c183dd5731e0a2f21ce11ce282ef996b1c831a04e5dc883e13055ce3b72fc", + "aa7b20faf2de5d30fca4f38c4124437bd7eba9b6761d7339debb3778c1bd6884", + "c0451d1386c757d326716aef727d963ee29212afaa95ddfe3426b22c07b165fa", + "c086f865233d5eff001e9c1eeec825a960347006e390b53238ebdbbdf1e71aaa", + "9bd927d3d92053f802cd726bf36c6b4f49f4d33c0261ee1bba0be84186a5afee", + "3fa156b2c36aba2ddaa3519f5661d382efb98450f5fbadb9b2f2c46a327ff6e6", + "6604979d7aa0a281bf14776f4d9ee2a55954b165e7c9928a748fc164a46e90a6", + "f756f36ce8d044ed7d5945b644649fd4358a97bc63826c0fc33acbbba86f25bf", + "af64126ae1679509ad9f7b526d0417804b21ccd311fb31a17f00752eb51f0a22", + "6843c93732097af4da88c56988d8e08924a39daa5904e791575d2671305bc205", + "ce7d658b2c6e52a1450e12ad3bf1c7872520f5b5e8a7394b2d48f05cdfd39878", + "31300346eea7e5523aa99c29a2e4c9169d65a120ebf49c663c4ecae447426b2e", + "5549db755cdc2a6cbada2916dcdbd34ad2a3e6156dba5440c6a9fa8f4d01cddf", + "a8b8cd1b90b7289aa96ebe79715f21e8275ddd9c58415f0f1134164ad805fd05", + "339fd5d8842800994abd194f105c59fbd5c7edd35c652735ee40e3999735f80b", + "4c211bc1f5ee923b97aad3587695d3c173644f4ee522ee0f9c2d5569d528bc96", + "ef02d815060d7de45c7ed03bdaa83d2448e0eb1dd1f9ee2758f8b1b2eaacbda9", + "4c2e494c91478d0f83b22cd8244aef5e838ed3e920dc6d33b278f26d715f7597", + "0836aa1c5d8acead15d3830afad798a0fcbba1cd3867d26437fd56edc4e3ad04", + "f13942aefabe6b0ff1aa32881bca303f1144ce77c8c0b18ba94a9e6061944a1d", + "cbf48bc62143e252ce833b261ee457dbb558f6f4c0f95248e98bbe94a4dd04f6", + "cbdea21e1d1c5ead0baba3dd5b48771f362eada2a2abc072174f7dd5a50eef01", + "cb8cbdc6a72cc6ea1abb4f369ef962383f8dfb815225f7ffc3a59140f5a63ded", + "bf855ca1a9018f812fd5cdf150e6227c6d02b5de4e6f7f3479c10f4e95820b50", + "56a40c43410442ec2729674bb47b387fa84a9918e0774f65de5fbcb61428ee16", + "d7413d4ba78bdc80931d1f420466e9a7b5a269966b80fbdf8b02008f752d830a", + "0299ec3ad1bdb997020a4e1e228c837a86c54c64794009f8ee164f961ab79675", + "e7bc6594b33823b6895945c1f2e8d620fcd2f5cee35703931c446bb6d8988c44", + "ab653b60ab81ff4167d1fc9b687f4609975fa3eca4fcb2dfd6590a97841fd32f", + "7836acb4009436c89cf2a908bf3a0c53f40bc453498d01657cba1ba2ffee8a00", + "3079f73a6b19193cf4a4508e749a382851c4467e1ec6016d7cfc55cbf675ec4a", + "f27b68f83145fe2d9367b78f9923a4ec82ac395bc2001e6f8f0867be0a38d17d", + "7882d61899a335b012627ba8b62232d46f088e981553f857a05d600d97bc36a3", + "2c501fba304d9b6c4dc0d884d95bdf22608272373caa666fbc581b13f8a27053", + "4c1c7c27f6bfa6bf59e479973d7dc491ef1addd37085b9d44138f5050105bded", + "4d68fcd6da048a7e88c64186f1fb8f35c49f89d369116580b6daf4c9e7d4491b", + "c1185d9fe8a3ae97890105109e7079719b2f7e09268f6d96df8980f3bf20ef5f", + "0591d0931525d15ad7b2393cb05e853f443cb9be2ff00a7a270228e123df7962", + "3d4a27aa3e67eeedec3a3b5876eb31f704f04404048d55d6c8ba8ae11b309e4f", + "0c91a923a955092e23a8e20a29a895f3c1b6a2a7cb764daae3834da3792c2b89", + "5d45e8a94a3f69801c40379b47157ede5f5cf68959ed109a3dd61318c815b6bc", + "fa45ffc12affd28e1cf493f83c2057e9b3418f9787332dfa1bf447499d00873f", + "7fa88a89228e5eda6798e39d912e92a6aa1eb77e45d568a9a5e729a86cdd581c", + "0332112f87f1cd403d7f16f8e98af5165c472db42ba0e38b68b02bfd469c4b2d", + "d18e366350b60eede7c1873b74f401952146d6de4fc28e07499e3a716b2f076a", + "14366d88ad2be289c0b6a58951b558eb3edccded0c3cad6861c920f7a7d2fc94", + "29f405892878a6c7d9a9d2fb750f12ae5faee61d374897f39618903173f82e13", + "cd094a6ec25a1f823c52435c653b9c5e612dc0dc4a8d5868f31c6cdd333f8f48", + "eb90cb4945827eb9d246e8d905f055f20bf3b30a8fda2c102fe5e73dfcdc99ce", + "fe5a55791a67f67c4022f7dad5c4985b63156336d5c650af36a909ae316c9c80", + "9d2cd0c065f645f15f44a00bb238202f332ae9b67e9438693f78820232abd2f9", + "f84ba45595a56f194e9ee7f12ccc7d7d9c6e612f689ee1876578483f6a8978d5", + "fccaf16f0b14611aa6db828fdd303d6204806d603c9ba315beab8d339dfad3ef", + "4daf74e5cacec1c51f6953a3242622d4af088a06ecafdd83963634bc3d8fe783", + "d3a830fbb717a1d86efee3204722ac7211d69ca357945681b190351be05c5437", + "c0caa3f7f0d4863da02f73661346d08990680b9cf7f606f81006097e7dac5dd6", + "40c177b8bc7710bab88a79d7bc0a99544cf122777a31f167fbe833fc65911953", + "30575c6e48f1f71d1cebbbdd30579ac054e72ea78dca661467dd2f5d19ef206b", + "8b8d2a815f93e5531eb32bbed568b881ab15019c98afa825926d928c54786791", + "840d753f6a69fba08b84ea2bd888d276e73ddd1b7cb382e2220c78167fad01ad", + "ca7965779ea20a987897ebfba08d7205e1b0a8bd93d310b6fc0b4fe94b931bb8", + "a2556c24fd1b853eaad7a62be6ce2bb611af769b35b18b6d8e88afd3350d5220", + "f65873b9b82a1131d10f412c953b23bc0e6b8fb7c105790654e5d48c0855793f", + "60b79b8f53decb12d2369b84828c31c5a9ce149f7a1fb1fbd5db2eb94d6c5af7", + "5fb1dd394ab746544c3ab19a6e2357a6f6bed8fc2d88475d35fce2d02e20dab9", + "9d7a8849abf69c9851eedc3e94f391f5f35b450b16eaeafac990633bacc56405", + "bc6fa1987f441078a4309b46890c49dbc9979513dcd9c2201c4e3462dac73876", + "d182bca4b43eff597d408a53313ae7d4b3c8f98e1df4155a4915c715a597da32", + "a6b61e13e44221f5829c29551f7b185c0c49b937d5cf1d790878d4a3b0a1ed70", + "464fb5c5b2112053280a3024ff319da3823213040ee4a3a4e69010f0b13c7940", + "2df9efc3f00ef02a8d325c7d23cff75966bf2014744fb169ba235ff1b9fe8fda", + "b9623f1dee830ad59c82131c7de9cf9a3a3919e2be72e5f0ff5fb8393b75316d", + "ba69376efe5164d8c301bce6a2aa26bc326e716b243aaefe801b83ead6bcef68", + "996793985f498c44f3d70fe9e4f1aff763722b9bb0b2bc64a361799947e4e835", + "2eb076a9d7616440d1ccbbb61bbcbdfe65c901356ab8070c53cbf05c97c729f8", + "b205fd4409eb142a0b2bd1e227f9c7688e39cff134d6ca29b6bfaa868c4e018c", + "2f2d882835304ecb671aa783f79abc24ee6e90b7bc66248bcb15f15bb5a466ce", + "a7fd7f374157b5137cfefe5245b51cf4a3d25bbb7c7c170d4b2a1efc689fb00b", + "8cf252b39d9dad758e0ce045f13bffaad721833f669a74fa990a25ccd8acac3e", + "e27e34cf94826bf942bab06d21274c2e2c06301cd5d8d930e739a37fec219668", + "d2757efbcf5070f0fff47961ce4b95620213351e5bdfedc127f4d2d694511a66", + "73e4c4515fe3e73c5e1043c03534dab3b13c50e99febd19e63091d8e9cb80309", + "2ac8672ebccf140cd154bd9fffeb538dcd4cea91fe97d8dcdc12e8a0e0788a81", + "05605b6d33069bd93d12d8d7bedf7b28e39fea87ee1effa2b16e28f1ed723858", + "2b95b7f7628a4dbb9cb8dd0175949f4e9d23cc5b66a15b515091aa67bd320b25", + "2b41d370b84c59ab9032652887eac1dfe4ef51e2ab65e3f21282ebb93f3fb45e", + "ad313833200129d9f5b8be0865e93a3f511687fbb8c3a5ce963b63aa68a51145", + "95042151d2cc7957a491172dd5c2bbb551de6ecf99902a58468022dd5dcf521a", + "3a3e69f16dae69a50b0a313fe461431fae8ee4b3ee0435320d19833e941c7cf4", + "d9fe7a72eb37b123f1c9395b88b2482e4066d2bb08add1babf3536bd332491ba", + "8133e68efef108f900c12dd9852468f6a2d6abefa5fa17d4153547837d27645d", + "d434cb33ed489de9b11b2f5a675468483a3358af5938ede7e7429770a922587f", + "cfc4f4aeea513b0af02c943e0c34cbbb180dded09bf6376d8ff5c1b3589e8b54", + "811fe10185589fae68c64139889b697e8462e260a8474ca690657d407eab6b31", + "0ff92445b4ce0a91d2005e2eb82f4080bbad1a128ddc471c828b4d1c757c4653", + "5dcc10509d8c41abc30c592a6d8d8511b142cad05d19cddffebfbfd6493a9aa5", + "86cbcda7cd792297f67b5f442a218d5042d5d1b481189ea0daef414056e4cd42", + "88052aee4c5735819e8256a23ef8fce1b1349f4dfcbae28002feca2b14e22b7a", + "85a1c9e69f087766eea1f3183d50941d6027b1355d8a67369aadcbf809dfc637", + "74c4fe9ab823e498b397509a1da3bd79f8caf2894aaf7e233172ce2f1a09d6d5", + "17b0e1ae7b4a909f0d13275edeaf0150dbd1bf2eea2b4098df38efcfa4094210", + "57efb6434d8d92043ef69c11f1ecb34d1fc97f54201e663c3e4cd8c84e772d4e", + "c1daf39c95b26410b4a7917cd640d4473d2bf18b256a021c41bfc376b875b9ee", + "0304aaaa827ed412d986e4f43f20cf17bfd216ade18ea0b4e494c24a4614594b", + "b5820b2649111a63b25fb161147498691cdac0aec837a55d5d4170c06d8526ea", + "3dcb0c6ea74fb14f42e4137ad3b6e574d92494f2fb3958fd81ac485510096792", + "d9e0f4cccb9251f2179af392f063a74e00a8f5b842b2fba28af4004ee1798b8a", + "68285691f7ef141c68889555a48baf69d0263e55120fe6304663b1e40c868bc6", + "63955acedf5db881867d2f80d88fca3b09568eddcd0f4613cd7542d9333ecbf8", + "b65e4f1fbd1eddd800a47ef471a82baa2f269ee12584844877dab8010b5e5a83", + "2431db053dd0f9fe557d72496b60730726a44936aa09b64b5329e21c52e08c91", + "701a492c7ef65fa32290f9b24e1d1271535e6b14a6a88e7f714819f91f4c56e7", + "cd756d6a40f2555216f197272ff52ca8cb513f0e2e93822828468a47e3467abc", + "f8193d7ef046a67b758b4c95fd377199df9056e839f755099cb052110a1af0b2", + "7aeced62ff9703905e8884bdf618dd9746714d271060faed2f19c1d57f8e8826", + "a9b73d665c532c2c9dc13753335a8ea0782991740fc7687485371532e9c756c4", + "2e95ffe8df9abe9a8366867340962d72d21605413f8b0e893a1b65480c9fc406", + "1e2e6ad2d67e3fa015202234b35a21bdcdd9a0f44af1a800002188f938de0b52", + "e247cb56524dad29cd6e18fa3b7ebc35f1e4511fe8ca1d38f22d455a1efae2a6", + "57bbfb9cce58c86d56e9a85cb63d147361eaa7eea334b71912699f4014e2998d", + "fcedc9412c087616f70bcb1784e15bfcd997239fa2040324f6af1063b23b23db", + "d792cab1aef053a56c7c3d366a7f9160a38b262bf6b24cefad315d1ba19f37c5", + "54d4272502e2ed75cc84cdec3b411e3a6bf9ea05b08842015bd7c6d8c3057691", + "cae638378fc8f86e4e5afebf995dd16f4111789b49f16e76e67d7510be0458db", + "ed13b30dbb444a3ed0adb4cdc196c479f455b66bc64c5f878efcec5111d1dfb3", + "52e7c089d7da5a646816e2f767428cbb5694998c895bff3a33370c4554377ebb", + "05848f1641d466e40f4bd4ebf262b5e8ab829aeeef0fec40a7b866c0257018ee", + "b02b8baf7717a4914318b09bfcdfbca315168c3a167433ab492f0d95d29fd935", + "0040aca7ae8628785dbf4edda585859e92e4b00c0961575a3dcc5bf1eb28d5f6", + "af1c2b8554954de3d8c89da43736a0838267d50a22c06befc48ac60af25f4720", + "0fc5ec85399b8f6b19f4037a75c305b2fb3849ac2581aacae86cae5ae3738a8d", + "4d9635ed8b73678a5deeebe53806b325a0352dea7d1fe7c1a7480537168d3676", + "8883f65c07bd8190d31b7f478126b35b6d1e14be5c86c048c4795b3c463e10fc", + "cd8730666bce497358ad651bdffb340bbe7eea42e0b636a86224d9e47bde1a06", + "238f13723a9e8c3c4a3165d1018f84f96daf7f7310c056f7e0842bc5381eb6a2", + "23b5ef7ff2c8e034823bf0c04a904dad17eb88453ae6514b922956e4d2773c0b", + "a8f6cea35e29c6938c996db910c014e61b76e0203fd348df2be832973412a994", + "0a08e0f42b8bcd074db84bddad9aa24ff5fa9cf9dafeda383502b6fe6d1205f4", + "cb8afa6282f194fe24728e31d211a4b55c4e82af482fb0bbc2c6e101b751bbf3", + "5c64c563190b5fc5d17f2bc55ae9be5be67a9dce1470d778dcb428dc38f9ebfe", + "51d226751c474eade7be87137bdc33dcbb4e0786ecdb5b6ad1cd13c4acfe33bd", + "835f5004783a04f98156a3a3c7e670f4c18b5ad6a2fc967c317f32c01a51e4fe", + "0aebfe196b037e5f2c5387189ef00f3c073ac17d72a4d1b48a7e2cb7096ab0c5", + "453fa4c0d83f8edcd2c9b644fbbb467282977c11e31463f81f3a8e5e0ad16239", + "df6ce91d1af4535446fd949700e9230c28d4d32b6f4fa2a97e15594d56d6acd2", + "790a5f0eb594d9427b1e2a426dd16eb4675f3d54aac4ff40201beea8f9128d77", + "c7a3848b2b56059407d8e260ded1c866440b40aaec8b1355f6d8954a7fef43a1", + "2d7e01a20f4137ebc4ae3c4d0f3794514f443fe260b50491454414907a247585", + "37f63b730765561cd8ccdfae8e9d73059e265e0ab27afebe726aa3bb13b9e6f3", + "2e87608b23b867115a23b0f191dc43d14cfb68752e29e915d579257489422f75", + "b38eeb421ecc1ada2f654b115bbddc7cb14c3b7780379c6be088801ba988d44a", + "0df1e4fc7849f383742215a1fdfc56a23de8e4b28c7186fcd682aadd5ca01005", + "dda75fb6ae50a684e2654be480a74e976d66d9978c3d5c2011bce0afca192455", + "36b0285f7af6d7f91132d7136bc83e1d61bfb7787d7b6623f4b1eaa08a798399", + "f05af72eeb91f6bc389ff4c1d1040c1479705fa0c9ebe9c1abc3130ebc6f1855", + "9c7ccf21086710593608e5f293f7515244a8990587c2a2b08bc6fa29e9637856", + "53b60a07bd2269b05f1468c684089b5b154a4756ca010ab95ffc39e8036fbcca", + "269b89fc74646ac4e425ab96090445e931c9c6c15f52f55ef4dcda116abf96fa", + "a77d6c478629a95dc55e852ee4f856cb9262f49c00666957ecd9faeb7090da2b", + "3feb188c9ce64048d0b6080ae4ec9537b7f66454d699d4ebb7fde2edf6089e67", + "067e674b06a23789985cf45ab6af5001263b1957299059116381d04d778163d0", + "b0da138ad71d5492e0970c883b6b4aea20690e0f0ce4f401cb0857db1ccbdb39", + "bd49bfbb25ef6f2fdc4ebee737f47a54bd89a2fdb72c1f129f17bd9f2f5bc877", + "c2c216cad8bdcf81ec07250f17bfd86272395f5ea4f633bb3433d2dd9e226a23", + "6e81b358469e4df1051298a4ddcbda2ecb8a1c17ceddbdb6bcee4e4f2e20f53b", + "549e52b4167e8732ec33c00688affdeb77a5e3fe33fc29b80af239df39bd041d", + "0a8d0cbd28a010e1c3eeb117047cf9a4f2cb6af0dc6941a8407d646e98227114", + "57550f44bfe80423542e3e9eb2c6dff22577c39014ad9e22ab171a6cc473e7b6", + "0f565b4ef5c2fd82543878ee98cca662f9eb5f28b67a6576a808a5ff08a32a49", + "faaa685bcd5c00659f58a612cdda9f46d27cf48df8e4cb4fb234c75525568d64", + "a24aa7ff73f48fab8cd57c45cbcfeb352ad2fa3f96f7e8c9beafb62081eb0c9d", + "b5df4019d67b2b76cf28464745120da01a8c8a5779099e12e9517fcd95eb12bb", + "7ac40c2a59a5b57ca3517570547c02a9aeb73e6468c4a70360d4746f0dc1a1af", + "d0864a47cb3d2e48d9fc1a9dfce56608048a4120d992c2300baa0776161a090f", + "7b2504823be90fa9963ced33dd7b536f695bb35ba319a9a4df23ce7a38daa2a5", + "c8598f5c5b17d88856cb9ce80518bbaa334bc3b4de29b551c7aa4c49aeddc4eb", + "d201dab4b7a20c5bad7def71405d03e8bd04be94f51fe4890e1572f980ea6938", + "d195c78b8e77b5c4f5a7aba8f110dd49844ff55690334735803efc6de6f8ce0a", + "a628164aff760397c40cf6aed80623135aa62636d522ca5d14da9272ffb6df2e", + "e63711774f86e5f370ef7434867bd4d837fe63abbaf6f6a8d327b96e945baad0", + "0db8eb1fa7f8828287f95533dd354ff284dd75a0bee51cdcae0e4ee98f5bb85d", + "62b8144e51cfc6fe90f8924ea076e410d2f276284fe478f4670fd3ae875e9b1c", + "1952559ea9ed971d00bf402f2cdf5d84a6ad19601c1d56c3015f9334d57b3d58", + "cc307ab0e36dd02a9fc1cf9d4fa61338cf2c7e261140479853aa624b1e748e95", + "c84a153f6c932c56e7425e8a2dae6b11b4f5ad04ae1439c206bb949ac1723e19", + "ad98bf2036acb58b5d9f46aadc6e4ee3d79e77bc2dfa7405934be1505bba5986", + "8f06f5ddfa489545e6a9ed206666b354e5af70a8dd597c41f2549680c774ddf7", + "7e00aa3a41135ff546b3095c09706e9a5f0918b04bb9960d79e8bdd0c92891c0", + "9e05063cf99513e8f73df3d1ad127627751dc7db7879f3d637e9f9e96a3844b0", + "2697f883a70a37b61ef2ac8ae812a45624a2a5b0c3fde3b822fa1642e9dc9b07", + "2145083b9b7b573fc4d3217b9537b830be911ee729372f1aa07db7480967fd23", + "44d36319db44ffa6b33781dba5699ef1c0b85f62519fb27c3fd369ba8bd04230", + "fb941f217ff37a5260b6bedb5c3e562c21c17166b77918e2707cd238b868972f", + "d7ca4d52d50beeb6c7aa13a91dcce183a2e5add1a488ce5c44bb89421dff4183", + "69ae6de59754d04d6401593fc839f88a07e5ebf2eba267fb5edd07dd65479968", + "2e2615b39254393da4ebc7b166166605c0c96afcb289b878be47c82cd91079c5", + "1cbf9bac1ea423c1dd1c2a4ec3b6538f5ea307158e24f40153eef420a18cc4d4", + "734d8d86bce126f06e7fc576cf4c0349c955d3f2ecec0774ff8313eaa9feb3b3", + "adc7d1229d441e80e5a4f2c2621f00906a54e8765b9d0c06756497bd86baf347", + "5de13163a3a682b327310fbe795cd0cecfb535651b794561ba442a9526bac65e", + "b3afb2c95be6c801c566e4f71d409b215f3c5ffcd65e049836e484a67e0a7e9f", + "c6fdad6700bc15cdf468e427b414b12376dc2ba8ea34a724c6a17ed488c364de", + "bc54f167a1934455c643ae70fe42770684bd7508fe637db30e67776d2ef88bfd", + "f2915d9474eeecce6f0f73c6ef38400bdcba35e84fc96a4d63fe25e7b22a6853", + "f69b5a6050bc92fb3b061992ddbeae38b7539d40c8309ec02659420a10630fae", + "7c4562a193834e1489d27cd76d6be09a665335ba289d8eee0af8938509718576", + "02823e644a99b26ac109b62247c55f39106fe7d65bf29ce317e840366298893d", + "91f0ad44478e69d087328642f44225cd140d3586cf620a8cb46a59ce1585df4e", + "d88e148d0e8f23fe5d9ef813bf7e317303f407cf6ee196564e336490e6486ad9", + "e4e77753653cb929b119ffca71295ce9baed62dba679140583f8eb9580bb2bf0", + "454602d231e08ee932787dd49306062bc160cb439d25da51863d28ed48948e74", + "036cf526798eafc31809dc6997c594d1d0792f9cfc967c8017c3fc218f95390a", + "37a930a18148f30403335702f58a4f30059c2163f6128a52cbcb933b879faa81", + "4a32e84bab8c391bd64056af719e26f1f88e21278a319f9f212863cf202e7ed7", + "02fe79248d0704a57b587c6ab33d53a1c0ef30654279842aa986186725584391", + "e30d62e7ca862b3940c9c2e67a2570a13c7497b3f6a5c96b693380634e99b773", + "ed78febf8a5fe584e822efd627f4d8d00c0965da3bb383b5f040051469c4b589", + "cb808d017268879579e62ce056bdd6dd084da69cb94f5bb5b10233636180242a", + "a684c33a6cd5dfa6eb2e07c03fc7a750bec3be4da21bd234ad314abff81dd9f3", + "92802a76f649807b868820c936c57f14951cdacae639d3126931c4461dd19012", + "8ec67be421c00516a1f185b87adf87c04686e5dedab6301065695284e48b9693", + "6a7c5f09a10bf1106c9b48b8f2fced58ea1fd223d091ebf84ef02b6e3e8e17e4", + "6c43281d5c81865182d6921dbfb6e1425a923f17d887897d2e9133b34a8e4377", + "8a722500ae4647aca5dd3bb0e9560bd65fbb94829de2e80a5bc1baff418a9208", + "75a098acae79ebeb61a27fed5d64bf0c81e6d3bdb8d9635ce1d055477d0a725b", + "cdd6576b8ab2ce897649962acb9cd30d927b28cc72f5f93a6314094778c50983", + "6b15f3be1cb4f0feca25db432e3df80889f928c10f3af832b9a30dfe6a5adcc7", + "49417ff392abd0445f08990d83267caaec501f123b0ed354318a07d2cf631def", + "db18056c1ac7c0153df5ee4150fdf7c142ff5250eb578c84f24ee44486cc36e7", + "be8e389fafff8ba816f5f5eaf345f7125828ad32857b4aae311d27055762cbfa", + "7bade65e959e179fba24fb5c30455cd7c68b33f8c4c4b989a5639fafec63a4ce", + "791d49925a24d0bfc4dd14cf91660fc6498c2c409ef7c5ec019bc393dfbda8e2", + "76096c7268ce1f710ab66e6f4f71893a031da93a83225840267be1eb1f5a5c0f", + "aff57a01827ef03dc78b9935909c78c1632bcd68a08098cfacbf82327111863e", + "187f136635ecdd7cc80ccda1be758b2824641f952ff8c27ec900da7463c74f6f", + "bd99b5368f9cc482f5beb51fbbcd330cac27f347d6743d3f02e328639e2aaa8e", + "d5e608a6dd9483e2357792900e1bdfcba8f3f4fa795b0d00bf7595b51ff13d5a", + "5def85825bb966c9d07ca75a0810b2c8298dc8f5ca78381fd96a41fbb7e7439e", + "58f849abd816d39946324d4d69ea6c3700d09193c7edf4cf71b57ce20aa285c9", + "6315de0b506665979841bdd0187eca515135051af80ebd3c28b3157d635962d2", + "f7c7d0c9a51098ef7a88afb91fa26eb9a5372dd9837f088b518f6f2a42d203fe", + "8b7c93b904c75eb43e4fb6a12e28b8361b748d79ee7a53b253ff8da3d6572187", + "353f0a16fc71a75f9f4fa9a5c0c24fbcc804d0c5c67bb3c5cbfcb28baf6b0bdc", + "c7b84aaf53c2ce50d194487dadeed26f3be8a611801622ed2b2b56eea8c2e99d", + "d053ff5440eda0bb2a23ee8ca282ddb4be337c0865f811a727689857b0daa13f", + "6c80455f4682d8feb6da357c1b56c6d2ef01731ccbb8af160a1b2317eba7c0d5", + "fce37804eed334a469c558210390fdc88289d3607a9acababe923694f016aa34", + "c5bf1f9e1458aaf8e0d0d0e12fd9d87713cb0de610662297d84d12038c86c672", + "a3398dbc713a6104fa8a1564f33ff1c64bea743c048b2ced0b9d365a5e5d164d", + "805ebc2ae29da1e690c0212bcce2465930076406e306fecb86e9f56009a16104", + "091287e91b31a55755a0bbd49dccf468612e7b4637c1d3a93c0cbbf6823bc037", + "84e52a8d2a8ac4909955b48345f96c7dbe17cc9223177c1423fc1b4867ac3844", + "c801f345a0599f20bdb0c4f8786761f2beed71752df16b7efc0912fcf1e0f6c1", + "23754236336175907bd2949ff9d4f93f5b1c6d0e691d84378e51fc0295b80bd8", + "657915abf56e6c58be5787de18ea3bf3c5fd0ef6e0b7151ee0ea80dec3cd48de", + "2246aebe47666199fb7871a80f11ed1e60eccbabfa3bba8ff474857c95e7ac8d", + "3d39b74967ed1d5f5337addce3db38eb73fe7fa1dfa8174b0530ecc8066159fa", + "c4b6dc0833b549b7d04d8c9a9e1317c3aa0aab608079a67d2f844cb134886738", + "f3ab654470fa359f5b8e2625a7c4cce6d3af06f67c2d448dd0cf995cefdac0cc", + "c2eedc0411aa7a8a048727a17933a8c9c7bd057d0d4b8453783ccf1302593141", + "9189aa722b367a5e480dfb1d5b6706822466ed504bc7e2c180f1c3e946eaa32c", + "6142cf38e8f1f00684bc7ae70b1ffb5d9c6aa0964c699dbb1798ae548ba0ed86", + "127bd1a635369fd687dcf2fe8655331ca74b1088ab7a80fc42d86fb16d5eb2c4", + "2770927cc8a69d8466aaa8bc4672ffd68835802f2a03c965cce2ba7bfec24560", + "2439ed065b94a40bd61790f09e70a0b1c3d4333f8d80ed06d7031433477c3cb6", + "a4c33e31fc1d2aa3aebf54abb789684758eac98485b7e6a95d1ef6830a6854e8", + "87ece0425563efcd69314b1562c7fe870a4a2a53442311eb1d7536be5e02787d", + "e6668c7427563ebd495e0cd8210946d2243f45516c95003002ee0e17db9f2e0b", + "6305323fa3c528810dcf66288075c1161559994a83945e64f221adc249880823", + "d27353cf0d9bae564f3d7ef30666f1ced760ed204c0024ff89b39782b76aaf06", + "bf3629bad47a6964a36acc7fb7539c5058961954965b580fd0b0d726c5ac09e7", + "2dd321ef167b49c631b8913f3ff84439ff37d44bff0f6e05a58789f41d955557", + "73e029160de8d133f9439b4ec908919708765b2197cc0f0cac18e4fb4a220b9b", + "10133140ff78862ca8403623d3537b169082a04cbfe190ec2deb56ee3c490aba", + "59116d3e8e78700a44bb18db531b42567ac59fb3516e820acb3d5e5e722633ec", + "686509834343dca8918655554e612edf4c048229bae45c21cfc15ad60cbaec27", + "bc7c3e7075db3be4f351daf448649230aae381d4beeb74263e789a2dd695a4cd", + "14d28f3ff5cb82a142831ba41d151208aaade48598382bffef79c9d36f134239", + "e66cae691cc0c1b37d81d196f5f8ccba49a0eefe1c492b7abedcc8e0494f6c30", + "a5a8255f49b633a2de060dca272a60c70ed4dfae9340fe4397a8002fb9ab3482", + "9ef56b8629eaf42061907f733a783f8cfc983e53e45c51dc1d2cb17b5d2af803", + "9684668a6b6785c033f765aa4b10aaaa0c0d76f6dd7a3032497ebc7b26db976b", + "80b7d2b23c2d93dde7b05b90d847a2e092e0ff5cc4a2f1aa99b0129524614794", + "13fb3534d786e19d7490f3e111751871cdebc98fef45801bb11b043349b4a560", + "c54399f02fcd8d508fb8dddedc062f3d834f11f40e64ed7f2eefdec6c9613d1b", + "d4cfc118de13622561179347dba799d74fc9fb18885cd18f8948cc1cb8f9b721", + "b35798d1da5db1e90328e6377f0cc15735be8869990dba663ca0f3fcf2069674", + "4c10e33c50bb9f150e281eeef623a207ec71512ef23075dff7804e70810c86e2", + "876ec83034521e20827cfc83c9a8289cfd457b1f0ddd615d6cfa418b75b839f5", + "0eb84b6bb8fafede5da5ef9863b4ba5b4a366379ea6ef4aa3cd4b729188119d8", + "ac1aae82c8f59287101a6aadfb91f418f53dfb722cbe22674b4c4dfdd309cb25", + "ee866862c8114ffa0f80e885b67de3e4ac0ca550faa0f76735c57f73beb0b240", + "57097c4cfee79f779ef147c5ab0e1b3a357eb99cff3a4d199386a14a5f152d9b", + "f2dbe48d3d43bb0dfd08b916e7cbc399082b667450cac96c886b88ec62bb0361", + "5782b975c017d81f8a73ab4b5aea36f55ce4e68087102b871b7e23139fd66888", + "ce12543ed5d8a45a57f3d85196dd1e9ec8eb866b9c9158dd77251c198eea51f1", + "1f439a4e764a129c31eaccba08a549f336d664a7b0241fb21b7b8f9586663a63", + "dfc6d2feed6e58d0d9acb39ae2420f9a84901d099463e25c06b838c3f8e197f3", + "7a24bc2c3ded3ac4bb743a132876b576c8f39cc04f0fa7fb15c03ab931445a23", + "e5fe0275895b0692cd1da2f144450cb7771621551fd59eb5482b4845729fb261", + "0fb49811035c61e8792cb5e3537d4d162469e896efd5a85767c6bebc838cdb71", + "b947a48dfa3fad6c872e277da83ca2a066a52773f8d2d14ef7c67a1ec797f4af", + "bed4588ca73d49ad52c9e8ffb3a781882b1e8f7c34944f3ac1bee0e530c86aca", + "becff5626b6dba4645429658a950d5912fbf773948136eeda8d08dc3e51b1bfc", + "8f23d90c79c717c63741816898495c800ca3d78f2461a7941153a81ff64209c3", + "593f5a2e29035bc890f89e92908088fa4afca80f6ea4fca53888686cf14dd3fa", + "cbe9c5bc7a07f1f17cb959523ff68715fdbe402b6ffa8e33bfca257adf3e4da2", + "20c47fdfc7e19b6480ffd90a699eb41ef57d6b191f98853e3848c808df6f43f8", + "e16537fa5d6d93478d177171cea42de0df5a7cc88446030089658dcdc0b816c6", + "39ed0b74840e7992ad5335d0c29231491bbf4e568dcb06d1fe1252f1bdfd58e4", + "e34e7357949459317bb27442dc15443f7d67835fe7281d0dcae61a8a07884a0f", + "30908754089453418ab6b8cda218bf26152d7499a873f493ce495bb545e0c866", + "9486cc3c3694d0f67fc5c85b7bc4456e6390f1b55a16343e702436c4afef49f4", + "4351d30b76cffcdad38a6d287e7653b86c24562fb94f9201251f210b37d12409", + "aebbbbbbc0957131c673d232a1999814640185deee7b1d4331721f3cedd34f4a", + "5dc724987ac3c53c2e3aadd1b2938546fe843883cea2395f93517ae4a7bc5806", + "9922ac4d70ad8ed624f7f33fa48bfbc770fdfdb7abf22e385a516e93faf3d571", + "2292d69ba3afaadec3b2038f8b2626370db590192356fa33693c6ba7c324e4d7", + "eaab0f59b9e6e291179b8c557f82190cc363e0b78fa74ea0630abf5ffa8c91bd", + "a71caaf2f865965f79aa9b4f40bc06b3c1418f20e67657279f25a19a22401e11", + "29a437e4bcdac6dcaea8e70263be353ca4adc621c635de0a7aa1690db96829b3", + "ca1b9b3bd03d4f53785fe40dc56be5c6f12cf635a5d205d0c4b5f3d4a271d7eb", + "1e6a9f3e12eb83eb5681731125311d9a7930c78330e6235056055dc61cb6ab6a", + "8ec46cf01198ed5f69f1e05ffbf14f10c965f8891b34d78bc948847ac09187ec", + "a14a5577783f414846b9c97b8a4b0a81b03631bd459429938b2fe5f0951195e5", + "7b5ca20e14ecec5e88ced90f48576b6cacbb471f2ba26e6651031e07b50e8988", + "7da97a08bcbbcfe1cf9b3cb7dff03d57106c37e3f3ac9852c6edcf077f47ef0c", + "2b18467036276a91aeeb3b28c5e664e00041f79008e7aa25fa26c2daa8dd3abb", + "d5255014a49df09db8c14d6bb608c92c0469d64ebd241ef3c33287356ecfcd1a", + "f0f1d0420065ef2bce25c460167b43c03fb49305bf144af75b1e06ffa1099e45", + "ee3b9ff9add060466e8b14603fb0f75c24a8dc6058b2c1c780fff46501d8b4f6", + "2272996ea75a6a8309977443b4bc8c994ce2529c1e7f5524d818c220b0640560", + "29988fc6ea1a521e8eb8b1800ad9c969a794a891e284f8c9e98501ae2607ab4e", + "31e889b7f5582e575f612a090d9e157289fd9a6d4430b862fb89d75a7dcf16f6", + "3081141a8c18a18c39d90a594e786a9853c6349f1ac7643eecff60b480fe5b32", + "ffa4796d714ddceaa31a9af3b277a43bc576174308504a67970a48286c79b349", + "314395debf0f2933dc9133091d0b787533ac96ba3fe02df60fcf466126a46550", + "3d73956679158adeabb2bccc0fe17b636b164fd14505f01ada277a803b59ec49", + "53aa3869adb50d7815d94b65f2841b68012bc3456248ac8bedaff80b6907c64f", + "61e6b45ee7cf1ac4e707f5d14d9523cad161311e57f9f10f583028a04d212bb6", + "67f978024703fffaf9e32179e14be06581bc565645f82da36f68db5bde3f49bc", + "c27774190526971033581ee4ecba95157179beb045d1c29a7dfcb3898a918fcb", + "3e1b2c05972b8cbf24215f3ef5a540936902538b82145127e5b4884f619a2acd", + "9003bb5420d2cc9aabd1a0ab35f2159b777f8ca20ee5df321cba7cddf55e6bdd", + "f72d3faff43c51301e447b978451f570f5836c3d465003906b1ee4626bcb1c1e", + "2cc4f30e52a9ad2a6c8e203b4a97266a5f4a5a800f5f7646fa7d6cf5beec6627", + "063d0a1dca5d4c34ea5b354e53469af5649f247fd83b8b6bd28cd10d5280122e", + "833ec8573177165ad97915049c3c9cb9d40b89bed416c8eff30182ba8a53b7b3", + "df74b9f55e5d8d4c0c1fb7ccddcb3ef4ae93e76745aa4a75dc30d048656d6a23", + "ac205116ed02c34537fcc4f5f918f0eec5c6e5d20e1ddf660ee14d2b67808dbd", + "05becc5ec86643a08b5b49dce8289b794d05f88b5f132555fa61b06485faaa52", + "1b825b1a1924920172afb647525b347b61cd62772963a44f7bfc0cc794276713", + "c43e5bc8f9021545f203d661796d8e26589c3a518e5b6eba86f0942610b79ab3", + "481f557f16b32a914d315411ecf390e42176febe4157e47c75d89566de4e6bd4", + "aaf5181f9c76c18b57355300132975aa693f4cf987511ea2f322920ac0aee2fa", + "5adc836559cd5cadaae4580cd834f630a49d78182d234e9693217db6b70881a6", + "eaeec0ecfb69fd1d07c9dd45bfa8da1a7af42684dfe2ecdebe9b134b3fa521f3", + "9c2d1e16dd54e899d4f4a1815264eb7c1904c23fdd4b24b653df9543bde3ee23", + "35fd9fa796ff76c41b51572c8783fd40ad683de338bb1eaadc26d77f4ebfeb1e", + "d88349084d549c0940e895470a09edac2f143031d087cb9858edf0b84e2d6aef", + "98a97493ca478fa1c0150db95fe6b6b5bad5d4ecc5716a1df2f8a64a19254b78", + "8aac0ea24eb3dffa366e98ec893fe01434e747aab02064e8a9882f053d36caa3", + "5cd8b1b23f3a7029fd694616968093e9ee8fe4cdb33b8597ad13b467c5a14105", + "c03aff7f1f4e0b3eba1ca826c855d7fd54b8fe277309ccb8c944d3325c42ad3c", + "9c99a7c681dc31890559b6582a880c1ddf0d760db321d11ca0763d8aa89e7ec7", + "7551442ac4938464ce1c7e4f5b69f6aa155014de11b5e524ab33e41ba138ba0d", + "2befe4d8321a2f51568a5fbe7622f46bc4b2fd1e37b03a3ca2eba122b8632688", + "30cb34107a06af4797a35e8764061a945f5dd1371d61fbbe86b3d5bfa94c07f7", + "6bf19359977c4835627e55c7a528f45a455c9a1112b1e28684eee44db6ab88e8", + "cb20a31baa04e08d49888582144ad8e1433e5fd18bd9d0087e0f1e9d7fd71e5d", + "5b84d53b5b552fb95a149ef4de05cadcc9584dbe52ef6fc74828e3843441132b", + "7ce61d6772bc85261b3f53353e5d9d388808324ddf7ad9372feeb230f00493ca", + "12bc3cf6e6f244325464ed6081439151b2a0470ae63ee6d674a94c977cbfd85a", + "b3d15f892d7bb9ac463e4d06886a80e980eee45fb345274e92f5d8c6217e3968", + "8c8c748aa5c76d0f8552fbf7900ecb87440d9291081a6caa9a3bcd5a7fcedb51", + "2c46542030dd8ca5258283f335957e884e649bea734276bd42c5466f3e2f4b94", + "f47d853dd3ce2f5eb0d03db6e430816f071c617fda058ae34a163a116125dc6b", + "2f62bcc617709d8a3503b799ae2addd7c596741061b3e94a6bd92d234245bd06", + "a581f9c07f7103b5d8a1b90e0807227f44ac8cb2cb7e560248a5d26b616c0b4d", + "7408ecb8e0e591f4f3bf5a899ba15f07ff662c12fc82971985330bbd1a0283fe", + "30bf53b8e6c626b3c3aa1ace6f4a78f06bf51d91d986d424282d19af60e4b597", + "719043406a29fbfa6382663ab347b1dbabd0545a5cb15ac9a07e7082952bf13e", + "34103b382edd88d82addfdaa5735bad96891a47ef4427305ce08e6cbafe0fca4", + "4a9c1422f75bb98b82ae0f9e04ffdb552b5737904a0bda64ccf6a35a23479041", + "1c075ec18c796253b9a5eb03e48fad37462dee22ad9432f851ad4c6800483317", + "0e0ce3c251a5aed3337a4eeabd56e7730222d6886a6129af505b09c9ca893465", + "02020a29a301e231a091e80ee30d966fe52d72b8014f68b5cd592bd727da0770", + "c3c02f6220ca5fc7baec158a9917f67032992e823a6a20b172f0a4215b427ad3", + "826782ca28dff57ad2f19004cc163052ad139931700cf58008af544745c27f98", + "62a7bb3ab04e526f000d40b8c0a8f09b38f2e057ec439886705f16c2bd5f64ee", + "b793495f8b093985635b19813ea384a52a20f4a3d5f2ab61a5924ca09d108f79", + "27c23712c8439da0f90cc748f5d0c1c7e76c291046ba5896bfea7e3e9d667328", + "6ff954758f3ce214f5d0f9cdbe9dd463e4da0ef0e3f4dd4c675d6b61426da468", + "00bb6268a859406cdbd6b243c988ad477151a265187827f4c6786ae36354d808", + "ee3de952054dca0afd97eff8b54636c6a05e7518dc0d121dd73b2c79ae55279d", + "28bc492de119d140148c6d930d086b1a27e4eec5794c0a71461e7c41443f00d6", + "8ef2d4b72d7ce8bd4eecfb25aca2dfcc7e1e91b86d7eeb0a03b094526e4f4be1", + "ded536228f61495c036d26c499bdf8e58428de595aac479b814d12262137dd20", + "2e4e2e0a21a46e8677cfd189d1a85d6307f9fdf15fe66edf9240df5675e7502f", + "1d23a162536bfdf378c81686c45c5abafb750dc45ae7fda79bfa90cee08ab713", + "61f69d4d477c72a7d834d2c33e29daa4db3577e6f5f139ae65b398daf3cae5ff", + "9bc5e9f4b1ec76ceec39cf7774c4d4cbca512b1ec24392d413ca54ce70d37231", + "7456777909e7c77237c754942fc8718e61e4fe316a568a084680fde215dfd861", + "2b951bc65c7d11fda698f03c478896d681414d695d008470324e9404d8f9503e", + "283871f7c05bbd5dc0629741a49de773df8ccc9d55cf9c005daf23234f63702d", + "dbe9f799bf009ccf56e015e12f34ab91ee79816e5efe7e07365285a3f6b96f05", + "a28ecf6c8cb55975eba95747ad4cba85b12fdbf25f3ff4fb54b35764f4d18f05", + "944fa865dab13fd1f4c58222c06d99683f00162f2a9a2a43f693d126394a035b", + "cf524007a0f6dd39f2f9ea47df23f8eaac63b6458b7186e6864d0821bda2bd6f", + "2923e189762373bba2cc5a8ebf5e4aa1f3b6b26f653705fcd5d3d8175ec6b871", + "2b1744f84c5d6da8fa220cb696cb5fcfdb0269dd7c6cfcc01f89692d63a3ba89", + "c3c5a49819617bb58abec82b57034595f9c01e9256e6d65fd17f4ddc6c70ef9f", + "942a6b03cad1f78bfc6b7622e491885a6a2270a3dc98bc61b1f18de3f67ec8a9", + "e4a6d2c68026d03a1a3295d1a952d63254ff034a42b00fba5271a88e885168df", + "e47fcbf90145cb502f31362ccdb8b51df54131b28b33b20ddfe2be1b60b6eef8", + "3016f5c43efef2803cecbf79f4af5fa8439f0c9fd9d191b773cbd0f47d88c21e", + "5a742d667194e2c3ff8bcb4b232a9f9c4a0e65675558682b4c230832a34df425", + "f48da638098f3522deaf18c842cc1eae33c42da35773187b1576d3bc60203b33", + "31e617f5f159debe1f2ee542762f166f915273868dc1134588e6b3d313dec033", + "10ab8742d4d25b40e47b0ad9eb35c49fb61101ad818d3da5bb33c8c982e88350", + "5106f500f66fc798f2c2caf598218979d8351c4f600deede3669e4a566901654", + "62ab1dfb09d0a4ea2d7e825b71a27cfabe22e8ef9c3d4903ec1c32bcc0debd55", + "0e45d431f867687ac88bf5e7cf02bf565d6a2df773d61124e4f4f0ef20d7435a", + "966db021a254f058acd0e4dc9e9310638315e8fb0e4eb6944bd9581cf0607669", + "2295c762587565fdb3d329c7cafeeea6120be02f761d4579a10c511a265b1474", + "2555348670122d192daa4e43cb807b2c18d395e22a4f22b08b920eea456c1b80", + "eda1b422b995b84b2bfc369a4fe085e6945fe767712902040b1d2d43c1e50b84", + "d09eb86bd5f1e050f392992b28188f22dc148c1af5534d184f6f8b6800645886", + "46772eedc561b26f78014f41f5c07b8a9785f5c82dd4fed064bab8d3e036288c", + "f1797ef505054dd1d63a0216bd3761c84ee2312cec06da79eb5ee818168b0aa1", + "eeb61172f764ca628385625768dce505bc1fbcbef180bba2cd674f9a31e01aa5", + "04c7141898869d9675652d6023ea19c9fbabbb8737d3457d58a59afff6fceeb2", + "f97da0645fa61ce68b0d0ff30eed89346a61c556df0def8d953ffbd6bb8df4cf", + "c94b2f1488d0ca93418e5a9bca9a1171954d1dafc9e65f56c230ec1e9e837cd5", + "9ce1872316bea53828a8bbcbe485a05b2efac6567307015bfe81fe8056fcdce2", + "612ef4b7cd9672bfe8f82bcb099c1dda401986ef86039ffb2c452fae42d4faf1", + "38ff6918010effd989d3bb101048bd7ee0a1ae194e9304745b3067e498b3ed01", + "c20088c8dbc3117c245e20ac53a84ef0b27164fcaf07f9c882e2e790e2d3711f", + "0577e91b44b6ed783bc45a501875852f817df1e9ef98d9a090bee82634454280", + "52a760e09a080956fb689093b776c9695ef0d08a8731ce463b3952dc0e9dd005", + "64bc069783f7484ff9d3d66e1867804f7502e5c1b529a5f043e91664850af263", + "1d426fa6afd66826721829e9c223e580ab62b59ce5937765382dd0f4af6aabcd", + "6515e9d71b71f8c04b798a4c585163d432e727c01502f8581505ffbed0d2559f", + "eb147858fe0a8c33c1bce36b22df683b38c8b7f3df4706780d5e5fa9a5e637b2", + "cc2bbedf47d08eb71ef1c8f40617edeab45e5be9d840d1f08fdae5506951424c", + "fc68a45db02af89dba6aa4235ca84978c421277a5810b84f510fe9a11367c87b", + "a344f017a36c61f398ea8f5e23bd4a3a1a429b76d1415d3161b9e11fdb924d13", + "ea2958dcf28beb5a373cfb65c758af60d4cc8d561c0f42425309e385b6d77414", + "6cc006e28290c53796d7c106e5e9a73d1eef8d191ee25bd3da584103b4407243", + "48ced243921464dfa24d8a29fa267dfa7ffbef8bfd5f63123313bd2d0aa19692", + "bf52e4c1ae9edf78fc61042cbd156bd451733a238b614329e7f7750fd225c611", + "c4b6c48e6d24d587f316959ca18d29f25c7796f59d3ef3271fff07ae07312e97", + "676fbbc33a6cf25ae5c2983f5cb28aadc011559caa92ccdb8a68186250593198", + "5105888b69580180b0aad9169b97acbe23aab5998765e5973c7db65f16e4866b", + "b6f4e8b2e48156e88c0d61ce6c18168a3279cb5426f84a29f0bc34ac73571ede", + "1eb6e0812f1a4c522d1595c5280d70449332f94972d8f7db8d1f1415aa2c9256", + "4eb5d3331f4a6422fa37720969d98b68ce9ac7d86f35c0f65c0310059a893fd6", + "45960b24f33e9e84d9d6680f82ec1976475d8a51ed7bff505b0abf7c73f414fc", + "349a885d187338e71ebde201942acd12b84f15f9bd29fc7a1e8e1e7f48001357", + "1888d7a9ca2f1a1bd598837692c5d8c610315a0def29d839797586bbed5bad8e", + "ee64524c632bdfc9fd0611c09b807bdd33d931f9fbc0f6b484934f1cb88135d4", + "a254a14ceb1631178389588fa1c5fd2a6381cbb025f604ecb4dc7964ada15840", + "76777a9a38584453765ab52efae236b43a92a85ff99421a85c047fb84eaa3da3", + "18d92cb8042c26e8d803d7b61d1024351f87c0538ca54798143bcdc781e68e1f", + "97dec06eea065708e2264d2a6d106d5b70703063a107c100a919ee95699d1efb", + "b40d9220ee8093ec1322f4d5cfed2458886ca16e2af1eececbf64bf02be6c7fc", + "2cae4684e28c9501a6b79f14c1f8f8b1d3a989d4093b1eb767d3244eab9d2ca7", + "f883a6573d5d90676a55129e50f92bd74fc4e998708ccaac90bdb1f84f561896", + "dc5e3597e04e58af0de74e47b70f6414964054fb5df91f5024803708f6989646", + "02a67fe0075656f1836803885319978f1092cbb1d664f34665024e3955959f5d", + "8d745ec8cbc8c6959ad470e59ab32cdb5e6856f0040f3455dd3fa02a40e85dff", + "dc76c6119f14fddbb0c47e0b392adc61e7b1b8974b9ece529bae3f6bd2f08035", + "f0c7bf1f54b24d4ace04adb7f087d2e0773f6fe143082d0dbc3f5582b6b092fa", + "ed605c66129606237fbf01ff509c7f10ad327fb7ad27c33499c8b43412f75272", + "06a3e1ca0a8ec035f87c6152092e6ef6816bb21954a3e76541b93b80658c8d72", + "16e76bd717730602bf03d963e423e6d39303fec03c32d4bc6eb3acf3bb40f458", + "d26412e73b10ae2e6f3450be078d1a0ade6814a2259eab1f9c660c35fefb1feb", + "0b6a9b4a402cdf7c4b56f27103e832345a3f6e4aeb903ef71d24b5d1c3f845cd", + "00b62c8bf3350d339ecab6082965af7b1ddf463bcef6b8370c28d304d854971d", + "e5a7e82416e4adf7f9786661c4eb7fb0ec9bb6a9bde5cb6be82c18e790ee4924", + "e1fe687c11e1f70610fdf1f5510d010570ad6ac04e74105707e485be75164a58", + "e588fd7031d8372a68e7591d1093988bb48fbfeffb45ee7d25ce35c501be55a6", + "7e7bc3bdac44eebe0a4a7f6b353337b52742cb4b9585982fc891683d736602c1", + "1336afdf34bdb14531116f478f69ce5639eeae67148245b4d216e5da3763afe2", + "576b424aed2be1ba43092486b414a57bc187c683d41420e2c0449071463f39fb", + "9d7a13f835788e0d6a737dd75802af9bc6ca8e292005c4dc2f5020652aa09603", + "0be978080cb75ed4e86d4bd9936cddf0ab617430124fff0df750767824281efc", + "535265c74b560ac0b6032b6ccc1899f23260fc23e08220515a245e8e83ef0f18", + "e32f34523627ff97cb2f57db055a4ad51c1b2ef90fb45085a63df62b07914852", + "8d69d9e1e186da9b3713782520bfc46271a42339dc54c5856ae10f124282cd64", + "c241ee188bf771b85c663c26f213b24524b2c6383b59aad602a68ac82830d4fd", + "0b83dc3bee2950e12255517bba63209cc6430ae6549558aada9b302b55c2f838", + "936ec0f72bb9126e6b0e7a9a8ff9170a2276c01f58a3839c3c586493306fd1e9", + "f7858b1cc34f284f37d0d5159b25a6cf6f0c48289f4f53d273ca512e2f6b3cb6", + "2390fd07c1bf1e05671845fd67b4a3e08e0b9163232bb9ccbeffd09f0d9afb4a", + "4673e8ff1598ad56d378c12873067135c01b3debd165c2b6651f9b0c0625cf11", + "c93d604e69c68dade70ff709fe6417ad257be92a4a9835fa454d2806c16ea800", + "4ce5943379dd74340f399b61fc5514c2c15bcc69d181219a83ab6a0c63a448bc", + "7702f2b32b419c9778d8b7a29123c616a075f4e3ba6f8f061cd61706a69cdc2e", + "713b6b640bf7db564250778405104df6132cc21f32e1dda7321bab29e4dc0d2a", + "9b34ec47e1fa1537700e3c739ae3df87eba18dffd164df6bd095fbf26f6f43fe", + "0d238b9784cec1c8d40d99de886807a3d9a636fb27e92176d80acd2b9648ef17", + "e1532ed8113cbba8dc46ccdd529d17ba22a8d125eb5ff7df0c5a1f53141f5f6d", + "752b4541ead7be65cc804833913c1c2450b0045d92cc193cfa67b42e705bf6a4", + "852a4eab49901aee9aa2a3f6553d5ae400675bdc521dba8589f2125cd5dcd947", + "9a48587ee6a08c373ece725cbc207fa9bd5f59bbbde77782269c2bff2e5d825c", + "fdba53953d46d86fecdad6e34db511e3027e383964f6a96f262672965b9c5dda", + "b370f6ab8e67b6480da12b3800fa50f70ce91c22ce8ff82e48cbbfd68503d6e5", + "6e7320dd1201e6e8dcfaff10c61a028b1a642ed2bba555fa40d4bbe5f6355feb", + "6772bcb50862129fc357e3dc76d316a217d16fc10f0e3f76c5d0f8cd20b2e098", + "7d777e36fe2d25003e8fb420e6cee20976c0861d9b287ef845692cf2f3a2e2b5", + "353d7dc50109db8336dbfd7a5a1c5e5c7ca697f92594d4c78d9188bfea2a534a", + "06d933a0938dcab8659c0c28b03b5a981ff4ed7c8b7d7e4dcbe41c8dd56afb44", + "f99bedb4591c6b7a029bec241427eccc1c5c1e77fdb59462815d2121cdb1534b", + "d90a8e096b1909567e8013f6980db3e498faaf9cb3c7129b754f9da0afed3511", + "e4a36cca280c5094eb751b8bbf3668ae0618babb6fdd41aa4d2d023f6ac18e55", + "ccd472cf664d4aca3f9ac779e1d88c3e142552e2752307fda18fdbc8c31a4a5c", + "66f4ec0ac68457fd7052f888297d52efe8b8a4fd07dedfa7f565ff7055e57c49", + "a075450ab7b9e089a391e1f6d5fdbb0b7004b9317033520e96331eb5dd278d69", + "62d1e7124e4198d12f6e4d272282b0a15213bc64c59e241145f27d626dc6d768", + "8c568074c22d556d3ff7b90e64bc54bc5a6fad6e406b5b79c259e28c54f20fce", + "863c55666a32a365d290b92065ed2186cba6e4549fad22f6b70d281c4b088188", + "956b14cff33c6ddad4dbeee554f695d936b04e1cee28f08ec4a186fd126de558", + "eee2deab5bf053f357fce084f3be3713bbfa6ab69612aaa81d782208a6b977ea", + "dd7b23edcdfba83dd3c2b6f965cd9382a92f06003d388ee4630727267e25a1c2", + "9f161fe499182bea38fa484b9e3971b3ac170a140b7146f0789e998bf7a125c8", + "dc7b9edab18d1168b6af53c3fe467c802376c899c7331888ca5b0e4b1def78fb", + "185c24e84b4fa0afe1e5592881436557ad4178337ec5320cd0ab2bc05d347583", + "578b40354302a8042e36af010a707890fd5c06810a971179557bfb7ba851c61d", + "36148d5c39a83bf7ef9d67da239735f59a772d8f863aba96ddeda1b6ccd3c63d", + "d9fc338912027b8dce909e6ed161598671199baad5110d758d62fd5fd4ef1743", + "e10572d32e20a230fd4ed963cf58ef313beff38d376d78c411293433ccf95bd5", + "415a1eaeed6367330d30dfe5727d24beb33311a8c4bf29a766a1b0ca4f795f2e", + "42ffd86cb3f0a1aff21b121d3a83ab019992b13ea7abfd197e56f3b9d4479e31", + "8639f6c362380f5ca9b42594beddfdf416b95425c62f6cbe36f8e5cf3e3187bf", + "ea68708bd8e3dcf28db835e6d48f9ae427585642c387b3880d8d33830f3173ac", + "c68f40dc66eebb9b7dd166725452e8dc0c76009d821dd7b4116c0b8abe6e1874", + "032f39bcc5c1180c83f4558d9a75e26c07ac823357f03c6ab8f3dcea6da820af", + "093ff264e7121eb953ced1ab669f8df1ab7ca77bb56769c8e593523c1c529b05", + "f571808a3782dcc0196e40ddc758a10ba667b8f4435482f79ca273b81e78ed7a", + "321a9a588ea6944f40e345ea25c5ee33e23929108743979e322801425e76ff00", + "5751ec2f3b3c511f417ed8064396ea9bea98fc8a734bf5b9d1eb046cfbac58a8", + "730f7ba7ac0decdb2b1427d5c4e39f05282d6bab84fbaefdd935983cb4b73d69", + "774b9a7a078a2d33b9c59d5ee021ae51f7c3d1981e1f416131b975a8fa73b171", + "0be34e887a503101e291e0a7275b56917d235c74e9deed90f344eaa33e3bde96", + "dd0553ec6836c49534a7193c08626ed70e18633d6d55550e83af4cb80b5d1208", + "086eb9ca8e1561e1cc20dcbcbe4255034bf9e995ac9cd425cd3cd04976698109", + "2cf5ddc2eea07914bd33e61935de7afbf5deeb6f296ed1a92fe24f8a272ec20f", + "d64cb84bed985d8349a855da2dbedac537cbb75316f694ed2569e449c7c93910", + "f1b1e6bdd83c9cd476fb9b371b0b96bd17ed4543f870ffcc5165a72eddd80324", + "138e7e92d38f21424064aa6202963d5ab36013d2cdaab2aab59604b109204d2c", + "83bafa2d6d17aff2b2c673b3a35f627c3f27edcda121af044b5f8357f6d8fd36", + "3242a78b7c76dd5879349ec45c52a944437457afe862e7c37ce963d8ab72293a", + "21aecce39bc8431b8a64a588ee3cff1ba6664fab4995e0de425b9ca1b827a543", + "202a12bdce15e4e4f4537fe6b0b13105a387c1bc9e04245674efb65fddf9e150", + "2ba225f7e0dc4d8ea5c18588f1ce5abab0a306772c3d48e4701b714b5c198353", + "542da92cd8f93c0e018cb0e58c5c0011e843c58d517cbc3502cd8bd30b0f5457", + "c5f75ddb098d45e7bda2bf736a46bda1eccf4dd9129e5ecdd24d98ad66a87059", + "efde7f01b410216607cb43db27224db733c8ba8f924bf4ce9118666715429759", + "36e799814a243bbd145f765cfeb01890e1dbc22722fc35e7c8da81a053bb6c5b", + "ad36786fee03b6c34313736720155c05d3d6a102c06e15d2087540322fcf2f6e", + "c30cb3186e6e6056676007a408bdc31772e782f2476832accd8fff3647f4ce71", + "2cb9ed472b5b67df11adafc7d24510789ac92b0ab94f78f7a4d4ffa5aec44376", + "9ee2b04d4b52d33cdc7caa1fc3822d401fc172e90c0c69b75f74dac6ddbd3478", + "d89f8cd089faadff062dff957e7bd664ce780553571f03dfcf7a4538a0d52179", + "19a8e0a88c914fc9ef953f0e528141d08c900519709e861e7a15092ec51b0183", + "08523965b56a370f0820268bcd46f0e40f5a08efa7ffaf2d0be589f7e06b0c8f", + "32e67e59cc228bdd03aecbfce663ef2cd0beb8590f683f758b7ab1f18096a596", + "e567e08bc47c838da307ccd4f4714769af76d5d3ece06ff4ad9de4b9b43139a0", + "134f57164aa8e71815e96108a500ec2d6e2d5fed1d4362bdd5418dcbe8440aa6", + "f1a289323e8bbf8b47b74fb765d7bc34778e89fbf838b88c0b2e89a0ac061cb1", + "54f8f2e4917aa481c740c76482d2dde80149d66bd5a63ea6c2856c360a05b3b9", + "6926c235956272d374746848703c42ddaaa97d215635400c6e2b09950f8238ba", + "a8b268e3f612955aebfe028c46a53785323e89bb0d50381b6248fc6addac7bc3", + "4260cd297499c52b5d60112cc949b01df1ecf08d0a94a9e8fc225307d13cbcd1", + "473015561a650bdce6c8235764fd5a6d913fa56c8f66f85b9745be11a79e26da", + "fb8d5378c2b46b28f21cf7a11acb3a7f157466a893c6d6bbcc763605dd5529db", + "e5c94bf0236858941283e7cfabd5395882a754aed7cf0a279b1d1e06cc5543de", + "e096b09e819b3235d3d9d02ef9440ffe32e91be897b424bfad28164e2d41ade2", + "357f824d32e0b724b05deb850f616c87a9397c6b54bd71c2e7453c20192b3ee3", + "4e0b5522a384c3b840db7f130e2da193c29ac1cf4d45c7b3ad60bfda5163b4fc", + "cc2e72969f9790fa3e08ca607d5fa967cc92207565ae8a459237aeb737f2f442", + "39b23bb017a77c8eecb86ab1f5b735bd99427884727bd40eba86e2b7422d4fa8", + "034694cacb85977d25e5d48f4d10708135b7fe59e60d255f8358cf1adbbea2f1", + "91218eadb18ff53b46944433c47704be6691f10a649c5cd0ca82a0caff7b0616", + "45b56b405e78b3ab8bb5f780fcc83eb3d81ddf54707e508a4054214a3691169a", + "a970e45e0ab5f38d643b7c26d1ed5ccfc8c4ba8f84752cbaa3bc94a8a0b69df4", + "90fa307b6a12428fc724392f49603ff7d77b5bcdc7ac30a96b2888d3ca1b2c0c", + "13d7a3800898f5de147a8a7847ef9fa2439526b9c0bb505cf8010e77bf5a519f", + "74d4c8184a3be922ef318910d8b2e6d9bd36c56e0001f70af73efc47bf885cf2", + "27a7299a88bee7768ad0ac57dcd758ad7ee47b6dd5f696d4635e389767641ffd", + "30d5ff22255caab9d5284c983ac59d43325da680597a35d382029e4addbe4898", + "f993bd1866e649a213df6eec43f88d865958b77ea9bf5f08ac85c628dd9c767f", + "0a7f7b96ff5b9f0c55e3d94bb4d48cc7c943f66a74d61b1de54276332db4ea58", + "8e5ff2be8bd9d72b1eabe99f6fccdecfbf0e07d37a476f8b8842b863a0ffcea6", + "afac9f324c14fb4684169c85740bc43f79ca664be36f7370ef583ed862f28e96", + "8b5c202932a7030531cbff0d3ebba8e4a97f815cadb76289fbc6fc618676c0cc", + "74a8b8fa2237d295890919be5cddb90a8ecd7b599180c5901aecc81466a6f9ec", + "ee3a4250218fa673acf905db87e6c62ad55eb160b192c18bdea9de4586d91a3e", + "ff17d4450869a11848858675f8a40c1b671f0d3134959fdcbf96a31ebf723d00", + "fd61712e7f3b508a4bebf2b2fb10852bb95144414e976b09faba62d577b4f271", + "0e8ffac9c804368584f055c919e90cca0a6b8ce9517cdcfb99bab642ab30697c", + "692aa1184a8624e6430eb494760de40811473b20dcac8e2127be3828a26f370b", + "07ec98c3b62450ca95b0c37eef23ac7bc864bf67d863789b9991250ba389dde1", + "5def5e14261afa6a3a51860d6c57d24bb1e0b1200b1acd3e52ab71aa742739a9", + "202c1e95dc1d876b2e592d586958e1092a470002dda77771b325fb28ccfa94a9", + "f13fe26c361d430a5c0fe8f07831c1f74b338878a8c9b9c1cf5f781cffb947ac", + "e351e88625b9f552a4db58bfd31df853996e712ab703aecfbd4110b6b8fe9fcb", + "3031dc47f9c75ab382082838ad64703d4ac2d53bad65de5f6871b95f1f51d305", + "7e9f9041bec93b6e4a1a6777bba28e24c9130ca5f5d0ab73b797834675df5603", + "adf50cb92f08127de44d829da97b9939f0cd94d2a17187b1065c5c80387fa7b0", + "6638cd3658e27e7b23462a52c2b77006f30e1493ce7ac20040a586223fa3608f", + "423fc4c8f8b8161cb8750585d0a7ad03c45b29b8934c10dc25a0878ec3e8c3b0", + "fb4b6eec8897f1064b46de91c1539ccfb281df49df49ec2d3b2e70aeca1fe642", + "c035203e267d0ea71322755085a49fabbe4b27a5ed44a807ff09d3443c016d24", + "2f8666bffa567337d67017a69cbd30c4afbf9cba1a5573c031cf38b685ab912e", + "7b72084df6bbaf4674390be1dcdf4dcabe444df6ae3cef6d335a397b00b8445b", + "d8160bfd5ea1c4ba6154f7b5f3ede8943f805dc5c52f90c815c4d402e12ec16b", + "75e2a3615e041093ad641b6bc2f04c47dde26ef4770abadf54de252a6bf86cc2", + "e21c75508acd422718b3bc48b5cac0bf13d300a58fe5a28f6d3b34cd3bf707f6", + "0a5b7258c565444b96daa710933bb0f9c275ceb0a21ac7f5fd7bb67cf431d89f", + "776373972d7b001465784dd2040cd4e6fca577f70bacb6cb1b6889b0aa8bf4bb", + "98b890b602d6f75caab2fa3087d8d8769a22e7df58a7e1ecdeecb5d8a89f28cc", + "110b6a99e45de3d957617e721e9a6e7ec778738acd149cd82e7ca47699ecb07e", + "cc458e336f650942a3f457b3613ebd05e6f23d6c5ed1d93e17b53deb0803168c", + "86ec64abb2db4c4b950322903ad54688c63bcc57d4504d383a79bb9f65510548", + "41fc95209709099f44ec617b64108070a0e5f71ff42fdd066357ccd78f7ac259", + "250dc15c36ac5b0af08fa959382182d2f12fae0f715785d7a7d14c4af72020d2", + "acdd44ab15d385544fed20dcccfc4451073abab85e6a2d0fa9f2fd43bf6a53a0", + "5daea3793af80ca23ece52691af6193516bdfd4d47c6b3b146d65c4f1b321cd7", + "b6636aba359a1105da263a01c4d1130b89ac28e812994235e3d02332954c20a2", + "80dd2dd9d6011f32fd7f535d6402d50e30ffdc22f94a24514514f0cf87586e6c", + "6516a4499b3c6a8c3ae81af5d28d02776b0a0a15ff542a363d0c85fdd1746814", + "8c0a653a30916469606924add5edb161a1c792198b70a40efe185e1d06734def", + "e1ef4329ffbd97cfb0639ad0a9306cec8303512bc573d2e7d1ff3198a3ef876e", + "e6157fceb2ec190502515d179fd3e917b741a5d802a8fb46f7133d89f36e5659", + "1171b867f9475b215e842eca3fb54e6700430b7935d3b87cafcfebc387ee9bd8", + "c551a320e4f36a0c1eba0b5760e5ae8bf75760ae20bab905596f3b3662677441", + "bbb16b75e0005a1b91d7c8da921be9ebca33b1161957608d33d16bc99b3c9c3a", + "fa7db095679214d85dc658d3b782f2a60e26456662b16753cac7a124eb37e7b9", + "fe0df3fd3fdc4e8620d2ee43f02b219ca204c0978e6e8d51dc3ddf894b1225b0", + "17c99eb0164df2d08c18cc9292cbef54db78953e78b10b538728f5f7f5d43077", + "6b92981882e8494d0be50682ae888fe35989913994df6b7676f57f377c9998e4", + "a1af602890909325843da1563e274aca1e37dc3ca519fb271f209d662789d439", + "21a18f13d2399df1d43bd9699124b0c65b96ef8b750752bbbb882ff5b8f5cf10", + "127a24346d0e682ef972b1d85f7f7ad708d6cc5b20dacf4190918ba44127e01c", + "4e2656ade5da9883ee116bf0132416e7d6e426b0f8792886ac66549a47a3d606", + "058997696460aaf11ecc3d9dee04bf7cd256a642343d05503309031764748d32", + "9b4737d5613a715468ef9a99570b7fff4dadcedc950f9743132409f8a2127938", + "7f6b5cc0f62460f66ad46417ff1b5cada436fbfb92c34cec291772eeaf0af1b2", + "fca46619267f63af3b83f75a52d853b68b7437300894f8498851986669a4075e", + "c6dbdc0ebd7ae1f47699c7d443d805cad80aeaa5dedace47d4b1f298b05f40e6", + "5ff009d5f6cb22d9ec9bcec33d5728976a0775042b4dca8c484a6c0c293523bb", + "c542c1640e2aa3d7240c65758268f8b072a030c98cecefd3edd5d250c4018b04", + "5efd80dc275d0e7069d0275e6059c26504360ea423cca6facb1e514ef2f1140d", + "703a2e907e22b184a55b6c0b14deb71e3fcd8274c90fb96eba18eabc3934b813", + "d613ce3f914241d38995d452aa9917cf64821140fb4d4860ebd015abdba54420", + "ab3228eae4609c88c9480b19b5dd45a980afabb662af0db40266cdb9e08b2245", + "b9b4bb4988a3e7c492ec4e976adcb3bac0e0ccf05c17f83f3b906e8db08bc364", + "159186a28b57ca58ec5bae13213635613f4fabfa0d43a3e711585422c749a970", + "5bb73b4e4f4f4eccdf9a0648e2daa45fedc44b1c88f2f70da5cdd9aee55e3872", + "df39d44e474eaf72b4043a17c2e718eafe76f9fb255ac71d53c3f564b1dcef90", + "f94ecf40692ebaca40b9d9c6c5eef07178358d6e3d2b4455b719e26fce5e3ab1", + "34254a7095ffabf24e7f335282dc47d5c708bf0153b3ab51cdc07ad98352c8bc", + "88aac52a1589c4eaaeac86581059d10e083b92f858d935401eeb8dd7d2acc1cd", + "fdc1c7e7ac262b52e1d7bdefd53ccf868d84ff8c7b9d3a7c57083152844adfd9", + "b5389562b6f491272bae1d8de46c9153f0f9d5ecbc855fb425b9e923c6daa4db", + "e4fa2c18279bdae39e8507d60b970d573e72e2afd3ea6e21b733907c4eeca35d", + "361683e4ae90542b6e0726a8da228580dfe7943553d71a962de4ebd1416ca48b", + "3bee3beb0bce09998bdb8577a3cc612e79fc0361337160a0fdbc5c00bb5a6e8b", + "7f76661e6e3bdea469a45fd6b2e0e32404a1d1d7ac9bea9e636dbdeaa58cfc0c", + "8e0d76f97d4e2177648a9a313ba6c44b49f05b41e25ddcc5b30fb7833a169161", + "9795ebed48408d86681fbeb80843e788660421d2caf79e2c8d148146c2f8b920", + "ce2b1cbab760b32cde27eb67e6528c373f4d3d12c9834ce8dc02b11651011d9e", + "0c59afb50ce46875ea859e55c8989a7c93960268bc1812efea4bbb3fdb760202", + "da0bd1de6dddb846992e22146aea0248766ed9291bc38cd1053012d68b9ba3e6", + "c6c5129968f26417d0e3a2ce4213d06fc802536d631d4f8ec7bff76ec9818919", + "b20dc0337dba63d7c042d31ab30c2d3c73cb0b476be03dab0d728d99db0fc2da", + "3269fcdd2fc54878538d434ee0e5f4b34556f244f6fd28732ae870cf3738bd89", + "9c3720ed4f0827c1a86ca82971c409f5544645eabe2f43e6fbaf34e9b3d7368a", + "75d5279dcd54a3ed52832e83eb88328ce34d519e1af03eef72f7b16d67881ec6", + "c7936e1b8c231c635f0e8ba4f36075c0a5c7542a0a88b152af5fd73eb8e38b43", + "fc2ad37687baa7973e4bae13a81a816293cc711e7b431277f9d47b3868b558d4", + "12e80cb945f289604b58ae017fb1c6b8178e15f311dd4924f19a4002ceb49c30", + "494f1bec1c9c4569762f3103f19b6e581d5540da664ba287aefa0bbbb94c2449", + "11517f6e61ee2cb16705f6c6bf34a2451f3e37105e5aadd83c52beecfa17ed8e", + "76df00e357f52119a3e3c62db4b0858c5d9e33377385f8704654b754d407a30b", + "d919b2a126ee39d024aa1621f40602090718d6373e5864806521774ae69ea0b4", + "4360ca292883b9dcb21cb357d35ca7e61506b33aaf843db3f6a1bf10f88c515a", + "c6fbe99e3ccf2b96854ec67e5abd12e924534e6874c1f4e18227df5d5773189f", + "10b83486cc3d2ca0487baa7ee8887aca724f22e9558c38de01003e7ee22f80a0", + "18f5f675de975f81aadeaa049512d0d66aee6649b7865bf50c438ecbdaa3dc2c", + "a0114fd6cdf72544a0f3f730fc45ac25970483d9bc934bb774db0576a80a2e89", + "92c42d3a78e24db27da4bbeaf1030739003ff7754aed25a1719b8580e78d87af", + "87d596f6c9516f7aa6c18b7a8d4402aff0d97a7a7eaecc215ca8e1bd4281b1b6", + "8901ca29aaa8d83931130e740cbf0610967e702a881b09d91cb25acbaa47639a", + "1f67f6fe82ca848b1ce3641a11eb22816d3ff70311ff667cd75ad73ff131c09c", + "a7f925eb02280114bd0e52c1e5596a1621e56d04c99a4e4ea3ed6f1bea98dcc6", + "52ecf62f8614b01ab2501ec6c59f67f048c54cb067f6478a6b23104d02eee8cf", + "f06007497879fb6b3aa7434645349807808248d07ef4d56875c52582681e6d25", + "27e1971acd26637453bc63ed407220e9d2501d402c94a2e860c91d6c4a2f6c14", + "bfd3962e02349397e2b8066058a3a28c6a314375601319a98680331b6bc757c7", + "f71326d7547c7a58518dfdc7772e5765343c1eb9fbd1e9c12fcd3a5445861b16", + "e647ac7c0aff2e7254447e7b7d2c84ec218741f9670c51b07372e8cd13d8820f", + "5224145ff25631da5ad4f9cffa36f717a68bc9fa212e10c95a0b03903024bc4a", + "fa31afa9230ab8a86f13aa417b953ddfe58edf9a97b4d5dd2e2e2dcd0eda68ae", + "67999048949eca1c433283e9fa1aa516fd95ff03952df4e7fa37c904115b9640", + "5928dbc7598516491596eec01b9c106c27410d57fd7cabdd6ac737b5f64ef2c3", + "6a52e29153df7da5855e49db656e90345d713fb63ce28075fae12dc27fd76f96", + "d08aea1e1736d1e71dbccaddd0073e42a31a35208346300ba09d1da65d314383", + "57381fb1a872b8aee2c9a9eebe8e6fa3f11dcf9c58346ed9e45bab312312925a", + "e157067848661402cf4a2677169dc44f15face56058b75a32c6b280493309a0c", + "08c778d125825620e7b53710056efcb4906077be6664b7578879cd77c6b93a02", + "8021cc0bd3f4b70df22a286fcfef87c43c8dbd3adae2e0089da5bf81d8854a82", + "9aab2b6ba0abdb87db89adc0d949712ed4d23f4b29f4be39e6ce93c103ea7b48", + "79a2947d2f3a8f3e8d56a929f04449b386cebce40d96ed7947062a1d0337e963", + "d31e6a1e2b1a6fd346be1ba15a7c3f335fe672fdf18411984f755032ab5ac272", + "5719cc1c3cdce3049224bb62ed7f92a31056e66e0304c129ecccc819697ae4db", + "cc432acaf8074e3799728f1498cd3245e15cbdf3faca35a5799883b246ef3bb7", + "6ed2fa13aa6d363343bd0761376c3f7b94fea577b4e17b1ccbfa218bab87c12e", + "42ba8ba467d9f617604706962d665bbdaae2235a49b0c4593aa284103ec7c066", + "ee16ea7a9697ff688999e9e8a4010afe957e6020c06f01471c26c44fb37ae54e", + "ba0f4a376766855766ea39118626bc03354d6003113a9842e33a626aa8e66705", + "9a66236ad035ddbcdae275541d8946c05ace61ad4d0ab6ed50d32dcd49e4279d", + "0d2d5cc1c0c8ed4be70bcbd7117caa3fbe9eae9785ca1dfd2a0ad00542f99d9d", + "498bb6d790db51609fe4b76002bdc34f186953e846efb34cc3886cf780c34fd8", + "2a3065e9c8130888b8bb64e06c52e81b3d4b97250083a06aade4cbcb5c883248", + "bdf8c4df6cbeca63d3e0960a3396bd2618c714b3805ea6002940a08bdc943f42", + "941eb900445670ae3f31f73a5984689c3353df2de43cef774b08826d70b31e33", + "ef1c54a08b5f49c0043754151acf97e47f67f0e7f65a16ca45d49d4f81365000", + "1849f02dd6f23fe7abab8ca837e57b3dcdf4c7d167adf28de031af612c5b8da5", + "056a65fe9221b44f89777ec89cbcf570dd580b387fc165e26fd58784a7e2aad7", + "620ebeda0e49b2d1cb607d0f67c66c9b3d49f9e422c2a81dc4c640ebbcce6b53", + "6d6e64105a393e8664c7bdcd4f495dc17e2574f804128c3df827be1a40e0e179", + "1caa0115b202efd2e3f2606e8674d812feff73014bc0a86a649aca0a84d52274", + "254b74f7f28fc387a3cc7bed438632ba86ccbde2a9f5fb98632e826728757ce7", + "2cd1c523833c931f0db1b72a334ad6739a08445852fd9cbcc3e71ec821c6d3f7", + "e4a8e8bcbf8baa688ecc8d700e36bab1f250d9d2b0cebd78dd52155462f5b4d1", + "e4c66ecfea654a90a73d314d36b2128978357668d0b2b362d27110ced767cef1", + "a5c15c876d82186aeed499f657ff7b50abe4e7469c332e49861d6a268eedb213", + "3a6c0e5cc800d183d0a9cc7334b95856d74e3ca186b937303c88e7d81a65c324", + "0e0adf94e9a1462e8b4aafe1ac480f2f4a97484bce16961511c27ba4351e8042", + "c2a4709d5d8020ab9c490c784ab528329ce216c2f7515dd9f9970918a50a5b51", + "91b3b47002d295f3d89470139ce210241407269738d2736faa47b4ee20778b82", + "617b635c37ba52139fe16edc8562a2ecdfea8d59a3055b0c5ecd4d2f7bb2c28e", + "5478cc071ea965bff2837b4f8f8577611f839ff029a6547a9c766177b9658691", + "e1cd112dcf5b92c4f56f10cf1b32554f51de55a3c8b886cf54b0f1f7c67ef492", + "c0b7a87125b532af1e60ed4aab4dec881c32ca8d9091e2d160e92059749bfd92", + "49ec7a18b459161c62b749674d5ea9a72f82f7bdeffc80d5545c3c954ba3879d", + "b29c9f86cf46fa1add71368287b3332fb04fac32053cef2124e8a4c5b33bb3a5", + "cb44aa9ecf42342540d32e0b7c0179195a063aecb2a2b3c33e70f1e8c6a9d4ab", + "2c0671d271f30877a15a51c2ba08cabc81f41b55bf8b42dc3760c67f7f4187ca", + "3e96d04c7ce5296fef02f5295967baebf0b8134b1697508ea498d5a702a6c2f1", + "6758f8411b32b58f7048f480078b1272687ebd5bab32d4056b0ed7163cf7d1f5", + "37da3001ba6adaebfca8281050036f81220cf5b76a1b701d893e3a9c7120421c", + "729d6ce70da10258f3be909d16b14113db3575f025ba896b95336793f3f6df9e", + "7e65d6142232066559c0f8aee3f3457849741eb70d6a22eed236ba88fec83702", + "97f1830a6bce50747843421000628fe8299714d4ea2be4c471fb30a9b91b705c", + "4b39d7376553d04ace6021808511be1f55e8c52beb3ffe86c3ac008f2dd48484", + "3a500367e55675b65bfe2a7b4afda8ea3f8d8eebe0ae7e8b84e90f4a3c1b6eb3", + "3d60a2a210cea5214f6e7fc7255a2ad2fa20a8eb20c58131e7a22ea3cf950ec2", + "de12ae7ccdd7c89e9047c51ef6dbcdf122804a53c13ab9294567122c57081181", + "3b7d79c3b0918d5da7d86f189a296bd78b7762ea5fc5bd998c4a60aac486bb36", + "4e33ef08b08ca3e30cc54e9c97396b7509437996a948ff253d746ef4859af392", + "75ecaff41d363dfb7b2db98002f8c39785b5ccb48bb03140c15b845c317da4ba", + "1d6a7894a87b652eb7dc262566ce0504b7b86d02dc20efd1d7d14ed0afbe1c2d", + "676632d7f963285f8cdd18a2c172467016e89cd1aab8fe076986371ea5458c6a", + "a045928ba6807f50cd4f9008f651dca71539d59ceaa5e76bc4f7db0df27fda00", + "c7728cd99c1ed41882e12a521e28deeeb873b86e8c0008066a96d6a906766bde", + "1b00c296c1e6bacdda0a1a0b7d98b017caf370c66e8c12de1e99241820732d8d", + "02a7184ce6e711f919377f3d611ccb027e8c005a5d744a35466c7078ec8037a7", + "965864384ad16749d0b1430874352870ae289a6637519b5561790ed30bcd47cc", + "eefdc9383cc5f9d970adbce6ecf8ab384834343a1dd35b8e10c74cc9e9dc9780", + "07e99bac98692f2d42840d108074800c4530bc32889bf0a5a05f53bc6e54687d", + "df6c69bd073ed11405ff51f1a68191bbdd1f8d0d5a0dc19a9b2934613ec4af41", + "82c5bf31c81c6ec82d4c73223d8f4812681b636c1edf9fb4fcaec5a2839c5d21", + "8e3f6161aecaa495b71b4bce000ca44093c2e6f1d15eb5047bae131a87c94d2e", + "f59ef4d57c8caf82ee3886a8328ce822ffe7c3d14c7c36378c3c96010b6ee14f", + "98a3f58267ea49d05a486e83fa5bdc7e0429a71c10f1e194a937a71e4836b749", + "757193b9561c25895091290e6cbdf4f5d4de68ec1929ad6150929d85bba00dd8", + "20e5a63bc545a29a553cc940c90bc93d720965801b4f81c0f1f2efce2c9486cd", + "1161662085a19d988312b8417641f91e023df6b432f09310f0d51dd5fd37b0d2", + "074a81c334a70a4d9d9205a4a6e227829ddc5d4d5f760c481c61c9681b535185", + "8142bc954748af41bdda0c2b1c9814cbb274fe44b3a90009aa33cf7745114dcb", + "de26f575dd20c66b60e87eef8fb9d7623079837789764e8b7969ac853870f183", + "5a3edb67a352605edb3d8e5794dd6ee39875793b0e08f0b00d7ed03813e580eb", + "a7a7bfd9a878d33f0ce8a2443347bfe4be4c0b2b2ca2b51704fe8e075ec95718", + "0627c9f6b51b9fe8fc9d7b086e64ccec4f971896ec78c7d3a7b3a3f13a178e67", + "6610fc13a7f11b3e99aa200301843fceaaea138e5a3a555477258a7518074d9a", + "38b480cb3c3c458a4957c8bb96de3bf497c18cdb8caec2a4acffafa11f833013", + "97855ae70b649efeadab2e32757433b38547477c3a71502e248fbb8c369a9f9b", + "eb639a98962f612340b9c018f78afca347484bf7baa7832a883b61ff1021612a", + "dfd66810eba7d0ce986d4892abbc803c12f8c4ccec4d686d3418356f2a220c4e", + "52dbf74f07d442714918e72040e42451fd9df5023273c80d94856c2dda5c5568", + "02124bb863d96e540b2f7a11b0ff380b4d921d60e4f39ba92676a49098e7c285", + "15ba047510e01686c84058389f31a5b2abe846b37652dbeb8a9331a24014e1e2", + "91e27d2019f55167d78df2360e8861ab1379780a5d7bee20901f5671cc0b9761", + "fc69ab0fbb160736559363df99bb0d1027ffdcc37407ea6fd80995799c58d531", + "7d2e159662b4b9ef40a67324a05c7d3b98f67938fe101b581257180ea277daa1", + "86eb737153961353bc534f5b9031b624f823712b7000adb9419a5c8c6f3a3a59", + "002b2bdad4e1678e212595fb5c3f5224da5ed6d9fffb37d92b49c4b8e6d01ce1", + "1c2592ab17598a1c281d497499153780c75d649f3538680d40d795a2ad976a09", + "2f7f031d5e0615f2351d5bae3f040742a6179f162c0017ace9eef1570ab77c2a", + "73bebefbcb9ddd0abce6e3a6d28fb94fd79b07e56c4e36f2a0ea6f85bd0d5646", + "e98fe44c7cf1459aef87a721a67689d28ef8fa1bf7fe30d06a45f612b8c7f340", + "0121f00f27497b5976d16a9fc219e9446c5c095c85a10b2ba4e3aeb03e596e19", + "37489e05029e55488ae132ee15b66c2e34783922d661b3ac806cc9c90f36251c", + "407720aa0d27a2fdabd038f96d4dadafcf13a4d9b9886fbb5563e1084a62eb9e", + "9433997c06777619983725ea9db3959909a55d9885a3f848bdf7316a57641da0", + "7a8e9e9e589b814ac67ef6f8db17f1ee8aa4ce44d4d7777a9c87c44edb0e7ccd", + "62f354e78e54aad53dd065ab7742c88043c03aca88850965180d35fd1acf5d3e", + "63c1bfd8aa26304f310e19644349a0e4b8b59bf8e547bb7a65cea5bc4d27fa62", + "526a9d8d0fea4560380a96b06ed4a27d1d0a17402cab827dd7d8feddc4cffc37", + "3299b983304562e7910b30566f0bbcbb2b57386ca2ac873a7a47928fc619ba36", + "6a77226544e9d91d32267cd5b962933e5b88595b3ed49ab907d14a14b20f46f0", + "5b0c315a1a3149796b59fddddf86808d065c536bd4354c1dfdb1163a55a60c19", + "7d2d027d89fdaa289aa5b029934957255aa601bf1598ee162021fd9e9d26cc05", + "bd579a28140d0ccb3e23877b7791a58290a1b28588238a307065627928b0b271", + "ef251e3ac8af728da4c8ab76cf4cff7a12f386c4658ee1ba9b0311b63b3b5b9f", + "6fc5dcb526ca9f70e63512a88a87840c33e05f08bf066ff9084b6e6e78f187d1", + "92751b1d7d1b9917f0474a3aad00bd4f12a2b1a8e41183337a1d5c43e0c0687d", + "66d516afcba118038d6e40989ce77b97e8d2fd695ccc338d28e73066b23a671c", + "bf91636ac35720a43612187ef74ec80f1f1798bf58e67a744ca34c1ef90e7cd2", + "794045405ccf9aaa6606c7662eca8cdebb7bc9198632cd66584e737698350b48", + "6926daffd99b8fd2694ce6c0500152a5ad49a2bb45bddab6fee9bd4d9dc8b37a", + "ada328a0822a782281f46d68c39160f08ee299437ea75af313c8e29a0c91ea68", + "35ad5df5e06fa8357ed9c684a96365306653c875719b9a88120f27f0099b3fca", + "bed76d3b89734c615778b05cc83fac29db93419cba3d22582c170b94c65ba2cf", + "ab887fa562b604bd9d376ca2b42d8cb7f1c2f64d8f94ee80274f3c3e8e650ed7", + "57a73e9f238b5da0308918326b30d23a9b9360cfce6982aae399351941ca1d49", + "693ae769d7d168489c9e414c1d23981568b0fe8cf896b0cada2c290ce52eb2dd", + "c31227bbd0b5739c5442493d792aac08ec4f34f5de1171504368a779e37c1031", + "405e86a81d99f44ffff9ea1d4ab089adc796c35e6be041c83af611b164fa990a", + "b2ac5501e0c077d40249700cfe78239d4e465cc17d8a1fab6c0440e5b4156f4f", + "fb66eeda72a7e6bd5b95b55a1fdf9b4c54dac566a43ee6f4d02caebe77169781", + "225030bf730bf20dc3ef8ff463627183c9ada047ac47ac05a647a1b19a39788b", + "a7da10377fef19bb2e09a4b050388636d41568cf50223c78b6090582fdaa2148", + "fccd78fedf31538ca88da25a3c4e5e993962cccd75922220c138787be5042223", + "815be52131fac962e77b3c820828fef088c352bdf3ef3b03cd623e9373b2b42e", + "ab368fdfef9bed0272bf508c121c8cc9c9336932077b6872f8d950376b70c326", + "bf92b3bc224cc8ba0b979ed9034202d8cd35eb465f399d55b4ff7ebb12d2a008", + "5ff1e602b95d9830234a8fb0956fa81178906af91f56d8854cb3e87616240026", + "93e39499dbc362f318b2e178915a3f9f9178a3c98bda3c6a7ca9acf78a4cdc6b", + "7e6513345ec057a3fc301bfeafe7644c191cbfce6cfb4e737d501c4961ec4a82", + "ff4ad5b7cd5fb87f7f1f02e68b33d8b96fb9910552839d95949db2853e9f1a31", + "0bb29ec4d5a81d3321060cd4fb06445dc05c748a779400afda20c3bc18562850", + "5a6566f6ad6ec41c007c2f2ed08c06e2f3cc6ec3549f7454ff231a15eb7667dc", + "a01504fb4fcb7fa02a2089d7060792b164da4b8f079b78b1d0ecf16b4ddc10bd", + "3f427b41a9000545707e8fe9ef8ed52dc6b3e45a2e29dd994034a83a528e03be", + "d325db1232e7649dcc57b710d214cd84c9eac22a838ad9e4d0449dc727176d2a", + "1382221cac26f8d5cc2e176d0b61f5824a9d42838b842e46f0595d73f1c79964", + "51a8d8b44119b778db02994f0d8881e5db8825ed43f88fb6a670c732e0f46233", + "6844781f30a6df1e1b54189331c10d6c2b0883f01c7548c22a0e6cba3b7cef69", + "f2e46779887c48c64f9c865512c45ae2fcf91ba337b0646921439dc530d03945", + "1862b070ad856fafd24c65c809e1729d4a92ddd9d62bc0a7b69ce442734d8b40", + "647f5ce3356efdf3b40e647460414fe1ab09a7a2622e3e1df1ab701ef668931f", + "936282821a29544b4952bf5c13b4b4a38e850de4b732f6ccb85690c57efd71e2", + "d514125ed5192cccb6d4d79840e2e8be27dae6cb34788dd383d073a1816c890e", + "218dc886128fc66f8e4617fcd977303bd9843efb3788f6b6c3fd7768277eb610", + "040e3099d149e0526e952f0a76998d7fb83deabe50a248bcb2cf65b1ccce8cb4", + "047887aafe5139a73ee42caeb3b4d4caea63609173b87350f4a586c63e1fcdc5", + "8d91dd3150488112948eb867754ca9a4d71944a9fe1b85c80c205914a890cbb6", + "521d0bdc97ad570e8b7b55f9487666ff41eeb004a83b7b75e641fcad24383ab8", + "de2f79530d43bcd76a221efb403ece0f7d8d3d6af61a956f0fb82777392a0a34", + "4d3bc10b295fd36fcd6034f60c9712da872ca57861a34d14722b0f3f552d024d", + "422dc21ea3472e0f454901b3597f3ce28ba0ef6ef61855e719eeb8f837c6925d", + "351bde8b328515c6c4c1987c4c04e72a76dfe38e2c67d96fe86ce1587178286d", + "f03a8f415b3ea2542fec0ab97bfd14587262eb4af72c815798d95f8e8a06037d", + "97b6c7a7a7fa8209a3187f819e67f436221c90c5aaeef60f9b85fb60f8c42fce", + "560a23a22c221327caccdaee0e7e3b54c485fcc972b133bcd1cd2b6c1f2068de", + "be7c017a9d124ca51f1fa662ea43bb2ad60886c97fe476da27f069cd837b5208", + "db46327a012ef6a55f38bd6b4a8f49d68f98b8ce75f0d898a1cfc5552fe0b6c2", + "14f5f6f0e351e978d8354b024ae49833db2634750b02ccf3142048efd6bf6517", + "93592c7e4d4b5237121f25fef8d980e56e0a86469734594ec531d3568a3d8db6", + "ceab796dd4e7dc4222ca1f67bb4ee0c13dd6edfd744ad36e5823e5392ad49f33", + "a1a1aa514f943047cefabe9f3b579b43bad91395cdebc1fdaeb9009d9f5b1865", + "7868be8dd2e28482f12fc34347a9c58bded0c9ba481874a6d14efec9cf8623af", + "9bb08a3a334657a0d760fd8369dfd5325965243fcb570c18cdb4e89998aeebb8", + "590201cfdde55f6626aacde9e6e4da5c0975f3716ae237b2f428471b51a8f0c0", + "160c7d459c1e83d7b27c3194eda1a9018123d8d7581da8bd7fddbfe2a6241ebc", + "0d487a65d96fc45945aaa8ff893a956a0db2b3717c7bf16c3722b44bac47e88d", + "c0303490ba09d9ed9ac4a5dad1e85a30a5c9661b87befab5c4115e9985ffed52", + "f343c09fabfcbb5b40032ab67fc3ed3e215f90d611ea60e50ff4900ad7e2b742", + "8da1c700be5ebe13432edd42d21e6a85c34fd3b3a79c00479aa45f78b0f0fdb3", + "4ad7adb6d99d7093ab244819580123c30a5d0040b60873c64665ed8150938565", + "eda6d8307b625e1fe9752c356e7dde732717cb5b847b1689fd5f8ee251623606", + "cdb406e21af293c0ba9eb9144e1398c567795769ab4f4bb16481dc3d7c97fe35", + "d33b349957417ae326e337276700feda661bb8b56028d7b8796f10b56ee446bd", + "6829c59ffe1b5793cf1b9038fbab728923d5c181dc6818f8ae359b53b497edfd", + "1681a355a2f903b08b999b8b6e68354d14bbb227361b10f8865ec153fbe5cd3d", + "2ee6fc587ffa9ce1648cd78ee4fa237726cd1415bcffd01af0199a601f974ecb", + "c33a4a2034f026425ebfab6bb4a5a8ed9f75f316c463b8c0b936cc3dd3bc4f4a", + "de917a404791f1f236c5f3059541a8b560e1b2a64dbe710b2de0b64a6c655bd3", + "d5624e67b84406fd40c63cd611e47ffcf8c80b1969db2d8e42d92c67f6d179e6", + "079b29b3e107a68447bad01a913e7e3dbfe0c874b7f4e5ab4033f08e90f41720", + "7035d727120e72d0790af41b103d02de47c66820c572ecbdf41f37c3ed310e5d", + "41d5fcddca900bdf7960f5aa4dcdb9f5592845e3a39e7df4e1230f1e76d46677", + "90f1581877e0cf9bfbc775c79e557f074296adf07e4b94edd5738e67c775c0dd", + "896f6a5d3a3e731a825af5c8dd3ad47d74ccc01226b766d835377bfab63702eb", + "157b77664a7f74b4d6c7976469759c42982f619e9f93259240b024de92ad3220", + "fe52aa6b23e8be0ecad9a9bf587eae810e73bc6d6a23245ae4b7e9e2f21a4e28", + "e464d4801300e7115fc3586b63e7c233ef2584ad0be1674bb18d012a0fa9a45f", + "30ae7537e92a46812ade45936662288c7a1d1535347dfcb690da81fbddb3640d", + "fc321561fd802bfddab7517606f1d406cbf2f8e3c6d2ddbcf5ec371df6cb6191", + "d16b9e6920b59e273415ba7f2c10be8b3d0d235d495a1bb2e76a618fc3a71539", + "d54e6e5b5de9e8abb85616289ec5cafc5714e967318a98e6ea599759f9052fb9", + "6046e1066c39a1827d564d2ec02e937fe5caeac4b52e24707915933ef63dbb7f", + "2ce1be1a13f84af9b8a45bd406a7da2b0ed94f70e9775144291c435ae556ccd5", + "15bff067151465223a46c6b90e0839c5199ce3cf09c9679dce52a7fa3b7e8559", + "9ce6fbc21adba8a2925ea7a3851673aaeb6ef17659a5ac08b70e66773bb4c476", + "eb30a7c3fc3bacd1bdd22f552d685650047a2b240d0efb1feebe2b0cae153c64", + "4d101e243fc013a859940680ecfcded8e701012978d9685b311a8b5f888c4130", + "118a26b0590aa9d02f575f2e6c81ba40eed5cf6c2302011251d23504680c9498", + "ae7146d53a547be602bf2b36a8aba5b752f35427b746871b619c52d6f42c0284", + "2c6de6fd3c3bb032c889685f6ff7e375ecd8c3f1c08231b8c4b96d0c03d6fe47", + "dbb574b9255c563a0fb4d6d02998039b63d595902e14344260900771451cd66b", + "309d9b7bca75e9b1f55fe532a9ede769118893ef92c1f5f62b8ca66ef28bd085", + "80b4e9a8e2783743e1c10af1256913ddb0d042752a7db7ed1573973e3cc0813c", + "059ff2503b691dbb821363aabcf03273049faee668708464c4e4d8d5811a488a", + "48ee54f19c93f6a35df39a52e2230131f2bed25dbdac40fa2490a39115188917", + "b70018f986e6b6bfdb7f4f25e5026957dff62b841066f953217ff8cfd2b37e93", + "a9089f562a7228762c6ffacbe4ac1946961dc26c2471cbabf7394844c5de2f9d", + "b90ff985dc566f1cd7cfd3f88a6d6007a3c9b77f8a2ba970008d94c26f8166e2", + "77400180ef5b4d961c05dde48fca7d940a1089eeb97e2fdd16c26073da221a01", + "a5c575cb85904015a46d6b36c49195cdf2b73b7027c587a9266c5fd248238bc7", + "032a82fa3d46bf997748510667725847ec85c1f1e1a02a52bf0967c8c96e855f", + "3296e447a00bd35bfcc685e7d6df81e89823330a76a92412a6a25b079e25ae9d", + "cc29dc00b2b0b8c4aaccfb7baa63996e138a48f9b4036640116f82dacc06a887", + "c04c53e192b88e88dc5bd55e48dbe4aacf278c5174d54ef1f8210e3173a65b0d", + "493d976b12c8b01818c9b6703775a29d7bcc66cc115ff823a0662ef8ffc7a044", + "a92a53acf878166e51b2bff31a17c9994e3408163144d4fd7a5793c609ec3b2e", + "b89034ba8c59d23a1acfd85699fc72c11e619cf739b9fb813a08384243f73acd", + "f341c2da0152d5559d27fdb98209df2cc3bded13200ef6462e6d658cbecc31e8", + "07785830a1fbc5ccd65e3993ea71c8eab96960d97ab163b2ef550c7d9152adfc", + "22ea310af02f78df3820a72cba919b36b3183d3b3443047e51f42aafa14329f8", + "2ab5dc04de2d5fa243856e30b3ca5edfddb93d333616c949ffb6e2c063728dda", + "b37a4cb479c57e545241be9f1f26055675895d8b20e5b4de18cbabf6c222e355", + "27018e22d42c069e3b238565aa9600b3c606030a4c12d5e6e0f9b63f45a10da1", + "49b923dacb757b15afc25b0dd37166637d2d2d5014587eb77c1dedaf148ce44e", + "ed3e7363684269c32c4d3a4bd6f0d45fddbaac8e5a155d735207fb469160d670", + "411bfbe902582736de614f0a8c896904a69373e643386063ed4b3576d2c50fb0", + "6e5e659c3fea6a2aba32bb16751c1a3fa457ec7eb56e5c46210bef3e6c5d0509", + "45b0eeeccdac6a9c88a374a3ddbed2c674aecc379c5dbdb57c826ac81334f636", + "7e482d12c965de4d310e5364ac641c9206abd8cb4ff94023da601c793fabb6e4", + "5f321d8a280a489bcebc32fd87f90db8583d57d752ae9cd06ee69d6a3219cbfb", + "c2a7d6b9329497474a43395ae64be0dc8054e81589fd9b42f3150be88b400935", + "418bc638efeadb29189aa8171537c10746e2dd2b56578fff4ef14f77c2fac39f", + "e0776aea1e1dcc1839cf1e95ccd297b4b5ac004084e922659a9aba619058c737", + "e7b0b37583f3ddc686c67ac7bb294a2bbf5a64d964531527b0bd3fa576b43b00", + "7af4e51b39e75b6f66640c0028c211ff6da2116aa3f82b58a6fa770c96c7dd16", + "466ed8010c951283405c52608abf6a433850c5ffa982a16b448d98df60dc4f1c", + "aed6fcca61ef326e0e1ef845cdcb0971ba3c6a78f29796c90bb17364de91432c", + "fd34ae72f6aa40e7eeeaa5c16d21062ad02e1c4b914eb2ff48cfacc163949337", + "c2cbfb4405612c2db1008a704f2343ee68fb0ce70acd3321b4f3bc813aa3e054", + "8fd68c40cf1577ec4822d94e1d71b637789e66ccda1476e4defaa24126bb685a", + "95877387fd5d23f0bb5f93e5554777cad8a9f00e07f0395b6e912b9e7d0e355d", + "dfbd321d571165bff8a1251d39339e709c45f568ccff51e71f3b0ae23cd8b260", + "f90239d66050e3683db08d5b2ae600fdf2a6bc089b181cc25be205e701b77169", + "35f37ccd37cffe0690cb7c28b3ccea5e362ed91afdf4a0c9f4b9a0da8346a073", + "0146fdcb318694d5fe20bbd36014fcf423d9bfdd9659ab91ead16f56f88d9679", + "817369a4fb36280b476321573b32c11b49cea2c7d70937b1a9b262abbf83029c", + "1fabfe13332ca1f165e0ada58759fd8ddfc9f6a1d014997d79cdc985e0d94fa0", + "6a235ec9a4f5b0b5550fffa3c95145437822ae8bc208f5a301dadd6e957405a1", + "e815142cbc8adac6310b74d3542e822778fc8fb12beb2440afdfc08cda2c16ad", + "c4e7c691ebe3f32ece47fbdd997d205dc1a9b4ed7e386d088c8db1421f8d79f2", + "c1cbc5c4837f93aec69291755562a46717872294b39f1b7f7ec4b2ae28664ea7", + "b7d39ece9a4f43697ab83167cf79630fd504b495e792ae5a6e43e5d2809a82c8", + "86c875b4696f31f8316ffb0b06b7af49084affbd4f73e6b2d4f264ac2b1d757e", + "8a93001be211a141102dfd060a6f1c0e23db204ecb17d1d2c04082d045a59bdf", + "c49aaaae458509ad912c394ceba8d4aed11d506f6eb563a7177b624019d65707", + "a94ea00a3f4d894ab85d2082dffe8384243c9c04cb351592e191f9ad0024c970", + "d34e7cf2569d5742b53dd59c69a3802f5167d8abd0e7c47d2eba89be4610f482", + "e93cc03f3cd29e969f4cee9c36a3ef047c87657683492b7e3a85f905aa4244bf", + "f040263a75c33bb266a6d9027737cf060a62ef15953c228f601e4778baf71547", + "e84fbb431c80da477e4006a12c70784b11a902cf530e34018bae1fbda4414657", + "f49b99c197de2b703b1f12d640da92a7a5b969fe811b147d26352f7159760093", + "47ea5b85c2449d8c4d9ccedd3b7036d0ade6403b7ef99bf7e5e08fb99e6d0f9b", + "b1052d169ce70b7b30f3e765ef2b9e0ba0b7ed03df88a980a57bb940a87fd8b6", + "1a2beae43b0213cd7ffea9528b6bbd541e579321b3ac61563378cef163bf2fe4", + "e95e20b9de5e712969b8d8758abf5856d2e8b56e6b91dbc1c55af9414bc9f8fd", + "387655d771a6a4cc51ea7d161df985a7de25d77f28c7a5a1804a092aead9bf92", + "e93a3c3cd3cb58fe3c948cfd221e7d16fb66cb607dd889a140343a2b4dcc61e4", + "45f00bbb150a02eed1445ef3bf9a6787e291b0ce109468b088244a6f714df12f", + "c08fd6cbeb40310288b741fac607dd37ba029f91c4e27fe9d88d41ea3ec93735", + "c932933700234734f5dc48e3d7c5bf5c9c4d13ea8599a36a4d7be56734cfe14d", + "5de8c427642f319294186fcf877d2d3ea6bb54e564c0304f0ce392a015c8b38f", + "9714e6d37ed90c491175d312f8209094e28c1076f08f18af1364257beae9adab", + "43c42b543c827a6211f71ae4d95eabe94f168f2ddf745e3e2cede27dd115ce7e", + "2bc6942c33d230104420a7d69d246b2657a28366dd9942c8f9fe083f1e6ac07a", + "d7c5be1e4ffa20f660b4153e784b1bb746e3275d357693836713a60b88fcff80", + "58a6e32b1bbf41f088cfa5e470a35727dc9250db64ad4d9deb3bcd38446ce565", + "5af7b7e4ae1b238c126ba0ba9fe0098c6d4e1f2ab410446197d0a2406e7184cd", + "cc5fcab2829d9bd75bed07c8eb4d1095f3df58931d2cddfd3c190a74a887d1e3", + "65c0091c30674946fdbd4d2e12596bc3201f4ab43a0510f5d0af803b42e170c4", + "5447114920c780c03df367e156d774c5595f6b51a0246d440c4f45a488d32215", + "166101727698182691c677e6963f62cd0fc061c75f8ec36ded0134a8ae267ae7", + "fadf493304df01ad74f108f6018bb63d652c76b19634be963b0ac063046ad61a", + "6e79c0317b1b24a44fb19463db8beb6d9a8e1ca270ee8bd9ce6d8fc95a3d746d", + "5b48176f9f33c1de736b0c3984d7291df8d7d3c433207d57eafac5b2eb32fd6d", + "8572c3a8cd67b7c18fddb2f434f1c809538cf9cb4322828b8acf22ead3e74ffa", + "ef094bc32a6731ab59964a8def3602e796d651cfcd0478e949b03da3663c5032", + "89432388d915e7a4e4fe3fc7e638041e410bba76fdf06e4f747dfbe3b9bfbc48", + "474f3853148c0a0ac0fe741e5b02a302aed5bbce5c558eddf9035b754d3f5b66", + "5c33f67fccc4c3ef0218ab73746281a2c7296b4f796d4f65df5f8c3dae307718", + "e9e88725a27e372adfa00f5c5502d67cd73a545a2a222c11f629b237c883ad24", + "86a36fbc0c78612f05eb7ca58b79ad84ca108ebeefb270649147320628f1b638", + "504828fad1e6ca19ebeaa7cf375f44dc21d87ad6f8db4dc0537dc7a94569999b", + "8a8579b28d9a699de04b32b0352b5db917ffa50ab158011d17a7b53a13e509c1", + "3623572b1420ec93827a3186ac87736b8fff420358a1658990b63364a463b9c6", + "95bf16243bc204089eaa59e39d84544c9488545f21577f2f85d26d70a83f59c7", + "d327b6de8ee5d75619db20e355e5ab54afff7906dec7c1d4f39af6fb5dd155d6", + "35c77b65a78cc50ef73a4531dc68689167d892cd35612d5b2b57545522b281f5", + "cfb7ec751469f847aaf70962d365a72abe2bec7f7fe9097eb8ee6f3836aca5fe", + "db2e1cb69103ee4d8337df074e6c84bf6074c8eb790c1147a9a7a6e378c98166", + "9bfe2ab730171f87126ac6bbddcc5514b4efebe984f2c3e64d4b78b80bbd9cfd", + "557b2347d9d191e30cb19cc3fa9b0439ee776f8fb048070bb0796ad5d54b6205", + "c8bb236aa883d025e529cdc69bf35220cf0e1bcc7a5ae7ea3a1c8c90b3e61395", + "c57eb15ec398c615412ca174cc150299faeb8fd99a3019c1d71ce1c6dc604e86", + "46615da20092232a8d7c5ec929f051143b044c5ccca4fd891c6003e99dc9f5e7", + "e02df280a42d6ff2ac96f47000ea5aeaf32087a7ffd91dfe5a71bac7ea53097e", + "9b041c9b7965e6055b77c143beaeff33ac93d8a6f69b4ded8fe7da00586a0608", + "15702a15fcd2847c2653566c2d3263b258403274d3049f02ca893d0f2f086c47", + "9072f289157724c5049afe65c43933e40c1505eb5151b5f57c05ff5251e0b68c", + "5e018963fd77def1a8b93f8ac42bd9bdf48b95d2384e023284577d652b3edf65", + "ef0ddbc62e9c8ac56382141458ea202e898dab8943fd12ddb728f7d14bdb615f", + "edcc26a58fc37fe2d8da6c17c121302325de900344935a931fcdee7e31fcfebb", + "13c9d9ef014a0c674982e5b92adcf5588592763e4906e0a817f720baa8697720", + "2112bb760256b03daa1d13af848414c8fb77ed3cb4074a42a16898de4e144e34", + "231cda7b0e6bd3871a50c2ee7f1d9ba9cff403a519ca48f1220fe6717a6ef24c", + "9d67e240c82748b1ede011a33fd13c0ea9fc66bbb36ee13142fbadcbf56f1169", + "b2afe291b0eda0d15fcf77da7b80c1f0fa7f090439d5efcb63650f66ba92cf56", + "ca422ca452b53a7970f61061db51811349bd7faa748d99f7a177886078f765a3", + "ac3310f54128e969928ca198d135c971a370a8a023d636b7d3afbf8a9f6ae12d", + "67040a751c068ae97d8321a8b5432a76fbbc70b024af17ecf5f29a93ea01a536", + "c01b9c3a2ac48f74b9b5a8b7aeab8021609df541d0e634419dfe86a3fabc8c45", + "68bc58a55fc2b0652faf540279efb974a55bad2a1933c1b87db11358d1213451", + "b6c0adaf0cf27549785d198fcda1276a25d48698a03847daeff21d4ae3b49f89", + "ea4a5b93165ac8cff93c7fcd39df8978b2a08528b059fb6144482d54a4b291c2", + "f5065a179271057614045bd5aeab2502df2500e073591e6973205704d7ded803", + "f3594946a02aeed3df5ad17d71c3fa8d03b32af4b6e199aedb752c43449b9822", + "3929edee1cc42ce42362dd8b307d15e1f5b12539ad7ef39942ab10fdca17eb3b", + "22e284d1bbb777b6269f689edc0c41e52e5ee36f3fdc8e2bfa5180627c53f652", + "072bc285678af08694ae074d4f972bfca18a6dc618cd4067d4a7842d2c9aa288", + "b95f8e8bfef93ae29c16dd0cc358d8dac0b0bb05ca8faf6f18508904fd3c75d3", + "64241219dbef9cd87b4778f3c6ab0ade40edd837cfb15bd2ec9b90dbf57ce8e5", + "581f298970e57ce09bf83bb9d6cf715c8d207a795e28cbc75c2b9b93f956a94e", + "7d1dd08ec30d19ba93898e08313c9eeb7f2675a39964eb6f66c7697c879b0e50", + "89f51ce3b64f1e197877d538c00604eeff53f979f0133e5f14373c40fd803872", + "d561cdcd5965699bf8d2f01861c31c4c3361df24a9b0d3e2d81b8c97f529c0d8", + "a2269089a9f10abc7d3d1f63db86142ed33e4a47a928ee57fe7c0e96467cbde2", + "84d03d09918b84806a64d6b934b9f83ab9ac6c9639c3ba1997af0f0e26f2a7e5", + "a661d3729099fa24d0cf78eef271ad202e7e505f4fd9baf668f25e32f5ee77b5", + "21edbcc42f901bdec0741754560199d4c6c814a84fe45c542a902b3fe48b2462", + "d044026ee4bf865dd51ee7b79a6acbe08e6299e896601c9cc291bc779cd0be42", + "f637c2797f81a7e0d0035bc49abc5e787707dea12dc7c6377b0255c1bbeee797", + "b7a96f7dc7e1d83c2a5958ab50173864c77b459d38c9b7f2b16725900fb8b0b3", + "f716092d7bc1a68e17302b959ed0ad700227ba192b4c14bdec9312ff8bdf8160", + "79119169f5919448fc86be0ee2711946f1e48fae518d7c109cf5e20f969d924f", + "728ed9819e65bb55e9f5e12f8fcd9cc6df10e112b2624f1995ada85c8018089b", + "68f1fd152128649a3dbd24b05e58fee18784bf929a03de468cf6eb9f47ea614a", + "98c9717bd44b7f2798e8c79a1b2c68cac17a31c935702d1119a99bb159abe197", + "725e91eb6b6d52e2e221ab261125c9e8a30d8084760f650bb59c7d5eddcdc55d", + "dc4528b7d110563aed5d88b8ec2920243f845c04f1a2bb04856ba15a916ce9f5", + "12b23af2e8ca66406330672b1a47c905fe7631336e4c0724d64cedf9dd842fe3", + "9bd20faf38dfc3ea4258ac5db9ff47b7396b87be693cacb33695d0148927951a", + "41e763455676936a116904cdb33bd8820a40a1229012e0b416667daa8fdb3b57", + "002f1dcd0c9d9b95b5a1309125e1108fce4304717f3662703ac21118949c6c67", + "7c4517af1846bde66c1caa9c3db260745906e98578f54e2a8332e7089dc70702", + "184d1d9b0b55bd04169b728324707c522f5803a1e5b55e372d358da131c65f37", + "c1438a145763797d9b9653f7ffa923a62325493043f710a03df0148d15f5b246", + "bf5e99d0da0a9e9d6574b3448b5fcc9a65ace79c535121605983c592be0a7f6b", + "ca77fe5edb07920e0b16aefea564b680e083bfaad8b2ccceb5f2b7a1f8e365e7", + "fdbcf8bfacfcd69dcd27366224919aad7d2dbfbe25f7027b1147b68baeea6503", + "7593995498ca6f8c15095dcd20a00415f1d9417b9eb4b6b58cbfc3bab4001782", + "7bf436a1f7e22ea222dbe508603750bdcc259ece4089bd44dcad0904ff6e9dc4", + "e14acfa5ee08106a9032fe70b1db7434a4960b99257d3534a4c96f40f16fbae4", + "52378a2dedc439dd81ce21a28a1fbfe7dfe03d5b3473633289d1ea614b92de42", + "4bee1e47e6e655aa440902216f20458e7fa21ec370b6434c121e641753466b2c", + "c8739248ccbbea6e6be71f3a0de6ca090dac7d5da4194bfc946dc4eacda815b1", + "8086906efa62c08d34ea9d4d3eb01a634d9ff00c98bc61ee2d66629863f84825", + "db78c235c13105c638bd497bbcc364c282cbc1bf28d5f8b189c3ab5b14c0bc57", + "4afef79c314a280297a942cd115bf3c086922fab200d1d8bd6e5b58201d383e3", + "567a2ae0d173f23a985fb393afa38d5d62944528c758fe4acd88d75e8b990aea", + "7ef82e2d74c6b76fbbe19d36a6898362864dd4e905cef34c5cf652979a06a700", + "37947088164ef470b6d3cddf6ea78755794142b509dc31519aad2d7a88258e04", + "95f1d78ee20b35c304975ef4335bedf9b0e99de25fecf2498dbb0bfa2c92900a", + "0eb1ac83480cfbbc35b5ad21231d8fdb1dcedae45adacc830cb1201d5bf03b0e", + "66a1eef7ffb2395def11a81e8086a01a4502855bb12ccf38e0e3cff785353539", + "36bf7af36388c34443d2a2483398bd6c055d8ddbf32c2d87c95c5bbd7157c145", + "ec61f03a5d0a2fcc3ec03044f4962d60c1fc8dd997bed0a86f90b4294240c148", + "943e4f71b64ca562824621af0dd17032065ff6310013580b7c56f89850c2b74b", + "05bb3bdfa02cd14634891c5626ff7ee6528d33c1d188d1c80722b418ff01c55d", + "2648d7d51b729911478afa15d79f654e45c598182e0d2817ec4130ff692d4b61", + "9ae890a5353b8b6a05c8c4592877abf150acff8605dd3565c4ad481927736864", + "cd8fa7f6757787e6abe325a44a508da647661c2c6e9564acc2c33013f4fa8376", + "95a334d87055907d678f459b9545ff93e0734df31b2c4ec45ce3ed4f1a89087e", + "fa50e0b7b25f572cda4e2e4a3472defbe554f7200a376c950c7ecc2c4ee3989a", + "5134a2bdbaf9f256661653e94c0d0a24f3f79490a111703dd13d0e19a67a1cd9", + "3ee6c202d30995837b8b5da364079fe4bba21986419bc6b23a268af919e94eee", + "0fda4917eb28e03e6fae6417dc6ae865c29ecb596793103bac0d85c5236e902f", + "8608ff05a45aa1058f024426bef722d0716948be4e067f2158c66e34e187315e", + "e5bcf1326f0b07dd9c02c4c86fa62208febf97a28cd34eaf1884f3731526a089", + "4d6d99840a22d46619bce7c27439821ac48247d3a49908d702ded9c4c57ffb89", + "23d40bc276317e1a02706d3ebee249b34212babea10fc3c3b2f168a79cb4e1c6", + "51ff4b1f8b56137d294dbb81ff277b5959bcd18a213b78518fe81228dcdbd8ea", + "76e469a27ef211c58037d5a63c3817039845d13aca0b3052bc43845d011d44d3", + "7ef10411df7913cb103371ac71461d66bc34353aa89f2d24ae9b7c27a46594bb", + "12a89a55c1480a9036b3ab9ac862f78444e2b018b9a6e164880cbe9236fa321c", + "e20c61210c9d2ea8ebda093d4ee35359ff194e253a6bc2d68e9fa5b0786db808", + "52e211dae01d67003941bc6ded936225ecf9ec70a14dba6f2a21a84901c84f8b", + "4dd4ee02e06536dcba76bb8b98ec5d99835d5164a02559b904a9905fd72c14df", + "9cbb925c9390c4330c705d250ef32870d0ef070183421867d23b48415227e03d", + "b64def455083e1c6f46d8393fbac8a8d42c5fed736d3499898ed56fba3d79b67", + "d1a4fafce6a22e647ac9fb1ff87eced2b8748c008d6b7f75f44a56a5fd7309af", + "512d23517f3c49084c3c01551c6b83e0740b6f804b5d9020cecad0598f6d0d00", + "f0b1be77c8935b0727d2297fb32f46e7fc5380eb63386ae71dacb5ab16588140", + "0b21879e5bf4ce0b344020f10f23268560a420ca21bf14427a17de18bf80554d", + "cd2b8235c407ebd716e4e0bb311383e99979508dd1a4c111a22f2a8f848c1b57", + "296be9938f8e57103e687ef85f49f2ba254b74969b84d86d593ff9a72f19e460", + "e8c1e16c7182272ade8d1d7413d3aeb45bdc6794464efa1aaeed1ede1889cc61", + "f05825a8dfd50d68926f5287c2682a52f60f8e06705314df6f9fbaaa96e73c63", + "4a7496a09aca807df68278da0b620e20b39f1f298cc642678fc10dff7dea4c8b", + "b2a8ffeb8f2b98b5e2b78b7e31852fd2da92313c87610e887a9dc9f70e294f98", + "6aba914302a05d6896625e7dcca4e372becfb2fcfbc31bb782cafbbdebfd03a8", + "0b18e85a0759b7c29570d8c3f3154bfb258cd9c310015349d86b8c1d03b53fb8", + "2078ca3e701c5a6787cd48b58aeba6a1f64626f8a0971599298ad6c5954072b9", + "1560670d2e29b4ad742a6454adc05afe3fc864348b5c6adbffe92bb19aa561ca", + "294a972af8939046cb7c3bb12264e753a068be6df8a5f43468ebc183e4fcfdd3", + "413c65408d7cf1ca53765855d6066df405c7168b6cd88534d083e33403ac34d8", + "8d5f2484bf0a62213667aebfa143955fd5a9be5b97ef5c037532eae9fad11fe6", + "c4014c1a2d59cbc9c6b78c27677ed7fb71ed387ffe9930634c5b098d5e88ac34", + "9033a457837436d8e1d26a37802e5088c2e8e7409aaf86c359fe36481328dea4", + "c7e608373f61f0abdf7f2f3a2f0e4d5d8732f8621c9e148009cfda18315ceaae", + "d1a598798dd97e608646835a59e853f4c699e7799ec5eacd0c8f1a55ec281ec7", + "d80d1baef1b916554e4a11806fc726e0b27fcaa81f6573074bd119449c535bcd", + "ac6d10865fd1f4eba25b6727176f40ae4973e301a6bc98e8906b47d08643d4e3", + "cf0f8e194eb174a2fea2b4d1869f8eb3dc625e7bb5e7af35320bcc690c231ef2", + "bd7d8f0953401f35a5d68dac4e1bcf59a0cb1d8f96d81da5dda3cf26f29eaaf3", + "e0699e11fcca22ed1b0a3437bb48821f818c3ddf2205cb5626b569c4bd6fb85b", + "974471ac25151711bf096c8e4e6ce83d371324dc06a4e3e75bcccbd8fbd43d8c", + "eae7b5e3c5963f3b2d5ab0e0c0df605c799141380dfb4810cb67cd4ad094d051", + "48c26132ba9113dfe0abca76b9d708cf30dc6562f6cb8b67d8ed15f4bcd5b0f3", + "fe591189d6a958c7646fce16b7e8808d9f0a48f3d0d0f3ced4dce49c2ab1fc2f", + "55efc17f256b5bee261da022cf180e9938b48fdb67443dd585f0d31b3ce00f57", + "2801902506e483fa2d543874696e083d426b87c7a22ac5418d86c1e2cccdad46", + "e519402cc18181c479058922be5a9083cbffa9656053d23dcd28e20b33d4e255", + "dbec693e68c0aa405eb8a96db6225f2f586f4af798cc20363c086685feb09cfa", + "99e489b7add08ef4a4e3f19b5569452650d5df19f414b44734e4fd23dc16bbeb", + "34895cf2f8d26157f651de477b1f1de37b81ba875e911d010a5879928f514245", + "1f1bfbbc60e0921afe83fd1b7180991411a02d79718f4b3b142cee16a7b19e34", + "405a5774ebba5c56f02b260fed2fa3394b4eb287f1700f1d091ce52d9c9b9b0b", + "3aee446615c836bbd514b3eaf8b9ffacd148f541c639153acc2a0334d5c1bee4", + "30150429ca6d6acef8ede8fcd759f56769bd873827328c5135094a2e424d6763", + "acc25daf778add4ceea9bce2e636cc75b158b4a38ac33ddb5dacc181c581b8d7", + "f0a9abe3ad94539f6daa2c1eadf5cd65638939a06c74dede6201e4352cbaf565", + "d5fd794a0534f4b174cc7ebd3e1bbbd6eb9d1a3960ad72aa2f8f652157b5b281", + "28b0982aea21de348d95819089b7b639023b4acb789fc83c7d2f8ab4d9acdf00", + "8401c6b61d7ed591163e74c5a3c1c436f8bb280729dea084ffbce391cacfda28", + "c74d9e5e45a5a5e4438d2528693d98abfebc67c027872b017043feb9da364f29", + "8bc1e09f46430fd358426ae0c6f1d76945c062746d435769502b3eaa69aeb62c", + "00afa97c92c74a6979160421a090dc6421e56ee4daf3bcb7c34c12b309fd082f", + "806e17fdd51a589d25cd7db35d23b09c041b3bd0523b49b429182ca66ad27a30", + "0736d59354ccd473c46fb1f74a4e080dfacaa46bfa286cc880dd73f26b528b46", + "3a040d155e8c97580690c129339fdd5703810a4f5077958376fc6534bccbd64c", + "64148ca19f969360ddbd6288db15c77fad60eaa0eb2fff190f97cc5182cac351", + "02d77bec0e4473618b916df2e20c8e43120fe2e0252c8f481300533bc49ab556", + "3526433110d99834e1f435a748c65a07dbe50fad475ee78e39454d27f8f5a057", + "3aab4c563e788d38731ed56fd5493b44b8711d4ea8ae758e407491ba8ade7262", + "05fefca7e03a2b04e41f8ae26c7228178a7a7185f1455d9f3a3a05ec36b793e2", + "e3a711666bc77f06802c0752eb2a24ffbb23b958f862dbf618d8f373a08005ad", + "beed69c7572b102ba3c1fcadfe0b2550c3643b0feb17bfebdd2f1026913948b2", + "6468d0494ff5bf4c0d59f7f19fdd3f92e0cd1b9742e4c60f22a5b4a6c90292bb", + "8e42034a244affdf6d4b2e7e89b310cf02f71e0cb2252afea1f007a927ec3dbc", + "c49eac0df81e65bbf155c1de3932c66ca9337f989486d176c192efd8ca7511e0", + "1ef4b0f67c19bcee1baae11500c40675e838cbf9976c57a31778479bd73ff1e2", + "6bfbe43e9e183eb771eba0d4ac8677403ffcd336cb7c4eebc268cf5e852c6bf9", + "f55a7cf360d96f12e5b2d7c1c7f0c87e954eb1654123f52bbc1d39fed41ecb0e", + "28afde083d089aebbe09db81d796a2327763a066ffe62e0a3ddb851556a9de33", + "922a1cd85a1419b7c3c2a38a318d04dfe2f0c828d8943b7804680a0cd3438762", + "8a2e7642764343f569efc3de63cfe8a8f759251c572204e3b0cff0d9e4254b6e", + "e1bb38fad133cb9896c37ad4303003c06634588508f808b1963b5d6bd6b7147e", + "829f9846cd07c045f23f8f01983fb652b245f8b4e79cecaabcff1cafc3cdc48a", + "094a0f1c1cb4f0eaca17d3a69888296adfb8d18170514fd0e020065ed0728196", + "c2297975cfacc0d26c53b5fbf7eb1f8b9a8a47718874ed63a80cc20b4b7a9bad", + "192d81fa52782b48194ea1625830610925746771818e68244914b7c8b7cb2fbc", + "f64c8cad742d31bc14ef3d6ea0c89db97e2d10b20bc2d23c71b202d313d679dc", + "3b58ade9e7b84e8bc8e563821c2bd52c5e6d8ac65e080e8c6e981ee1bd8cc5f0", + "deb224a4d27208932ef64523ba6dbdac0ec97188fb837fade5ab508b1eb68c62", + "fc6ccf19e35eb799fdae695d29e1b8b5d973320a0652dde87868a2ca7817e828", + "dd09d65424ecd9ec27c512e46d6a7716d7486ffe60b829679acd3b32109b228e", + "7369537c9a8c711406fbda4be13ca61edca3ad81def0e1447937eeb9815c3a47", + "fe51147adbea48721aff2c63023173f64cc5debe02233bfdc57079aacdaace07", + "748bc74d0c5e0ccaf76691dcae78a4afe7685f8e3b609a08ff8d4e9cb14d420e", + "9edada8e22963650dccb068db2dafe304cb0cbd30aeb6cdbe2e6bd41d9d44d18", + "6e7bc6dc7c59cd81d6249dfd5a1db94a4ab3fd973ff1945ca73b3a979bc1e218", + "bf64fce5754eafe5cb19103e64090caec829523fb5fa26a3afa8bac484208c4b", + "80247598d1b9d6585a71cab39569e0957dd73413411c36862f0b5aa296ae3d4d", + "d9499b84ed0a76425383e7c7263f21b3d6558906f1517761291d70821cef354e", + "fc17afed7b1ecb4509d8d7886ea0af3d65d7aa4c4ed631893221c599b086b656", + "c9105ace9da12c7f5e4fc624226dd67ca9de1c48ae9d75d586918bbf0d0fd65b", + "11b9e2210d290d5d101d4a1c9022a231c53fc1f24997b5eb25c0bcb0e4e9b562", + "8278a30a8148b570fbe4260a67ae04dcac03b38052b9ba721e3fb7c4cc634767", + "35d8f8bb6ce5fb66157d803fe6d078e1a4a3c33c6c8e6fe39b31a8145d0b078c", + "9dee339e6d71ab0ad7da23f52e8b9e59c17c44c00149363999f752466f969191", + "cf3f6854312ce6366f29d6615cbe57a3c21eed4b60b667acddb014f34dba2792", + "c7a3180e04517fd68b89fdc4ae10da534218fb74764ac135888914915e974da1", + "7566f9ff8d4a58c92e9e7b4cfeba03fc37c6389fff7f887cbaee8d993f09aeaa", + "cfc4d5c0c0a434978c7b8ce5f00599b420c03653b3bd72f2e7e9f9e26a4487b2", + "a60c955439e9854a2de94f804b69257110ec3d3dd50b424468ceb312bc61f9b7", + "4da0aadad58310d341b45bb92f7fd62c88bed86a8eaabafc882f4f609606fcd4", + "fbeeecef255f59bb94adcc8d192594b803bcd40250ff6adbf6d718b9fdd1c3d9", + "f65371d7f6c88d0949c0524a48c048cc6ead38456deb991690215b1c39afb0dd", + "00e82f17de82e92a45311c78d0331038c1c362c83020676ae3b6414a75a92ae0", + "673cc40658d773fdbdc0c0c560903e0bed851cd1d3ec2d7f9c2b6b3ffb6b72e2", + "e01db88597fbc09d772c3b926e9f421260eab05e18b48843965438738b78a0e3", + "22c45e3a37a6d5ad9cdfe0d43fe93040481ae72a6ff615137f084244d1b4afe5", + "f8b18af34005e940d1b1948f303891bbaa63e9b9e7e3090f55cc7527b76b27ed", + "af45db83fac5b002909ee256e812cb6e221379ac45a6091b36f895497af415ee", + "737ee79f7dd17c2155303fc2f6c26c14e705c4ca4136c315c902c378c9755ff4", + "e197f9435340641bdd4cf0f7c3baac6216aae307ca20735e4d35053a5bd93c25", + "41c6044bd31ce9f72a603e6c1c68133bb007dd23410d949d5e410acc24d20137", + "a5f6c2185e2b9b240ac1726907216afca40f56737c91d21d0642e208c89cb5b3", + "204a47098478b062982ac41586a7e7762256337171a2f987800cda1e55b2e2c9", + "b94dfc31e60ad1de1e4e38a9b8133d4180d414b006319d01924f6ed0f0c7e3d0", + "ec9011ed62ef47f9e4c07312e286330e0747b8b674fde4c4ed537773e97cb708", + "1145ccec21799fbf42c4d219b476424d812d6201b51a2dbede9f321cf9882826", + "2b6295038ad539ea5720a8d65710d7c17c990cfaaa5d829b0eaa6f64c018182c", + "2003589745a936c42de4c8228b2e246e19f3e7c6089bc2236dbd6eb3eb191415", + "0e5a1fd1ded14dc6d3dec577e220262bdb3a6afa0529ad0e8f119cca0e5edc8a", + "4bb67c218418e1662a0e24f6b475e9842c8f580ef4201232c8d9ce7da792cb2e", + "503d3620497d9a62a8088abb399fa83c34c31c19963ddfbd4aa9629039285dd4", + "e473c828b8e78455ab365b980a77b239b60b203e5fe81653a64f617b4ed92368", + "ab0fb491ac78556ff66ad20b03dbc2efc5c4696c46bd7cfb30ab7e056c7778ef", + "a70faa6d455091542f11ae96840bb578a676ab42130d4e0477e1f91d9aee0332", + "af19977038a44338a99789713d8e9d5141065bc6d72e5974dbafbdbcf1a4ef86", + "c84ca704087ff9f06e6778ea59ddd631acb7d37387dd552341580ce2a9da6362", + "9ef9391fa9006c8842182fbdeb73dd5256514af498c3840c12b61d4c59a416fe", + "5a77ca44ac24115482e8ec6e89780604f79c5bd48d651707274e744010c9b49f", + "7b7e16caf894ff8e976eddd0c10b18d925307dc96b915f8a1447cbb12f06dce2", + "07cadd38ef710cdb27ed978723b508d2d5b7b87b1a02535abe8c56ddf78c35cf", + "2f78fc69a3c2ea7e0da31c8d7cc5099100fc6885aa3a158d6f375364c6b4e6a3", + "68f259bf579fb970109879f77252b1ad0da05ae9ec696f8a915cf2b0b14433fe", + "0d13967574ae23ebf6a618e2855f6b0818960e5163721c8483596673f494a2e5", + "6c6775f923f19a87f59079605f95ebe2afd4eb8cdbf40ccf19b5feda64dd4f0a", + "bbc130d72714796719d965e0f9a15912579ac2e1e13ab60ed19385c9f80e3222", + "383be58623ffa6084cc31b875836424152c54c704c28f62b130aede7f52cddd1", + "d9d2d14decf9833e6d5f2f1545d78f7d45e83b766b90eb572840e4f11a01b798", + "7e4db9c54b3ecfbefd4eb83b5b2000d7cf8b68a7e78a7441e8115c4aa3418899", + "98b76848aeb05b1d2adeff45bb502aa05933c26adceeb15fc88ac23ea145e5db", + "987a66555d461d0448beb9dcdb4a9bb6b1ceaef005a90c3bcadef49222c2c554", + "465ef3c52da280e35ef838d935e2da3b220290bbc4bc9309f53fa52010ff1b5b", + "624b6b74b4838ef4b46a3be11e49f30609973b980b96bf0b517fb1df4a42d5f3", + "6626746f3947571ebb0283cc51fbf736261daacd4bf7fb3dc08344bb5d577374", + "7631ad0eed419d6e823f64cb5f1395d64b6352c25fb211576b034422284c3942", + "ba0aedeb830703f68de1ac12425f885844d92fd4f51ecdcaa72603b3d5285778", + "4d0fdbb0602ac690831393198485d38bfad0a63a009460bd6b56492e518436f6", + "ad5baa9cacc158f266a394da66e58ca6fbf1f3be74ffe2f4a47ece08ee9b79ad", + "c7358ff360c695160a3fc4346993871dadec80578936783c29936558992bc55d", + "055268d5313086de02e8d37b38b494bc6d55a7b02fbcf526bc0dc5e896e2ff5e", + "c1f881c84b53cab36cc3834afbf0f3997e81e8fcf6c8cdf20ed25012674b2ff9", + "921705111671c5e2574187b31b9f937a28d2e124d19e5c0d5cafc00f7155cd64", + "127c6fc84d4d82e62b04b92b08debd909273df038b59d78b76170f0599ad831d", + "839247cb2add83eb3fecd6145ab8df737cc58a3e545d7278d67bf0e2ade46339", + "ab7863d4a2805e7b13a0d7daa84b5cbd6a9fc69bfb2d109d7865c5cd71f32542", + "002c6ee260c8e77c5e4ed79934bb4625eb06363a94b212ef35fc986fd39cff76", + "98d7e3165791bfe2d3ce51a17af05e10f7ccbf9b1729492f72287c137cf2d8aa", + "4ed3199919e61eafe9c1734af34ec7cad5f868963a93d922d930c4ebc69088cd", + "948d35f741d79e0fe1159bdf6f98519b79c82f2afa60f624331ef487c1c16506", + "ed8e6827920031f9c22c034ad764fe7abcb4b09420add34e1d6dc1bb65840a1f", + "6a22c95d7b5afc65cdd5fc1cc872190ba426830b4499599f48f2fc3e10abae48", + "72cda147283c250819748bece46401c4eb43a587e59ac54ff516bb29025b936d", + "b0eb1ae58012532232aa94b178f2236bda7508f525f27b0bd9bf6af7916844c0", + "45b9232b100febf3a520b8d2ed53bc420c3d60acbc6c2fa475f5ef369b4258c0", + "71f2db177be3212a0160975270902db6c11fbd5bd02827f7b4494a4f9cf111fa", + "90c81403765fa5e3435631918f935233693766f659ae1f24d08533dda5758d59", + "1efda6e2f9ab07755164b04b031a63ae035acd75a86c2bca2865b1a5ce8ee410", + "c120447d16b7891facce7d30fa081278a59587e50e6d8994c2af538830474586", + "ef07da70de74384832c354bfb36a32571a6b5b7866848e4831d22a136b7772d8", + "8132c976326028adb0eae8297c6e19b1c4aae44b8473c98bbd01e01f54c21b4b", + "52430aad9fed82d0dbfc6c75bf5c750c239c9ab6055409be9041e85d53ea75bf", + "afa075ce2ed611ae5d4a0e04c2fbb55c1643c64188fe9fcbe4a04c5eb513968f", + "af54b32ebe980b34a04509d9b677d7d28ca65e20a45b6944e3b1ea4e8f94644f", + "c308c9e5ab2f6679dd13a917f66785684c1995039ecf116d11880963a4ff82c2", + "952610f437d9fe01427f5ed171dd52a32f0f78850ef4d0445997fe873ae31595", + "273de9495acc836f54d91376fdea18bc93b0d5fb67b568e58d935094de753715", + "3e39aa9f0817acf05bd3e15cf016c2289f4569bf5c36f21efc69e59c0c2f2d6a", + "4f9e23b30ce1c44e2c68a5949fa68b21290cbb6c6719bb181948e923f5dc670c", + "18eaace26a1aa41317d0dca58aed12ff782dc1441e3f7d41de2f0995461ff328", + "b2b10f9d95789fa1b0c3aa376fdafa0966ca91fca3dadcd30b8d0780506dd757", + "1df31c5b6fa68027eead9177f153d7389a49f7e679a6e4dc505ad33743fa1883", + "fc0fc42fd8e0739df4a98191a4fb4543119f594fbd98b60cc3287442fc0f4cb2", + "d4799ac36e0241a21fb255aca1f4f2482ef3c22340b17bb0c79181aa5e0380cc", + "5036577b6342a037264078b1deefb9405fc759d6741a8d1669a697a01982dccd", + "d8f0652ec362292de21b394118f0e97c295b095fad3911c408889ff58c7d5c3c", + "18d8c9f32bfc5369c9916e62bb67745189172ac8ebda9f057d2483d985b76880", + "8112eb540bbfd0c52f8c91b37965fea9abff32c6a811d900c2333b68e51bfafa", + "41ead8fd0e56c999e1c7941e62baada26ee292299479b4bf0bef6ebd78f484f9", + "c2203a109b1f301348f12e676a0bc7fe4a76246e01c3587b1ba5f50c795e7abf", + "bcd7f57346f6d88bf40ff9a252cc670751cc9abdd7c05794274b1bc07924b241", + "0c394e1ebdfb57a7877c39e299ff0ab9563f878d14ebe9dbbcf189faf0b36a5f", + "d6da8fec56526e5a0302240b66474daa816893a0e5ddd69db367119646f20018", + "ee11047493903e61cee0d9e0ad9b70d3ded9d55973ac7f5566206052be356f1c", + "f8a2cda7e40d5c0e70c18b4028e2d4064707bee57ddd36217135de83bb092e26", + "5e8e12ad700da95a02d283a427a1a412b6b6629cf43be34f9e909d4c70ba9627", + "199c579cc113538ff7a5f8e57f3fc5175d21548c64faf5f05602428227e0a333", + "f820f4a30601c48baf125a6f7267de4252f406ac27b6f5a1ec161786d068eb3c", + "5c338ed7fb877efe70781d788effee56e8a4323a23cf3c44a61b1fe0db90b66f", + "0d6c6e2ef5412dee7ec990a5646039a584f786eecc471a098ba09a85ca1ddbe4", + "8ad775cd9036b739dc54ff38e4c6bbc686c8f810e6051bb9df0f70e733eacee6", + "54fb7b014b9a040561e4856ef367a1eebe5a24fab6a6d4502ffbc48503f5c189", + "a87612f4d9338da64fe25432e9fae44304594f06d6159844b731091167baecce", + "59cf4d5e20c5861b74999e91dc8c6e1c2d92d7f07559d7312a0f84b9ef47fca4", + "118a5f8431aed6f3a3ad93afd2d54e3147a62741a9fc328041d8f231d186f3d4", + "15990e18abd42769d712162fb8cc76be481d275a3ea92d85ae43a04bea61e9b1", + "749e0b74cf98b8a066090abcfde46ce4834f53dca5b6487d782056d622771678", + "59eeb062fec2aae83b00553bd4a26bda7672a39dcf0423cbce50af49403c9809", + "510582485c7f7326521f8afa26f8f234b8b6af5bf67e37997003921d2edb269f", + "43fc6c0d0541eca92370e3b0872c77dc63c5bb375262c775d2792a2bb3e32fc1", + "8fb6a5b8174912256c7ca021f2b0ba79bfdb0463e3ff2040e912d3b11d83bb9e", + "8985ff75c7097565ebb463078f33b5b1f68919bb1b9506edb447c75877e888a2", + "3e2e040047f83b08218dde2ec7c1c644421f0fc422632168de936e21c88a0d93", + "a441701731be9a2360c7eb3d97c39c8cf75e1696939c877f689713fd14fee0c7", + "c04d16210e2cae3c7d46c6c07545b4a1ccb725581238af8e42df1b0326db273a", + "feefea46c98ae8bd891274a8074742f50c93ccc4c8e10a0fa6dc5a2423976c74", + "2e6ec3bbd3ac9537ac06132cb3a7dfb703aba8458ed3040bf8dc9ba435d1274d", + "3a437b4861a0d5fe9143368db5c35297ae4d3a05f20285d24e34b3ec1e7dd7ec", + "6fc78e923c1456e64c93c0f4288425be7d30564af71cb6b3cef360ddd35e7a37", + "4da547aed58e9962657b61f119f63fd0e0950a4fef783ee292881af8bba72c87", + "c93d3fa5cd5c2451443709a29de2ef3f4a602cf0799ec24f54e118e651196294", + "aa1cdbc51eca5ea16aed239c4d4329622079e3d6f76836429db09c304aebe2d1", + "b4367cacd19b420ecb5440a2f03f46622f17b8003b5dbdf27efae5f8c4ae2ac4", + "a28e20ef4f2ecbe3e794240023c747ec994718f575baec5f7ef8cb111779a226", + "215ea5147cbb10b13ba81f6e1727e655289fa8c4685e028e4e027582800ef16a", + "2836ba8184da8cb7a2dd53681ae12af28b57697b875c1f60800ce2bc4ed4521f", + "24088b8dcced5ccedc7b403e5b227e89ea77f01f9399df08dae8c7b77eb329cc", + "8caf687ca5db7f2203a245406242b98338b334fb332197a6ea58814a0d7a8e08", + "7590c56dcc7dac27017381e65239e1206c9c53092d96375ab582383e906ae865", + "00992135e307a49ab718b0032a773975490d53a8b251f03b455c0f9bdd1a1e10", + "65435d8a9c2aea66c2cc642d2931ca2b0e67314597025df1737bceabb2ab2d6f", + "29b0e5180f10fa17a48c317ba4e13ab65d076c8b5e23aa0252acd9a1c5d48b1e", + "5443f385a3c587073e736d6be9b0d6a0d202a5dc3be9f6c9bb866f12b82369ef", + "33fa1f325610c4aae037daa0e9944a71b60a1e83904952df7cff3681c0745c1a", + "3cc503d446afdeb01a5614500845bd6b59516f219f2e2ce5d0e7ff1f5ec1f884", + "1dceb50d74ff488469bd8ea0425e28add8ac66e2d7e472010f90c107df17c976", + "49289512ed481461ba5812ea8ad4c97b071f1d347fc679099bee16ed9b7c1005", + "dee47629ad03b87340b7581f47e5032f3d5d3f1e70b1ffcfa428828486a0253b", + "108e74c8359893e8f32efaa6c7ff1ff766b53d59d9919733380536a4c5959540", + "204f362c44a728c7a20a3e59de7b36122d8849d2e1a20f4c53f1d985031e2542", + "8d2713f2365085d9fc6e3c5e6b45cbc65e223f6bc51c83c56845059468bc7864", + "b76191caa23db17cbe680866c0f7ed0ccc03facaebc52d6053028223fd1e1388", + "4ed1d1eed31fd22ab3512225ab5a0080bc16560aa6a817cb03a6eff71f0036b7", + "b470a3ed6f2f0d630d24e5f3c35563ddbe56d86b63de9c33cc0c599d6d0fd609", + "63f0c8bab72b56dab11fc92348237cdaa20a3b88534b8dd53439a3a31f35e69e", + "e5f28692800022f340524fd5533ece707cf8bfbc6eab19a5d4ec78fb0c9a399d", + "627adc31067bc5f7c58526087d59bc3e265ae2a5ed23f13f8736086578688b79", + "f7ae571c8c0842a2a87fcd834a3030fe73c87ba0120e45a02514818ad05042c4", + "12b35f61b0301bef43a334f12dc6d3b7e0930c75ca5db4fa161c47d66fd668b5", + "0934a9016cfc93f5a54b0ea93c00e3e00cd6b88d41faa0491285abf9868e0253", + "dc9f3af956c3b0e41d101375d4c341e0c7cfc65e85b98687a49f8830f0ed15b2", + "0fd3d9c2abb76b5edf220f96e9c4235bdbe2dfb00b5ece59efced6f7c2c46fb5", + "b7405add7715e2b41bda09f0cfa80199ee2de1d5b07019664409f3a04eeb37c0", + "b2ae7cffc5251f98997bd62fed6d30a760b7d77c11dca2bdad8a91d488f5d53b", + "7bfa7482ced2c9eda6192ce76b5036f64e0adb44c688773a00ad8ca384cb5abd", + "fdb3da878c0b262284cc0a47a15c28f22f4ef06a5cb353e01317fa65a23a99f2", + "33336d070c7a4f6fc91a0423fe0218e197d3741ee0d2e90623fe1877adbc78e6", + "95393c32b2569654b7db774177ba4a581af22ab8b582b2c24a84edf1d74ccfb8", + "dba97b5dd22ad675d3728cb0b22ab02d1a1bbd4050ba5343247736dd250f930c", + "2b810b12e4b080e096f62f7b19a84b614b438551a92c14dc52c529c8111e7b1a", + "81ecfa6b018411cd2a09adb4da2599325f43017a32984cbe7f89780c1df5c14f", + "e910e92e52c6f86e3d1966e6697383953f60a69c92dc0297277324a5eb89b272", + "28e37a1c5e19fe1ebc9e85a1d959a6f7ec2e2bbf2d73b0f35f9b28148d77a341", + "ba097539a30f9f790e6dab1762d90936200d1893f2fbbbc3368dd9b1ffb702b8", + "e198415324f15736e8b760a525a338f9d33cb416698326692245fb0c6455ce08", + "0ce2f13a0a0418bed74eb9cb1d2967a22b9f9b8087596ea7b58f3da609adaaa2", + "f47e90718daccc0c7b5ec8c617be64c605b3a7e13ec633b0227b1fb7fedf0382", + "831156b10e0c3aa9483747bbf92023ce7cd1fd84422eb3fc6dec0ff391d0806c", + "382d9684163d316b9a98ccb8dac747a43ada06870dab1c20eb90c07d0f85f9b1", + "42a2a82cfb969e69f1b3020b2f13544a4a54325b9bd40bc2fc1a576549a92597", + "2f15b390eb000ade274c033c524a52a4ced756289c3d88a30f7ea492f60084fd", + "589aaed54e6d50e53a9b4a0ce5a8c52067f3226e4d165528a24220c6fd0cf92b", + "8789a8c91c2a88bd6068c0fa68d017f0a8d269547127842aaeb0ca23f0b46d30", + "3a38c96bd40123a324c4943379a0d7d457981da414985befb2d5150dc74c1761", + "6dee983efea76c44713ed5e3f00326a83fbb06b69ed799e8aa6e50ea512711df", + "e81eac89bbd1b71d7ad8ad9057c725be8f02040e047fac7fb34e3a3895b337b8", + "f8c0aeb530166c3e9a7c966c0cde1265f1fd4f0c94547584b760d33543c93afc", + "f59e819b06b0898a8328f853f57e0cacda0bd0a06014d5ba113d3065cf514d2b", + "add7a2fc0382fc4e3f20f52fdcf876f2065437cd76465b3415ed4c7cd971b4b1", + "afc41762a7e186250ad16e95c21f4272c93f745d4476bb40cb5099e7d96a9775", + "158bf1b8cb1541262403a220f683d9fe9f92e0a22104b699e35381876b174915", + "7d4f909e42952601a817c218f96d978b8cc96e9c152b82370582c85994510ec4", + "3598053f0925352858cb1f491b964c21806e9c42e278346a9746cc8a5a3f57be", + "6ce979f3f8ad04e5779b93828bb2fe2f6c97791864e0944bca6e602c8fcb5586", + "fe257d720374dc993c025e04b4f904e07b0d9898c7a18e3ce4e812a82e8858e9", + "9d8f03817895404850afbba484bb2bbc743a84d75f0311f11363ad5cd17b6631", + "8e9c08a0bf999699f1f672e8c89d151c52f77cd2b3b68a9319fa0ad8a39eef4a", + "30ec07d52a3fc1dacc203d40f4d73a46e26bb7bad2d3004130cee9aff0f9cd9b", + "f26b153e51c8354531fee29d347d0d2398776c8dc4e6f6d3890146037bc4288d", + "2f26e4e16d9299efe89b7a3c0f2926d60d7e68fa9462e2b83853e391b1a8dc0c", + "431cbf26e799edbe8f431386f163f2625d84410d90b351bd5c547d64525b0438", + "2cfbea26cfca9e51e9b271cf0726019526ddd586f73957389d57f6f83a7f1ff8", + "09b904372320be6ff8d30a5f2d65592ea60a12d75802c33022be9bdcedc11219", + "a7c3b4a9cfc4d0666cb6fdf1aa08105a8e5d68614544dab769c7676d93c83437", + "1fc9749c6d3bd2394a2dba4763135496bdf3586874073eb3d2557bd7256df040", + "e394e331ec1d5b496f882958d27324bfc9d9505824ca4238c2507f4f08f6606c", + "7b652ccf180d21004d88a0209b85048d940572caf4c46b4c4837039fb71d33a0", + "c5742cf938a6985e0b1912d99f530390d6ff1cc76c47e5ccc273a995492e3bad", + "695770629d1e4a7678b4251fc42a5779660b82ae3c3817671f75012f6d47b38a" + ], + "time": 1587171526, + "mediantime": 1587168930, + "nonce": 4111305375, + "bits": "171320bc", + "difficulty": 14715214060656.53, + "chainwork": "00000000000000000000000000000000000000000e9baec5f63190a2b0bbcf6b", + "nTx": 1487, + "previousblockhash": "000000000000000000106b8cb739dd3c412613aa5d459a739794ec2572a74c10", + "nextblockhash": "0000000000000000001263a5a98fb5f732bc5cbb3de807b8f7ab78f0c3ad0bda" +} \ No newline at end of file