···1414 return nil, stacktrace.Propagate(err)
1515 }
1616 if result.Code == 0 {
1717- if action == TransactionActionAuthoritativeImport {
1717+ if !action.SubmittableViaMempool() {
1818 // this type of transaction is meant to be included only by validator nodes
1919 return &abcitypes.ResponseCheckTx{
2020 Code: 4002,
2121- Log: "AuthoritativeImport transactions can only be introduced by validator nodes",
2121+ Log: "This action can only be introduced by validator nodes",
2222 }, nil
2323 }
2424
+87-56
abciapp/range_challenge.go
···1313 "github.com/Yiling-J/theine-go"
1414 "github.com/cometbft/cometbft/crypto"
1515 cmtlog "github.com/cometbft/cometbft/libs/log"
1616- "github.com/cometbft/cometbft/privval"
1716 "github.com/cometbft/cometbft/rpc/core"
1818- bftstore "github.com/cometbft/cometbft/store"
1917 cmttypes "github.com/cometbft/cometbft/types"
2018 "github.com/cosmos/iavl"
2119 "github.com/cosmos/iavl/db"
2220 ics23 "github.com/cosmos/ics23/go"
2321 "github.com/gbl08ma/stacktrace"
2422 cbornode "github.com/ipfs/go-ipld-cbor"
2323+ "github.com/samber/lo"
2524 "github.com/samber/mo"
2625 "tangled.org/gbl08ma.com/didplcbft/store"
2726 "tangled.org/gbl08ma.com/didplcbft/transaction"
2827 "tangled.org/gbl08ma.com/didplcbft/types"
2928)
30293131-type RangeChallengeCoordinator struct {
3030+type rangeChallengeCoordinator struct {
3231 runnerContext context.Context
3332 logger cmtlog.Logger
34333534 isConfiguredToBeValidator bool
3635 validatorPubKey crypto.PubKey
3736 validatorPrivKey crypto.PrivKey
3838- validatorAddress []byte
3937 txFactory *transaction.Factory
4040- nodeBlockStore *bftstore.BlockStore
3838+ blockChallengeCoordinator *blockChallengeCoordinator
3939+ blockHeaderGetter BlockHeaderGetter
4140 nodeEventBus *cmttypes.EventBus
4241 mempoolSubmitter types.MempoolSubmitter
4342 consensusReactor consensusReactor
···5756 WaitSync() bool
5857}
59586060-func NewRangeChallengeCoordinator(
5959+func newRangeChallengeCoordinator(
6160 runnerContext context.Context,
6261 logger cmtlog.Logger,
6363- pv *privval.FilePV,
6262+ validatorPubKey crypto.PubKey,
6363+ validatorPrivKey crypto.PrivKey,
6464 txFactory *transaction.Factory,
6565- blockStore *bftstore.BlockStore,
6565+ blockChallengeCoordinator *blockChallengeCoordinator,
6666+ blockHeaderGetter BlockHeaderGetter,
6667 nodeEventBus *cmttypes.EventBus,
6768 mempoolSubmitter types.MempoolSubmitter,
6868- consensusReactor consensusReactor) (*RangeChallengeCoordinator, error) {
6969- c := &RangeChallengeCoordinator{
7070- txFactory: txFactory,
6969+ consensusReactor consensusReactor) (*rangeChallengeCoordinator, error) {
7070+ c := &rangeChallengeCoordinator{
7171 runnerContext: runnerContext,
7272 logger: logger,
7373- nodeBlockStore: blockStore,
7373+ txFactory: txFactory,
7474+ blockChallengeCoordinator: blockChallengeCoordinator,
7575+ blockHeaderGetter: blockHeaderGetter,
7476 nodeEventBus: nodeEventBus,
7577 mempoolSubmitter: mempoolSubmitter,
7678 consensusReactor: consensusReactor,
7777- isConfiguredToBeValidator: pv != nil,
7979+ isConfiguredToBeValidator: validatorPubKey != nil && validatorPrivKey != nil,
7880 newBlockCh: make(chan int64),
7981 }
8082 if c.isConfiguredToBeValidator {
8181- c.validatorPubKey = pv.Key.PubKey
8282- c.validatorPrivKey = pv.Key.PrivKey
8383- c.validatorAddress = c.validatorPubKey.Address()
8383+ c.validatorPubKey = validatorPubKey
8484+ c.validatorPrivKey = validatorPrivKey
8485 }
85868687 var err error
···9293 return c, nil
9394}
94959595-func (c *RangeChallengeCoordinator) Start() error {
9696+func (c *rangeChallengeCoordinator) Start() error {
9697 c.startMu.Lock()
9798 defer c.startMu.Unlock()
9899 if c.started {
···115116 defer cancel()
116117 err := c.onNewBlock(ctx, newHeight)
117118 if err != nil {
118118- // note: this is expected in certain circumstances, such as the proof for the toHeight block not being ready yet as the block was just finalized
119119- // (and the block may have been finalized without our votes)
120119 c.logger.Error("range challenge block handler error", "error", stacktrace.Propagate(err))
121120 }
122121 }()
···127126 return nil
128127}
129128130130-func (c *RangeChallengeCoordinator) Wait() {
129129+func (c *rangeChallengeCoordinator) Wait() {
131130 c.startMu.Lock()
132131 started := c.started
133132 c.startMu.Unlock()
···147146 root []byte
148147}
149148150150-func (c *RangeChallengeCoordinator) getOrFetchNextProofFromHeight(tx transaction.Read) (int64, error) {
149149+func (c *rangeChallengeCoordinator) getOrFetchNextProofFromHeight(tx transaction.Read, toHeight int64) (int64, error) {
151150 if completion, hasCache := c.cachedNextProofFromHeight.Get(); hasCache {
152151 return completion, nil
153152 }
154153155155- completion, err := store.Consensus.ValidatorRangeChallengeCompletion(tx, c.validatorAddress)
154154+ completion, err := store.Consensus.ValidatorRangeChallengeCompletion(tx, c.validatorPubKey.Bytes())
156155 if err != nil {
157156 if !errors.Is(err, store.ErrNoRecentChallengeCompletion) {
158157 return 0, stacktrace.Propagate(err)
···160159 completion = 0
161160 }
162161163163- minProvenBlock := int64(0)
164164- for proofHeight := range store.Consensus.BlockChalengeProofsIterator(tx, 0, &err) {
165165- minProvenBlock = int64(proofHeight)
166166- break
162162+ minConsecutiveProvenBlock := uint64(toHeight)
163163+ for proofHeight := range store.Consensus.BlockChallengeProofsReverseIterator(tx, minConsecutiveProvenBlock, &err) {
164164+ if proofHeight+1 != minConsecutiveProvenBlock {
165165+ break
166166+ }
167167+ minConsecutiveProvenBlock = proofHeight
167168 }
168169 if err != nil {
169170 return 0, stacktrace.Propagate(err)
170171 }
171172172172- minProvable := max(minProvenBlock, int64(completion+1))
173173+ if minConsecutiveProvenBlock == uint64(toHeight) {
174174+ return 0, stacktrace.Propagate(errMissingProofs)
175175+ }
176176+177177+ minProvable := int64(max(minConsecutiveProvenBlock, completion+1))
173178174179 c.cachedNextProofFromHeight = mo.Some(minProvable)
175180 return minProvable, nil
176181}
177182178178-func (c *RangeChallengeCoordinator) newBlocksSubscriber() error {
183183+func (c *rangeChallengeCoordinator) newBlocksSubscriber() error {
179184 subscriber := "rangeChallengeCoordinator"
180185181186 subCtx, cancel := context.WithTimeout(c.runnerContext, core.SubscribeTimeout)
···203208 case c.newBlockCh <- newBlockHeaderEvent.Header.Height:
204209 default:
205210 }
211211+212212+ if c.isConfiguredToBeValidator && !c.consensusReactor.WaitSync() {
213213+ // ensure we don't skip creating any proof even if we are not an active validator
214214+ // (i.e. ExtendVote won't trigger the computation of block challenge proofs, and/or we may be slightly behind, so notifyOfIncomingBlockHeight in ProcessProposal won't be called)
215215+ c.blockChallengeCoordinator.notifyOfIncomingBlockHeight(newBlockHeaderEvent.Header.Height)
216216+ }
206217 case <-blocksSub.Canceled():
207218 err := blocksSub.Err()
208219 if err == nil {
···213224 }
214225}
215226216216-func (c *RangeChallengeCoordinator) onNewBlock(ctx context.Context, newBlockHeight int64) error {
227227+func (c *rangeChallengeCoordinator) onNewBlock(ctx context.Context, newBlockHeight int64) error {
217228 if !c.isConfiguredToBeValidator {
218229 return nil
219230 }
···222233 return nil
223234 }
224235236236+ // if we are not an active validator, when we get to this point, the block challenge for newBlockHeight might not be ready yet, so we need to wait for it
237237+ // even if we are an active validator, the block may have been finalized without our votes
238238+ _, err := c.blockChallengeCoordinator.loadOrComputeBlockChallengeProof(ctx, newBlockHeight)
239239+ if err != nil {
240240+ return stacktrace.Propagate(err)
241241+ }
242242+225243 tx := c.txFactory.ReadCommitted()
226244 if tx.Height() < newBlockHeight {
227245 return stacktrace.NewError("read committed transaction height lower than expected: new block height %d, transaction height %d", newBlockHeight, tx.Height())
···230248 shouldCommitToChallenge := false
231249 shouldCompleteChallenge := false
232250233233- fromHeight, toHeight, provenHeight, includedOnHeight, _, err := store.Consensus.ValidatorRangeChallengeCommitment(tx, c.validatorAddress)
251251+ fromHeight, toHeight, provenHeight, includedOnHeight, _, err := store.Consensus.ValidatorRangeChallengeCommitment(tx, c.validatorPubKey.Bytes())
234252 if errors.Is(err, store.ErrNoActiveChallengeCommitment) {
235253 deleteOldProofs := false
236254 if c.hasSubmittedChallengeCompletion {
···240258 c.hasSubmittedChallengeCompletion = false
241259 c.cachedNextProofFromHeight = mo.None[int64]()
242260 }
243243- nextFromHeight, err := c.getOrFetchNextProofFromHeight(tx)
261261+ nextFromHeight, err := c.getOrFetchNextProofFromHeight(tx, newBlockHeight)
244262 if err != nil {
245263 return stacktrace.Propagate(err)
246264 }
···267285 } else if err != nil {
268286 return stacktrace.Propagate(err)
269287 } else {
270270- commitmentBlockMeta := c.nodeBlockStore.LoadBlockMeta(int64(includedOnHeight))
288288+ commitmentBlockHeader, err := c.blockHeaderGetter(int64(includedOnHeight))
271289 commitmentExpired := false
272272- if commitmentBlockMeta != nil {
290290+ if err == nil {
273291 commitmentExpired = uint64(newBlockHeight) >= includedOnHeight+CompleteChallengeMaxAgeInBlocks ||
274274- time.Since(commitmentBlockMeta.Header.Time) >= CompleteChallengeMaxAge-1*time.Second
292292+ time.Since(commitmentBlockHeader.Time) >= CompleteChallengeMaxAge-1*time.Second
275293276294 // shouldCompleteChallenge if not too many blocks have passed AND enough blocks have passed
277295 shouldCompleteChallenge = !commitmentExpired && includedOnHeight+1 <= uint64(newBlockHeight)
278296 }
279297280280- if !shouldCompleteChallenge {
281281- shouldCommitToChallenge = commitmentExpired
298298+ if !shouldCompleteChallenge && commitmentExpired {
299299+ nextFromHeight, err := c.getOrFetchNextProofFromHeight(tx, newBlockHeight)
300300+ if err != nil {
301301+ return stacktrace.Propagate(err)
302302+ }
303303+304304+ shouldCommitToChallenge = nextFromHeight+CommitToChallengeTargetInterval-1 <= newBlockHeight
282305 }
283306 }
284307···290313 return stacktrace.Propagate(err)
291314 }
292315 } else if shouldCommitToChallenge {
293293- c.logger.Info("Creating challenge commitment transaction", "toHeight", toHeight)
316316+ c.logger.Info("Creating challenge commitment transaction", "toHeight", newBlockHeight)
294317 transactionBytes, err = c.createCommitToChallengeTx(ctx, tx, newBlockHeight)
295318 if err != nil {
296319 if errors.Is(err, errMissingProofs) {
320320+ c.cachedNextProofFromHeight = mo.None[int64]()
297321 // this is expected on nodes that take longer to generate the proofs
298322 // shouldCommitToChallenge will be true again on the next block and we'll try again
299323 return nil
···320344 return nil
321345}
322346323323-func (c *RangeChallengeCoordinator) createCommitToChallengeTx(ctx context.Context, tx transaction.Read, toHeight int64) ([]byte, error) {
324324- fromHeight, err := c.getOrFetchNextProofFromHeight(tx)
347347+func (c *rangeChallengeCoordinator) createCommitToChallengeTx(ctx context.Context, tx transaction.Read, toHeight int64) ([]byte, error) {
348348+ fromHeight, err := c.getOrFetchNextProofFromHeight(tx, toHeight)
325349 if err != nil {
326350 return nil, stacktrace.Propagate(err)
327351 }
···340364 return nil, stacktrace.Propagate(err)
341365 }
342366343343- toHeightBlockMeta := c.nodeBlockStore.LoadBlockMeta(toHeight)
344344- if toHeightBlockMeta == nil {
345345- return nil, stacktrace.NewError("block not found at height")
367367+ toHeightBlockHeader, err := c.blockHeaderGetter(toHeight)
368368+ if err != nil {
369369+ return nil, stacktrace.Propagate(err)
346370 }
347371348348- if tx.Timestamp().Sub(toHeightBlockMeta.Header.Time) > CommitToChallengeMaxAge {
372372+ if tx.Timestamp().Sub(toHeightBlockHeader.Time) > CommitToChallengeMaxAge {
349373 return nil, stacktrace.NewError("too much time passed since block at height")
350374 }
351375352352- proveHeight := computeHeightToProveInRange(toHeightBlockMeta.Header.LastCommitHash, c.validatorAddress, int64(fromHeight), int64(toHeight), mo.None[int64]())
376376+ proveHeight := computeHeightToProveInRange(toHeightBlockHeader.LastCommitHash, c.validatorPubKey, int64(fromHeight), int64(toHeight), mo.None[int64]())
353377354378 commitToRoot, membershipProof, err := c.computeRangeChallengeProof(ctx, tx, fromHeight, toHeight, proveHeight)
355379 if err != nil {
···384408 return out, nil
385409}
386410387387-func (c *RangeChallengeCoordinator) createCompleteChallengeTx(ctx context.Context, tx transaction.Read, fromHeight, toHeight, prevProvenHeight, commitmentIncludedOnHeight int64) ([]byte, error) {
388388- nextBlockMeta := c.nodeBlockStore.LoadBlockMeta(commitmentIncludedOnHeight + 1)
389389- if nextBlockMeta == nil {
390390- return nil, stacktrace.NewError("block not found at height")
411411+func (c *rangeChallengeCoordinator) createCompleteChallengeTx(ctx context.Context, tx transaction.Read, fromHeight, toHeight, prevProvenHeight, commitmentIncludedOnHeight int64) ([]byte, error) {
412412+ nextBlockHeader, err := c.blockHeaderGetter(commitmentIncludedOnHeight + 1)
413413+ if err != nil {
414414+ return nil, stacktrace.Propagate(err)
391415 }
392416393393- proveHeight := computeHeightToProveInRange(nextBlockMeta.Header.LastCommitHash, c.validatorAddress, int64(fromHeight), int64(toHeight), mo.Some(prevProvenHeight))
417417+ proveHeight := computeHeightToProveInRange(nextBlockHeader.LastCommitHash, c.validatorPubKey, int64(fromHeight), int64(toHeight), mo.Some(prevProvenHeight))
394418395419 _, membershipProof, err := c.computeRangeChallengeProof(ctx, tx, fromHeight, toHeight, proveHeight)
396420 if err != nil {
···405429 transaction := Transaction[CompleteChallengeArguments]{
406430 Action: TransactionActionCompleteChallenge,
407431 Arguments: CompleteChallengeArguments{
408408- Validator: c.validatorAddress,
409409- Proof: proofBytes,
432432+ ValidatorPubKey: lo.Must(MarshalPubKeyForArguments(c.validatorPubKey)),
433433+ Proof: proofBytes,
410434 },
411435 }
412436···417441 return out, nil
418442}
419443420420-func (c *RangeChallengeCoordinator) computeRangeChallengeProof(ctx context.Context, tx transaction.Read, startHeight, endHeight, proveHeight int64) ([]byte, *ics23.CommitmentProof, error) {
444444+func (c *rangeChallengeCoordinator) computeRangeChallengeProof(ctx context.Context, tx transaction.Read, startHeight, endHeight, proveHeight int64) ([]byte, *ics23.CommitmentProof, error) {
421445 ctx = context.WithValue(ctx, contextTxKey{}, tx)
422446 ct, err := c.treeCache.Get(ctx, treeCacheKey{
423447 startHeight: startHeight,
···437461 return ct.root, membershipProof, nil
438462}
439463440440-func computeHeightToProveInRange(lastCommitHash, validatorAddress []byte, fromHeight, toHeight int64, avoidHeight mo.Option[int64]) int64 {
464464+func computeHeightToProveInRange(lastCommitHash []byte, validatorPubKey crypto.PubKey, fromHeight, toHeight int64, avoidHeight mo.Option[int64]) int64 {
441465 lastCommitHashBigInt := new(big.Int).SetBytes(lastCommitHash)
442442- validatorBigInt := new(big.Int).SetBytes(validatorAddress)
466466+ validatorBigInt := new(big.Int).SetBytes(validatorPubKey.Bytes())
443467 seed := new(big.Int).Xor(lastCommitHashBigInt, validatorBigInt)
444468445469 numBlocks := toHeight - fromHeight + 1
···458482459483var errMissingProofs = errors.New("missing block challenge proofs in requested range")
460484461461-func (c *RangeChallengeCoordinator) proofTreeLoader(ctx context.Context, cacheKey treeCacheKey) (theine.Loaded[cachedTree], error) {
485485+func (c *rangeChallengeCoordinator) proofTreeLoader(ctx context.Context, cacheKey treeCacheKey) (theine.Loaded[cachedTree], error) {
462486 var zeroValue theine.Loaded[cachedTree]
463487464488 anyTx := ctx.Value(contextTxKey{})
···473497 tree := iavl.NewMutableTree(db.NewMemDB(), 16, false, iavl.NewNopLogger(), iavl.AsyncPruningOption(false))
474498475499 var err error
476476- for proofHeight, proof := range store.Consensus.BlockChalengeProofsIterator(tx, uint64(max(cacheKey.startHeight-1, 0)), &err) {
500500+501501+ afterHeight := uint64(max(cacheKey.startHeight-1, 0))
502502+ for proofHeight, proof := range store.Consensus.BlockChallengeProofsIterator(tx, afterHeight, &err) {
477503 select {
478504 case <-ctx.Done():
479505 return zeroValue, stacktrace.Propagate(ctx.Err())
···483509 if proofHeight > uint64(cacheKey.endHeight) {
484510 break
485511 }
512512+513513+ if proofHeight != afterHeight+1 {
514514+ return zeroValue, stacktrace.Propagate(errMissingProofs)
515515+ }
516516+ afterHeight = proofHeight
486517487518 _, err := tree.Set(binary.BigEndian.AppendUint64(nil, proofHeight), slices.Clone(proof))
488519 if err != nil {
+128-4
abciapp/snapshots.go
···2121 "github.com/cosmos/iavl"
2222 "github.com/gbl08ma/stacktrace"
2323 "github.com/klauspost/compress/zstd"
2424+ _ "tangled.org/gbl08ma.com/didplcbft/badgertodbm" // for reference in comment
2425 "tangled.org/gbl08ma.com/didplcbft/store"
2526)
2627···273274 }
274275 defer f.Close()
275276276276- err = writeSnapshot(f, d.indexDB, it)
277277+ err = writeSnapshot(f, d.indexDB, d.blockHeaderGetter, it)
277278 if err != nil {
278279 return stacktrace.Propagate(err)
279280 }
···309310 return nil
310311}
311312312312-func writeSnapshot(writerSeeker io.WriteSeeker, indexDB dbm.DB, it *iavl.ImmutableTree) error {
313313+func writeSnapshot(writerSeeker io.WriteSeeker, indexDB dbm.DB, blockHeaderGetter BlockHeaderGetter, it *iavl.ImmutableTree) error {
313314 writtenUntilReservedFields := 0
314315315316 bw := bufio.NewWriter(writerSeeker)
···357358 return stacktrace.Propagate(err)
358359 }
359360360360- numIndexEntries, err := exportIndexEntries(indexDB, it.Version(), zstdw)
361361+ numIndexEntries, err := exportIndexEntries(indexDB, blockHeaderGetter, it.Version(), zstdw)
361362 if err != nil {
362363 return stacktrace.Propagate(err)
363364 }
···406407 return nil
407408}
408409409409-func exportIndexEntries(indexDB dbm.DB, treeVersion int64, w io.Writer) (int64, error) {
410410+func exportIndexEntries(indexDB dbm.DB, blockHeaderGetter BlockHeaderGetter, treeVersion int64, w io.Writer) (int64, error) {
411411+ numDIDEntries, err := exportIndexDIDEntries(indexDB, treeVersion, w)
412412+ if err != nil {
413413+ return 0, stacktrace.Propagate(err)
414414+ }
415415+416416+ numValidatorParticipationEntries, err := exportIndexValidatorParticipation(indexDB, treeVersion, w)
417417+ if err != nil {
418418+ return 0, stacktrace.Propagate(err)
419419+ }
420420+421421+ numRecentBlockHeaders, err := exportRecentBlockHeaders(blockHeaderGetter, treeVersion, w)
422422+ if err != nil {
423423+ return 0, stacktrace.Propagate(err)
424424+ }
425425+426426+ return numDIDEntries + numValidatorParticipationEntries + numRecentBlockHeaders, nil
427427+}
428428+429429+func exportIndexDIDEntries(indexDB dbm.DB, treeVersion int64, w io.Writer) (int64, error) {
410430 didLogKeyStart := make([]byte, store.IndexDIDLogKeyLength)
411431 didLogKeyStart[0] = store.IndexDIDLogKeyPrefix
412432 didLogKeyEnd := slices.Repeat([]byte{0xff}, store.IndexDIDLogKeyLength)
413433 didLogKeyEnd[0] = store.IndexDIDLogKeyPrefix
414434435435+ // reading the index using an iterator while writes happen to its domain technically violates the documented contract for [dbm.Iterator]
436436+ // in practice, we know this is safe because of how we implemented [badgertodbm.BadgerDB.IteratorWithOptions] - it uses a read-only transaction per iterator
415437 iterator, err := indexDB.Iterator(didLogKeyStart, didLogKeyEnd)
416438 if err != nil {
417439 return 0, stacktrace.Propagate(err)
···450472 iterator.Next()
451473 }
452474 return numEntries, nil
475475+}
476476+477477+func exportIndexValidatorParticipation(indexDB dbm.DB, treeVersion int64, w io.Writer) (int64, error) {
478478+ epochHeight := uint64(treeVersion) - uint64(treeVersion)%UpdateValidatorsBlockInterval
479479+ startKey := store.MarshalValidatorVotingActivityKey(uint64(epochHeight), make([]byte, store.AddressLength))
480480+ endKey := store.MarshalValidatorVotingActivityKey(uint64(epochHeight), slices.Repeat([]byte{0xff}, store.AddressLength))
481481+482482+ // reading the index using an iterator while writes happen to its domain technically violates the documented contract for [dbm.Iterator]
483483+ // in practice, we know this is safe because of how we implemented [badgertodbm.BadgerDB.IteratorWithOptions] - it uses a read-only transaction per iterator
484484+ iterator, err := indexDB.Iterator(startKey, endKey)
485485+ if err != nil {
486486+ return 0, stacktrace.Propagate(err)
487487+ }
488488+ defer iterator.Close()
489489+490490+ numEntries := int64(0)
491491+ for iterator.Valid() {
492492+ key := iterator.Key()
493493+ value := iterator.Value()
494494+495495+ header := make([]byte, 4+4)
496496+ binary.BigEndian.PutUint32(header, uint32(len(key)))
497497+ binary.BigEndian.PutUint32(header[4:], uint32(len(value)))
498498+499499+ _, err = w.Write(header)
500500+ if err != nil {
501501+ return 0, stacktrace.Propagate(err)
502502+ }
503503+504504+ _, err = w.Write(key)
505505+ if err != nil {
506506+ return 0, stacktrace.Propagate(err)
507507+ }
508508+509509+ _, err = w.Write(value)
510510+ if err != nil {
511511+ return 0, stacktrace.Propagate(err)
512512+ }
513513+514514+ numEntries++
515515+516516+ iterator.Next()
517517+ }
518518+519519+ if numEntries == 0 {
520520+ // there should always be at least one active validator
521521+ return 0, stacktrace.NewError("unexpectedly missing index entries for validator voting participation - treeVersion may be too old to export")
522522+ }
523523+524524+ return numEntries, nil
525525+}
526526+527527+func exportRecentBlockHeaders(blockHeaderGetter BlockHeaderGetter, treeVersion int64, w io.Writer) (int64, error) {
528528+ // export sufficient block headers for the nodes resuming from this snapshot to be able to e.g. execute TransactionActionCompleteChallenge
529529+ // (i.e. transactions which depend on recent block headers to determine end state)
530530+531531+ numBlockHeaders := max(CommitToChallengeMaxAgeInBlocks, CompleteChallengeMaxAgeInBlocks) + 5 // 5 blocks safety margin
532532+ startHeight := treeVersion - int64(numBlockHeaders)
533533+ if startHeight < 1 {
534534+ startHeight = 1
535535+ }
536536+537537+ numExportedBlockHeaders := int64(0)
538538+ for height := startHeight; height <= treeVersion; height++ {
539539+ blockHeader, err := blockHeaderGetter(height)
540540+ if err != nil {
541541+ return 0, stacktrace.Propagate(err)
542542+ }
543543+544544+ blockHeaderProto := blockHeader.ToProto()
545545+ blockHeaderBytes, err := blockHeaderProto.Marshal()
546546+ if err != nil {
547547+ return 0, stacktrace.Propagate(err)
548548+ }
549549+550550+ key := make([]byte, 1+8)
551551+ key[0] = store.IndexBlockHeaderKeyPrefix
552552+ binary.BigEndian.PutUint64(key[1:], uint64(height))
553553+554554+ header := make([]byte, 4+4)
555555+ binary.BigEndian.PutUint32(header, uint32(len(key)))
556556+ binary.BigEndian.PutUint32(header[4:], uint32(len(blockHeaderBytes)))
557557+558558+ _, err = w.Write(header)
559559+ if err != nil {
560560+ return 0, stacktrace.Propagate(err)
561561+ }
562562+563563+ _, err = w.Write(key)
564564+ if err != nil {
565565+ return 0, stacktrace.Propagate(err)
566566+ }
567567+568568+ _, err = w.Write(blockHeaderBytes)
569569+ if err != nil {
570570+ return 0, stacktrace.Propagate(err)
571571+ }
572572+573573+ numExportedBlockHeaders++
574574+ }
575575+576576+ return numExportedBlockHeaders, nil
453577}
454578455579func exportNodes(it *iavl.ImmutableTree, w io.Writer) (int64, error) {