This repository has been archived by the owner on Apr 14, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.go
754 lines (684 loc) · 26.4 KB
/
app.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
package app
import (
"bytes"
"context"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"math/rand"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/cometbft/cometbft/abci/example/kvstore"
abci "github.com/cometbft/cometbft/abci/types"
"github.com/cometbft/cometbft/crypto"
cryptoenc "github.com/cometbft/cometbft/crypto/encoding"
"github.com/cometbft/cometbft/libs/log"
"github.com/cometbft/cometbft/libs/protoio"
cryptoproto "github.com/cometbft/cometbft/proto/tendermint/crypto"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
"github.com/cometbft/cometbft/version"
)
const (
appVersion = 1
voteExtensionKey string = "extensionSum"
voteExtensionMaxVal int64 = 128
prefixReservedKey string = "reservedTxKey_"
suffixChainID string = "ChainID"
suffixVoteExtHeight string = "VoteExtensionsHeight"
suffixInitialHeight string = "InitialHeight"
)
// Application is an ABCI application for use by end-to-end tests. It is a
// simple key/value store for strings, storing data in memory and persisting
// to disk as JSON, taking state sync snapshots if requested.
type Application struct {
abci.BaseApplication
logger log.Logger
state *State
snapshots *SnapshotStore
cfg *Config
restoreSnapshot *abci.Snapshot
restoreChunks [][]byte
}
// Config allows for the setting of high level parameters for running the e2e Application
// KeyType and ValidatorUpdates must be the same for all nodes running the same application.
type Config struct {
// The directory with which state.json will be persisted in. Usually $HOME/.cometbft/data
Dir string `toml:"dir"`
// SnapshotInterval specifies the height interval at which the application
// will take state sync snapshots. Defaults to 0 (disabled).
SnapshotInterval uint64 `toml:"snapshot_interval"`
// RetainBlocks specifies the number of recent blocks to retain. Defaults to
// 0, which retains all blocks. Must be greater that PersistInterval,
// SnapshotInterval and EvidenceAgeHeight.
RetainBlocks uint64 `toml:"retain_blocks"`
// KeyType sets the curve that will be used by validators.
// Options are ed25519 & secp256k1
KeyType string `toml:"key_type"`
// PersistInterval specifies the height interval at which the application
// will persist state to disk. Defaults to 1 (every height), setting this to
// 0 disables state persistence.
PersistInterval uint64 `toml:"persist_interval"`
// ValidatorUpdates is a map of heights to validator names and their power,
// and will be returned by the ABCI application. For example, the following
// changes the power of validator01 and validator02 at height 1000:
//
// [validator_update.1000]
// validator01 = 20
// validator02 = 10
//
// Specifying height 0 returns the validator update during InitChain. The
// application returns the validator updates as-is, i.e. removing a
// validator must be done by returning it with power 0, and any validators
// not specified are not changed.
//
// height <-> pubkey <-> voting power
ValidatorUpdates map[string]map[string]uint8 `toml:"validator_update"`
// Add artificial delays to each of the main ABCI calls to mimic computation time
// of the application
PrepareProposalDelay time.Duration `toml:"prepare_proposal_delay"`
ProcessProposalDelay time.Duration `toml:"process_proposal_delay"`
CheckTxDelay time.Duration `toml:"check_tx_delay"`
FinalizeBlockDelay time.Duration `toml:"finalize_block_delay"`
VoteExtensionDelay time.Duration `toml:"vote_extension_delay"`
}
func DefaultConfig(dir string) *Config {
return &Config{
PersistInterval: 1,
SnapshotInterval: 100,
Dir: dir,
}
}
// NewApplication creates the application.
func NewApplication(cfg *Config) (*Application, error) {
state, err := NewState(cfg.Dir, cfg.PersistInterval)
if err != nil {
return nil, err
}
snapshots, err := NewSnapshotStore(filepath.Join(cfg.Dir, "snapshots"))
if err != nil {
return nil, err
}
return &Application{
logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)),
state: state,
snapshots: snapshots,
cfg: cfg,
}, nil
}
// Info implements ABCI.
func (app *Application) Info(context.Context, *abci.RequestInfo) (*abci.ResponseInfo, error) {
height, hash := app.state.Info()
return &abci.ResponseInfo{
Version: version.ABCIVersion,
AppVersion: appVersion,
LastBlockHeight: int64(height),
LastBlockAppHash: hash,
}, nil
}
// Info implements ABCI.
func (app *Application) InitChain(_ context.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) {
var err error
app.state.initialHeight = uint64(req.InitialHeight)
if len(req.AppStateBytes) > 0 {
err = app.state.Import(0, req.AppStateBytes)
if err != nil {
return nil, err
}
}
app.logger.Info("setting ChainID in app_state", "chainId", req.ChainId)
app.state.Set(prefixReservedKey+suffixChainID, req.ChainId)
app.logger.Info("setting VoteExtensionsHeight in app_state", "height", req.ConsensusParams.Abci.VoteExtensionsEnableHeight)
app.state.Set(prefixReservedKey+suffixVoteExtHeight, strconv.FormatInt(req.ConsensusParams.Abci.VoteExtensionsEnableHeight, 10))
app.logger.Info("setting initial height in app_state", "initial_height", req.InitialHeight)
app.state.Set(prefixReservedKey+suffixInitialHeight, strconv.FormatInt(req.InitialHeight, 10))
// Get validators from genesis
if req.Validators != nil {
for _, val := range req.Validators {
val := val
if err := app.storeValidator(&val); err != nil {
return nil, err
}
}
}
resp := &abci.ResponseInitChain{
AppHash: app.state.GetHash(),
}
if resp.Validators, err = app.validatorUpdates(0); err != nil {
return nil, err
}
return resp, nil
}
// CheckTx implements ABCI.
func (app *Application) CheckTx(_ context.Context, req *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) {
key, _, err := parseTx(req.Tx)
if err != nil || key == prefixReservedKey {
return &abci.ResponseCheckTx{
Code: kvstore.CodeTypeEncodingError,
Log: err.Error(),
}, nil
}
if app.cfg.CheckTxDelay != 0 {
time.Sleep(app.cfg.CheckTxDelay)
}
return &abci.ResponseCheckTx{Code: kvstore.CodeTypeOK, GasWanted: 1}, nil
}
// FinalizeBlock implements ABCI.
func (app *Application) FinalizeBlock(_ context.Context, req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) {
txs := make([]*abci.ExecTxResult, len(req.Txs))
for i, tx := range req.Txs {
key, value, err := parseTx(tx)
if err != nil {
panic(err) // shouldn't happen since we verified it in CheckTx and ProcessProposal
}
if key == prefixReservedKey {
panic(fmt.Errorf("detected a transaction with key %q; this key is reserved and should have been filtered out", prefixReservedKey))
}
app.state.Set(key, value)
txs[i] = &abci.ExecTxResult{Code: kvstore.CodeTypeOK}
}
valUpdates, err := app.validatorUpdates(uint64(req.Height))
if err != nil {
panic(err)
}
if app.cfg.FinalizeBlockDelay != 0 {
time.Sleep(app.cfg.FinalizeBlockDelay)
}
return &abci.ResponseFinalizeBlock{
TxResults: txs,
ValidatorUpdates: valUpdates,
AppHash: app.state.Finalize(),
Events: []abci.Event{
{
Type: "val_updates",
Attributes: []abci.EventAttribute{
{
Key: "size",
Value: strconv.Itoa(valUpdates.Len()),
},
{
Key: "height",
Value: strconv.Itoa(int(req.Height)),
},
},
},
},
}, nil
}
// Commit implements ABCI.
func (app *Application) Commit(_ context.Context, _ *abci.RequestCommit) (*abci.ResponseCommit, error) {
height, err := app.state.Commit()
if err != nil {
panic(err)
}
if app.cfg.SnapshotInterval > 0 && height%app.cfg.SnapshotInterval == 0 {
snapshot, err := app.snapshots.Create(app.state)
if err != nil {
panic(err)
}
app.logger.Info("created state sync snapshot", "height", snapshot.Height)
err = app.snapshots.Prune(maxSnapshotCount)
if err != nil {
app.logger.Error("failed to prune snapshots", "err", err)
}
}
retainHeight := int64(0)
if app.cfg.RetainBlocks > 0 {
retainHeight = int64(height - app.cfg.RetainBlocks + 1)
}
return &abci.ResponseCommit{
RetainHeight: retainHeight,
}, nil
}
// Query implements ABCI.
func (app *Application) Query(_ context.Context, req *abci.RequestQuery) (*abci.ResponseQuery, error) {
value, height := app.state.Query(string(req.Data))
return &abci.ResponseQuery{
Height: int64(height),
Key: req.Data,
Value: []byte(value),
}, nil
}
// ListSnapshots implements ABCI.
func (app *Application) ListSnapshots(context.Context, *abci.RequestListSnapshots) (*abci.ResponseListSnapshots, error) {
snapshots, err := app.snapshots.List()
if err != nil {
panic(err)
}
return &abci.ResponseListSnapshots{Snapshots: snapshots}, nil
}
// LoadSnapshotChunk implements ABCI.
func (app *Application) LoadSnapshotChunk(_ context.Context, req *abci.RequestLoadSnapshotChunk) (*abci.ResponseLoadSnapshotChunk, error) {
chunk, err := app.snapshots.LoadChunk(req.Height, req.Format, req.Chunk)
if err != nil {
panic(err)
}
return &abci.ResponseLoadSnapshotChunk{Chunk: chunk}, nil
}
// OfferSnapshot implements ABCI.
func (app *Application) OfferSnapshot(_ context.Context, req *abci.RequestOfferSnapshot) (*abci.ResponseOfferSnapshot, error) {
if app.restoreSnapshot != nil {
panic("A snapshot is already being restored")
}
app.restoreSnapshot = req.Snapshot
app.restoreChunks = [][]byte{}
return &abci.ResponseOfferSnapshot{Result: abci.ResponseOfferSnapshot_ACCEPT}, nil
}
// ApplySnapshotChunk implements ABCI.
func (app *Application) ApplySnapshotChunk(_ context.Context, req *abci.RequestApplySnapshotChunk) (*abci.ResponseApplySnapshotChunk, error) {
if app.restoreSnapshot == nil {
panic("No restore in progress")
}
app.restoreChunks = append(app.restoreChunks, req.Chunk)
if len(app.restoreChunks) == int(app.restoreSnapshot.Chunks) {
bz := []byte{}
for _, chunk := range app.restoreChunks {
bz = append(bz, chunk...)
}
err := app.state.Import(app.restoreSnapshot.Height, bz)
if err != nil {
panic(err)
}
app.restoreSnapshot = nil
app.restoreChunks = nil
}
return &abci.ResponseApplySnapshotChunk{Result: abci.ResponseApplySnapshotChunk_ACCEPT}, nil
}
// PrepareProposal will take the given transactions and attempt to prepare a
// proposal from them when it's our turn to do so. If the current height has
// vote extension enabled, this method will use vote extensions from the previous
// height, passed from CometBFT as parameters to construct a special transaction
// whose value is the sum of all of the vote extensions from the previous round.
//
// Additionally, we verify the vote extension signatures passed from CometBFT and
// include all data necessary for such verification in the special transaction's
// payload so that ProcessProposal at other nodes can also verify the proposer
// constructed the special transaction correctly.
//
// If vote extensions are enabled for the current height, PrepareProposal makes
// sure there was at least one non-empty vote extension whose signature it could verify.
// If vote extensions are not enabled for the current height, PrepareProposal makes
// sure non-empty vote extensions are not present.
//
// The special vote extension-generated transaction must fit within an empty block
// and takes precedence over all other transactions coming from the mempool.
func (app *Application) PrepareProposal(
_ context.Context, req *abci.RequestPrepareProposal,
) (*abci.ResponsePrepareProposal, error) {
_, areExtensionsEnabled := app.checkHeightAndExtensions(true, req.Height, "PrepareProposal")
txs := make([][]byte, 0, len(req.Txs)+1)
var totalBytes int64
extTxPrefix := fmt.Sprintf("%s=", voteExtensionKey)
sum, err := app.verifyAndSum(areExtensionsEnabled, req.Height, &req.LocalLastCommit, "prepare_proposal")
if err != nil {
panic(fmt.Errorf("failed to sum and verify in PrepareProposal; err %w", err))
}
if areExtensionsEnabled {
extCommitBytes, err := req.LocalLastCommit.Marshal()
if err != nil {
panic("unable to marshall extended commit")
}
extCommitHex := hex.EncodeToString(extCommitBytes)
extTx := []byte(fmt.Sprintf("%s%d|%s", extTxPrefix, sum, extCommitHex))
extTxLen := int64(len(extTx))
app.logger.Info("preparing proposal with special transaction from vote extensions", "extTxLen", extTxLen)
if extTxLen > req.MaxTxBytes {
panic(fmt.Errorf("serious problem in the e2e app configuration; "+
"the tx conveying the vote extension data does not fit in an empty block(%d > %d); "+
"please review the app's configuration",
extTxLen, req.MaxTxBytes))
}
txs = append(txs, extTx)
// Coherence: No need to call parseTx, as the check is stateless and has been performed by CheckTx
totalBytes = extTxLen
}
for _, tx := range req.Txs {
if areExtensionsEnabled && strings.HasPrefix(string(tx), extTxPrefix) {
// When vote extensions are enabled, our generated transaction takes precedence
// over any supplied transaction that attempts to modify the "extensionSum" value.
continue
}
if strings.HasPrefix(string(tx), prefixReservedKey) {
app.logger.Error("detected tx that should not come from the mempool", "tx", tx)
continue
}
if totalBytes+int64(len(tx)) > req.MaxTxBytes {
break
}
totalBytes += int64(len(tx))
// Coherence: No need to call parseTx, as the check is stateless and has been performed by CheckTx
txs = append(txs, tx)
}
if app.cfg.PrepareProposalDelay != 0 {
time.Sleep(app.cfg.PrepareProposalDelay)
}
return &abci.ResponsePrepareProposal{Txs: txs}, nil
}
// ProcessProposal implements part of the Application interface.
// It accepts any proposal that does not contain a malformed transaction.
// NOTE It is up to real Applications to effect punitive behavior in the cases ProcessProposal
// returns ResponseProcessProposal_REJECT, as it is evidence of misbehavior.
func (app *Application) ProcessProposal(_ context.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) {
_, areExtensionsEnabled := app.checkHeightAndExtensions(true, req.Height, "ProcessProposal")
for _, tx := range req.Txs {
k, v, err := parseTx(tx)
if err != nil {
app.logger.Error("malformed transaction in ProcessProposal", "tx", tx, "err", err)
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil
}
switch {
case areExtensionsEnabled && k == voteExtensionKey:
// Additional check for vote extension-related txs
if err := app.verifyExtensionTx(req.Height, v); err != nil {
app.logger.Error("vote extension transaction failed verification, rejecting proposal", k, v, "err", err)
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil
}
case strings.HasPrefix(k, prefixReservedKey):
app.logger.Error("key prefix %q is reserved and cannot be used in transactions, rejecting proposal", k)
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil
}
}
if app.cfg.ProcessProposalDelay != 0 {
time.Sleep(app.cfg.ProcessProposalDelay)
}
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil
}
// ExtendVote will produce vote extensions in the form of random numbers to
// demonstrate vote extension nondeterminism.
//
// In the next block, if there are any vote extensions from the previous block,
// a new transaction will be proposed that updates a special value in the
// key/value store ("extensionSum") with the sum of all of the numbers collected
// from the vote extensions.
func (app *Application) ExtendVote(_ context.Context, req *abci.RequestExtendVote) (*abci.ResponseExtendVote, error) {
appHeight, areExtensionsEnabled := app.checkHeightAndExtensions(false, req.Height, "ExtendVote")
if !areExtensionsEnabled {
panic(fmt.Errorf("received call to ExtendVote at height %d, when vote extensions are disabled", appHeight))
}
ext := make([]byte, binary.MaxVarintLen64)
// We don't care that these values are generated by a weak random number
// generator. It's just for test purposes.
//nolint:gosec // G404: Use of weak random number generator
num := rand.Int63n(voteExtensionMaxVal)
extLen := binary.PutVarint(ext, num)
if app.cfg.VoteExtensionDelay != 0 {
time.Sleep(app.cfg.VoteExtensionDelay)
}
app.logger.Info("generated vote extension", "num", num, "ext", fmt.Sprintf("%x", ext[:extLen]), "height", appHeight)
return &abci.ResponseExtendVote{
VoteExtension: ext[:extLen],
}, nil
}
// VerifyVoteExtension simply validates vote extensions from other validators
// without doing anything about them. In this case, it just makes sure that the
// vote extension is a well-formed integer value.
func (app *Application) VerifyVoteExtension(_ context.Context, req *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error) {
appHeight, areExtensionsEnabled := app.checkHeightAndExtensions(false, req.Height, "VerifyVoteExtension")
if !areExtensionsEnabled {
panic(fmt.Errorf("received call to VerifyVoteExtension at height %d, when vote extensions are disabled", appHeight))
}
// We don't allow vote extensions to be optional
if len(req.VoteExtension) == 0 {
app.logger.Error("received empty vote extension")
return &abci.ResponseVerifyVoteExtension{
Status: abci.ResponseVerifyVoteExtension_REJECT,
}, nil
}
num, err := parseVoteExtension(req.VoteExtension)
if err != nil {
app.logger.Error("failed to parse vote extension", "vote_extension", req.VoteExtension, "err", err)
return &abci.ResponseVerifyVoteExtension{
Status: abci.ResponseVerifyVoteExtension_REJECT,
}, nil
}
if app.cfg.VoteExtensionDelay != 0 {
time.Sleep(app.cfg.VoteExtensionDelay)
}
app.logger.Info("verified vote extension value", "height", req.Height, "vote_extension", req.VoteExtension, "num", num)
return &abci.ResponseVerifyVoteExtension{
Status: abci.ResponseVerifyVoteExtension_ACCEPT,
}, nil
}
func (app *Application) Rollback() error {
return app.state.Rollback()
}
func (app *Application) getAppHeight() int64 {
initialHeightStr, height := app.state.Query(prefixReservedKey + suffixInitialHeight)
if len(initialHeightStr) == 0 {
panic("initial height not set in database")
}
initialHeight, err := strconv.ParseInt(initialHeightStr, 10, 64)
if err != nil {
panic(fmt.Errorf("malformed initial height %q in database", initialHeightStr))
}
appHeight := int64(height)
if appHeight == 0 {
appHeight = initialHeight - 1
}
return appHeight + 1
}
func (app *Application) checkHeightAndExtensions(isPrepareProcessProposal bool, height int64, callsite string) (int64, bool) {
appHeight := app.getAppHeight()
if height != appHeight {
panic(fmt.Errorf(
"got unexpected height in %s request; expected %d, actual %d",
callsite, appHeight, height,
))
}
voteExtHeightStr := app.state.Get(prefixReservedKey + suffixVoteExtHeight)
if len(voteExtHeightStr) == 0 {
panic("vote extension height not set in database")
}
voteExtHeight, err := strconv.ParseInt(voteExtHeightStr, 10, 64)
if err != nil {
panic(fmt.Errorf("malformed vote extension height %q in database", voteExtHeightStr))
}
currentHeight := appHeight
if isPrepareProcessProposal {
currentHeight-- // at exactly voteExtHeight, PrepareProposal still has no extensions, see RFC100
}
return appHeight, voteExtHeight != 0 && currentHeight >= voteExtHeight
}
func (app *Application) storeValidator(valUpdate *abci.ValidatorUpdate) error {
// Store validator data to verify extensions
pubKey, err := cryptoenc.PubKeyFromProto(valUpdate.PubKey)
if err != nil {
return err
}
addr := pubKey.Address().String()
if valUpdate.Power > 0 {
pubKeyBytes, err := valUpdate.PubKey.Marshal()
if err != nil {
return err
}
app.logger.Info("setting validator in app_state", "addr", addr)
app.state.Set(prefixReservedKey+addr, hex.EncodeToString(pubKeyBytes))
}
return nil
}
// validatorUpdates generates a validator set update.
func (app *Application) validatorUpdates(height uint64) (abci.ValidatorUpdates, error) {
updates := app.cfg.ValidatorUpdates[fmt.Sprintf("%v", height)]
if len(updates) == 0 {
return nil, nil
}
valUpdates := abci.ValidatorUpdates{}
for keyString, power := range updates {
keyBytes, err := base64.StdEncoding.DecodeString(keyString)
if err != nil {
return nil, fmt.Errorf("invalid base64 pubkey value %q: %w", keyString, err)
}
valUpdate := abci.UpdateValidator(keyBytes, int64(power), app.cfg.KeyType)
valUpdates = append(valUpdates, valUpdate)
if err := app.storeValidator(&valUpdate); err != nil {
return nil, err
}
}
return valUpdates, nil
}
// parseTx parses a tx in 'key=value' format into a key and value.
func parseTx(tx []byte) (string, string, error) {
parts := bytes.Split(tx, []byte("="))
if len(parts) != 2 {
return "", "", fmt.Errorf("invalid tx format: %q", string(tx))
}
if len(parts[0]) == 0 {
return "", "", errors.New("key cannot be empty")
}
return string(parts[0]), string(parts[1]), nil
}
func (app *Application) verifyAndSum(
areExtensionsEnabled bool,
currentHeight int64,
extCommit *abci.ExtendedCommitInfo,
callsite string,
) (int64, error) {
var sum int64
var extCount int
for _, vote := range extCommit.Votes {
if vote.BlockIdFlag == cmtproto.BlockIDFlagUnknown || vote.BlockIdFlag > cmtproto.BlockIDFlagNil {
return 0, fmt.Errorf("vote with bad blockID flag value at height %d; blockID flag %d", currentHeight, vote.BlockIdFlag)
}
if vote.BlockIdFlag == cmtproto.BlockIDFlagAbsent || vote.BlockIdFlag == cmtproto.BlockIDFlagNil {
if len(vote.VoteExtension) != 0 {
return 0, fmt.Errorf("non-empty vote extension at height %d, for a vote with blockID flag %d",
currentHeight, vote.BlockIdFlag)
}
if len(vote.ExtensionSignature) != 0 {
return 0, fmt.Errorf("non-empty vote extension signature at height %d, for a vote with blockID flag %d",
currentHeight, vote.BlockIdFlag)
}
// Only interested in votes that can have extensions
continue
}
if !areExtensionsEnabled {
if len(vote.VoteExtension) != 0 {
return 0, fmt.Errorf("non-empty vote extension at height %d, which has extensions disabled",
currentHeight)
}
if len(vote.ExtensionSignature) != 0 {
return 0, fmt.Errorf("non-empty vote extension signature at height %d, which has extensions disabled",
currentHeight)
}
continue
}
if len(vote.VoteExtension) == 0 {
return 0, fmt.Errorf("received empty vote extension from %X at height %d (extensions enabled); "+
"e2e app's logic does not allow it", vote.Validator, currentHeight)
}
// Vote extension signatures are always provided. Apps can use them to verify the integrity of extensions
if len(vote.ExtensionSignature) == 0 {
return 0, fmt.Errorf("empty vote extension signature at height %d (extensions enabled)", currentHeight)
}
// Reconstruct vote extension's signed bytes...
chainID := app.state.Get(prefixReservedKey + suffixChainID)
if len(chainID) == 0 {
panic("chainID not set in database")
}
cve := cmtproto.CanonicalVoteExtension{
Extension: vote.VoteExtension,
Height: currentHeight - 1, // the vote extension was signed in the previous height
Round: int64(extCommit.Round),
ChainId: chainID,
}
extSignBytes, err := protoio.MarshalDelimited(&cve)
if err != nil {
return 0, fmt.Errorf("error when marshaling signed bytes: %w", err)
}
//... and verify
valAddr := crypto.Address(vote.Validator.Address).String()
pubKeyHex := app.state.Get(prefixReservedKey + valAddr)
if len(pubKeyHex) == 0 {
return 0, fmt.Errorf("received vote from unknown validator with address %q", valAddr)
}
pubKeyBytes, err := hex.DecodeString(pubKeyHex)
if err != nil {
return 0, fmt.Errorf("could not hex-decode public key for validator address %s, err %w", valAddr, err)
}
var pubKeyProto cryptoproto.PublicKey
err = pubKeyProto.Unmarshal(pubKeyBytes)
if err != nil {
return 0, fmt.Errorf("unable to unmarshal public key for validator address %s, err %w", valAddr, err)
}
pubKey, err := cryptoenc.PubKeyFromProto(pubKeyProto)
if err != nil {
return 0, fmt.Errorf("could not obtain a public key from its proto for validator address %s, err %w", valAddr, err)
}
if !pubKey.VerifySignature(extSignBytes, vote.ExtensionSignature) {
return 0, errors.New("received vote with invalid signature")
}
extValue, err := parseVoteExtension(vote.VoteExtension)
// The extension's format should have been verified in VerifyVoteExtension
if err != nil {
return 0, fmt.Errorf("failed to parse vote extension: %w", err)
}
app.logger.Info(
"received and verified vote extension value",
"height", currentHeight,
"valAddr", valAddr,
"value", extValue,
"callsite", callsite,
)
sum += extValue
extCount++
}
if areExtensionsEnabled && (extCount == 0) {
return 0, errors.New("bad extension data, at least one extended vote should be present when extensions are enabled")
}
return sum, nil
}
// verifyExtensionTx parses and verifies the payload of a vote extension-generated tx
func (app *Application) verifyExtensionTx(height int64, payload string) error {
parts := strings.Split(payload, "|")
if len(parts) != 2 {
return fmt.Errorf("invalid payload format")
}
expSumStr := parts[0]
if len(expSumStr) == 0 {
return fmt.Errorf("sum cannot be empty in vote extension payload")
}
expSum, err := strconv.Atoi(expSumStr)
if err != nil {
return fmt.Errorf("malformed sum %q in vote extension payload", expSumStr)
}
extCommitHex := parts[1]
if len(extCommitHex) == 0 {
return fmt.Errorf("extended commit data cannot be empty in vote extension payload")
}
extCommitBytes, err := hex.DecodeString(extCommitHex)
if err != nil {
return fmt.Errorf("could not hex-decode vote extension payload")
}
var extCommit abci.ExtendedCommitInfo
if extCommit.Unmarshal(extCommitBytes) != nil {
return fmt.Errorf("unable to unmarshal extended commit")
}
sum, err := app.verifyAndSum(true, height, &extCommit, "process_proposal")
if err != nil {
return fmt.Errorf("failed to sum and verify in process proposal: %w", err)
}
// Final check that the proposer behaved correctly
if int64(expSum) != sum {
return fmt.Errorf("sum is not consistent with vote extension payload: %d!=%d", expSum, sum)
}
return nil
}
// parseVoteExtension attempts to parse the given extension data into a positive
// integer value.
func parseVoteExtension(ext []byte) (int64, error) {
num, errVal := binary.Varint(ext)
if errVal == 0 {
return 0, errors.New("vote extension is too small to parse")
}
if errVal < 0 {
return 0, errors.New("vote extension value is too large")
}
if num >= voteExtensionMaxVal {
return 0, fmt.Errorf("vote extension value must be smaller than %d (was %d)", voteExtensionMaxVal, num)
}
return num, nil
}