refactoring fork choice rules out of abstract head and priority fork choice support for PoS Ethereum

This commit is contained in:
terminal
2022-07-29 15:32:48 +04:00
parent 92218623f2
commit eb95c591f7
51 changed files with 995 additions and 153 deletions

View File

@@ -27,7 +27,7 @@ class HeightByHashAddingSpec extends Specification {
def block = new BlockContainer(
12079192L, BlockId.from("0xa6af163aab691919c595e2a466f0a7b01f1dff8cfd9631dee811df57064c2d32"),
BigInteger.ONE, Instant.now(), false, "".bytes, null, []
BigInteger.ONE, Instant.now(), false, "".bytes, null, [], 0
)
def "use memory if available"() {

View File

@@ -86,7 +86,8 @@ class ReceiptMemCacheSpec extends Specification {
false,
"{}".bytes,
null,
[TxId.from(receipt.transactionHash)]
[TxId.from(receipt.transactionHash)],
0
)
when:

View File

@@ -271,7 +271,7 @@ class TrackBitcoinAddressSpec extends Specification {
Head head = Mock(Head) {
1 * getFlux() >> Flux.concat(
Flux.just(
new BlockContainer(0L, BlockId.from(hash1), BigInteger.ZERO, Instant.now(), false, null, null, [])
new BlockContainer(0L, BlockId.from(hash1), BigInteger.ZERO, Instant.now(), false, null, null, [], 0)
),
blocks.asFlux()
)
@@ -312,7 +312,7 @@ class TrackBitcoinAddressSpec extends Specification {
StepVerifier.create(resp)
.expectNext("0")
.then {
blocks.tryEmitNext(new BlockContainer(1L, BlockId.from(hash1), BigInteger.ONE, Instant.now(), false, null, null, []))
blocks.tryEmitNext(new BlockContainer(1L, BlockId.from(hash1), BigInteger.ONE, Instant.now(), false, null, null, [], 0))
}
.expectNext("1230000")
.then {

View File

@@ -142,7 +142,7 @@ class TrackBitcoinTxSpec extends Specification {
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
// start with the current block
def next = Flux.fromIterable([10, 12, 13, 14, 15]).map { h ->
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, [])
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, [], 0)
}
Head head = Mock(Head) {
1 * getFlux() >> next
@@ -173,7 +173,7 @@ class TrackBitcoinTxSpec extends Specification {
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
// start with the current block
def next = Flux.fromIterable([10, 12, 13]).map { h ->
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, [])
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, [], 0)
}
Head head = Mock(Head) {
1 * getFlux() >> next
@@ -268,7 +268,7 @@ class TrackBitcoinTxSpec extends Specification {
])
}
def next = Flux.fromIterable([10, 11, 12]).map { h ->
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, [])
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, [], 0)
}
Head head = Mock(Head) {
_ * getFlux() >> next

View File

@@ -198,7 +198,7 @@ class TrackEthereumTxSpec extends Specification {
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
def block = new BlockContainer(
100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes, null,
[TxId.from(txId)]
[TxId.from(txId)], 0
)
when:
@@ -220,7 +220,8 @@ class TrackEthereumTxSpec extends Specification {
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
def block = new BlockContainer(
100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes, null,
[TxId.from("0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22")]
[TxId.from("0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22")],
0
)
apiMock.answer("eth_getTransactionByHash", [txId], null)

View File

@@ -111,7 +111,8 @@ class TestingCommons {
false,
null,
null,
[]
[],
0
)
}

View File

@@ -17,6 +17,9 @@ package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
import org.jetbrains.annotations.NotNull
import reactor.core.publisher.Flux
import reactor.core.publisher.Sinks
import reactor.test.StepVerifier
@@ -31,7 +34,7 @@ class AbstractHeadSpec extends Specification {
def blocks = [1L, 2, 3, 4].collect { i ->
byte[] hash = new byte[32]
hash[0] = i as byte
new BlockContainer(i, BlockId.from(hash), BigInteger.valueOf(i), Instant.now(), false, null, null, [])
new BlockContainer(i, BlockId.from(hash), BigInteger.valueOf(i), Instant.now(), false, null, null, [], 0)
}
def "Calls beforeBlock on each block"() {
@@ -85,7 +88,7 @@ class AbstractHeadSpec extends Specification {
.verify(Duration.ofSeconds(1))
}
def "Ignores block will less difficulty"() {
def "Ignores block that is filtered by forkchoice"() {
setup:
Sinks.Many<BlockContainer> source = Sinks.many().unicast().onBackpressureBuffer()
def head = new TestHead()
@@ -93,7 +96,7 @@ class AbstractHeadSpec extends Specification {
blocks[1].height, BlockId.from(blocks[1].hash.value.clone().tap { it[1] = 0xff as byte }),
blocks[1].difficulty - 1,
Instant.now(),
false, null, null, []
false, null, null, [], 0
)
when:
head.follow(source.asFlux())
@@ -113,6 +116,23 @@ class AbstractHeadSpec extends Specification {
}
class TestHead extends AbstractHead {
TestHead() {
super(new ForkChoice() {
@Override
boolean filter(@NotNull BlockContainer block) {
return block.hash != BlockId.from("02ff000000000000000000000000000000000000000000000000000000000000")
}
@Override
ForkChoice.ChoiceResult choose(@NotNull BlockContainer block) {
return new ForkChoice.ChoiceResult.Updated(block)
}
@Override
BlockContainer getHead() {
return null
}
})
}
}
}

View File

@@ -0,0 +1,74 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.rpc.json.BlockJson
import spock.lang.Specification
import java.time.Instant
class DistanceExtractorSpec extends Specification {
def "Correct distance for PoW"() {
expect:
def top = new BlockJson().with {
it.number = topHeight
it.totalDifficulty = topDiff
it.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915123")
it.timestamp = Instant.now()
return it
}
def curr = new BlockJson().with {
it.number = currHeight
it.totalDifficulty = currDiff
it.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915123")
it.timestamp = Instant.now()
return it
}
delta as DistanceExtractor.ChainDistance == DistanceExtractor.@Companion.extractPowDistance(BlockContainer.from(top), BlockContainer.from(curr))
where:
topHeight | topDiff | currHeight | currDiff | delta
100 | 1000 | 100 | 1000 | new DistanceExtractor.ChainDistance.Distance(0)
101 | 1010 | 100 | 1000 | new DistanceExtractor.ChainDistance.Distance(1)
102 | 1020 | 100 | 1000 | new DistanceExtractor.ChainDistance.Distance(2)
103 | 1030 | 100 | 1000 | new DistanceExtractor.ChainDistance.Distance(3)
150 | 1500 | 100 | 1000 | new DistanceExtractor.ChainDistance.Distance(50)
100 | 1000 | 101 | 1010 | new DistanceExtractor.ChainDistance.Distance(0)
100 | 1000 | 102 | 1020 | new DistanceExtractor.ChainDistance.Distance(0)
100 | 1000 | 100 | 1010 | DistanceExtractor.ChainDistance.Fork.INSTANCE
100 | 1100 | 100 | 1000 | DistanceExtractor.ChainDistance.Fork.INSTANCE
}
def "Correct distance for priority"() {
setup:
def hash1 = "0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915123"
def hash2 = "0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915124"
expect:
def top = new BlockJson().with {
it.number = topHeight
it.totalDifficulty = 0
it.hash = BlockHash.from(hashA == 0 ? hash1 : hash2)
it.timestamp = Instant.now()
return it
}
def curr = new BlockJson().with {
it.number = currHeight
it.totalDifficulty = 0
it.hash = BlockHash.from(hashB == 0 ? hash1 : hash2)
it.timestamp = Instant.now()
return it
}
delta as DistanceExtractor.ChainDistance == DistanceExtractor.@Companion.extractPriorityDistance(BlockContainer.from(top), BlockContainer.from(curr))
where:
topHeight | hashA | currHeight | hashB || delta
100 | 0 | 100 | 0 || new DistanceExtractor.ChainDistance.Distance(0)
101 | 0 | 100 | 1 || new DistanceExtractor.ChainDistance.Distance(1)
102 | 0 | 100 | 1 || new DistanceExtractor.ChainDistance.Distance(2)
103 | 0 | 100 | 1 || new DistanceExtractor.ChainDistance.Distance(3)
150 | 0 | 100 | 1 || new DistanceExtractor.ChainDistance.Distance(50)
100 | 0 | 101 | 1 || DistanceExtractor.ChainDistance.Fork.INSTANCE
100 | 0 | 102 | 1 || DistanceExtractor.ChainDistance.Fork.INSTANCE
100 | 0 | 100 | 1 || DistanceExtractor.ChainDistance.Fork.INSTANCE
}
}

View File

@@ -23,6 +23,7 @@ import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.dshackle.upstream.ethereum.connectors.EthereumConnectorFactory
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
import io.emeraldpay.grpc.Chain
import reactor.test.StepVerifier
import spock.lang.Retry
@@ -47,7 +48,7 @@ class FilteredApisSpec extends Specification {
def httpFactory = Mock(HttpFactory) {
create(_, _) >> TestingCommons.api().tap { it.id = "${i++}" }
}
def connectorFactory = new EthereumConnectorFactory(false, null, httpFactory)
def connectorFactory = new EthereumConnectorFactory(false, null, httpFactory, new MostWorkForkChoice())
new EthereumUpstream(
"test",
Chain.ETHEREUM,

View File

@@ -111,45 +111,10 @@ class HeadLagObserverSpec extends Specification {
.verifyComplete()
}
def "Correct distance"() {
setup:
Head master = Mock()
HeadLagObserver observer = new TestHeadLagObserver(master, [])
expect:
def top = new BlockJson().with {
it.number = topHeight
it.totalDifficulty = topDiff
it.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915123")
it.timestamp = Instant.now()
return it
}
def curr = new BlockJson().with {
it.number = currHeight
it.totalDifficulty = currDiff
it.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915123")
it.timestamp = Instant.now()
return it
}
delta as Long == observer.extractDistance(BlockContainer.from(top), BlockContainer.from(curr))
where:
topHeight | topDiff | currHeight | currDiff | delta
100 | 1000 | 100 | 1000 | 0
101 | 1010 | 100 | 1000 | 1
102 | 1020 | 100 | 1000 | 2
103 | 1030 | 100 | 1000 | 3
150 | 1500 | 100 | 1000 | 50
100 | 1000 | 101 | 1010 | 0
100 | 1000 | 102 | 1020 | 0
100 | 1000 | 100 | 1010 | 11
100 | 1100 | 100 | 1000 | 11
}
class TestHeadLagObserver extends HeadLagObserver {
TestHeadLagObserver(@NotNull Head master, @NotNull Collection<? extends Upstream> followers) {
super(master, followers)
super(master, followers, DistanceExtractor.@Companion::extractPowDistance)
}
@Override

View File

@@ -15,6 +15,7 @@
*/
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
import org.springframework.context.Lifecycle
import reactor.core.publisher.Flux
import spock.lang.Specification
@@ -36,7 +37,7 @@ class MergedHeadSpec extends Specification {
}
when:
def merged = new MergedHead([head1, head2, head3])
def merged = new MergedHead([head1, head2, head3], new MostWorkForkChoice())
merged.start()
then:
@@ -44,11 +45,17 @@ class MergedHeadSpec extends Specification {
}
class TestHead1 extends AbstractHead {
TestHead1() {
super(new MostWorkForkChoice())
}
}
class TestHead2 extends AbstractHead implements Lifecycle {
TestHead2() {
super(new MostWorkForkChoice())
}
@Override
void start() {

View File

@@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.rpc.json.BlockJson
import reactor.core.publisher.Flux
@@ -30,7 +31,7 @@ import java.time.Instant
class DefaultEthereumHeadSpec extends Specification {
DefaultEthereumHead head = new DefaultEthereumHead()
DefaultEthereumHead head = new DefaultEthereumHead(new MostWorkForkChoice())
ObjectMapper objectMapper = Global.objectMapper
def blocks = (10L..20L).collect { i ->

View File

@@ -0,0 +1,37 @@
package io.emeraldpay.dshackle.upstream.forkchoice
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import spock.lang.Specification
import java.time.Instant
class MostWorkForkChoiceSpec extends Specification {
def blocks = [1L, 2, 3, 4].collect { i ->
byte[] hash = new byte[32]
hash[0] = i as byte
new BlockContainer(i, BlockId.from(hash), BigInteger.valueOf(i), Instant.now(), false, null, null, [], 0)
}
def "filters blocks"() {
def choice = new MostWorkForkChoice()
choice.choose(blocks[1])
expect:
!choice.filter(blocks[0])
choice.filter(blocks[2])
}
def "chooses correct block as head"() {
def choice = new MostWorkForkChoice()
choice.choose(blocks[1])
when:
choice.choose(blocks[0])
then:
choice.getHead() == blocks[1]
when:
choice.choose(blocks[2])
then:
choice.getHead() == blocks[2]
}
}

View File

@@ -0,0 +1,51 @@
package io.emeraldpay.dshackle.upstream.forkchoice
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import spock.lang.Specification
import java.time.Instant
class NoChoiceWithPriorityForkChoiceSpec extends Specification {
def blocks = [1L, 2, 3, 4].collect { i ->
byte[] hash = new byte[32]
hash[0] = i as byte
new BlockContainer(i, BlockId.from(hash), BigInteger.valueOf(i), Instant.now(), false, null, null, [], 0)
}
def "filters blocks"() {
def blockR0 = blocks[0].copyWithRating(10)
def blockR1 = blocks[1].copyWithRating(10)
def choice = new NoChoiceWithPriorityForkChoice(10)
when:
choice.choose(blocks[0])
then:
choice.getHead() == blockR0
when:
choice.choose(blocks[1])
then:
choice.getHead() == blockR1
when:
choice.choose(blocks[0])
then:
choice.getHead() == blocks[1]
}
def "chooses blocks and adds rating"() {
def blockR0 = blocks[0].copyWithRating(10)
def blockR1 = blocks[1].copyWithRating(10)
def choice = new NoChoiceWithPriorityForkChoice(10)
when:
choice.choose(blocks[0])
then:
choice.getHead() == blockR0
when:
choice.choose(blocks[1])
then:
choice.getHead() == blockR1
when:
choice.choose(blocks[0])
then:
choice.getHead() == blockR1
}
}

View File

@@ -0,0 +1,41 @@
package io.emeraldpay.dshackle.upstream.forkchoice
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import spock.lang.Specification
import java.time.Instant
class PriorityForkChoiceSpec extends Specification {
def blocks = [1L, 2, 3, 4].collect { i ->
byte[] hash = new byte[32]
hash[0] = i as byte
new BlockContainer(i, BlockId.from(hash), BigInteger.valueOf(i), Instant.now(), false, null, null, [], i.toInteger())
}
def "filters blocks"() {
def choice = new PriorityForkChoice()
choice.choose(blocks[1])
expect:
!choice.filter(blocks[0])
choice.filter(blocks[2])
!choice.filter(blocks[1])
}
def "chooses correct block according to node rating"() {
def choice = new PriorityForkChoice()
choice.choose(blocks[1])
when:
choice.choose(blocks[0])
then:
choice.getHead() == blocks[1]
when:
choice.choose(blocks[2])
then:
choice.getHead() == blocks[2]
when:
def seenblock = blocks[1].copyWithRating(20)
choice.choose(seenblock)
then:
choice.getHead() == blocks[2]
}
}

View File

@@ -21,6 +21,7 @@ import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.test.MockGrpcServer
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
import io.emeraldpay.grpc.Chain
import io.grpc.stub.StreamObserver
import reactor.test.StepVerifier
@@ -60,7 +61,7 @@ class GrpcHeadSpec extends Specification {
Chain.BITCOIN,
Stub(DefaultUpstream),
client,
convert, null
convert, null, new MostWorkForkChoice()
)
when:
def act = head.getFlux()
@@ -121,7 +122,7 @@ class GrpcHeadSpec extends Specification {
Chain.BITCOIN,
Stub(DefaultUpstream),
client,
convert, null
convert, null, new MostWorkForkChoice()
)
when:
def act = head.getFlux()