···203203}
204204205205func (c *blockChallengeCoordinator) verifyBlockChallengeProof(height int64, validatorAddress []byte, proofBytes []byte) (bool, error) {
206206- tx := c.txFactory.ReadCommitted()
207207- if tx.Height() != height-1 {
208208- return false, stacktrace.NewError("challenge being verified for unexpected height %d, expected %d", height, tx.Height()+1)
206206+ // timestamp shouldn't matter for this
207207+ // it is however important that we read the tree exactly as it was on the height prior to the one where the proof was supposedly generated
208208+ // this is because operations can change over time (nullification) and also the returned data for the highest operation indexes will be different
209209+ tx, err := c.txFactory.ReadHeight(time.Time{}, height-1)
210210+ if err != nil {
211211+ return false, stacktrace.Propagate(err, "")
209212 }
210213211214 sharedPart, err := c.fetchOrBuildBlockChallengeCircuitAssignmentShared(tx, height)
+216-40
abciapp/range_challenge.go
···11package abciapp
2233import (
44- "bytes"
54 "context"
65 "encoding/binary"
66+ "errors"
77 "math/big"
88 "slices"
99+ "time"
9101011 "github.com/Yiling-J/theine-go"
1112 "github.com/cometbft/cometbft/crypto"
···1314 "github.com/cosmos/iavl"
1415 "github.com/cosmos/iavl/db"
1516 ics23 "github.com/cosmos/ics23/go"
1717+ cbornode "github.com/ipfs/go-ipld-cbor"
1618 "github.com/palantir/stacktrace"
1919+ "github.com/samber/mo"
1720 "tangled.org/gbl08ma.com/didplcbft/store"
1821 "tangled.org/gbl08ma.com/didplcbft/transaction"
2222+ "tangled.org/gbl08ma.com/didplcbft/types"
1923)
20242125type rangeChallengeCoordinator struct {
···2731 validatorAddress []byte
2832 txFactory *transaction.Factory
2933 nodeBlockStore *bftstore.BlockStore
3434+ mempoolSubmitter types.MempoolSubmitter
30353131- treeCache *theine.LoadingCache[treeCacheKey, cachedTree]
3636+ treeCache *theine.LoadingCache[treeCacheKey, cachedTree]
3737+ cachedNextProofFromHeight mo.Option[int64]
3238}
33393434-func newRangeChallengeCoordinator(runnerContext context.Context, txFactory *transaction.Factory, blockStore *bftstore.BlockStore, pubKey crypto.PubKey, privKey crypto.PrivKey) (*rangeChallengeCoordinator, error) {
4040+func newRangeChallengeCoordinator(runnerContext context.Context, txFactory *transaction.Factory, blockStore *bftstore.BlockStore, mempoolSubmitter types.MempoolSubmitter, pubKey crypto.PubKey, privKey crypto.PrivKey) (*rangeChallengeCoordinator, error) {
3541 c := &rangeChallengeCoordinator{
3642 txFactory: txFactory,
3743 runnerContext: runnerContext,
3844 nodeBlockStore: blockStore,
4545+ mempoolSubmitter: mempoolSubmitter,
3946 isConfiguredToBeValidator: pubKey != nil,
4047 validatorPubKey: pubKey,
4148 validatorPrivKey: privKey,
···6370 root []byte
6471}
65726666-type rangeChallengeProof struct {
6767- treeRoot []byte // the tree root we commit to. must be the same between the "commit proof" and the "confirmation proof"
6868- membershipProof *ics23.CommitmentProof
6969-}
7373+func (c *rangeChallengeCoordinator) getOrFetchNextProofFromHeight(tx transaction.Read) (int64, error) {
7474+ if !c.isConfiguredToBeValidator {
7575+ return 0, stacktrace.NewError("not configured to be a validator")
7676+ }
7777+ if completion, hasCache := c.cachedNextProofFromHeight.Get(); hasCache {
7878+ return completion, nil
7979+ }
70807171-func (c *rangeChallengeCoordinator) computeRangeChallengeProof(ctx context.Context, tx transaction.Read, startHeight, endHeight, proveHeight int64) (rangeChallengeProof, error) {
7272- ctx = context.WithValue(ctx, contextTxKey{}, tx)
7373- ct, err := c.treeCache.Get(ctx, treeCacheKey{
7474- startHeight: startHeight,
7575- endHeight: endHeight,
7676- })
8181+ completion, err := store.Consensus.ValidatorRangeChallengeCompletion(tx, c.validatorAddress)
7782 if err != nil {
7878- return rangeChallengeProof{}, stacktrace.Propagate(err, "")
8383+ if !errors.Is(err, store.ErrNoRecentChallengeCompletion) {
8484+ return 0, stacktrace.Propagate(err, "")
8585+ }
8686+ completion = 0
7987 }
80888181- proofKey := binary.BigEndian.AppendUint64(nil, uint64(proveHeight))
8282-8383- membershipProof, err := ct.tree.GetMembershipProof(proofKey)
8989+ minProvenBlock := int64(0)
9090+ for proofHeight := range store.Consensus.BlockChalengeProofsIterator(tx, 0, &err) {
9191+ minProvenBlock = int64(proofHeight)
9292+ break
9393+ }
8494 if err != nil {
8585- return rangeChallengeProof{}, stacktrace.Propagate(err, "")
9595+ return 0, stacktrace.Propagate(err, "")
8696 }
87978888- return rangeChallengeProof{
8989- treeRoot: ct.root,
9090- membershipProof: membershipProof,
9191- }, nil
9898+ minProvable := max(minProvenBlock, int64(completion+1))
9999+100100+ c.cachedNextProofFromHeight = mo.Some(minProvable)
101101+ return minProvable, nil
92102}
931039494-func verifyMembershipOfRangeChallengeProofs(proofs ...rangeChallengeProof) (bool, error) {
9595- if len(proofs) < 1 {
9696- return false, stacktrace.NewError("insufficient proofs")
104104+func (c *rangeChallengeCoordinator) onNewBlock(ctx context.Context, newBlockHeight int64) error {
105105+ if !c.isConfiguredToBeValidator {
106106+ return nil
97107 }
981089999- treeRoot := proofs[0].treeRoot
109109+ // TODO if ABCI is just replaying old blocks -> do nothing
110110+ // TODO wait until block is actually committed
111111+ // ^ solution to both of these problems: don't call this method from the ABCI app, instead, call it from an "outside subscriber" that listens for truly new blocks
100112101101- for _, proof := range proofs {
102102- if !bytes.Equal(proof.treeRoot, treeRoot) {
103103- // did not commit to the same root in all proofs
104104- return false, nil
113113+ tx := c.txFactory.ReadCommitted()
114114+ if tx.Height() < newBlockHeight {
115115+ // the ABCI app is probably still catching up to new blocks
116116+ return nil
117117+ }
118118+119119+ shouldCommitToChallenge := false
120120+ shouldCompleteChallenge := false
121121+122122+ fromHeight, toHeight, provenHeight, includedOnHeight, _, err := store.Consensus.ValidatorRangeChallengeCommitment(tx, c.validatorAddress)
123123+ if errors.Is(err, store.ErrNoActiveChallengeCommitment) {
124124+ nextFromHeight, err := c.getOrFetchNextProofFromHeight(tx)
125125+ if err != nil {
126126+ return stacktrace.Propagate(err, "")
105127 }
128128+ shouldCommitToChallenge = nextFromHeight+CommitToChallengeMinRange+50 <= newBlockHeight
129129+ } else if err != nil {
130130+ return stacktrace.Propagate(err, "")
131131+ } else {
132132+ commitmentBlockMeta := c.nodeBlockStore.LoadBlockMeta(int64(includedOnHeight))
133133+ commitmentExpired := false
134134+ if commitmentBlockMeta != nil {
135135+ commitmentExpired = uint64(newBlockHeight) >= includedOnHeight+CompleteChallengeMaxAgeInBlocks ||
136136+ time.Since(commitmentBlockMeta.Header.Time) >= CompleteChallengeMaxAge-1*time.Second
106137107107- // just use the key and value claimed in the proof as those should have been validated previously
108108- if exist := proof.membershipProof.GetExist(); exist != nil {
109109- if !ics23.VerifyMembership(ics23.IavlSpec, treeRoot, proof.membershipProof, exist.Key, exist.Value) {
110110- return false, nil
111111- }
112112- } else {
113113- return false, stacktrace.NewError("proof is not an existence proof")
138138+ // shouldCompleteChallenge if not too many blocks have passed AND enough blocks have passed
139139+ shouldCompleteChallenge = !commitmentExpired && includedOnHeight+1 <= uint64(newBlockHeight)
140140+ }
141141+142142+ if !shouldCompleteChallenge {
143143+ shouldCommitToChallenge = commitmentExpired
114144 }
115145 }
116146117117- return true, nil
147147+ var transactionBytes []byte
148148+ if shouldCompleteChallenge {
149149+ transactionBytes, err = c.createCompleteChallengeTx(ctx, tx, int64(fromHeight), int64(toHeight), int64(provenHeight), int64(includedOnHeight))
150150+ if err != nil {
151151+ return stacktrace.Propagate(err, "")
152152+ }
153153+ } else if shouldCommitToChallenge {
154154+ transactionBytes, err = c.createCommitToChallengeTx(ctx, tx, newBlockHeight)
155155+ if err != nil {
156156+ return stacktrace.Propagate(err, "")
157157+ }
158158+ }
159159+160160+ _, err = c.mempoolSubmitter.BroadcastTxCommit(ctx, transactionBytes)
161161+ if err != nil {
162162+ return stacktrace.Propagate(err, "")
163163+ }
164164+ c.cachedNextProofFromHeight = mo.None[int64]()
165165+ return nil
118166}
119167120120-func computeHeightToProveInRange(lastCommitHash, validatorAddress []byte, fromHeight, toHeight int64) uint64 {
168168+func (c *rangeChallengeCoordinator) createCommitToChallengeTx(ctx context.Context, tx transaction.Read, toHeight int64) ([]byte, error) {
169169+ if !c.isConfiguredToBeValidator {
170170+ return nil, stacktrace.NewError("not configured to be validator")
171171+ }
172172+173173+ fromHeight, err := c.getOrFetchNextProofFromHeight(tx)
174174+ if err != nil {
175175+ return nil, stacktrace.Propagate(err, "")
176176+ }
177177+178178+ if toHeight < fromHeight+CommitToChallengeMinRange {
179179+ return nil, stacktrace.NewError("insufficient blocks passed")
180180+ }
181181+182182+ if toHeight > fromHeight+CommitToChallengeMaxRange {
183183+ fromHeight = toHeight - CommitToChallengeMaxRange
184184+ }
185185+186186+ pubKeyArg, err := MarshalPubKeyForArguments(c.validatorPubKey)
187187+ if err != nil {
188188+ return nil, stacktrace.Propagate(err, "")
189189+ }
190190+191191+ toHeightBlockMeta := c.nodeBlockStore.LoadBlockMeta(toHeight)
192192+ if toHeightBlockMeta == nil {
193193+ return nil, stacktrace.NewError("block not found at height")
194194+ }
195195+196196+ if time.Since(toHeightBlockMeta.Header.Time) > CommitToChallengeMaxAge {
197197+ return nil, stacktrace.NewError("too much time passed since block at height")
198198+ }
199199+200200+ proveHeight := computeHeightToProveInRange(toHeightBlockMeta.Header.LastCommitHash, c.validatorAddress, int64(fromHeight), int64(toHeight), mo.None[int64]())
201201+202202+ commitToRoot, membershipProof, err := c.computeRangeChallengeProof(ctx, tx, fromHeight, toHeight, proveHeight)
203203+ if err != nil {
204204+ return nil, stacktrace.Propagate(err, "")
205205+ }
206206+207207+ proofBytes, err := membershipProof.Marshal()
208208+ if err != nil {
209209+ return nil, stacktrace.Propagate(err, "")
210210+ }
211211+212212+ transaction := Transaction[CommitToChallengeArguments]{
213213+ Action: TransactionActionCommitToChallenge,
214214+ Arguments: CommitToChallengeArguments{
215215+ ValidatorPubKey: pubKeyArg,
216216+ FromHeight: int64(fromHeight),
217217+ ToHeight: int64(toHeight),
218218+ Root: commitToRoot,
219219+ Proof: proofBytes,
220220+ },
221221+ }
222222+223223+ transaction, err = SignTransaction(c.validatorPrivKey, transaction)
224224+ if err != nil {
225225+ return nil, stacktrace.Propagate(err, "")
226226+ }
227227+228228+ out, err := cbornode.DumpObject(transaction)
229229+ if err != nil {
230230+ return nil, stacktrace.Propagate(err, "")
231231+ }
232232+ return out, nil
233233+}
234234+235235+func (c *rangeChallengeCoordinator) createCompleteChallengeTx(ctx context.Context, tx transaction.Read, fromHeight, toHeight, prevProvenHeight, commitmentIncludedOnHeight int64) ([]byte, error) {
236236+ if !c.isConfiguredToBeValidator {
237237+ return nil, stacktrace.NewError("not configured to be validator")
238238+ }
239239+240240+ nextBlockMeta := c.nodeBlockStore.LoadBlockMeta(commitmentIncludedOnHeight + 1)
241241+ if nextBlockMeta == nil {
242242+ return nil, stacktrace.NewError("block not found at height")
243243+ }
244244+245245+ proveHeight := computeHeightToProveInRange(nextBlockMeta.Header.LastCommitHash, c.validatorAddress, int64(fromHeight), int64(toHeight), mo.Some(prevProvenHeight))
246246+247247+ _, membershipProof, err := c.computeRangeChallengeProof(ctx, tx, fromHeight, toHeight, proveHeight)
248248+ if err != nil {
249249+ return nil, stacktrace.Propagate(err, "")
250250+ }
251251+252252+ proofBytes, err := membershipProof.Marshal()
253253+ if err != nil {
254254+ return nil, stacktrace.Propagate(err, "")
255255+ }
256256+257257+ transaction := Transaction[CompleteChallengeArguments]{
258258+ Action: TransactionActionCompleteChallenge,
259259+ Arguments: CompleteChallengeArguments{
260260+ Validator: c.validatorAddress,
261261+ Proof: proofBytes,
262262+ },
263263+ }
264264+265265+ out, err := cbornode.DumpObject(transaction)
266266+ if err != nil {
267267+ return nil, stacktrace.Propagate(err, "")
268268+ }
269269+ return out, nil
270270+}
271271+272272+func (c *rangeChallengeCoordinator) computeRangeChallengeProof(ctx context.Context, tx transaction.Read, startHeight, endHeight, proveHeight int64) ([]byte, *ics23.CommitmentProof, error) {
273273+ ctx = context.WithValue(ctx, contextTxKey{}, tx)
274274+ ct, err := c.treeCache.Get(ctx, treeCacheKey{
275275+ startHeight: startHeight,
276276+ endHeight: endHeight,
277277+ })
278278+ if err != nil {
279279+ return nil, nil, stacktrace.Propagate(err, "")
280280+ }
281281+282282+ proofKey := binary.BigEndian.AppendUint64(nil, uint64(proveHeight))
283283+284284+ membershipProof, err := ct.tree.GetMembershipProof(proofKey)
285285+ if err != nil {
286286+ return nil, nil, stacktrace.Propagate(err, "")
287287+ }
288288+289289+ return ct.root, membershipProof, nil
290290+}
291291+292292+func computeHeightToProveInRange(lastCommitHash, validatorAddress []byte, fromHeight, toHeight int64, avoidHeight mo.Option[int64]) int64 {
121293 lastCommitHashBigInt := new(big.Int).SetBytes(lastCommitHash)
122294 validatorBigInt := new(big.Int).SetBytes(validatorAddress)
123295 seed := new(big.Int).Xor(lastCommitHashBigInt, validatorBigInt)
···126298127299 randOffset := new(big.Int).Mod(seed, big.NewInt(numBlocks))
128300129129- return uint64(fromHeight) + randOffset.Uint64()
301301+ candidateHeight := fromHeight + int64(randOffset.Uint64())
302302+ if h, ok := avoidHeight.Get(); ok && candidateHeight == h {
303303+ candidateHeight = (candidateHeight + 1) % int64(numBlocks)
304304+ }
305305+ return candidateHeight
130306}
131307132308func (c *rangeChallengeCoordinator) proofTreeLoader(ctx context.Context, cacheKey treeCacheKey) (theine.Loaded[cachedTree], error) {