github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/test/e2e/app/app.go (about) 1 package app 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "errors" 7 "fmt" 8 "os" 9 "path/filepath" 10 "strconv" 11 "time" 12 13 "github.com/badrootd/nibiru-cometbft/abci/example/code" 14 abci "github.com/badrootd/nibiru-cometbft/abci/types" 15 "github.com/badrootd/nibiru-cometbft/libs/log" 16 "github.com/badrootd/nibiru-cometbft/version" 17 ) 18 19 const appVersion = 1 20 21 // Application is an ABCI application for use by end-to-end tests. It is a 22 // simple key/value store for strings, storing data in memory and persisting 23 // to disk as JSON, taking state sync snapshots if requested. 24 25 type Application struct { 26 abci.BaseApplication 27 logger log.Logger 28 state *State 29 snapshots *SnapshotStore 30 cfg *Config 31 restoreSnapshot *abci.Snapshot 32 restoreChunks [][]byte 33 } 34 35 // Config allows for the setting of high level parameters for running the e2e Application 36 // KeyType and ValidatorUpdates must be the same for all nodes running the same application. 37 type Config struct { 38 // The directory with which state.json will be persisted in. Usually $HOME/.cometbft/data 39 Dir string `toml:"dir"` 40 41 // SnapshotInterval specifies the height interval at which the application 42 // will take state sync snapshots. Defaults to 0 (disabled). 43 SnapshotInterval uint64 `toml:"snapshot_interval"` 44 45 // RetainBlocks specifies the number of recent blocks to retain. Defaults to 46 // 0, which retains all blocks. Must be greater that PersistInterval, 47 // SnapshotInterval and EvidenceAgeHeight. 48 RetainBlocks uint64 `toml:"retain_blocks"` 49 50 // KeyType sets the curve that will be used by validators. 51 // Options are ed25519 & secp256k1 52 KeyType string `toml:"key_type"` 53 54 // PersistInterval specifies the height interval at which the application 55 // will persist state to disk. Defaults to 1 (every height), setting this to 56 // 0 disables state persistence. 57 PersistInterval uint64 `toml:"persist_interval"` 58 59 // ValidatorUpdates is a map of heights to validator names and their power, 60 // and will be returned by the ABCI application. For example, the following 61 // changes the power of validator01 and validator02 at height 1000: 62 // 63 // [validator_update.1000] 64 // validator01 = 20 65 // validator02 = 10 66 // 67 // Specifying height 0 returns the validator update during InitChain. The 68 // application returns the validator updates as-is, i.e. removing a 69 // validator must be done by returning it with power 0, and any validators 70 // not specified are not changed. 71 // 72 // height <-> pubkey <-> voting power 73 ValidatorUpdates map[string]map[string]uint8 `toml:"validator_update"` 74 75 // Add artificial delays to each of the main ABCI calls to mimic computation time 76 // of the application 77 PrepareProposalDelay time.Duration `toml:"prepare_proposal_delay"` 78 ProcessProposalDelay time.Duration `toml:"process_proposal_delay"` 79 CheckTxDelay time.Duration `toml:"check_tx_delay"` 80 // TODO: add vote extension and finalize block delays once completed (@cmwaters) 81 } 82 83 func DefaultConfig(dir string) *Config { 84 return &Config{ 85 PersistInterval: 1, 86 SnapshotInterval: 100, 87 Dir: dir, 88 } 89 } 90 91 // NewApplication creates the application. 92 func NewApplication(cfg *Config) (*Application, error) { 93 state, err := NewState(cfg.Dir, cfg.PersistInterval) 94 if err != nil { 95 return nil, err 96 } 97 snapshots, err := NewSnapshotStore(filepath.Join(cfg.Dir, "snapshots")) 98 if err != nil { 99 return nil, err 100 } 101 return &Application{ 102 logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)), 103 state: state, 104 snapshots: snapshots, 105 cfg: cfg, 106 }, nil 107 } 108 109 // Info implements ABCI. 110 func (app *Application) Info(req abci.RequestInfo) abci.ResponseInfo { 111 return abci.ResponseInfo{ 112 Version: version.ABCIVersion, 113 AppVersion: appVersion, 114 LastBlockHeight: int64(app.state.Height), 115 LastBlockAppHash: app.state.Hash, 116 } 117 } 118 119 // Info implements ABCI. 120 func (app *Application) InitChain(req abci.RequestInitChain) abci.ResponseInitChain { 121 var err error 122 app.state.initialHeight = uint64(req.InitialHeight) 123 if len(req.AppStateBytes) > 0 { 124 err = app.state.Import(0, req.AppStateBytes) 125 if err != nil { 126 panic(err) 127 } 128 } 129 resp := abci.ResponseInitChain{ 130 AppHash: app.state.Hash, 131 } 132 if resp.Validators, err = app.validatorUpdates(0); err != nil { 133 panic(err) 134 } 135 return resp 136 } 137 138 // CheckTx implements ABCI. 139 func (app *Application) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { 140 _, _, err := parseTx(req.Tx) 141 if err != nil { 142 return abci.ResponseCheckTx{ 143 Code: code.CodeTypeEncodingError, 144 Log: err.Error(), 145 } 146 } 147 148 if app.cfg.CheckTxDelay != 0 { 149 time.Sleep(app.cfg.CheckTxDelay) 150 } 151 152 return abci.ResponseCheckTx{Code: code.CodeTypeOK, GasWanted: 1} 153 } 154 155 // DeliverTx implements ABCI. 156 func (app *Application) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { 157 key, value, err := parseTx(req.Tx) 158 if err != nil { 159 panic(err) // shouldn't happen since we verified it in CheckTx 160 } 161 app.state.Set(key, value) 162 return abci.ResponseDeliverTx{Code: code.CodeTypeOK} 163 } 164 165 // EndBlock implements ABCI. 166 func (app *Application) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { 167 valUpdates, err := app.validatorUpdates(uint64(req.Height)) 168 if err != nil { 169 panic(err) 170 } 171 172 return abci.ResponseEndBlock{ 173 ValidatorUpdates: valUpdates, 174 Events: []abci.Event{ 175 { 176 Type: "val_updates", 177 Attributes: []abci.EventAttribute{ 178 { 179 Key: "size", 180 Value: strconv.Itoa(valUpdates.Len()), 181 }, 182 { 183 Key: "height", 184 Value: strconv.Itoa(int(req.Height)), 185 }, 186 }, 187 }, 188 }, 189 } 190 } 191 192 // Commit implements ABCI. 193 func (app *Application) Commit() abci.ResponseCommit { 194 height, hash, err := app.state.Commit() 195 if err != nil { 196 panic(err) 197 } 198 if app.cfg.SnapshotInterval > 0 && height%app.cfg.SnapshotInterval == 0 { 199 snapshot, err := app.snapshots.Create(app.state) 200 if err != nil { 201 panic(err) 202 } 203 app.logger.Info("Created state sync snapshot", "height", snapshot.Height) 204 } 205 retainHeight := int64(0) 206 if app.cfg.RetainBlocks > 0 { 207 retainHeight = int64(height - app.cfg.RetainBlocks + 1) 208 } 209 return abci.ResponseCommit{ 210 Data: hash, 211 RetainHeight: retainHeight, 212 } 213 } 214 215 // Query implements ABCI. 216 func (app *Application) Query(req abci.RequestQuery) abci.ResponseQuery { 217 return abci.ResponseQuery{ 218 Height: int64(app.state.Height), 219 Key: req.Data, 220 Value: []byte(app.state.Get(string(req.Data))), 221 } 222 } 223 224 // ListSnapshots implements ABCI. 225 func (app *Application) ListSnapshots(req abci.RequestListSnapshots) abci.ResponseListSnapshots { 226 snapshots, err := app.snapshots.List() 227 if err != nil { 228 panic(err) 229 } 230 return abci.ResponseListSnapshots{Snapshots: snapshots} 231 } 232 233 // LoadSnapshotChunk implements ABCI. 234 func (app *Application) LoadSnapshotChunk(req abci.RequestLoadSnapshotChunk) abci.ResponseLoadSnapshotChunk { 235 chunk, err := app.snapshots.LoadChunk(req.Height, req.Format, req.Chunk) 236 if err != nil { 237 panic(err) 238 } 239 return abci.ResponseLoadSnapshotChunk{Chunk: chunk} 240 } 241 242 // OfferSnapshot implements ABCI. 243 func (app *Application) OfferSnapshot(req abci.RequestOfferSnapshot) abci.ResponseOfferSnapshot { 244 if app.restoreSnapshot != nil { 245 panic("A snapshot is already being restored") 246 } 247 app.restoreSnapshot = req.Snapshot 248 app.restoreChunks = [][]byte{} 249 return abci.ResponseOfferSnapshot{Result: abci.ResponseOfferSnapshot_ACCEPT} 250 } 251 252 // ApplySnapshotChunk implements ABCI. 253 func (app *Application) ApplySnapshotChunk(req abci.RequestApplySnapshotChunk) abci.ResponseApplySnapshotChunk { 254 if app.restoreSnapshot == nil { 255 panic("No restore in progress") 256 } 257 app.restoreChunks = append(app.restoreChunks, req.Chunk) 258 if len(app.restoreChunks) == int(app.restoreSnapshot.Chunks) { 259 bz := []byte{} 260 for _, chunk := range app.restoreChunks { 261 bz = append(bz, chunk...) 262 } 263 err := app.state.Import(app.restoreSnapshot.Height, bz) 264 if err != nil { 265 panic(err) 266 } 267 app.restoreSnapshot = nil 268 app.restoreChunks = nil 269 } 270 return abci.ResponseApplySnapshotChunk{Result: abci.ResponseApplySnapshotChunk_ACCEPT} 271 } 272 273 func (app *Application) PrepareProposal( 274 req abci.RequestPrepareProposal) abci.ResponsePrepareProposal { 275 txs := make([][]byte, 0, len(req.Txs)) 276 var totalBytes int64 277 for _, tx := range req.Txs { 278 totalBytes += int64(len(tx)) 279 if totalBytes > req.MaxTxBytes { 280 break 281 } 282 txs = append(txs, tx) 283 } 284 285 if app.cfg.PrepareProposalDelay != 0 { 286 time.Sleep(app.cfg.PrepareProposalDelay) 287 } 288 289 return abci.ResponsePrepareProposal{Txs: txs} 290 } 291 292 // ProcessProposal implements part of the Application interface. 293 // It accepts any proposal that does not contain a malformed transaction. 294 func (app *Application) ProcessProposal(req abci.RequestProcessProposal) abci.ResponseProcessProposal { 295 for _, tx := range req.Txs { 296 _, _, err := parseTx(tx) 297 if err != nil { 298 return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} 299 } 300 } 301 302 if app.cfg.ProcessProposalDelay != 0 { 303 time.Sleep(app.cfg.ProcessProposalDelay) 304 } 305 306 return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT} 307 } 308 309 func (app *Application) Rollback() error { 310 return app.state.Rollback() 311 } 312 313 // validatorUpdates generates a validator set update. 314 func (app *Application) validatorUpdates(height uint64) (abci.ValidatorUpdates, error) { 315 updates := app.cfg.ValidatorUpdates[fmt.Sprintf("%v", height)] 316 if len(updates) == 0 { 317 return nil, nil 318 } 319 320 valUpdates := abci.ValidatorUpdates{} 321 for keyString, power := range updates { 322 323 keyBytes, err := base64.StdEncoding.DecodeString(keyString) 324 if err != nil { 325 return nil, fmt.Errorf("invalid base64 pubkey value %q: %w", keyString, err) 326 } 327 valUpdates = append(valUpdates, abci.UpdateValidator(keyBytes, int64(power), app.cfg.KeyType)) 328 } 329 return valUpdates, nil 330 } 331 332 // parseTx parses a tx in 'key=value' format into a key and value. 333 func parseTx(tx []byte) (string, string, error) { 334 parts := bytes.Split(tx, []byte("=")) 335 if len(parts) != 2 { 336 return "", "", fmt.Errorf("invalid tx format: %q", string(tx)) 337 } 338 if len(parts[0]) == 0 { 339 return "", "", errors.New("key cannot be empty") 340 } 341 return string(parts[0]), string(parts[1]), nil 342 }