From c8625636b5c21ee7dd200e392df0e28dff94cafd Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:49:36 +0200 Subject: [PATCH] Update Docs with Finalize Block (#760) (#1049) The PR closes issue #25 partially. Changes made: - Bump version v0.37 to v0.38 in docs. - Update docs to remove reference to `BeginBlock`, `EndBlock`, and `DeliverTx` and consolidate them into `FinalizeBlock.` - Minor changes to the formatting of docs. #### PR checklist - [ ] Tests written/updated - [ ] Changelog entry added in `.changelog` (we use [unclog](https://github.com/informalsystems/unclog) to manage our changelog) - [X] Updated relevant documentation (`docs/` or `spec/`) and code comments (cherry picked from commit d31be6b37a18b13b31fdbdb7dfbead2600035c40) Co-authored-by: Aliasgar Merchant <44069404+alijnmerchant21@users.noreply.github.com> --- docs/guides/go-built-in.md | 598 ++++++++++++++++++------------------- docs/guides/go.md | 455 ++++++++++++++-------------- 2 files changed, 517 insertions(+), 536 deletions(-) diff --git a/docs/guides/go-built-in.md b/docs/guides/go-built-in.md index c0e780bf95c..4d68e5dfc18 100644 --- a/docs/guides/go-built-in.md +++ b/docs/guides/go-built-in.md @@ -82,11 +82,11 @@ Hello, CometBFT We are going to use [Go modules](https://github.com/golang/go/wiki/Modules) for dependency management, so let's start by including a dependency on the latest version of -CometBFT, `v0.37.0` in this example. +CometBFT, `v0.38.0` in this example. ```bash go mod init kvstore -go get github.com/cometbft/cometbft@v0.37.0 +go get github.com/cometbft/cometbft@v0.38.0 ``` After running the above commands you will see two generated files, `go.mod` and `go.sum`. @@ -98,7 +98,7 @@ module github.com/me/example go 1.20 require ( - github.com/cometbft/cometbft v0.37.0 + github.com/cometbft/cometbft v0.38.0 ) ``` @@ -127,7 +127,7 @@ Create a file called `app.go` with the following contents: package main import ( - abcitypes "github.com/cometbft/cometbft/abci/types" + abcitypes "github.com/cometbft/cometbft/abci/types" ) type KVStoreApplication struct{} @@ -135,63 +135,63 @@ type KVStoreApplication struct{} var _ abcitypes.Application = (*KVStoreApplication)(nil) func NewKVStoreApplication() *KVStoreApplication { - return &KVStoreApplication{} + return &KVStoreApplication{} } -func (app *KVStoreApplication) Info(info abcitypes.RequestInfo) abcitypes.ResponseInfo { - return abcitypes.ResponseInfo{} +func (app *KVStoreApplication) Info(_ context.Context, info *abcitypes.RequestInfo) (*abcitypes.ResponseInfo, error) { + return &abcitypes.ResponseInfo{}, nil } -func (app *KVStoreApplication) Query(query abcitypes.RequestQuery) abcitypes.ResponseQuery { - return abcitypes.ResponseQuery{} +func (app *KVStoreApplication) Query(_ context.Context, req *abcitypes.RequestQuery) (*abcitypes.ResponseQuery, error) { + return &abcitypes.ResponseQuery{} } -func (app *KVStoreApplication) CheckTx(tx abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { - return abcitypes.ResponseCheckTx{} +func (app *KVStoreApplication) CheckTx(_ context.Context, check *abcitypes.RequestCheckTx) (*abcitypes.ResponseCheckTx, error) { + return &abcitypes.ResponseCheckTx{}, nil } -func (app *KVStoreApplication) InitChain(chain abcitypes.RequestInitChain) abcitypes.ResponseInitChain { - return abcitypes.ResponseInitChain{} +func (app *KVStoreApplication) InitChain(_ context.Context, chain *abcitypes.RequestInitChain) (*abcitypes.ResponseInitChain, error) { + return &abcitypes.ResponseInitChain{}, nil } -func (app *KVStoreApplication) PrepareProposal(proposal abcitypes.RequestPrepareProposal) abcitypes.ResponsePrepareProposal { - return abcitypes.ResponsePrepareProposal{} +func (app *KVStoreApplication) PrepareProposal(_ context.Context, proposal *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) { + return &abcitypes.ResponsePrepareProposal{}, nil } -func (app *KVStoreApplication) ProcessProposal(proposal abcitypes.RequestProcessProposal) abcitypes.ResponseProcessProposal { - return abcitypes.ResponseProcessProposal{} +func (app *KVStoreApplication) ProcessProposal(_ context.Context, proposal *abcitypes.RequestProcessProposal) (*abcitypes.ResponseProcessProposal, error) { + return &abcitypes.ResponseProcessProposal{}, nil } -func (app *KVStoreApplication) BeginBlock(block abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { - return abcitypes.ResponseBeginBlock{} +func (app *KVStoreApplication) FinalizeBlock(_ context.Context, req *abcitypes.RequestFinalizeBlock) (*abcitypes.ResponseFinalizeBlock, error) { + return &abcitypes.ResponseFinalizeBlock{}, nil } -func (app *KVStoreApplication) DeliverTx(tx abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { - return abcitypes.ResponseDeliverTx{} +func (app KVStoreApplication) Commit(_ context.Context, commit *abcitypes.RequestCommit) (*abcitypes.ResponseCommit, error) { + return &abcitypes.ResponseCommit{}, nil } -func (app *KVStoreApplication) EndBlock(block abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock { - return abcitypes.ResponseEndBlock{} +func (app *KVStoreApplication) ListSnapshots(_ context.Context, snapshots *abcitypes.RequestListSnapshots) (*abcitypes.ResponseListSnapshots, error) { + return &abcitypes.ResponseListSnapshots{}, nil } -func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit { - return abcitypes.ResponseCommit{} +func (app *KVStoreApplication) OfferSnapshot(_ context.Context, snapshot *abcitypes.RequestOfferSnapshot) (*abcitypes.ResponseOfferSnapshot, error) { + return &abcitypes.ResponseOfferSnapshot{}, nil } -func (app *KVStoreApplication) ListSnapshots(snapshots abcitypes.RequestListSnapshots) abcitypes.ResponseListSnapshots { - return abcitypes.ResponseListSnapshots{} +func (app *KVStoreApplication) LoadSnapshotChunk(_ context.Context, chunk *abcitypes.RequestLoadSnapshotChunk) (*abcitypes.ResponseLoadSnapshotChunk, error) { + return &abcitypes.ResponseLoadSnapshotChunk{}, nil } -func (app *KVStoreApplication) OfferSnapshot(snapshot abcitypes.RequestOfferSnapshot) abcitypes.ResponseOfferSnapshot { - return abcitypes.ResponseOfferSnapshot{} +func (app *KVStoreApplication) ApplySnapshotChunk(_ context.Context, chunk *abcitypes.RequestApplySnapshotChunk) (*abcitypes.ResponseApplySnapshotChunk, error) { + return &abcitypes.ResponseApplySnapshotChunk{Result: abcitypes.ResponseApplySnapshotChunk_ACCEPT}, nil } -func (app *KVStoreApplication) LoadSnapshotChunk(chunk abcitypes.RequestLoadSnapshotChunk) abcitypes.ResponseLoadSnapshotChunk { - return abcitypes.ResponseLoadSnapshotChunk{} +func (app KVStoreApplication) ExtendVote(_ context.Context, extend *abcitypes.RequestExtendVote) (*abcitypes.ResponseExtendVote, error) { + return &abcitypes.ResponseExtendVote{}, nil } -func (app *KVStoreApplication) ApplySnapshotChunk(chunk abcitypes.RequestApplySnapshotChunk) abcitypes.ResponseApplySnapshotChunk { - return abcitypes.ResponseApplySnapshotChunk{} +func (app *KVStoreApplication) VerifyVoteExtension(_ context.Context, verify *abcitypes.RequestVerifyVoteExtension) (*abcitypes.ResponseVerifyVoteExtension, error) { + return &abcitypes.ResponseVerifyVoteExtension{}, nil } ``` @@ -199,7 +199,7 @@ The types used here are defined in the CometBFT library and were added as a depe to the project when you ran `go get`. If your IDE is not recognizing the types, go ahead and run the command again. ```bash -go get github.com/cometbft/cometbft@v0.37.0 +go get github.com/cometbft/cometbft@v0.38.0 ``` Now go back to the `main.go` and modify the `main` function so it matches the following, @@ -218,7 +218,6 @@ not do anything. So let's revisit the code adding the logic needed to implement our minimal key/value store and to start it along with the CometBFT Service. - ### 1.3.1 Add a persistent data store Our application will need to write its state out to persistent storage so that it @@ -235,14 +234,14 @@ Next, let's update the application and its constructor to receive a handle to th ```go type KVStoreApplication struct { - db *badger.DB - onGoingBlock *badger.Txn + db *badger.DB + onGoingBlock *badger.Txn } var _ abcitypes.Application = (*KVStoreApplication)(nil) func NewKVStoreApplication(db *badger.DB) *KVStoreApplication { - return &KVStoreApplication{db: db} + return &KVStoreApplication{db: db} } ``` @@ -253,15 +252,15 @@ Next, update the `import` stanza at the top to include the Badger library: ```go import( - "github.com/dgraph-io/badger/v3" - abcitypes "github.com/cometbft/cometbft/abci/types" + "github.com/dgraph-io/badger/v3" + abcitypes "github.com/cometbft/cometbft/abci/types" ) ``` Finally, update the `main.go` file to invoke the updated constructor: ```go - _ = NewKVStoreApplication(nil) + _ = NewKVStoreApplication(nil) ``` ### 1.3.2 CheckTx @@ -277,22 +276,22 @@ For that, let's add the following helper method to app.go: ```go func (app *KVStoreApplication) isValid(tx []byte) uint32 { - // check format - parts := bytes.Split(tx, []byte("=")) - if len(parts) != 2 { - return 1 - } + // check format + parts := bytes.Split(tx, []byte("=")) + if len(parts) != 2 { + return 1 + } - return 0 + return 0 } ``` Now you can rewrite the `CheckTx` method to use the helper function: ```go -func (app *KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { - code := app.isValid(req.Tx) - return abcitypes.ResponseCheckTx{Code: code} +func (app *KVStoreApplication) CheckTx(_ context.Context, check *abcitypes.RequestCheckTx) (*abcitypes.ResponseCheckTx, error) { + code := app.isValid(check.Tx) + return &abcitypes.ResponseCheckTx{Code: code}, nil } ``` @@ -311,83 +310,76 @@ information on why the transaction was rejected. Note that `CheckTx` does not execute the transaction, it only verifies that the transaction could be executed. We do not know yet if the rest of the network has agreed to accept this transaction into a block. - -Finally, make sure to add the bytes package to the `import` stanza at the top of `app.go`: +Finally, make sure to add the `bytes` package to the `import` stanza at the top of `app.go`: ```go import( - "bytes" + "bytes" - "github.com/dgraph-io/badger/v3" - abcitypes "github.com/cometbft/cometbft/abci/types" + "github.com/dgraph-io/badger/v3" + abcitypes "github.com/cometbft/cometbft/abci/types" ) ``` - -### 1.3.3 BeginBlock -> DeliverTx -> EndBlock -> Commit +### 1.3.3 FinalizeBlock When the CometBFT consensus engine has decided on the block, the block is transferred to the -application over three ABCI method calls: `BeginBlock`, `DeliverTx`, and `EndBlock`. +application via `FinalizeBlock`. +`FinalizeBlock` is an ABCI method introduced in CometBFT `v0.38.0`. This replaces the functionality provided previously (pre-`v0.38.0`) by the combination of ABCI methods `BeginBlock`, `DeliverTx`, and `EndBlock`. `FinalizeBlock`'s parameters are an aggregation of those in `BeginBlock`, `DeliverTx`, and `EndBlock`. -- `BeginBlock` is called once to indicate to the application that it is about to -receive a block. -- `DeliverTx` is called repeatedly, once for each application transaction that was included in the block. -- `EndBlock` is called once to indicate to the application that no more transactions -will be delivered to the application within this block. +This method is responsible for executing the block and returning a response to the consensus engine. +Providing a single `FinalizeBlock` method to signal the finalization of a block simplifies the ABCI interface and increases flexibility in the execution pipeline. -Note that, to implement these calls in our application we're going to make use of Badger's -transaction mechanism. We will always refer to these as Badger transactions, not to -confuse them with the transactions included in the blocks delivered by CometBFT, -the _application transactions_. +The `FinalizeBlock` method executes the block, including any necessary transaction processing and state updates, and returns a `ResponseFinalizeBlock` object which contains any necessary information about the executed block. -First, let's create a new Badger transaction during `BeginBlock`. All application transactions in the -current block will be executed within this Badger transaction. -Then, return informing CometBFT that the application is ready to receive application transactions: +**Note:** `FinalizeBlock` only prepares the update to be made and does not change the state of the application. The state change is actually committed in a later stage i.e. in `commit` phase. -```go -func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { - app.onGoingBlock = app.db.NewTransaction(true) - return abcitypes.ResponseBeginBlock{} -} -``` +Note that, to implement these calls in our application we're going to make use of Badger's transaction mechanism. We will always refer to these as Badger transactions, not to confuse them with the transactions included in the blocks delivered by CometBFT, the _application transactions_. -Next, let's modify `DeliverTx` to add the `key` and `value` to the database transaction every time our application -receives a new application transaction through `RequestDeliverTx`. +First, let's create a new Badger transaction during `FinalizeBlock`. All application transactions in the current block will be executed within this Badger transaction. +Next, let's modify `FinalizeBlock` to add the `key` and `value` to the Badger transaction every time our application processes a new application transaction from the list received through `RequestFinalizeBlock`. -```go -func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { - if code := app.isValid(req.Tx); code != 0 { - return abcitypes.ResponseDeliverTx{Code: code} - } - - parts := bytes.SplitN(req.Tx, []byte("="), 2) - key, value := parts[0], parts[1] - - if err := app.onGoingBlock.Set(key, value); err != nil { - log.Panicf("Error writing to database, unable to execute tx: %v", err) - } +Note that we check the validity of the transaction _again_ during `FinalizeBlock`. - return abcitypes.ResponseDeliverTx{Code: 0} +```go +func (app *KVStoreApplication) FinalizeBlock(_ context.Context, req *abcitypes.RequestFinalizeBlock) (*abcitypes.ResponseFinalizeBlock, error) { + var txs = make([]*abcitypes.ExecTxResult, len(req.Txs)) + + app.onGoingBlock = app.db.NewTransaction(true) + for i, tx := range req.Txs { + if code := app.isValid(tx); code != 0 { + log.Printf("Error: invalid transaction index %v", i) + txs[i] = &abcitypes.ExecTxResult{Code: code} + } else { + parts := bytes.SplitN(tx, []byte("="), 2) + key, value := parts[0], parts[1] + log.Printf("Adding key %s with value %s", key, value) + + if err := app.onGoingBlock.Set(key, value); err != nil { + log.Panicf("Error writing to database, unable to execute tx: %v", err) + } + + log.Printf("Successfully added key %s with value %s", key, value) + + txs[i] = &abcitypes.ExecTxResult{} + } + } + + return &abcitypes.ResponseFinalizeBlock{ + TxResults: txs, + }, nil } ``` -Note that we check the validity of the transaction _again_ during `DeliverTx`. -Transactions are not guaranteed to be valid when they are delivered to an -application, even if they were valid when they were proposed. -This can happen if the application state is used to determine transaction -validity. Application state may have changed between the initial execution of `CheckTx` -and the transaction delivery in `DeliverTx` in a way that rendered the transaction -no longer valid. +Transactions are not guaranteed to be valid when they are delivered to an application, even if they were valid when they were proposed. + +This can happen if the application state is used to determine transaction validity. +The application state may have changed between the initial execution of `CheckTx` and the transaction delivery in `FinalizeBlock` in a way that rendered the transaction no longer valid. -`EndBlock` is called to inform the application that the full block has been delivered -and give the application a chance to perform any other computation needed, before the -effects of the transactions become permanent. +**Note** that `FinalizeBlock` cannot yet commit the Badger transaction we were building during the block execution. + +Other methods, such as `Query`, rely on a consistent view of the application's state, the application should only update its state by committing the Badger transactions when the full block has been delivered and the Commit method is invoked. -Note that `EndBlock` **cannot** yet commit the Badger transaction we were building -in during `DeliverTx`. -Since other methods, such as `Query`, rely on a consistent view of the application's -state, the application should only update its state by committing the Badger transactions -when the full block has been delivered and the `Commit` method is invoked. The `Commit` method tells the application to make permanent the effects of the application transactions. @@ -395,11 +387,8 @@ Let's update the method to terminate the pending Badger transaction and persist the resulting state: ```go -func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit { - if err := app.onGoingBlock.Commit(); err != nil { - log.Panicf("Error writing to database, unable to commit block: %v", err) - } - return abcitypes.ResponseCommit{Data: []byte{}} +func (app KVStoreApplication) Commit(_ context.Context, commit *abcitypes.RequestCommit) (*abcitypes.ResponseCommit, error) { + return &abcitypes.ResponseCommit{}, app.onGoingBlock.Commit() } ``` @@ -407,18 +396,20 @@ Finally, make sure to add the log library to the `import` stanza as well: ```go import ( - "bytes" - "log" + "bytes" + "log" - "github.com/dgraph-io/badger/v3" - abcitypes "github.com/cometbft/cometbft/abci/types" + "github.com/dgraph-io/badger/v3" + abcitypes "github.com/cometbft/cometbft/abci/types" ) ``` You may have noticed that the application we are writing will crash if it receives -an unexpected error from the Badger database during the `DeliverTx` or `Commit` methods. +an unexpected error from the Badger database during the `FinalizeBlock` or `Commit` methods. This is not an accident. If the application received an error from the database, there is no deterministic way for it to make progress so the only safe option is to terminate. +Once the application is restarted, the transactions in the block that failed execution will +be re-executed and should succeed if the Badger error was transient. ### 1.3.4 Query @@ -426,29 +417,29 @@ When a client tries to read some information from the `kvstore`, the request wil handled in the `Query` method. To do this, let's rewrite the `Query` method in `app.go`: ```go -func (app *KVStoreApplication) Query(req abcitypes.RequestQuery) abcitypes.ResponseQuery { - resp := abcitypes.ResponseQuery{Key: req.Data} - - dbErr := app.db.View(func(txn *badger.Txn) error { - item, err := txn.Get(req.Data) - if err != nil { - if err != badger.ErrKeyNotFound { - return err - } - resp.Log = "key does not exist" - return nil - } - - return item.Value(func(val []byte) error { - resp.Log = "exists" - resp.Value = val - return nil - }) - }) - if dbErr != nil { - log.Panicf("Error reading database, unable to execute query: %v", dbErr) - } - return resp +func (app *KVStoreApplication) Query(_ context.Context, req *abcitypes.RequestQuery) (*abcitypes.ResponseQuery, error) { + resp := abcitypes.ResponseQuery{Key: req.Data} + + dbErr := app.db.View(func(txn *badger.Txn) error { + item, err := txn.Get(req.Data) + if err != nil { + if err != badger.ErrKeyNotFound { + return err + } + resp.Log = "key does not exist" + return nil + } + + return item.Value(func(val []byte) error { + resp.Log = "exists" + resp.Value = val + return nil + }) + }) + if dbErr != nil { + log.Panicf("Error reading database, unable to execute query: %v", dbErr) + } + return &resp, nil } ``` @@ -465,14 +456,14 @@ included in blocks, it groups some of these transactions and then gives the appl to modify the group by invoking `PrepareProposal`. The application is free to modify the group before returning from the call, as long as the resulting set -does not use more bytes than `RequestPrepareProposal.max_tx_bytes' +does not use more bytes than `RequestPrepareProposal.max_tx_bytes` For example, the application may reorder, add, or even remove transactions from the group to improve the execution of the block once accepted. In the following code, the application simply returns the unmodified group of transactions: ```go -func (app *KVStoreApplication) PrepareProposal(proposal abcitypes.RequestPrepareProposal) abcitypes.ResponsePrepareProposal { - return abcitypes.ResponsePrepareProposal{Txs: proposal.Txs} +func (app *KVStoreApplication) PrepareProposal(_ context.Context, proposal *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) { + return &abcitypes.ResponsePrepareProposal{Txs: proposal.Txs}, nil } ``` @@ -484,8 +475,8 @@ by malicious nodes, in which case the block should not be considered valid. The following code simply accepts all proposals: ```go -func (app *KVStoreApplication) ProcessProposal(proposal abcitypes.RequestProcessProposal) abcitypes.ResponseProcessProposal { - return abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_ACCEPT} +func (app *KVStoreApplication) ProcessProposal(_ context.Context, proposal *abcitypes.RequestProcessProposal) (*abcitypes.ResponseProcessProposal, error) { + return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_ACCEPT}, nil } ``` @@ -499,103 +490,105 @@ Change the contents of your `main.go` file to the following. package main import ( - "flag" - "fmt" - "github.com/cometbft/cometbft/p2p" - "github.com/cometbft/cometbft/privval" - "github.com/cometbft/cometbft/proxy" - "log" - "os" - "os/signal" - "path/filepath" - "syscall" - - "github.com/dgraph-io/badger/v3" - "github.com/spf13/viper" - cfg "github.com/cometbft/cometbft/config" - cmtflags "github.com/cometbft/cometbft/libs/cli/flags" - cmtlog "github.com/cometbft/cometbft/libs/log" - nm "github.com/cometbft/cometbft/node" + "flag" + "fmt" + "github.com/cometbft/cometbft/p2p" + "github.com/cometbft/cometbft/privval" + "github.com/cometbft/cometbft/proxy" + "log" + "os" + "os/signal" + "path/filepath" + "syscall" + + "github.com/dgraph-io/badger/v3" + "github.com/spf13/viper" + cfg "github.com/cometbft/cometbft/config" + cmtflags "github.com/cometbft/cometbft/libs/cli/flags" + cmtlog "github.com/cometbft/cometbft/libs/log" + nm "github.com/cometbft/cometbft/node" ) var homeDir string func init() { - flag.StringVar(&homeDir, "cmt-home", "", "Path to the CometBFT config directory (if empty, uses $HOME/.cometbft)") + flag.StringVar(&homeDir, "cmt-home", "", "Path to the CometBFT config directory (if empty, uses $HOME/.cometbft)") } func main() { - flag.Parse() - if homeDir == "" { - homeDir = os.ExpandEnv("$HOME/.cometbft") - } - config := cfg.DefaultConfig() - - config.SetRoot(homeDir) - - viper.SetConfigFile(fmt.Sprintf("%s/%s", homeDir, "config/config.toml")) - if err := viper.ReadInConfig(); err != nil { - log.Fatalf("Reading config: %v", err) - } - if err := viper.Unmarshal(config); err != nil { - log.Fatalf("Decoding config: %v", err) - } - if err := config.ValidateBasic(); err != nil { - log.Fatalf("Invalid configuration data: %v", err) - } - - dbPath := filepath.Join(homeDir, "badger") - db, err := badger.Open(badger.DefaultOptions(dbPath)) - if err != nil { - log.Fatalf("Opening database: %v", err) - } - defer func() { - if err := db.Close(); err != nil { - log.Printf("Closing database: %v", err) - } - }() - - app := NewKVStoreApplication(db) - - pv := privval.LoadFilePV( - config.PrivValidatorKeyFile(), - config.PrivValidatorStateFile(), - ) - - nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) - if err != nil { - log.Fatalf("failed to load node's key: %v", err) - } - - logger := cmtlog.NewTMLogger(cmtlog.NewSyncWriter(os.Stdout)) - logger, err = cmtflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel) - if err != nil { - log.Fatalf("failed to parse log level: %v", err) - } - - node, err := nm.NewNode( - config, - pv, - nodeKey, - proxy.NewLocalClientCreator(app), - nm.DefaultGenesisDocProviderFunc(config), - nm.DefaultDBProvider, - nm.DefaultMetricsProvider(config.Instrumentation), - logger) - - if err != nil { - log.Fatalf("Creating node: %v", err) - } - - node.Start() - defer func() { - node.Stop() - node.Wait() - }() - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - <-c + flag.Parse() + if homeDir == "" { + homeDir = os.ExpandEnv("$HOME/.cometbft") + } + + config := cfg.DefaultConfig() + config.SetRoot(homeDir) + viper.SetConfigFile(fmt.Sprintf("%s/%s", homeDir, "config/config.toml")) + + if err := viper.ReadInConfig(); err != nil { + log.Fatalf("Reading config: %v", err) + } + if err := viper.Unmarshal(config); err != nil { + log.Fatalf("Decoding config: %v", err) + } + if err := config.ValidateBasic(); err != nil { + log.Fatalf("Invalid configuration data: %v", err) + } + dbPath := filepath.Join(homeDir, "badger") + db, err := badger.Open(badger.DefaultOptions(dbPath)) + + if err != nil { + log.Fatalf("Opening database: %v", err) + } + defer func() { + if err := db.Close(); err != nil { + log.Printf("Closing database: %v", err) + } + }() + + app := NewKVStoreApplication(db) + + pv := privval.LoadFilePV( + config.PrivValidatorKeyFile(), + config.PrivValidatorStateFile(), + ) + + nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) + if err != nil { + log.Fatalf("failed to load node's key: %v", err) + } + + logger := cmtlog.NewTMLogger(cmtlog.NewSyncWriter(os.Stdout)) + logger, err = cmtflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel) + + if err != nil { + log.Fatalf("failed to parse log level: %v", err) + } + + node, err := nm.NewNode( + config, + pv, + nodeKey, + proxy.NewLocalClientCreator(app), + nm.DefaultGenesisDocProviderFunc(config), + nm.DefaultDBProvider, + nm.DefaultMetricsProvider(config.Instrumentation), + logger + ) + + if err != nil { + log.Fatalf("Creating node: %v", err) + } + + node.Start() + defer func() { + node.Stop() + node.Wait() + }() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c } ``` @@ -603,39 +596,38 @@ This is a huge blob of code, so let's break it down into pieces. First, we use [viper](https://github.com/spf13/viper) to load the CometBFT configuration files, which we will generate later: - ```go - config := cfg.DefaultValidatorConfig() +config := cfg.DefaultValidatorConfig() - config.SetRoot(homeDir) +config.SetRoot(homeDir) - viper.SetConfigFile(fmt.Sprintf("%s/%s", homeDir, "config/config.toml")) - if err := viper.ReadInConfig(); err != nil { - log.Fatalf("Reading config: %v", err) - } - if err := viper.Unmarshal(config); err != nil { - log.Fatalf("Decoding config: %v", err) - } - if err := config.ValidateBasic(); err != nil { - log.Fatalf("Invalid configuration data: %v", err) - } +viper.SetConfigFile(fmt.Sprintf("%s/%s", homeDir, "config/config.toml")) +if err := viper.ReadInConfig(); err != nil { + log.Fatalf("Reading config: %v", err) +} +if err := viper.Unmarshal(config); err != nil { + log.Fatalf("Decoding config: %v", err) +} +if err := config.ValidateBasic(); err != nil { + log.Fatalf("Invalid configuration data: %v", err) +} ``` Next, we initialize the Badger database and create an app instance. ```go - dbPath := filepath.Join(homeDir, "badger") - db, err := badger.Open(badger.DefaultOptions(dbPath)) - if err != nil { - log.Fatalf("Opening database: %v", err) - } - defer func() { - if err := db.Close(); err != nil { - log.Fatalf("Closing database: %v", err) - } - }() +dbPath := filepath.Join(homeDir, "badger") +db, err := badger.Open(badger.DefaultOptions(dbPath)) +if err != nil { + log.Fatalf("Opening database: %v", err) +} +defer func() { + if err := db.Close(); err != nil { + log.Fatalf("Closing database: %v", err) + } +}() - app := NewKVStoreApplication(db) +app := NewKVStoreApplication(db) ``` We use `FilePV`, which is a private validator (i.e. thing which signs consensus @@ -643,19 +635,19 @@ messages). Normally, you would use `SignerRemote` to connect to an external [HSM](https://kb.certus.one/hsm.html). ```go - pv := privval.LoadFilePV( - config.PrivValidatorKeyFile(), - config.PrivValidatorStateFile(), - ) +pv := privval.LoadFilePV( + config.PrivValidatorKeyFile(), + config.PrivValidatorStateFile(), +) ``` `nodeKey` is needed to identify the node in a p2p network. ```go - nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) - if err != nil { - return nil, fmt.Errorf("failed to load node's key: %w", err) - } +nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) +if err != nil { + return nil, fmt.Errorf("failed to load node's key: %w", err) +} ``` Now we have everything set up to run the CometBFT node. We construct @@ -663,37 +655,37 @@ a node by passing it the configuration, the logger, a handle to our application the genesis information: ```go - node, err := nm.NewNode( - config, - pv, - nodeKey, - proxy.NewLocalClientCreator(app), - nm.DefaultGenesisDocProviderFunc(config), - nm.DefaultDBProvider, - nm.DefaultMetricsProvider(config.Instrumentation), - logger) - - if err != nil { - log.Fatalf("Creating node: %v", err) - } +node, err := nm.NewNode( + config, + pv, + nodeKey, + proxy.NewLocalClientCreator(app), + nm.DefaultGenesisDocProviderFunc(config), + nm.DefaultDBProvider, + nm.DefaultMetricsProvider(config.Instrumentation), +logger) + +if err != nil { + log.Fatalf("Creating node: %v", err) +} ``` Finally, we start the node, i.e., the CometBFT service inside our application: ```go - node.Start() - defer func() { - node.Stop() - node.Wait() - }() +node.Start() +defer func() { + node.Stop() + node.Wait() +}() ``` The additional logic at the end of the file allows the program to catch SIGTERM. This means that the node can shut down gracefully when an operator tries to kill the program: ```go - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - <-c +c := make(chan os.Signal, 1) +signal.Notify(c, os.Interrupt, syscall.SIGTERM) +<-c ``` ## 1.5 Initializing and Running @@ -705,15 +697,15 @@ For more information on what these files contain see [the configuration document From the root of your project, run: ```bash -go run github.com/cometbft/cometbft/cmd/cometbft@v0.37.0 init --home /tmp/cometbft-home +go run github.com/cometbft/cometbft/cmd/cometbft@v0.38.0 init --home /tmp/cometbft-home ``` You should see an output similar to the following: ```bash -I[2022-11-09|09:06:34.444] Generated private validator module=main keyFile=/tmp/cometbft-home/config/priv_validator_key.json stateFile=/tmp/cometbft-home/data/priv_validator_state.json -I[2022-11-09|09:06:34.444] Generated node key module=main path=/tmp/cometbft-home/config/node_key.json -I[2022-11-09|09:06:34.444] Generated genesis file module=main path=/tmp/cometbft-home/config/genesis.json +I[2023-25-04|09:06:34.444] Generated private validator module=main keyFile=/tmp/cometbft-home/config/priv_validator_key.json stateFile=/tmp/cometbft-home/data/priv_validator_state.json +I[2023-25-04|09:06:34.444] Generated node key module=main path=/tmp/cometbft-home/config/node_key.json +I[2023-25-04|09:06:34.444] Generated genesis file module=main path=/tmp/cometbft-home/config/genesis.json ``` Now rebuild the app: @@ -731,23 +723,23 @@ Everything is now in place to run your application. Run: The application will start and you should see a continuous output starting with: ```bash -badger 2022/11/09 09:08:50 INFO: All 0 tables opened in 0s -badger 2022/11/09 09:08:50 INFO: Discard stats nextEmptySlot: 0 -badger 2022/11/09 09:08:50 INFO: Set nextTxnTs to 0 -I[2022-11-09|09:08:50.085] service start module=proxy msg="Starting multiAppConn service" impl=multiAppConn -I[2022-11-09|09:08:50.085] service start module=abci-client connection=query msg="Starting localClient service" impl=localClient -I[2022-11-09|09:08:50.085] service start module=abci-client connection=snapshot msg="Starting localClient service" impl=localClient +badger 2023-04-25 09:08:50 INFO: All 0 tables opened in 0s +badger 2023-04-25 09:08:50 INFO: Discard stats nextEmptySlot: 0 +badger 2023-04-25 09:08:50 INFO: Set nextTxnTs to 0 +I[2023-04-25|09:08:50.085] service start module=proxy msg="Starting multiAppConn service" impl=multiAppConn +I[2023-04-25|09:08:50.085] service start module=abci-client connection=query msg="Starting localClient service" impl=localClient +I[2023-04-25|09:08:50.085] service start module=abci-client connection=snapshot msg="Starting localClient service" impl=localClient ... ``` More importantly, the application using CometBFT is producing blocks 🎉🎉 and you can see this reflected in the log output in lines like this: ```bash -I[2022-11-09|09:08:52.147] received proposal module=consensus proposal="Proposal{2/0 (F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C:1:C73D3D1273F2, -1) AD19AE292A45 @ 2022-11-09T12:08:52.143393Z}" -I[2022-11-09|09:08:52.152] received complete proposal block module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C -I[2022-11-09|09:08:52.160] finalizing commit of block module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C root= num_txs=0 -I[2022-11-09|09:08:52.167] executed block module=state height=2 num_valid_txs=0 num_invalid_txs=0 -I[2022-11-09|09:08:52.171] committed state module=state height=2 num_txs=0 app_hash= +I[2023-04-25|09:08:52.147] received proposal module=consensus proposal="Proposal{2/0 (F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C:1:C73D3D1273F2, -1) AD19AE292A45 @ 2023-04-25T12:08:52.143393Z}" +I[2023-04-25|09:08:52.152] received complete proposal block module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C +I[2023-04-25|09:08:52.160] finalizing commit of block module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C root= num_txs=0 +I[2023-04-25|09:08:52.167] executed block module=state height=2 num_valid_txs=0 num_invalid_txs=0 +I[2023-04-25|09:08:52.171] committed state module=state height=2 num_txs=0 app_hash= ``` The blocks, as you can see from the `num_valid_txs=0` part, are empty, but let's remedy that next. @@ -777,8 +769,8 @@ The request returns a `json` object with a `key` and `value` field set. ```json ... - "key": "dGVuZGVybWludA==", - "value": "cm9ja3M=", + "key": "dGVuZGVybWludA==", + "value": "cm9ja3M=", ... ``` diff --git a/docs/guides/go.md b/docs/guides/go.md index 3edb91691e2..2a07fb45d84 100644 --- a/docs/guides/go.md +++ b/docs/guides/go.md @@ -81,11 +81,11 @@ Hello, CometBFT We are going to use [Go modules](https://github.com/golang/go/wiki/Modules) for dependency management, so let's start by including a dependency on the latest version of -CometBFT, `v0.37.0` in this example. +CometBFT, `v0.38.0` in this example. ```bash go mod init kvstore -go get github.com/cometbft/cometbft@v0.37.0 +go get github.com/cometbft/cometbft@v0.38.0 ``` After running the above commands you will see two generated files, `go.mod` and `go.sum`. @@ -97,7 +97,7 @@ module github.com/me/example go 1.20 require ( - github.com/cometbft/cometbft v0.37.0 +github.com/cometbft/cometbft v0.38.0 ) ``` @@ -127,7 +127,7 @@ Create a file called `app.go` with the following contents: package main import ( - abcitypes "github.com/cometbft/cometbft/abci/types" + abcitypes "github.com/cometbft/cometbft/abci/types" ) type KVStoreApplication struct{} @@ -135,63 +135,64 @@ type KVStoreApplication struct{} var _ abcitypes.Application = (*KVStoreApplication)(nil) func NewKVStoreApplication() *KVStoreApplication { - return &KVStoreApplication{} + return &KVStoreApplication{} } -func (app *KVStoreApplication) Info(info abcitypes.RequestInfo) abcitypes.ResponseInfo { - return abcitypes.ResponseInfo{} +func (app *KVStoreApplication) Info(_ context.Context, info *abcitypes.RequestInfo) (*abcitypes.ResponseInfo, error) { + return &abcitypes.ResponseInfo{}, nil } -func (app *KVStoreApplication) Query(query abcitypes.RequestQuery) abcitypes.ResponseQuery { - return abcitypes.ResponseQuery{} +func (app *KVStoreApplication) Query(_ context.Context, req *abcitypes.RequestQuery) (*abcitypes.ResponseQuery, error) { + return &abcitypes.ResponseQuery{}, nil } -func (app *KVStoreApplication) CheckTx(tx abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { - return abcitypes.ResponseCheckTx{} +func (app *KVStoreApplication) CheckTx(_ context.Context, check *abcitypes.RequestCheckTx) (*abcitypes.ResponseCheckTx, error) { + return &abcitypes.ResponseCheckTx{Code: code}, nil } -func (app *KVStoreApplication) InitChain(chain abcitypes.RequestInitChain) abcitypes.ResponseInitChain { - return abcitypes.ResponseInitChain{} +func (app *KVStoreApplication) InitChain(_ context.Context, chain *abcitypes.RequestInitChain) (*abcitypes.ResponseInitChain, error) { + return &abcitypes.ResponseInitChain{}, nil } -func (app *KVStoreApplication) PrepareProposal(proposal abcitypes.RequestPrepareProposal) abcitypes.ResponsePrepareProposal { - return abcitypes.ResponsePrepareProposal{} +func (app *KVStoreApplication) PrepareProposal(_ context.Context, proposal *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) { + return &abcitypes.ResponsePrepareProposal{}, nil } -func (app *KVStoreApplication) ProcessProposal(proposal abcitypes.RequestProcessProposal) abcitypes.ResponseProcessProposal { - return abcitypes.ResponseProcessProposal{} +func (app *KVStoreApplication) ProcessProposal(_ context.Context, proposal *abcitypes.RequestProcessProposal) (*abcitypes.ResponseProcessProposal, error) { + return &abcitypes.ResponseProcessProposal{}, nil } -func (app *KVStoreApplication) BeginBlock(block abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { - return abcitypes.ResponseBeginBlock{} +func (app *KVStoreApplication) FinalizeBlock(_ context.Context, req *abcitypes.RequestFinalizeBlock) (*abcitypes.ResponseFinalizeBlock, error) { + return &abcitypes.ResponseFinalizeBlock{}, nil } -func (app *KVStoreApplication) DeliverTx(tx abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { - return abcitypes.ResponseDeliverTx{} +func (app KVStoreApplication) Commit(_ context.Context, commit *abcitypes.RequestCommit) (*abcitypes.ResponseCommit, error) { + return &abcitypes.ResponseCommit{}, nil } -func (app *KVStoreApplication) EndBlock(block abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock { - return abcitypes.ResponseEndBlock{} +func (app *KVStoreApplication) ListSnapshots(_ context.Context, snapshots *abcitypes.RequestListSnapshots) (*abcitypes.ResponseListSnapshots, error) { + return &abcitypes.ResponseListSnapshots{}, nil } -func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit { - return abcitypes.ResponseCommit{} +func (app *KVStoreApplication) OfferSnapshot(_ context.Context, snapshot *abcitypes.RequestOfferSnapshot) (*abcitypes.ResponseOfferSnapshot, error) { + return &abcitypes.ResponseOfferSnapshot{}, nil } -func (app *KVStoreApplication) ListSnapshots(snapshots abcitypes.RequestListSnapshots) abcitypes.ResponseListSnapshots { - return abcitypes.ResponseListSnapshots{} +func (app *KVStoreApplication) LoadSnapshotChunk(_ context.Context, chunk *abcitypes.RequestLoadSnapshotChunk) (*abcitypes.ResponseLoadSnapshotChunk, error) { + return &abcitypes.ResponseLoadSnapshotChunk{}, nil } -func (app *KVStoreApplication) OfferSnapshot(snapshot abcitypes.RequestOfferSnapshot) abcitypes.ResponseOfferSnapshot { - return abcitypes.ResponseOfferSnapshot{} +func (app *KVStoreApplication) ApplySnapshotChunk(_ context.Context, chunk *abcitypes.RequestApplySnapshotChunk) (*abcitypes.ResponseApplySnapshotChunk, error) { + + return &abcitypes.ResponseApplySnapshotChunk{Result: abcitypes.ResponseApplySnapshotChunk_ACCEPT}, nil } -func (app *KVStoreApplication) LoadSnapshotChunk(chunk abcitypes.RequestLoadSnapshotChunk) abcitypes.ResponseLoadSnapshotChunk { - return abcitypes.ResponseLoadSnapshotChunk{} +func (app KVStoreApplication) ExtendVote(_ context.Context, extend *abcitypes.RequestExtendVote) (*abcitypes.ResponseExtendVote, error) { + return &abcitypes.ResponseExtendVote{}, nil } -func (app *KVStoreApplication) ApplySnapshotChunk(chunk abcitypes.RequestApplySnapshotChunk) abcitypes.ResponseApplySnapshotChunk { - return abcitypes.ResponseApplySnapshotChunk{} +func (app *KVStoreApplication) VerifyVoteExtension(_ context.Context, verify *abcitypes.RequestVerifyVoteExtension) (*abcitypes.ResponseVerifyVoteExtension, error) { + return &abcitypes.ResponseVerifyVoteExtension{}, nil } ``` @@ -199,7 +200,7 @@ The types used here are defined in the CometBFT library and were added as a depe to the project when you ran `go get`. If your IDE is not recognizing the types, go ahead and run the command again. ```bash -go get github.com/cometbft/cometbft@v0.37.0 +go get github.com/cometbft/cometbft@v0.38.0 ``` Now go back to the `main.go` and modify the `main` function so it matches the following, @@ -218,7 +219,6 @@ not do anything. So let's revisit the code adding the logic needed to implement our minimal key/value store and to start it along with the CometBFT Service. - ### 1.3.1 Add a persistent data store Our application will need to write its state out to persistent storage so that it @@ -235,14 +235,14 @@ Next, let's update the application and its constructor to receive a handle to th ```go type KVStoreApplication struct { - db *badger.DB - onGoingBlock *badger.Txn + db *badger.DB + onGoingBlock *badger.Txn } var _ abcitypes.Application = (*KVStoreApplication)(nil) func NewKVStoreApplication(db *badger.DB) *KVStoreApplication { - return &KVStoreApplication{db: db} + return &KVStoreApplication{db: db} } ``` @@ -253,15 +253,15 @@ Next, update the `import` stanza at the top to include the Badger library: ```go import( - "github.com/dgraph-io/badger/v3" - abcitypes "github.com/cometbft/cometbft/abci/types" + "github.com/dgraph-io/badger/v3" + abcitypes "github.com/cometbft/cometbft/abci/types" ) ``` Finally, update the `main.go` file to invoke the updated constructor: ```go - _ = NewKVStoreApplication(nil) +_ = NewKVStoreApplication(nil) ``` ### 1.3.2 CheckTx @@ -277,22 +277,21 @@ For that, let's add the following helper method to app.go: ```go func (app *KVStoreApplication) isValid(tx []byte) uint32 { - // check format - parts := bytes.Split(tx, []byte("=")) - if len(parts) != 2 { - return 1 - } - - return 0 + // check format + parts := bytes.Split(tx, []byte("=")) + if len(parts) != 2 { + return 1 + } + return 0 } ``` Now you can rewrite the `CheckTx` method to use the helper function: ```go -func (app *KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { - code := app.isValid(req.Tx) - return abcitypes.ResponseCheckTx{Code: code} +func (app *KVStoreApplication) CheckTx(_ context.Context, check *abcitypes.RequestCheckTx) (*abcitypes.ResponseCheckTx, error) { + code := app.isValid(check.Tx) + return &abcitypes.ResponseCheckTx{Code: code}, nil } ``` @@ -311,83 +310,75 @@ information on why the transaction was rejected. Note that `CheckTx` does not execute the transaction, it only verifies that that the transaction could be executed. We do not know yet if the rest of the network has agreed to accept this transaction into a block. - Finally, make sure to add the bytes package to the `import` stanza at the top of `app.go`: ```go import( - "bytes" + "bytes" - "github.com/dgraph-io/badger/v3" - abcitypes "github.com/cometbft/cometbft/abci/types" + "github.com/dgraph-io/badger/v3" + abcitypes "github.com/cometbft/cometbft/abci/types" ) ``` - -### 1.3.3 BeginBlock -> DeliverTx -> EndBlock -> Commit +### 1.3.3 FinalizeBlock When the CometBFT consensus engine has decided on the block, the block is transferred to the -application over three ABCI method calls: `BeginBlock`, `DeliverTx`, and `EndBlock`. +application via the `FinalizeBlock` method. +`FinalizeBlock` is an ABCI method introduced in CometBFT `v0.38.0`. This replaces the functionality provided previously (pre-`v0.38.0`) by the combination of ABCI methods `BeginBlock`, `DeliverTx`, and `EndBlock`. +`FinalizeBlock`'s parameters are an aggregation of those in `BeginBlock`, `DeliverTx`, and `EndBlock`. -- `BeginBlock` is called once to indicate to the application that it is about to -receive a block. -- `DeliverTx` is called repeatedly, once for each application transaction that was included in the block. -- `EndBlock` is called once to indicate to the application that no more transactions -will be delivered to the application in within this block. +This method is responsible for executing the block and returning a response to the consensus engine. +Providing a single `FinalizeBlock` method to signal the finalization of a block simplifies the ABCI interface and increases flexibility in the execution pipeline. -Note that, to implement these calls in our application we're going to make use of Badger's -transaction mechanism. We will always refer to these as Badger transactions, not to -confuse them with the transactions included in the blocks delivered by CometBFT, -the _application transactions_. - -First, let's create a new Badger transaction during `BeginBlock`. All application transactions in the -current block will be executed within this Badger transaction. -Then, return informing CometBFT that the application is ready to receive application transactions: - -```go -func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { - app.onGoingBlock = app.db.NewTransaction(true) - return abcitypes.ResponseBeginBlock{} -} -``` +The `FinalizeBlock` method executes the block, including any necessary transaction processing and state updates, and returns a `ResponseFinalizeBlock` object which contains any necessary information about the executed block. -Next, let's modify `DeliverTx` to add the `key` and `value` to the database transaction every time our application -receives a new application transaction through `RequestDeliverTx`. +**Note:** `FinalizeBlock` only prepares the update to be made and does not change the state of the application. The state change is actually committed in a later stage i.e. in `commit` phase. -```go -func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { - if code := app.isValid(req.Tx); code != 0 { - return abcitypes.ResponseDeliverTx{Code: code} - } +Note that, to implement these calls in our application we're going to make use of Badger's transaction mechanism. We will always refer to these as Badger transactions, not to confuse them with the transactions included in the blocks delivered by CometBFT, the _application transactions_. - parts := bytes.SplitN(req.Tx, []byte("="), 2) - key, value := parts[0], parts[1] +First, let's create a new Badger transaction during `FinalizeBlock`. All application transactions in the current block will be executed within this Badger transaction. +Next, let's modify `FinalizeBlock` to add the `key` and `value` to the database transaction every time our application processes a new application transaction from the list received through `RequestFinalizeBlock`. - if err := app.onGoingBlock.Set(key, value); err != nil { - log.Panicf("Error writing to database, unable to execute tx: %v", err) - } +Note that we check the validity of the transaction _again_ during `FinalizeBlock`. - return abcitypes.ResponseDeliverTx{Code: 0} +```go +func (app *KVStoreApplication) FinalizeBlock(_ context.Context, req *abcitypes.RequestFinalizeBlock) (*abcitypes.ResponseFinalizeBlock, error) { + var txs = make([]*abcitypes.ExecTxResult, len(req.Txs)) + + app.onGoingBlock = app.db.NewTransaction(true) + for i, tx := range req.Txs { + if code := app.isValid(tx); code != 0 { + log.Printf("Error in tx in if") + txs[i] = &abcitypes.ExecTxResult{Code: code} + } else { + parts := bytes.SplitN(tx, []byte("="), 2) + key, value := parts[0], parts[1] + log.Printf("Adding key %s with value %s", key, value) + + if err := app.onGoingBlock.Set(key, value); err != nil { + log.Panicf("Error writing to database, unable to execute tx: %v", err) + } + log.Printf("Successfully added key %s with value %s", key, value) + + txs[i] = &abcitypes.ExecTxResult{} + } + } + + return &abcitypes.ResponseFinalizeBlock{ + TxResults: txs, + }, nil } ``` -Note that we check the validity of the transaction _again_ during `DeliverTx`. -Transactions are not guaranteed to be valid when they are delivered to an -application, even if they were valid when they were proposed. -This can happen if the application state is used to determine transaction -validity. Application state may have changed between the initial execution of `CheckTx` -and the transaction delivery in `DeliverTx` in a way that rendered the transaction -no longer valid. +Transactions are not guaranteed to be valid when they are delivered to an application, even if they were valid when they were proposed. + +This can happen if the application state is used to determine transaction validity. The application state may have changed between the initial execution of `CheckTx` and the transaction delivery in `FinalizeBlock` in a way that rendered the transaction no longer valid. + +**Note** that `FinalizeBlock` cannot yet commit the Badger transaction we were building during the block execution. -`EndBlock` is called to inform the application that the full block has been delivered -and give the application a chance to perform any other computation needed, before the -effects of the transactions become permanent. +Other methods, such as `Query`, rely on a consistent view of the application's state, the application should only update its state by committing the Badger transactions when the full block has been delivered and the `Commit` method is invoked. -Note that `EndBlock` **cannot** yet commit the Badger transaction we were building -in during `DeliverTx`. -Since other methods, such as `Query`, rely on a consistent view of the application's -state, the application should only update its state by committing the Badger transactions -when the full block has been delivered and the `Commit` method is invoked. The `Commit` method tells the application to make permanent the effects of the application transactions. @@ -395,11 +386,8 @@ Let's update the method to terminate the pending Badger transaction and persist the resulting state: ```go -func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit { - if err := app.onGoingBlock.Commit(); err != nil { - log.Panicf("Error writing to database, unable to commit block: %v", err) - } - return abcitypes.ResponseCommit{Data: []byte{}} +func (app KVStoreApplication) Commit(_ context.Context, commit *abcitypes.RequestCommit) (*abcitypes.ResponseCommit, error) { + return &abcitypes.ResponseCommit{}, app.onGoingBlock.Commit() } ``` @@ -407,16 +395,16 @@ Finally, make sure to add the log library to the `import` stanza as well: ```go import ( - "bytes" - "log" + "bytes" + "log" - "github.com/dgraph-io/badger/v3" - abcitypes "github.com/cometbft/cometbft/abci/types" + "github.com/dgraph-io/badger/v3" + abcitypes "github.com/cometbft/cometbft/abci/types" ) ``` You may have noticed that the application we are writing will crash if it receives -an unexpected error from the Badger database during the `DeliverTx` or `Commit` methods. +an unexpected error from the Badger database during the `FinalizeBlock` or `Commit` methods. This is not an accident. If the application received an error from the database, there is no deterministic way for it to make progress so the only safe option is to terminate. @@ -426,29 +414,29 @@ When a client tries to read some information from the `kvstore`, the request wil handled in the `Query` method. To do this, let's rewrite the `Query` method in `app.go`: ```go -func (app *KVStoreApplication) Query(req abcitypes.RequestQuery) abcitypes.ResponseQuery { - resp := abcitypes.ResponseQuery{Key: req.Data} - - dbErr := app.db.View(func(txn *badger.Txn) error { - item, err := txn.Get(req.Data) - if err != nil { - if err != badger.ErrKeyNotFound { - return err - } - resp.Log = "key does not exist" - return nil - } - - return item.Value(func(val []byte) error { - resp.Log = "exists" - resp.Value = val - return nil - }) - }) - if dbErr != nil { - log.Panicf("Error reading database, unable to execute query: %v", dbErr) - } - return resp +func (app *KVStoreApplication) Query(_ context.Context, req *abcitypes.RequestQuery) (*abcitypes.ResponseQuery, error) { + resp := abcitypes.ResponseQuery{Key: req.Data} + + dbErr := app.db.View(func(txn *badger.Txn) error { + item, err := txn.Get(req.Data) + if err != nil { + if err != badger.ErrKeyNotFound { + return err + } + resp.Log = "key does not exist" + return nil + } + + return item.Value(func(val []byte) error { + resp.Log = "exists" + resp.Value = val + return nil + }) + }) + if dbErr != nil { + log.Panicf("Error reading database, unable to execute query: %v", dbErr) + } + return &resp, nil } ``` @@ -471,8 +459,8 @@ execution of the block once accepted. In the following code, the application simply returns the unmodified group of transactions: ```go -func (app *KVStoreApplication) PrepareProposal(proposal abcitypes.RequestPrepareProposal) abcitypes.ResponsePrepareProposal { - return abcitypes.ResponsePrepareProposal{Txs: proposal.Txs} +func (app *KVStoreApplication) PrepareProposal(_ context.Context, proposal *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) { + return &abcitypes.ResponsePrepareProposal{Txs: proposal.Txs}, nil } ``` @@ -484,8 +472,8 @@ by malicious nodes, in which case the block should not be considered valid. The following code simply accepts all proposals: ```go -func (app *KVStoreApplication) ProcessProposal(proposal abcitypes.RequestProcessProposal) abcitypes.ResponseProcessProposal { - return abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_ACCEPT} +func (app *KVStoreApplication) ProcessProposal(_ context.Context, proposal *abcitypes.RequestProcessProposal) (*abcitypes.ResponseProcessProposal, error) { + return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_ACCEPT}, nil } ``` @@ -499,60 +487,61 @@ Change the contents of your `main.go` file to the following. package main import ( - "flag" - "fmt" - abciserver "github.com/cometbft/cometbft/abci/server" - "log" - "os" - "os/signal" - "path/filepath" - "syscall" - - "github.com/dgraph-io/badger/v3" - cmtlog "github.com/cometbft/cometbft/libs/log" + "flag" + "fmt" + abciserver "github.com/cometbft/cometbft/abci/server" + "log" + "os" + "os/signal" + "path/filepath" + "syscall" + + "github.com/dgraph-io/badger/v3" + cmtlog "github.com/cometbft/cometbft/libs/log" ) var homeDir string var socketAddr string func init() { - flag.StringVar(&homeDir, "kv-home", "", "Path to the kvstore directory (if empty, uses $HOME/.kvstore)") - flag.StringVar(&socketAddr, "socket-addr", "unix://example.sock", "Unix domain socket address (if empty, uses \"unix://example.sock\"") + flag.StringVar(&homeDir, "kv-home", "", "Path to the kvstore directory (if empty, uses $HOME/.kvstore)") + flag.StringVar(&socketAddr, "socket-addr", "unix://example.sock", "Unix domain socket address (if empty, uses \"unix://example.sock\"") } func main() { - flag.Parse() - if homeDir == "" { - homeDir = os.ExpandEnv("$HOME/.kvstore") - } - - dbPath := filepath.Join(homeDir, "badger") - db, err := badger.Open(badger.DefaultOptions(dbPath)) - if err != nil { - log.Fatalf("Opening database: %v", err) - } - defer func() { - if err := db.Close(); err != nil { - log.Fatalf("Closing database: %v", err) - } - }() - - app := NewKVStoreApplication(db) - - logger := cmtlog.NewTMLogger(cmtlog.NewSyncWriter(os.Stdout)) - - server := abciserver.NewSocketServer(socketAddr, app) - server.SetLogger(logger) - - if err := server.Start(); err != nil { - fmt.Fprintf(os.Stderr, "error starting socket server: %v", err) - os.Exit(1) - } - defer server.Stop() - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - <-c + flag.Parse() + if homeDir == "" { + homeDir = os.ExpandEnv("$HOME/.kvstore") + } + + dbPath := filepath.Join(homeDir, "badger") + db, err := badger.Open(badger.DefaultOptions(dbPath)) + if err != nil { + log.Fatalf("Opening database: %v", err) + } + + defer func() { + if err := db.Close(); err != nil { + log.Fatalf("Closing database: %v", err) + } + }() + + app := NewKVStoreApplication(db) + logger := cmtlog.NewTMLogger(cmtlog.NewSyncWriter(os.Stdout)) + + server := abciserver.NewSocketServer(socketAddr, app) + server.SetLogger(logger) + + if err := server.Start(); err != nil { + fmt.Fprintf(os.Stderr, "error starting socket server: %v", err) + + os.Exit(1) + } + defer server.Stop() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c } ``` @@ -561,18 +550,18 @@ This is a huge blob of code, so let's break it down into pieces. First, we initialize the Badger database and create an app instance: ```go - dbPath := filepath.Join(homeDir, "badger") - db, err := badger.Open(badger.DefaultOptions(dbPath)) - if err != nil { - log.Fatalf("Opening database: %v", err) - } - defer func() { - if err := db.Close(); err != nil { - log.Fatalf("Closing database: %v", err) - } - }() +dbPath := filepath.Join(homeDir, "badger") +db, err := badger.Open(badger.DefaultOptions(dbPath)) +if err != nil { + log.Fatalf("Opening database: %v", err) +} +defer func() { + if err := db.Close(); err != nil { + log.Fatalf("Closing database: %v", err) + } +}() - app := NewKVStoreApplication(db) +app := NewKVStoreApplication(db) ``` Then we start the ABCI server and add some signal handling to gracefully stop @@ -580,18 +569,18 @@ it upon receiving SIGTERM or Ctrl-C. CometBFT will act as a client, which connects to our server and send us transactions and other messages. ```go - server := abciserver.NewSocketServer(socketAddr, app) - server.SetLogger(logger) +server := abciserver.NewSocketServer(socketAddr, app) +server.SetLogger(logger) - if err := server.Start(); err != nil { - fmt.Fprintf(os.Stderr, "error starting socket server: %v", err) - os.Exit(1) - } - defer server.Stop() +if err := server.Start(); err != nil { + fmt.Fprintf(os.Stderr, "error starting socket server: %v", err) + os.Exit(1) +} +defer server.Stop() - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - <-c +c := make(chan os.Signal, 1) +signal.Notify(c, os.Interrupt, syscall.SIGTERM) +<-c ``` ## 1.5 Initializing and Running @@ -603,15 +592,15 @@ For more information on what these files contain see [the configuration document From the root of your project, run: ```bash -go run github.com/cometbft/cometbft/cmd/cometbft@v0.37.0 init --home /tmp/cometbft-home +go run github.com/cometbft/cometbft/cmd/cometbft@v0.38.0 init --home /tmp/cometbft-home ``` You should see an output similar to the following: ```bash -I[2022-11-09|09:06:34.444] Generated private validator module=main keyFile=/tmp/cometbft-home/config/priv_validator_key.json stateFile=/tmp/cometbft-home/data/priv_validator_state.json -I[2022-11-09|09:06:34.444] Generated node key module=main path=/tmp/cometbft-home/config/node_key.json -I[2022-11-09|09:06:34.444] Generated genesis file module=main path=/tmp/cometbft-home/config/genesis.json +I[2023-04-25|09:06:34.444] Generated private validator module=main keyFile=/tmp/cometbft-home/config/priv_validator_key.json stateFile=/tmp/cometbft-home/data/priv_validator_state.json +I[2023-04-25|09:06:34.444] Generated node key module=main path=/tmp/cometbft-home/config/node_key.json +I[2023-04-25|09:06:34.444] Generated genesis file module=main path=/tmp/cometbft-home/config/genesis.json ``` Now rebuild the app: @@ -629,11 +618,11 @@ Everything is now in place to run your application. Run: The application will start and you should see an output similar to the following: ```bash -badger 2022/11/09 17:01:28 INFO: All 0 tables opened in 0s -badger 2022/11/09 17:01:28 INFO: Discard stats nextEmptySlot: 0 -badger 2022/11/09 17:01:28 INFO: Set nextTxnTs to 0 -I[2022-11-09|17:01:28.726] service start msg="Starting ABCIServer service" impl=ABCIServer -I[2022-11-09|17:01:28.726] Waiting for new connection... +badger 2023-04-25 17:01:28 INFO: All 0 tables opened in 0s +badger 2023-04-25 17:01:28 INFO: Discard stats nextEmptySlot: 0 +badger 2023-04-25 17:01:28 INFO: Set nextTxnTs to 0 +I[2023-04-25|17:01:28.726] service start msg="Starting ABCIServer service" impl=ABCIServer +I[2023-04-25|17:01:28.726] Waiting for new connection... ``` Then we need to start CometBFT service and point it to our application. @@ -641,29 +630,30 @@ Open a new terminal window and cd to the same folder where the app is running. Then execute the following command: ```bash -go run github.com/cometbft/cometbft/cmd/cometbft@v0.37.0 node --home /tmp/cometbft-home --proxy_app=unix://example.sock +go run github.com/cometbft/cometbft/cmd/cometbft@v0.38.0 node --home /tmp/cometbft-home --proxy_app=unix://example.sock ``` This should start the full node and connect to our ABCI application, which will be reflected in the application output. ```sh -I[2022-11-09|17:07:08.124] service start msg="Starting ABCIServer service" impl=ABCIServer -I[2022-11-09|17:07:08.124] Waiting for new connection... -I[2022-11-09|17:08:12.702] Accepted a new connection -I[2022-11-09|17:08:12.703] Waiting for new connection... -I[2022-11-09|17:08:12.703] Accepted a new connection -I[2022-11-09|17:08:12.703] Waiting for new connection... +I[2023-04-25|17:07:08.124] service start msg="Starting ABCIServer service" impl=ABCIServer +I[2023-04-25|17:07:08.124] Waiting for new connection... +I[2023-04-25|17:08:12.702] Accepted a new connection +I[2023-04-25|17:08:12.703] Waiting for new connection... +I[2023-04-25|17:08:12.703] Accepted a new connection +I[2023-04-25|17:08:12.703] Waiting for new connection... ``` Also, the application using CometBFT Core is producing blocks 🎉🎉 and you can see this reflected in the log output of the service in lines like this: ```bash -I[2022-11-09|09:08:52.147] received proposal module=consensus proposal="Proposal{2/0 (F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C:1:C73D3D1273F2, -1) AD19AE292A45 @ 2022-11-09T12:08:52.143393Z}" -I[2022-11-09|09:08:52.152] received complete proposal block module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C -I[2022-11-09|09:08:52.160] finalizing commit of block module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C root= num_txs=0 -I[2022-11-09|09:08:52.167] executed block module=state height=2 num_valid_txs=0 num_invalid_txs=0 -I[2022-11-09|09:08:52.171] committed state module=state height=2 num_txs=0 app_hash= +I[2023-04-25|09:08:52.147] received proposal module=consensus proposal="Proposal{2/0 (F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C:1:C73D3D1273F2, -1) AD19AE292A45 @ 2023-04-25T12:08:52.143393Z}" +I[2023-04-25|09:08:52.147] received proposal module=consensus proposal="Proposal{2/0 (F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C:1:C73D3D1273F2, -1) AD19AE292A45 @ 2023-04-25T12:08:52.143393Z}" +I[2023-04-25|09:08:52.152] received complete proposal block module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C +I[2023-04-25|09:08:52.160] finalizing commit of block module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C root= num_txs=0 +I[2023-04-25|09:08:52.167] executed block module=state height=2 num_valid_txs=0 num_invalid_txs=0 +I[2023-04-25|09:08:52.171] committed state module=state height=2 num_txs=0 app_hash= ``` The blocks, as you can see from the `num_valid_txs=0` part, are empty, but let's remedy that next. @@ -673,7 +663,6 @@ The blocks, as you can see from the `num_valid_txs=0` part, are empty, but let's Let's try submitting a transaction to our new application. Open another terminal window and run the following curl command: - ```bash curl -s 'localhost:26657/broadcast_tx_commit?tx="cometbft=rocks"' ``` @@ -693,8 +682,8 @@ The request returns a `json` object with a `key` and `value` field set. ```json ... - "key": "dGVuZGVybWludA==", - "value": "cm9ja3M=", + "key": "dGVuZGVybWludA==", + "value": "cm9ja3M=", ... ```