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  }