diff --git a/emerald-grpc b/emerald-grpc index 6c969f4e..a952f47f 160000 --- a/emerald-grpc +++ b/emerald-grpc @@ -1 +1 @@ -Subproject commit 6c969f4e74bb5f363af57e66d9d89bf7da068d1b +Subproject commit a952f47fbe982dd6feae276f59fce0ae079d148a diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt index 1cc85978..2f86142c 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt @@ -151,6 +151,10 @@ abstract class AbstractHead @JvmOverloads constructor( return getCurrent()?.height } + override fun getCurrentSlotHeight(): Long? { + return getCurrent()?.slot + } + override fun stop() { stopping = true future?.let { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt index dac5b4d9..cc00e074 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt @@ -31,6 +31,10 @@ class EmptyHead : Head { override fun getCurrentHeight(): Long? { return null } + + override fun getCurrentSlotHeight(): Long? { + return null + } override fun start() { } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt index 1ba599b3..8ee80e71 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt @@ -38,6 +38,8 @@ interface Head { fun getCurrentHeight(): Long? + fun getCurrentSlotHeight(): Long? + fun start() fun stop() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/MatchesResponse.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/MatchesResponse.kt index cc389bc0..4180ac72 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/MatchesResponse.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/MatchesResponse.kt @@ -17,6 +17,7 @@ sealed class MatchesResponse { is ExistsResponse -> "Label ${this.name} does not exist" GrpcResponse -> "Upstream is not grpc" is HeightResponse -> "Upstream height ${this.currentHeight} is less than ${this.height}" + is SlotHeightResponse -> "Upstream slot height ${this.currentSlotHeight} is less than ${this.slot}" is MethodResponse -> "Method ${this.method} is not supported" is MultiResponse -> this.allResponses @@ -75,6 +76,11 @@ sealed class MatchesResponse { val currentHeight: Long, ) : MatchesResponse() + data class SlotHeightResponse( + val slot: Long, + val currentSlotHeight: Long, + ) : MatchesResponse() + data class SameNodeResponse( val upstreamHash: Byte, ) : MatchesResponse() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt index 006a31c5..8e1f121e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt @@ -25,6 +25,7 @@ import io.emeraldpay.dshackle.upstream.MatchesResponse.GrpcResponse import io.emeraldpay.dshackle.upstream.MatchesResponse.HeightResponse import io.emeraldpay.dshackle.upstream.MatchesResponse.NotMatchedResponse import io.emeraldpay.dshackle.upstream.MatchesResponse.SameNodeResponse +import io.emeraldpay.dshackle.upstream.MatchesResponse.SlotHeightResponse import io.emeraldpay.dshackle.upstream.MatchesResponse.Success import org.apache.commons.lang3.StringUtils import java.util.Collections @@ -44,6 +45,9 @@ class Selector { return selectors .map { when { + it.hasSlotHeightSelector() -> { + SlotMatcher(it.slotHeightSelector.slotHeight) + } it.hasHeightSelector() -> { val height = if (it.heightSelector.height == -1L) head.getCurrentHeight() else it.heightSelector.height if (height == null) { @@ -488,6 +492,39 @@ class Selector { } } + class SlotMatcher(val slotHeight: Long) : Matcher() { + + override fun matchesWithCause(up: Upstream): MatchesResponse { + val currentHeight = up.getHead().getCurrentSlotHeight() ?: 0 + return if (currentHeight >= slotHeight) { + Success + } else { + SlotHeightResponse(slotHeight, currentHeight) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SlotMatcher) return false + + if (slotHeight != other.slotHeight) return false + + return true + } + + override fun hashCode(): Int { + return slotHeight.hashCode() + } + + override fun describeInternal(): String { + return "slot height $slotHeight" + } + + override fun toString(): String { + return "Matcher: ${describeInternal()}" + } + } + class SameNodeMatcher(private val upstreamHash: Byte) : Matcher() { override fun matchesWithCause(up: Upstream): MatchesResponse = diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EnrichedMergedHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EnrichedMergedHead.kt index b6a4b6ec..3cfdfdf1 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EnrichedMergedHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EnrichedMergedHead.kt @@ -63,6 +63,10 @@ class EnrichedMergedHead constructor( return referenceHead.getCurrentHeight() } + override fun getCurrentSlotHeight(): Long? { + return referenceHead.getCurrentSlotHeight() + } + override fun isRunning(): Boolean { return cacheSub != null } diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy index 76ef2429..45de0592 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy @@ -68,6 +68,11 @@ class EthereumHeadMock implements Head { return latest?.height } + @Override + Long getCurrentSlotHeight() { + return latest?.slot + } + @Override void start() { diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/FilteredApisSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/FilteredApisSpec.groovy index 056993a7..a3db552c 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/FilteredApisSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/FilteredApisSpec.groovy @@ -462,7 +462,7 @@ class FilteredApisSpec extends Specification { _ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY _ * isAvailable() >> true _ * getHead() >> Mock(Head) { - _ * getCurrentHeight() >> 100000001 + _ * getCurrentSlotHeight() >> 100000001 } _ * getStatus() >> UpstreamAvailability.OK _ * getLabels() >> of(UpstreamsConfig.Labels.fromMap(Map.of("node", "test"))) @@ -474,7 +474,7 @@ class FilteredApisSpec extends Specification { _ * getId() >> "id1" _ * getStatus() >> UpstreamAvailability.OK _ * getHead() >> Mock(Head) { - _ * getCurrentHeight() >> 100000 + _ * getCurrentSlotHeight() >> 100000 } _ * getLabels() >> of(UpstreamsConfig.Labels.fromMap(Map.of("node", "archive"))) }, up @@ -484,7 +484,7 @@ class FilteredApisSpec extends Specification { Chain.ETHEREUM__MAINNET, ups, new Selector.MultiMatcher( of( - new Selector.HeightMatcher(100000000), + new Selector.SlotMatcher(100000000), new Selector.LabelMatcher("node", of("test")) ) ) diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/SelectorSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/SelectorSpec.groovy index f0b1dbc9..fc4c38ea 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/SelectorSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/SelectorSpec.groovy @@ -32,6 +32,21 @@ class SelectorSpec extends Specification { private BlockchainOuterClass.Selector selectLabel2Selector = BlockchainOuterClass.Selector.newBuilder() .setLabelSelector(selectLabel2).build() + def "Convert slot height selector"() { + setup: + def slotHeightSelector = BlockchainOuterClass.Selector.newBuilder() + .setSlotHeightSelector( + BlockchainOuterClass.SlotHeightSelector.newBuilder() + .setSlotHeight(10000) + .build() + ) + .build() + when: + def act = Selector.convertToMatcher(List.of(slotHeightSelector), Stub(Head)) + then: + act == new Selector.MultiMatcher(List.of(new Selector.SlotMatcher(10000))) + } + def "Convert height selector"() { setup: def heightSelector = BlockchainOuterClass.Selector.newBuilder()