diff --git a/build.gradle b/build.gradle index 5b0c9a3b..17a1fa37 100644 --- a/build.gradle +++ b/build.gradle @@ -111,6 +111,7 @@ dependencies { testImplementation libs.groovy testImplementation libs.bundles.testcontainers testImplementation libs.bundles.junit + testImplementation libs.mockito.kotlin testImplementation(libs.spring.boot.starter.test) { exclude module: 'spring-boot-starter-logging' diff --git a/emerald-grpc b/emerald-grpc index 2c7c3286..e65b28ea 160000 --- a/emerald-grpc +++ b/emerald-grpc @@ -1 +1 @@ -Subproject commit 2c7c3286a5fe8316efb1ab21ce6ad94d4254ee5b +Subproject commit e65b28ead63ed770b72d7c143e0259cf3177b78c diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 86b8d996..360360b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -77,6 +77,8 @@ micrometer-registry-prometheus = "io.micrometer:micrometer-registry-prometheus:1 mockserver-netty = "org.mock-server:mockserver-netty:5.11.2" +mockito-kotlin = "org.mockito.kotlin:mockito-kotlin:5.1.0" + netty-common = { module = "io.netty:netty-common", version.ref = "netty" } netty-transport = { module = "io.netty:netty-transport", version.ref = "netty" } netty-handler-core = { module = "io.netty:netty-handler", version.ref = "netty" } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt index d9ad6e70..2f838cfe 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt @@ -43,7 +43,7 @@ import java.util.concurrent.TimeUnit @Service @DependsOn("monitoringSetup") class BlockchainRpc( - private val nativeCall: NativeCall, + private val nativeCallStream: NativeCallStream, private val nativeSubscribe: NativeSubscribe, private val streamHead: StreamHead, private val trackTx: List, @@ -78,7 +78,7 @@ class BlockchainRpc( var startTime = 0L var metrics: RequestMetrics? = null val idsMap = mutableMapOf() - return nativeCall.nativeCall( + return nativeCallStream.nativeCall( request .subscribeOn(scheduler) .doOnNext { req -> diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCallStream.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCallStream.kt new file mode 100644 index 00000000..a8fc7dfb --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCallStream.kt @@ -0,0 +1,69 @@ +package io.emeraldpay.dshackle.rpc + +import com.google.protobuf.ByteString +import io.emeraldpay.api.proto.BlockchainOuterClass.NativeCallReplyItem +import io.emeraldpay.api.proto.BlockchainOuterClass.NativeCallRequest +import org.springframework.stereotype.Service +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import kotlin.math.min + +@Service +class NativeCallStream( + private val nativeCall: NativeCall, +) { + + fun nativeCall( + requestMono: Mono + ): Flux { + return requestMono.flatMapMany { req -> + nativeCall.nativeCall(Mono.just(req)) + .map { StreamNativeResult(it, req.chunkSize) } + .transform { + if (!req.sorted || req.itemsList.size == 1) { + it + } else { + it.sort { o1, o2 -> o1.response.id - o2.response.id } + } + } + }.concatMap { + val chunkSize = it.chunkSize + val response = it.response + if (chunkSize == 0 || response.payload.size() <= chunkSize || !response.succeed) { + Mono.just(response) + } else { + Flux.fromIterable(chunks(response, chunkSize)) + } + } + } + + private fun chunks(response: NativeCallReplyItem, chunkSize: Int): List { + val chunks = mutableListOf() + val responseBytes = response.payload + + for (i in 0 until responseBytes.size() step+chunkSize) { + chunks.add(responseBytes.substring(i, min(i + chunkSize, responseBytes.size()))) + } + + return chunks + .mapIndexed { index, bytes -> + NativeCallReplyItem.newBuilder() + .apply { + id = response.id + payload = bytes + succeed = true + upstreamId = response.upstreamId + chunked = true + finalChunk = index == chunks.size - 1 + if (this.finalChunk && response.hasSignature()) { + signature = response.signature + } + }.build() + } + } + + private data class StreamNativeResult( + val response: NativeCallReplyItem, + val chunkSize: Int + ) +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/SubscribeNodeStatus.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/SubscribeNodeStatus.kt index 590bbf1d..668cf6dd 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/SubscribeNodeStatus.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/SubscribeNodeStatus.kt @@ -176,6 +176,6 @@ class SubscribeNodeStatus( private fun buildStatus(status: UpstreamAvailability, height: Long?): NodeStatus.Builder = NodeStatus.newBuilder() - .setAvailability(BlockchainOuterClass.AvailabilityEnum.forNumber(status.grpcId)) + .setAvailability(Common.AvailabilityEnum.forNumber(status.grpcId)) .setCurrentHeight(height ?: 0) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/SubscribeStatus.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/SubscribeStatus.kt index a0bbc122..e9cf744c 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/SubscribeStatus.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/SubscribeStatus.kt @@ -51,7 +51,7 @@ class SubscribeStatus( fun chainUnavailable(chain: Chain): BlockchainOuterClass.ChainStatus { return BlockchainOuterClass.ChainStatus.newBuilder() - .setAvailability(BlockchainOuterClass.AvailabilityEnum.AVAIL_UNAVAILABLE) + .setAvailability(Common.AvailabilityEnum.AVAIL_UNAVAILABLE) .setChain(Common.ChainRef.forNumber(chain.id)) .setQuorum(0) .build() @@ -66,7 +66,7 @@ class SubscribeStatus( 0 } return BlockchainOuterClass.ChainStatus.newBuilder() - .setAvailability(BlockchainOuterClass.AvailabilityEnum.forNumber(available.grpcId)) + .setAvailability(Common.AvailabilityEnum.forNumber(available.grpcId)) .setChain(Common.ChainRef.forNumber(chain.id)) .setQuorum(quorum) .build() diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/SubscribeStatusSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/SubscribeStatusSpec.groovy index 24112957..89984421 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/SubscribeStatusSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/SubscribeStatusSpec.groovy @@ -47,7 +47,7 @@ class SubscribeStatusSpec extends Specification { then: StepVerifier.create(act) .expectNextMatches { - it.chainValue == Chain.ETHEREUM__MAINNET.id && it.availability == BlockchainOuterClass.AvailabilityEnum.AVAIL_OK + it.chainValue == Chain.ETHEREUM__MAINNET.id && it.availability == Common.AvailabilityEnum.AVAIL_OK } .expectComplete() .verify(Duration.ofSeconds(3)) diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/MultistreamSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/MultistreamSpec.groovy index 49a4691f..92ee5b3c 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/MultistreamSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/MultistreamSpec.groovy @@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.upstream import io.emeraldpay.api.proto.BlockchainOuterClass +import io.emeraldpay.api.proto.Common import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.config.UpstreamsConfig @@ -260,17 +261,17 @@ class MultistreamSpec extends Specification { ms.onUpstreamChange( new UpstreamChangeEvent(Chain.ETHEREUM__MAINNET, up2, UpstreamChangeEvent.ChangeType.ADDED) ) - up1.onStatus(status(BlockchainOuterClass.AvailabilityEnum.AVAIL_UNAVAILABLE)) - up2.onStatus(status(BlockchainOuterClass.AvailabilityEnum.AVAIL_OK)) - up1.onStatus(status(BlockchainOuterClass.AvailabilityEnum.AVAIL_OK)) + up1.onStatus(status(Common.AvailabilityEnum.AVAIL_UNAVAILABLE)) + up2.onStatus(status(Common.AvailabilityEnum.AVAIL_OK)) + up1.onStatus(status(Common.AvailabilityEnum.AVAIL_OK)) then: assert ms.getMethods().supportedMethods == Set.of("eth_test1", "eth_test2", "eth_test3") when: - up1.onStatus(status(BlockchainOuterClass.AvailabilityEnum.AVAIL_SYNCING)) + up1.onStatus(status(Common.AvailabilityEnum.AVAIL_SYNCING)) then: assert ms.getMethods().supportedMethods == Set.of("eth_test1", "eth_test2") when: - up1.onStatus(status(BlockchainOuterClass.AvailabilityEnum.AVAIL_OK)) + up1.onStatus(status(Common.AvailabilityEnum.AVAIL_OK)) then: assert ms.getMethods().supportedMethods == Set.of("eth_test1", "eth_test2", "eth_test3") } @@ -305,7 +306,7 @@ class MultistreamSpec extends Specification { .verify(Duration.ofSeconds(3)) } - private BlockchainOuterClass.ChainStatus status(BlockchainOuterClass.AvailabilityEnum status) { + private BlockchainOuterClass.ChainStatus status(Common.AvailabilityEnum status) { return BlockchainOuterClass.ChainStatus.newBuilder() .setAvailability(status) .build() diff --git a/src/test/kotlin/io/emeraldpay/dshackle/rpc/NativeCallStreamTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/rpc/NativeCallStreamTest.kt new file mode 100644 index 00000000..8ae7f5cc --- /dev/null +++ b/src/test/kotlin/io/emeraldpay/dshackle/rpc/NativeCallStreamTest.kt @@ -0,0 +1,173 @@ +package io.emeraldpay.dshackle.rpc + +import com.fasterxml.jackson.databind.JsonNode +import com.google.protobuf.ByteString +import io.emeraldpay.api.proto.BlockchainOuterClass +import io.emeraldpay.api.proto.BlockchainOuterClass.NativeCallRequest +import io.emeraldpay.dshackle.Global +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.springframework.util.ResourceUtils +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.test.StepVerifier +import java.time.Duration + +class NativeCallStreamTest { + private val upstreamId = "upstreamId" + private val mapper = Global.objectMapper + + @Test + fun `streaming response is equal to the original response`() { + val responseFile = ResourceUtils.getFile("classpath:responses/get-by-number-response.json") + val response = mapper.writeValueAsBytes(mapper.readValue(responseFile, JsonNode::class.java)) + val nativeCallResponse = BlockchainOuterClass.NativeCallReplyItem.newBuilder() + .setId(1) + .setSucceed(true) + .setUpstreamId(upstreamId) + .setPayload(ByteString.copyFrom(response)) + .build() + val nativeCallMock = mock { + on { nativeCall(any()) } doReturn Flux.just(nativeCallResponse) + } + val nativeCallStream = NativeCallStream(nativeCallMock) + val req = Mono.just( + NativeCallRequest.newBuilder() + .setChunkSize(1000) + .build() + ) + + val result = nativeCallStream.nativeCall(req) + .collectList() + .block()!! + .map { it.payload.toByteArray() } + .reduce { acc, bytes -> acc.plus(bytes) } + + assertTrue(response.contentEquals(result)) + } + + @Test + fun `streaming responses is correct`() { + val response = "\"0x1126938\"".toByteArray() + val nativeCallResponse = BlockchainOuterClass.NativeCallReplyItem.newBuilder() + .setId(15) + .setSucceed(true) + .setUpstreamId(upstreamId) + .setPayload(ByteString.copyFrom(response)) + .build() + val nativeCallMock = mock { + on { nativeCall(any()) } doReturn Flux.just(nativeCallResponse) + } + val nativeCallStream = NativeCallStream(nativeCallMock) + val req = Mono.just( + NativeCallRequest.newBuilder() + .setChunkSize(5) + .build() + ) + + val chunkResponse: (Int) -> BlockchainOuterClass.NativeCallReplyItem.Builder = { id -> + BlockchainOuterClass.NativeCallReplyItem.newBuilder() + .setId(id) + .setChunked(true) + .setSucceed(true) + .setUpstreamId(upstreamId) + } + + val result = nativeCallStream.nativeCall(req) + + StepVerifier.create(result) + .expectNext( + chunkResponse(15) + .setPayload(ByteString.copyFrom("\"0x11".toByteArray())) + .build() + ) + .expectNext( + chunkResponse(15) + .setPayload(ByteString.copyFrom("26938".toByteArray())) + .build() + ) + .expectNext( + chunkResponse(15) + .setFinalChunk(true) + .setPayload(ByteString.copyFrom("\"".toByteArray())) + .build() + ) + .expectComplete() + .verify(Duration.ofSeconds(3)) + } + + @Test + fun `no streaming if response is too small`() { + val response = "\"0x1\"".toByteArray() + val nativeCallResponse = BlockchainOuterClass.NativeCallReplyItem.newBuilder() + .setId(15) + .setSucceed(true) + .setUpstreamId(upstreamId) + .setPayload(ByteString.copyFrom(response)) + .build() + val nativeCallMock = mock { + on { nativeCall(any()) } doReturn Flux.just(nativeCallResponse) + } + val nativeCallStream = NativeCallStream(nativeCallMock) + val req = Mono.just( + NativeCallRequest.newBuilder() + .setChunkSize(1000) + .build() + ) + + val result = nativeCallStream.nativeCall(req) + + StepVerifier.create(result) + .expectNext( + nativeCallResponse + ) + .expectComplete() + .verify(Duration.ofSeconds(3)) + } + + @Test + fun `sort responses by request id is correct`() { + val response = "\"0x1\"".toByteArray() + val response2 = "\"0x2\"".toByteArray() + val response3 = "\"0x3\"".toByteArray() + + val nativeCallResponse: (Int, ByteArray) -> BlockchainOuterClass.NativeCallReplyItem = { id, resp -> + BlockchainOuterClass.NativeCallReplyItem.newBuilder() + .setId(id) + .setChunked(true) + .setSucceed(true) + .setUpstreamId(upstreamId) + .setPayload(ByteString.copyFrom(resp)) + .build() + } + val nativeCallMock = mock { + on { nativeCall(any()) } doReturn Flux.just( + nativeCallResponse(1, response), nativeCallResponse(2, response2), nativeCallResponse(3, response3) + ).flatMap { + when (it.id) { + 1 -> Mono.just(it).delayElement(Duration.ofMillis(200)) + 2 -> Mono.just(it).delayElement(Duration.ofMillis(100)) + else -> Mono.just(it) + } + } + } + val nativeCallStream = NativeCallStream(nativeCallMock) + val req = Mono.just( + NativeCallRequest.newBuilder() + .setSorted(true) + .build() + ) + + val result = nativeCallStream.nativeCall(req) + + StepVerifier.create(result) + .expectNextMatches { it.payload.toByteArray().contentEquals(response) } + .expectNextMatches { it.payload.toByteArray().contentEquals(response2) } + .expectNextMatches { it.payload.toByteArray().contentEquals(response3) } + .expectComplete() + .verify(Duration.ofSeconds(3)) + } +} diff --git a/src/test/resources/responses/get-by-number-response.json b/src/test/resources/responses/get-by-number-response.json new file mode 100644 index 00000000..eb482d53 --- /dev/null +++ b/src/test/resources/responses/get-by-number-response.json @@ -0,0 +1,256 @@ +{ + "baseFeePerGas": "0x531245500", + "difficulty": "0x0", + "extraData": "0x546974616e2028746974616e6275696c6465722e78797a29", + "gasLimit": "0x1c9c380", + "gasUsed": "0xe873fe", + "hash": "0xa78b98d7f2d89f043eed3dcb913994eafea2cfceb7c5a0bfe54e74a5751bba77", + "logsBloom": "0xa1650816e31260508f5500e2a11922e098623fb1059210504b4f88a0f802541989d02f58a808d0d555103383c04f01e2c76774499a6829031e10393be1292ce81508847a4f46ff68699ae2dad161603e0c008a48b4464a284db4525784f15960834c2004120b879368241a8d7d1aaf51c65ae0f834b4d6800a4429b95a4aa5844a30a8ce6f04190317c7ec02131214402c04980d0da2241e0623a341949683409fa0074a02c368991226e1e69c7f8690454246205889b05ba8e237a2080a80d0091116a21d481ac24be110024e7698c2006f2684466c70384189340a0e862880c710b26ed002408874ddd7e628c84924a0f095104b0a4a44a2089391c2626692", + "miner": "0xe94f1fa4f27d9d288ffea234bb62e1fbc086ca0c", + "mixHash": "0xab53a95a14779a4c6bd4ebf80dd198cee9eae15107ea05f077b383671b2dbc1c", + "nonce": "0x0000000000000000", + "number": "0x1126d31", + "parentHash": "0x4d7cf3a84149c9d2802e1f4d1665248b800fd0699d361eb094f1b89fa714880e", + "receiptsRoot": "0x00a47493c97b24ed3ecd8566381aacd3d604b6d39ee9eb9041a4bc4ba0bca8e9", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0x51076", + "stateRoot": "0x616bf9ab6b4aa0a454fe96ac3551e2c13e3bc2215b11ba08931575b83e24c97f", + "timestamp": "0x64e758e7", + "totalDifficulty": "0xc70d815d562d3cfa955", + "transactions": [ + "0x99e52a94cfdf83a5bdadcd2e25c71574a5a24fa4df56a33f9f8b5cb6fa0ac657", + "0x682a1a48ad5533b2300f9cd8410916368cba14e572ac5a2152f351167a1af593", + "0xf1f8da9d3b25882030d44f07c677f3f026a1dc8b5875dbec20fcb067acec1b0d", + "0x01a749cf123410310acf7e39f813f1e2afea5be1531a5f2d38448ab76e629ce8", + "0xe4c6e217f8dcd988a71ed7f75e53e337c61f70827a20f19f0f42383f5dee3e90", + "0x66c41ef41e32aeb1001ce0b74734d77488982fa94741f903662e01b836489c49", + "0x7cf7edd7113460852b379ad2b855013ab74e5022b21a33bcb50c30175fb1d994", + "0xdd9df85fd28191940ffa3c0dcdad920f34524d04a544ffc460c6f71afa0a7f43", + "0xb979ad5861f3bafc4ca5972b7bf161783aceb28ec3e2622c08b4063db37ad211", + "0x59955ade006d40aac8130450adf870288343d1d219b92d9a6f234908bfd863b5", + "0x811d044f89d64c985045481119d6ee67ea924396798f24799801f88c2da844d1", + "0x200dc5afdf9311594df24145d3a5f79d1ec14ced3b37a2f771839a469af56ba2", + "0x1fd3a62d91129831b29c89f6a54b879a25e13df872bc75441f85c919b8876173", + "0x07dd9cffb9a097fd8e04229045c69fbd840aa41b413e7b5d19e23ba7bdc58aae", + "0xdddd9d9e6b6a424381377311dc9b081cbc417462f22006943dac9f7a18a64960", + "0x99a05cc5f100d8d4cb346b38f0019bd6c60387726cd12edb75458e064927e17a", + "0x5f42b5f6566bc0cd3b5b2dec6d69d911b565cbe50ebac99c85d636294e990470", + "0x869687f58839efc9b297c4e78fd7ced0e2a54d36ece0140926c8ce21b8ac6336", + "0xbffa33aa3df990e0105670c2f04f326b50e2873c8faaa4a6cba6a604aced36d3", + "0xdbf0936d077bec717145a1dcd53f3065d6acf301b4ef6694469b72fac6ee66c7", + "0x28e45628603d37211471e6bf1ab5fe238ce5a75e6686b15533b0c281b6b5da16", + "0x216eab3b2edde19b253333a6571d0f60af39fd5c6235ff1ad5e4e4275977cd18", + "0x42420062730ee7782f63bd99516eac8cbde951eaab62d7fa001892ae19574c9a", + "0xbfd7192033f81ed3cba36c3df6c6231e63f0a822e879356f0ed0d5d2557a597f", + "0xfc8dc8383489d932126bd806cdc0710d6e39fa8332f40325d1606bd75caeae03", + "0xf213ec9bdf76ac8d8fb891243c83b90404db380fbb04309b62dec4df8503ef0b", + "0x16766af541e17d4a0ab7ada18484aec45f12d4ace017cb18d8cd6d030176fa5b", + "0x33298461c6ad7feb59510465cbe74cd08297da31022536012c61dbc9513d0f0c", + "0x3518a26a8ef5d7cc7280a03f2bfc29b48583e87f0cc21772671a683b27fcb569", + "0xdff2e6d31ccc4cf7bef2137d257a926d0386043856b331d56c907dab7567808a", + "0x300599c89a704dbbb57a3939f3dd12fe69bab334a77cc627bfbaddbe1814acc2", + "0x6c87bf67109ab37d9b646106d109a2f64e83fa752f887e735029d6f5fca9af1c", + "0x1882506af9f2794a66158ef3a1d468c1043c5875a0a8167ecb076ff0b5ecffc3", + "0xca363eecb0b1980539a88c7a551c69b1230aaa6a5abf9d29ba85c43049694e1d", + "0xc02d7c0ed86f112199339640bafa2217fceb5b3cbb17345155230950ab7ae5cd", + "0x1bf3f981e09037270a09cf0b83fe59cd09317a00d1111c20e1234b351a339260", + "0xde7a67cf88386a01c6d9fd71ecd944b99b369e201dc8375b99fc24562a21674d", + "0x04ae0a42862e82e0959f6df4865bf59e5c6c069dbf961960a13825ff2e1819f1", + "0xb0f873043c129b4a78664cdc069f961708871cc6f5467fe23230cf760d5a5d33", + "0x3fba4073aa954386d166c513744ad12ff69f2c8dfbe7c1f741220c4976f65e5f", + "0x1967e1aac0abb78770854c712b68dbb7ec261121059e0e2a8846458fbd57fa69", + "0x3775c9c3d34380090e969117a63b05b77df794fea4f10d697e9b43343264cb0a", + "0xa932537509e19e6b570cc8cb7fd8dbee5999a20dc23e3653ae02ccbc6e7f0010", + "0x4ca1c2a36015cc41a6caeed8e3dd4ba983bfb50fe943788dd5206df4bf6a4080", + "0xd8cbbee7e5d046981a11c04048fe1bfeb07ef7ceeed49c2d3bada9452032c5aa", + "0x26cca178594e3b43a4c925c2c52e00629017d3b2151ed163c9bed0672e40cb41", + "0x4407920a4402f88cb64ae923952973022450d42782b153978ef666fea29cea60", + "0x8efa29732c1104c8bf43cd56b8cb8a2af09b2986b12071ff9824bf3b722a7aa7", + "0x13a95e853a7d3db90a252af570456d92aafe5a6a37a9a88bcd1b439cf78b1a79", + "0x47247bd1a0e0f110acddc685ad93f3776170dffa21ddd7a8569f193145801732", + "0xc770c40390c4ce058ab59f25de98eb1d38b231ce812ee14aea98e3816fe38d67", + "0x05d69d25ccc43285fa0b3cbb8714cff6d41dd0a922a5b0993edda9007e5a39ce", + "0x98b60d945983410e681e6f398d5202acec6c0314f8052ee165aa77591dbc7cf0", + "0x5378af36ece74dc7f1d7cba34179b87bdfe20980733ba4b6c29c73895a366f43", + "0xff2532c6a2c0ca799297b0347ab90ce72ce3c92676b54d803cbbb17387aa7a01", + "0x21fd6beda7fa035e2cd0e3b34f99b62f988e0cc4908a06ed6be4021b6b9af6fb", + "0xa3e982a06ea62288bcf61d0555364c86229fcd008c26f045c39f2fa4d9ecda58", + "0xd52aa7e42a8bf13e5eff6a97e0463cac8db115e8c4d0960f7f2d0cb7d1b6ee9a", + "0x5509ddcc9d40f4b0c78015c3faa356464bc820b2f0670c76d1f15c0a14f086cf", + "0x1540d6556d8e72973770e5edddbd77d4d906f3df150acdec3e91ca76184e09db", + "0xc091342cd6fef2e872fefbb8f29073ffb099a3f2a1bc4cb32455839bd9ab2525", + "0x61208a3e051456d909286ea62257652df8f0a4ead627ee83b985bc16cf3a7ad6", + "0x9a48925833914b9ceed9cf380e475e7096424de35294b5d4bfc078811dab3d98", + "0x4d59167d2e30b0ba83b3918c7bf49ba532d53e60ab9e5fe4cb305ff214b71025", + "0x2ebb82a9eb57500cc1020751be3ac8aa840d68573d920270212a9809abdc5c54", + "0x22d976e060552c15509ed9d6e851b4cb2544214b38603089c3e0107ef1891d10", + "0xe87d14c0733bec4ce843ec8c0759722b4f303274bade3d1dd94f2dbf3a88b24a", + "0x744fae71eba4634b3ee077918109bf3ecb0a41ef1e61eeff362b5cb462a05a5e", + "0xf17786eca0e8dffc1f12c70886023180c7cb39fb8c5d9a1423e8dd81752ba7d9", + "0xe30f9c6e33473337d4e0136bc3841dc6f9adf12e99c770098769a2319aa1e001", + "0x2b2f9ca72d91fb86b5c43d6abd0387f919ebd78c84d44b61e9cb449069b5d0a8", + "0x559ea9cce82fafa613148215f657df27a704c9b6ca5048a0d4b98e27d1959951", + "0x106949735ce1fbf24a4dd8b9c587ba78bcadcced06ae47b6ba51376b6bb49431", + "0x6a8626a77d1ad4fa5fa72116916b1024827dd4ace8599301973f6e40003b7f27", + "0x04e8b0c1dbfa8f885a1243fddeaa25059ff7c7d2682b46255f33d6b0ac40581e", + "0xbf1ccecedc722f46548c955b60993806a6bc43454fa8c1c927a047b57904d2ff", + "0xbf004f34dd0bce8adf538d7876518dea393fd89ce0833884405684cdbfb2ef1e", + "0x05ea00c43a1606981bed43d9c62731e3b73acd16c2aac2b3c8dfb375ee32d020", + "0x880457f9cc0474a062b5e2bb02b364b2b7eb9b8cbefd3125f885abeb55f5a358", + "0x70a725352f3bfee9655cd509a6a3a08c97021325c659d2e3574dd88d9a19deee", + "0x23b698283b10f542caa0f883c02fca65c23b4af2e58673627fa7f6fd3d63c97b", + "0xe66f521d1f759bf5fafb406e28cc29818d9ad3086692e607f244bcedad2525bc", + "0xd4e0c0f2488ee67c1d68e85cd3e8918c2b66d26de6cb528c2db3ca2b4afce5c0", + "0xd7daa2e186a635cf48dd18524d6dab3e391b046929b54b35b517586802852d55", + "0x5512c6b8809910b7e8bcbe7d5b97e9227c4a3439b7421561923f07ee4808d650", + "0xbd12f4be3b507d1aff14b554e196e1e0ddb3b101dd3b2ea3a6a212b648c229e0", + "0x3ea421383e22402eb250303daa0766a5a0b0b8c026e3c7ed5a33ec470903119e", + "0x721d62413d4d3c158c6b17b817bf69d937d2e5c6538f35ce5a4e3bad4bd47a38", + "0x36bad1d1ca0f01da17d5bf3286d0ef20b5b67ec1f3fcb0acde74388dc60e88cd", + "0xb995e1215b63e8ec6cacac8b028d7a7be54445391a9e85d484ed13ede97bedae", + "0x14c0bd15c9437b9307cb78c8924c223d1f5ee4cd03b28e112da987bc7a5f7589", + "0x7df0d240b47346156a8fba2d354d35423580d1e63a06081d0386c4d695da1944", + "0xe36a10df8d1e3fc5112f922e36c2f91649f64d4de43a67f3b50e12eb9ab29522", + "0x7c0fbb0ef551d82216daa7ab0a93a794236434fae25e0d3eacbe9cb468c0be3b", + "0x13cb614593c9217b6c50655ae14dd53efe2dd4c65338547582c42aba1da85f47", + "0xd822609513faa8ad7191ab8491422e95c43667d6da3177d201021e7d48f1c31f", + "0xd6f27238aff12f46a44e760ea3dc2ba7217de56764590618531a72a13c4b40a7", + "0x86e69cb562883344f93c452e1bfdf0e96a6b59bf377b5d75cf56e12552050784", + "0xc794a442d0cd304be6ecc45a76b26e2c2cd072efcb3d66a770cae0a43893699c", + "0x46767f446b7df490fb28bb67a18a7fbe3d721afa871105f4b1eab3dba44c9704", + "0x8b222d2605f2b539d5de1ffbabd70b6ae3838c650e1de248481ab3e90c9a09b2", + "0x4b99f12b0c1a760c3fd0955d4af29eb5729d33cd34d3304bcddb10758f70b066", + "0xce40bd2d384a27bb6d7d902df16b5abe4ecefd36f672fa6195349b370fcac73a", + "0xf82d8b98f346cd234e80e1c55343ab2d87d16c0686ae315b7e540863c28c808c", + "0x2df97495d5e6ae9635486e0cb8bd15914d09688ca5db112a46b10575d010b1e7", + "0x3b00838382c605b6077121c946d485af8ba0ab66243443a98274b6753840ad23", + "0xbf01b6b721830f9556f28f22e05f2795590ccd4bf4d74220fb08fa84a38042c2", + "0xbb6193fa4768bda105731a9db6fffd9811347725ec35ed18e6fdd4aba8aaaf41", + "0x13c2b59066a9860f52c1690463a4bafbaf0067757bdd71189a1a62eb9cd261b4", + "0x7c910fd7e31c553db9ddf3ef0d6e4de8f5b34498f8abd5c8a85000b92b947372", + "0xcab29f042527e10c095e9b42fc27c8b9dfd31f46152fcad9d98ff0a4c146baa5", + "0x4ef34debc3720f0041252d56968baed60bad0a784e87f4c470f9d5d349b20f8f", + "0xf109619e60981e6c87a85f95b6daf74187cb726c319456852b8312e9162f4779", + "0x33a563685f8c1cd56481b0b84ea2bb49dd9d0b5976e371a01cae5f942f4bb3a2", + "0x147cc4e0306b18fdf038aa868ae303dc6128c4527a8e42528a6bb408566dca28", + "0x7388affa389ddd52888c69c249cff81663c69996cfae4c381c8651d36ce5036d", + "0x70320b2d788a3c7d1583ec4e8357b0011fe9a77d18c00416d90cebfe285d137c", + "0x3c6004026cdfe14b815f29e2cf241dcfdf8abb0b3bdc17d3f391037581f0b530", + "0x5f1d241c925497bc67818db01519cedd89ca49f4fd1851a918262017a0ea6c61", + "0xae7b0748076140848565d4b0b4afd478aaf15286db363a78544785ef3f44ff0b", + "0x4947383d0c9e1feff93cd5e845f7d5e3bb8d9a5b3d47a0e1c3bbf8b118699e2d", + "0xb1febd412c92ab47c889d02dbd62b887fbebafee163c3af7a1e14f8679517e15", + "0xc5b4b0bd4bfa79159727815cbd7fbb57d5e2b0024a3b4aebb873ee65bae6e97c", + "0x5fd7229a4bcba29ce0f44ffaaaf61d427d4846a779b3802f3d89a9f3f95c771b", + "0xdfd2a635a5dba8e8e151100b473aa9bfc8a276aa3820a33258aa718c1b7c9271", + "0x73dc889dbd03668016b8a2ebfbfc710fa8d0d22def22c20426b0b6f6c6df6fad", + "0xc7909fa1463651a949d8f1640eb786f693bd73154492835cbfcfafe3e71a05d1", + "0xe823de5bb19378e635d82bdb6e3efddc2330f79bb8227b2e76fc8319fcc49f00", + "0x255412935ab00554fa677bf4577dde4cf99173826edcf0fcd9db903d18f289e3", + "0x8e97ec39075876c1be8ef5ab3bd5a83c86f31cca782ac1c7fcb22b97c7f8928c", + "0x98cd471e6bae9a84bf77a5230b8ee6551a57d70788e4024c08efab16fb63413c", + "0x73e15ddec669e4e3fd5cdebb30586d335b4125f31d62ec0d37b780a70cd0d9c7", + "0x5e2729c8285c9ff16f35938b37b9965924bae90e9f49405838e57412733d98a8" + ], + "transactionsRoot": "0x58a1032558996cfab7e002ed8c06e1dba5c35cb4cd41b2013ef062e900abd760", + "uncles": [], + "withdrawals": [ + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xeafcb4", + "index": "0xe7e69c", + "validatorIndex": "0x6431e" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xebe5a6", + "index": "0xe7e69d", + "validatorIndex": "0x6431f" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xebaefb", + "index": "0xe7e69e", + "validatorIndex": "0x64320" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xebbab5", + "index": "0xe7e69f", + "validatorIndex": "0x64321" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xec197a", + "index": "0xe7e6a0", + "validatorIndex": "0x64322" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xeb9192", + "index": "0xe7e6a1", + "validatorIndex": "0x64323" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xec8834", + "index": "0xe7e6a2", + "validatorIndex": "0x64324" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xec281c", + "index": "0xe7e6a3", + "validatorIndex": "0x64325" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xebdafd", + "index": "0xe7e6a4", + "validatorIndex": "0x64326" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xec057f", + "index": "0xe7e6a5", + "validatorIndex": "0x64327" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xebdb5e", + "index": "0xe7e6a6", + "validatorIndex": "0x64328" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xebcd7c", + "index": "0xe7e6a7", + "validatorIndex": "0x64329" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xeb8763", + "index": "0xe7e6a8", + "validatorIndex": "0x6432a" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xeb8703", + "index": "0xe7e6a9", + "validatorIndex": "0x6432b" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xec9d1f", + "index": "0xe7e6aa", + "validatorIndex": "0x6432c" + }, + { + "address": "0x8e609ac80f4324e499a6efd24f221a2caa868224", + "amount": "0xeb8734", + "index": "0xe7e6ab", + "validatorIndex": "0x6432d" + } + ], + "withdrawalsRoot": "0x2391b96468359892950dce890b954c3b83bd23dcb30f5d110f22758d31e8ec9f" +} \ No newline at end of file