Skip to content

Commit c56f07d

Browse files
regenerate block candidate periodically
Co-authored-by: pragmaxim <pragmaxim@gmail.com>
1 parent f43954f commit c56f07d

File tree

9 files changed

+177
-25
lines changed

9 files changed

+177
-25
lines changed

src/main/resources/application.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ ergo {
5656
# Use external miner, native miner is used if set to `false`
5757
useExternalMiner = true
5858

59+
# Block candidate is regenerated periodically to include new transactions
60+
blockCandidateGenerationInterval = 20s
61+
5962
# How many internal miner threads to spawn (used mainly for testing)
6063
internalMinersCount = 1
6164

src/main/scala/org/ergoplatform/mining/CandidateGenerator.scala

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ class CandidateGenerator(
5050

5151
import org.ergoplatform.mining.CandidateGenerator._
5252

53+
private val candidateGenInterval =
54+
ergoSettings.nodeSettings.blockCandidateGenerationInterval
55+
5356
/** retrieve Readers once on start and then get updated by events */
5457
override def preStart(): Unit = {
5558
log.info("CandidateGenerator is starting")
@@ -85,7 +88,8 @@ class CandidateGenerator(
8588
context.become(
8689
initialized(
8790
CandidateGeneratorState(
88-
cache = None,
91+
cachedCandidate = None,
92+
cachedPreviousCandidate = None,
8993
solvedBlock = None,
9094
h,
9195
s,
@@ -112,7 +116,16 @@ class CandidateGenerator(
112116
case ChangedState(s: UtxoStateReader) =>
113117
context.become(initialized(state.copy(sr = s)))
114118
case ChangedMempool(mp: ErgoMemPoolReader) =>
115-
context.become(initialized(state.copy(mpr = mp)))
119+
if (hasCandidateExpired(
120+
state.cachedCandidate,
121+
state.solvedBlock,
122+
candidateGenInterval
123+
)) {
124+
context.become(initialized(state.copy(cachedCandidate = None, cachedPreviousCandidate = None, mpr = mp)))
125+
self ! GenerateCandidate(txsToInclude = Seq.empty, reply = false)
126+
} else {
127+
context.become(initialized(state.copy(mpr = mp)))
128+
}
116129
case _: NodeViewChange =>
117130
// Just ignore all other NodeView Changes
118131

@@ -124,20 +137,20 @@ class CandidateGenerator(
124137
log.info(
125138
s"Preparing new candidate on getting new block at ${header.height}"
126139
)
127-
if (needNewCandidate(state.cache, header)) {
140+
if (needNewCandidate(state.cachedCandidate, header)) {
128141
if (needNewSolution(state.solvedBlock, header.id))
129-
context.become(initialized(state.copy(cache = None, solvedBlock = None)))
142+
context.become(initialized(state.copy(cachedCandidate = None, cachedPreviousCandidate = None, solvedBlock = None)))
130143
else
131-
context.become(initialized(state.copy(cache = None)))
144+
context.become(initialized(state.copy(cachedCandidate = None, cachedPreviousCandidate = None)))
132145
self ! GenerateCandidate(txsToInclude = Seq.empty, reply = false)
133146
} else {
134147
context.become(initialized(state))
135148
}
136149

137150
case gen @ GenerateCandidate(txsToInclude, reply) =>
138151
val senderOpt = if (reply) Some(sender()) else None
139-
if (cachedFor(state.cache, txsToInclude)) {
140-
senderOpt.foreach(_ ! StatusReply.success(state.cache.get))
152+
if (cachedFor(state.cachedCandidate, txsToInclude)) {
153+
senderOpt.foreach(_ ! StatusReply.success(state.cachedCandidate.get))
141154
} else {
142155
val start = System.currentTimeMillis()
143156
CandidateGenerator.generateCandidate(
@@ -161,7 +174,7 @@ class CandidateGenerator(
161174
log.info(s"Generated new candidate in $generationTook ms")
162175
context.become(
163176
initialized(
164-
state.copy(cache = Some(candidate), avgGenTime = generationTook.millis)
177+
state.copy(cachedCandidate = Some(candidate), cachedPreviousCandidate = state.cachedCandidate, avgGenTime = generationTook.millis)
165178
)
166179
)
167180
senderOpt.foreach(_ ! StatusReply.success(candidate))
@@ -179,7 +192,7 @@ class CandidateGenerator(
179192
}
180193

181194
case preSolution: AutolykosSolution
182-
if state.solvedBlock.isEmpty && state.cache.nonEmpty =>
195+
if state.solvedBlock.isEmpty && state.cachedCandidate.nonEmpty =>
183196
// Inject node pk if it is not externally set (in Autolykos 2)
184197
val solution =
185198
if (CryptoFacade.isInfinityPoint(preSolution.pk)) {
@@ -188,7 +201,10 @@ class CandidateGenerator(
188201
preSolution
189202
}
190203
val result: StatusReply[Unit] = {
191-
val newBlock = completeBlock(state.cache.get.candidateBlock, solution)
204+
val newBlock = state.cachedCandidate
205+
.map(candidate => completeBlock( candidate.candidateBlock, solution))
206+
.filter(block => ergoSettings.chainSettings.powScheme.validate(block.header).isSuccess)
207+
.getOrElse(completeBlock( state.cachedPreviousCandidate.get.candidateBlock, solution))
192208
log.info(s"New block mined, header: ${newBlock.header}")
193209
ergoSettings.chainSettings.powScheme
194210
.validate(newBlock.header)
@@ -198,8 +214,8 @@ class CandidateGenerator(
198214
context.become(initialized(state.copy(solvedBlock = Some(newBlock))))
199215
StatusReply.success(())
200216
case Failure(exception) =>
201-
log.warn(s"Removing candidate due to invalid block", exception)
202-
context.become(initialized(state.copy(cache = None)))
217+
log.warn(s"Removing candidates due to invalid block", exception)
218+
context.become(initialized(state.copy(cachedCandidate = None, cachedPreviousCandidate = None)))
203219
StatusReply.error(
204220
new Exception(s"Invalid block mined: ${exception.getMessage}", exception)
205221
)
@@ -240,7 +256,8 @@ object CandidateGenerator extends ScorexLogging {
240256

241257
/** Local state of candidate generator to avoid mutable vars */
242258
case class CandidateGeneratorState(
243-
cache: Option[Candidate],
259+
cachedCandidate: Option[Candidate],
260+
cachedPreviousCandidate: Option[Candidate],
244261
solvedBlock: Option[ErgoFullBlock],
245262
hr: ErgoHistoryReader,
246263
sr: UtxoStateReader,
@@ -295,6 +312,33 @@ object CandidateGenerator extends ScorexLogging {
295312
solvedBlock.nonEmpty && !solvedBlock.map(_.parentId).contains(bestFullBlockId)
296313
}
297314

315+
/** Regenerate candidate to let new transactions in, miners are polling for candidate in ~ 100ms
316+
* interval so they switch to it.
317+
* If blockCandidateGenerationInterval elapsed since last block generation,
318+
* then new tx in mempool is a reasonable trigger of candidate regeneration
319+
*/
320+
def hasCandidateExpired(
321+
cachedCandidate: Option[Candidate],
322+
solvedBlock: Option[ErgoFullBlock],
323+
candidateGenInterval: FiniteDuration
324+
): Boolean = {
325+
def candidateAge(c: Candidate): FiniteDuration =
326+
(System.currentTimeMillis() - c.candidateBlock.timestamp).millis
327+
// non-empty solved block means we wait for newly mined block to be applied
328+
if (solvedBlock.isDefined) {
329+
false
330+
} else {
331+
cachedCandidate match {
332+
// if current candidate is older than candidateGenInterval
333+
case Some(c) if candidateGenInterval.compare(candidateAge(c)) <= 0 =>
334+
log.info(s"Regenerating block candidate")
335+
true
336+
case _ =>
337+
false
338+
}
339+
}
340+
}
341+
298342
/** Calculate average mining time from latest block header timestamps */
299343
def getBlockMiningTimeAvg(
300344
timestamps: IndexedSeq[Header.Timestamp]

src/main/scala/org/ergoplatform/settings/NodeConfigurationSettings.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ case class NodeConfigurationSettings(override val stateType: StateType,
3333
mining: Boolean,
3434
maxTransactionCost: Int,
3535
maxTransactionSize: Int,
36+
blockCandidateGenerationInterval: FiniteDuration,
3637
useExternalMiner: Boolean,
3738
internalMinersCount: Int,
3839
internalMinerPollingInterval: FiniteDuration,
@@ -77,6 +78,7 @@ trait NodeConfigurationReaders extends StateTypeReaders with CheckpointingSettin
7778
cfg.as[Boolean](s"$path.mining"),
7879
cfg.as[Int](s"$path.maxTransactionCost"),
7980
cfg.as[Int](s"$path.maxTransactionSize"),
81+
cfg.as[FiniteDuration](s"$path.blockCandidateGenerationInterval"),
8082
cfg.as[Boolean](s"$path.useExternalMiner"),
8183
cfg.as[Int](s"$path.internalMinersCount"),
8284
cfg.as[FiniteDuration](s"$path.internalMinerPollingInterval"),

src/test/scala/org/ergoplatform/mining/CandidateGeneratorSpec.scala

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import org.bouncycastle.util.BigIntegers
88
import org.ergoplatform.mining.CandidateGenerator.{Candidate, GenerateCandidate}
99
import org.ergoplatform.modifiers.ErgoFullBlock
1010
import org.ergoplatform.modifiers.history.header.Header
11-
import org.ergoplatform.modifiers.mempool.{ErgoTransaction, UnsignedErgoTransaction}
11+
import org.ergoplatform.modifiers.mempool.{ErgoTransaction, UnconfirmedTransaction, UnsignedErgoTransaction}
1212
import org.ergoplatform.network.ErgoNodeViewSynchronizerMessages.FullBlockApplied
13+
import org.ergoplatform.nodeView.ErgoNodeViewHolder.ReceivableMessages.LocallyGeneratedTransaction
1314
import org.ergoplatform.nodeView.ErgoReadersHolder.{GetReaders, Readers}
1415
import org.ergoplatform.nodeView.history.ErgoHistoryReader
1516
import org.ergoplatform.nodeView.state.StateType
@@ -149,9 +150,108 @@ class CandidateGeneratorSpec extends AnyFlatSpec with Matchers with ErgoTestHelp
149150
}
150151

151152
candidateGenerator.tell(block.header.powSolution, testProbe.ref)
152-
testProbe.expectMsg(blockValidationDelay, StatusReply.success(()))
153-
// after applying solution
154-
testProbe.expectMsgClass(newBlockDelay, newBlockSignal)
153+
// we fish either for ack or SSM as the order is non-deterministic
154+
testProbe.fishForMessage(blockValidationDelay) {
155+
case StatusReply.Success(()) =>
156+
testProbe.expectMsgPF(candidateGenDelay) {
157+
case FullBlockApplied(header) if header.id != block.header.parentId =>
158+
}
159+
true
160+
case FullBlockApplied(header) if header.id != block.header.parentId =>
161+
testProbe.expectMsg(StatusReply.Success(()))
162+
true
163+
}
164+
165+
system.terminate()
166+
}
167+
168+
it should "regenerate candidate periodically" in new TestKit(
169+
ActorSystem()
170+
) {
171+
val testProbe = new TestProbe(system)
172+
system.eventStream.subscribe(testProbe.ref, newBlockSignal)
173+
174+
val settingsWithShortRegeneration: ErgoSettings =
175+
ErgoSettingsReader.read()
176+
.copy(
177+
nodeSettings = defaultSettings.nodeSettings
178+
.copy(blockCandidateGenerationInterval = 1.millis),
179+
chainSettings =
180+
ErgoSettingsReader.read().chainSettings.copy(blockInterval = 1.seconds)
181+
)
182+
183+
val viewHolderRef: ActorRef =
184+
ErgoNodeViewRef(settingsWithShortRegeneration)
185+
val readersHolderRef: ActorRef = ErgoReadersHolderRef(viewHolderRef)
186+
187+
val candidateGenerator: ActorRef =
188+
CandidateGenerator(
189+
defaultMinerSecret.publicImage,
190+
readersHolderRef,
191+
viewHolderRef,
192+
settingsWithShortRegeneration
193+
)
194+
195+
val readers: Readers = await((readersHolderRef ? GetReaders).mapTo[Readers])
196+
197+
// generate block to use reward as our tx input
198+
candidateGenerator.tell(GenerateCandidate(Seq.empty, reply = true), testProbe.ref)
199+
testProbe.expectMsgPF(candidateGenDelay) {
200+
case StatusReply.Success(candidate: Candidate) =>
201+
val block = settingsWithShortRegeneration.chainSettings.powScheme
202+
.proveCandidate(candidate.candidateBlock, defaultMinerSecret.w, 0, 1000)
203+
.get
204+
candidateGenerator.tell(block.header.powSolution, testProbe.ref)
205+
// we fish either for ack or SSM as the order is non-deterministic
206+
testProbe.fishForMessage(blockValidationDelay) {
207+
case StatusReply.Success(()) =>
208+
testProbe.expectMsgPF(candidateGenDelay) {
209+
case FullBlockApplied(header) if header.id != block.header.parentId =>
210+
}
211+
true
212+
case FullBlockApplied(header) if header.id != block.header.parentId =>
213+
testProbe.expectMsg(StatusReply.Success(()))
214+
true
215+
}
216+
}
217+
218+
// build new transaction that uses miner's reward as input
219+
val prop: DLogProtocol.ProveDlog =
220+
DLogProverInput(BigIntegers.fromUnsignedByteArray("test".getBytes())).publicImage
221+
val newlyMinedBlock = readers.h.bestFullBlockOpt.get
222+
val rewardBox: ErgoBox = newlyMinedBlock.transactions.last.outputs.last
223+
rewardBox.propositionBytes shouldBe ErgoTreePredef
224+
.rewardOutputScript(emission.settings.minerRewardDelay, defaultMinerPk)
225+
.bytes
226+
val input = Input(rewardBox.id, emptyProverResult)
227+
228+
val outputs = IndexedSeq(
229+
new ErgoBoxCandidate(rewardBox.value, prop, readers.s.stateContext.currentHeight)
230+
)
231+
val unsignedTx = new UnsignedErgoTransaction(IndexedSeq(input), IndexedSeq(), outputs)
232+
233+
val tx = ErgoTransaction(
234+
defaultProver
235+
.sign(unsignedTx, IndexedSeq(rewardBox), IndexedSeq(), readers.s.stateContext)
236+
.get
237+
)
238+
239+
// candidate should be regenerated immediately after a mempool change
240+
candidateGenerator.tell(GenerateCandidate(Seq.empty, reply = true), testProbe.ref)
241+
testProbe.expectMsgPF(candidateGenDelay) {
242+
case StatusReply.Success(candidate: Candidate) =>
243+
// this triggers mempool change that triggers candidate regeneration
244+
viewHolderRef ! LocallyGeneratedTransaction(UnconfirmedTransaction(tx, None))
245+
expectNoMessage(candidateGenDelay)
246+
candidateGenerator.tell(GenerateCandidate(Seq.empty, reply = true), testProbe.ref)
247+
testProbe.expectMsgPF(candidateGenDelay) {
248+
case StatusReply.Success(regeneratedCandidate: Candidate) =>
249+
// regeneratedCandidate now contains new transaction
250+
regeneratedCandidate.candidateBlock shouldNot be(
251+
candidate.candidateBlock
252+
)
253+
}
254+
}
155255
system.terminate()
156256
}
157257

src/test/scala/org/ergoplatform/nodeView/history/extra/ExtraIndexerTestActor.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ class ExtraIndexerTestActor(test: ExtraIndexerSpecification) extends ExtraIndexe
2525

2626
val nodeSettings: NodeConfigurationSettings = NodeConfigurationSettings(StateType.Utxo, verifyTransactions = true,
2727
-1, UtxoSettings(utxoBootstrap = false, 0, 2), NipopowSettings(nipopowBootstrap = false, 1), mining = false,
28-
ChainGenerator.txCostLimit, ChainGenerator.txSizeLimit, useExternalMiner = false, internalMinersCount = 1,
29-
internalMinerPollingInterval = 1.second, miningPubKeyHex = None, offlineGeneration = false,
28+
ChainGenerator.txCostLimit, ChainGenerator.txSizeLimit, blockCandidateGenerationInterval = 20.seconds, useExternalMiner = false,
29+
internalMinersCount = 1, internalMinerPollingInterval = 1.second, miningPubKeyHex = None, offlineGeneration = false,
3030
200, 5.minutes, 100000, 1.minute, mempoolSorting = SortingOption.FeePerByte, rebroadcastCount = 20,
3131
1000000, 100, adProofsSuffixLength = 112 * 1024, extraIndex = false)
3232

src/test/scala/org/ergoplatform/settings/ErgoSettingsSpecification.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class ErgoSettingsSpecification extends ErgoCorePropertyTest {
3131
txCostLimit,
3232
txSizeLimit,
3333
useExternalMiner = false,
34+
blockCandidateGenerationInterval = 20.seconds,
3435
internalMinersCount = 1,
3536
internalMinerPollingInterval = 1.second,
3637
miningPubKeyHex = None,
@@ -80,6 +81,7 @@ class ErgoSettingsSpecification extends ErgoCorePropertyTest {
8081
txCostLimit,
8182
txSizeLimit,
8283
useExternalMiner = false,
84+
blockCandidateGenerationInterval = 20.seconds,
8385
internalMinersCount = 1,
8486
internalMinerPollingInterval = 1.second,
8587
miningPubKeyHex = None,
@@ -122,6 +124,7 @@ class ErgoSettingsSpecification extends ErgoCorePropertyTest {
122124
txCostLimit,
123125
txSizeLimit,
124126
useExternalMiner = false,
127+
blockCandidateGenerationInterval = 20.seconds,
125128
internalMinersCount = 1,
126129
internalMinerPollingInterval = 1.second,
127130
miningPubKeyHex = None,

src/test/scala/org/ergoplatform/tools/ChainGenerator.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ object ChainGenerator extends App with ErgoTestHelpers with Matchers {
6363
val txCostLimit = initSettings.nodeSettings.maxTransactionCost
6464
val txSizeLimit = initSettings.nodeSettings.maxTransactionSize
6565
val nodeSettings: NodeConfigurationSettings = NodeConfigurationSettings(StateType.Utxo, verifyTransactions = true,
66-
-1, UtxoSettings(false, 0, 2), NipopowSettings(false, 1), mining = false, txCostLimit, txSizeLimit, useExternalMiner = false,
67-
internalMinersCount = 1, internalMinerPollingInterval = 1.second, miningPubKeyHex = None, offlineGeneration = false,
66+
-1, UtxoSettings(false, 0, 2), NipopowSettings(false, 1), mining = false, txCostLimit, txSizeLimit, blockCandidateGenerationInterval = 20.seconds,
67+
useExternalMiner = false, internalMinersCount = 1, internalMinerPollingInterval = 1.second, miningPubKeyHex = None, offlineGeneration = false,
6868
200, 5.minutes, 100000, 1.minute, mempoolSorting = SortingOption.FeePerByte, rebroadcastCount = 20,
6969
1000000, 100, adProofsSuffixLength = 112*1024, extraIndex = false)
7070
val ms = settings.chainSettings.monetary.copy(

src/test/scala/org/ergoplatform/utils/HistoryTestHelpers.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ object HistoryTestHelpers extends FileUtils {
5050
val txCostLimit = initSettings.nodeSettings.maxTransactionCost
5151
val txSizeLimit = initSettings.nodeSettings.maxTransactionSize
5252
val nodeSettings: NodeConfigurationSettings = NodeConfigurationSettings(stateType, verifyTransactions, blocksToKeep,
53-
UtxoSettings(false, 0, 2), NipopowSettings(false, 1), mining = false, txCostLimit, txSizeLimit, useExternalMiner = false,
54-
internalMinersCount = 1, internalMinerPollingInterval = 1.second, miningPubKeyHex = None,
53+
UtxoSettings(false, 0, 2), NipopowSettings(false, 1), mining = false, txCostLimit, txSizeLimit, blockCandidateGenerationInterval = 20.seconds,
54+
useExternalMiner = false, internalMinersCount = 1, internalMinerPollingInterval = 1.second, miningPubKeyHex = None,
5555
offlineGeneration = false, 200, 5.minutes, 100000, 1.minute, mempoolSorting = SortingOption.FeePerByte,
5656
rebroadcastCount = 200, 1000000, 100, adProofsSuffixLength = 112*1024, extraIndex = false
5757
)

src/test/scala/org/ergoplatform/utils/Stubs.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,8 +377,8 @@ trait Stubs extends ErgoTestHelpers with TestFileUtils {
377377
val txCostLimit = initSettings.nodeSettings.maxTransactionCost
378378
val txSizeLimit = initSettings.nodeSettings.maxTransactionSize
379379
val nodeSettings: NodeConfigurationSettings = NodeConfigurationSettings(stateType, verifyTransactions, blocksToKeep,
380-
UtxoSettings(false, 0, 2), NipopowSettings(poPoWBootstrap, 1), mining = false, txCostLimit, txSizeLimit, useExternalMiner = false,
381-
internalMinersCount = 1, internalMinerPollingInterval = 1.second,miningPubKeyHex = None,
380+
UtxoSettings(false, 0, 2), NipopowSettings(poPoWBootstrap, 1), mining = false, txCostLimit, txSizeLimit, blockCandidateGenerationInterval = 20.seconds,
381+
useExternalMiner = false, internalMinersCount = 1, internalMinerPollingInterval = 1.second,miningPubKeyHex = None,
382382
offlineGeneration = false, 200, 5.minutes, 100000, 1.minute, mempoolSorting = SortingOption.FeePerByte,
383383
rebroadcastCount = 200, 1000000, 100, adProofsSuffixLength = 112*1024, extraIndex = false
384384
)

0 commit comments

Comments
 (0)