In this section you will learn how a user can send a message on the Forum Application.
In ABCI, the CheckTx
method is used to ask the application to check the validity of an individual transaction before it is included in the mempool.
Every node runs CheckTx before letting a transaction into its local mempool.
The CheckTx
method is responsible for performing any necessary validation checks on the transaction, such as verifying
the signature, checking for double spending, or enforcing application-specific rules. It is a lightweight and fast
operation, as it is meant to quickly determine whether a transaction is valid or not.
The method takes in a CheckTxRequest
object, which contains the transaction to be checked (req.Tx
) and any other
relevant information that the application needs to validate the transaction.
The CheckTx
method should return a CheckTxResponse
object, which indicates whether the transaction is valid or not.
If the transaction is valid, the response may include additional information about the transaction
such as the gas that the submitter is willing to spend executing the transaction.
Following is the code for the CheckTx
method:
// CheckTx handles validation of inbound transactions. If a transaction is not a valid message, or if a user
// does not exist in the database or if a user is banned it returns an error.
func (app *ForumApp) CheckTx(_ context.Context, req *abci.CheckTxRequest) (*abci.CheckTxResponse, error) {
app.logger.Info("Executing Application CheckTx")
// Parse the tx message
msg, err := model.ParseMessage(req.Tx)
if err != nil {
app.logger.Info("CheckTx: failed to parse transaction message", "message", msg, "error", err)
return &abci.CheckTxResponse{Code: CodeTypeInvalidTxFormat, Log: "Invalid transaction", Info: err.Error()}, nil
}
// Check for invalid sender
if len(msg.Sender) == 0 {
app.logger.Info("CheckTx: failed to parse transaction message", "message", msg, "error", "Sender is missing")
return &abci.CheckTxResponse{Code: CodeTypeInvalidTxFormat, Log: "Invalid transaction", Info: "Sender is missing"}, nil
}
app.logger.Debug("searching for sender", "sender", msg.Sender)
u, err := app.state.DB.FindUserByName(msg.Sender)
if err != nil {
if !errors.Is(err, badger.ErrKeyNotFound) {
app.logger.Error("CheckTx: Error in check tx", "tx", string(req.Tx), "error", err)
return &abci.CheckTxResponse{Code: CodeTypeEncodingError, Log: "Invalid transaction", Info: err.Error()}, nil
}
app.logger.Info("CheckTx: Sender not found", "sender", msg.Sender)
} else if u != nil && u.Banned {
return &abci.CheckTxResponse{Code: CodeTypeBanned, Log: "Invalid transaction", Info: "User is banned"}, nil
}
app.logger.Info("CheckTx: success checking tx", "message", msg.Message, "sender", msg.Sender)
return &abci.CheckTxResponse{Code: CodeTypeOK, Log: "Valid transaction", Info: "Transaction validation succeeded"}, nil
}
Explanation of code:
CheckTx
function parses the transaction message contained in req.Tx
using the model.ParseMessage
function. If
there is an error parsing the message, it prints an error message and returns a response with an error code indicating
an invalid transaction format.
Then, it searches for a user in the database using the app.state.DB.FindUserByName
function. If the user is not found,
it prints a message indicating that the user was not found. If there is an error other than a key not found error,
it prints an error message and returns a response with an error code indicating an encoding error.
If the user is found and is not banned, it returns a response with a success code (CodeTypeOK
which in CometBFT a 0
code value to indicate success).
Finally, it prints a success message indicating the success of the check transaction.
Tip: The function CheckTx
is a stateless function that is primarily used by the application to check if a tx is
valid or not as per the application criteria (well-formed and from a valid user)
Note: You will learn about different packages and functions like app.state.DB.FindUserByName
in the upcoming sections.
In this section you will learn about the ABCI methods in the ForumApp
.
The PrepareProposal
method is responsible for creating the contents of the proposed block, typically by selecting a
set of transactions that should be included in the next block. It may use various criteria to determine which transactions
to include, such as transaction fees, priority, or application-specific rules (as defined by the application).
The method takes in a PrepareProposalRequest
object, which contains information about the current state of the blockchain,
such as the current height and the last committed block hash. It may also include other relevant information that the
application needs to generate the proposal.
The PrepareProposal
method should return a PrepareProposalResponse
object, which includes the proposed block contents.
This typically includes the list of transactions (txs) that should be included in the next block.
Following is the code for the PrepareProposal
method:
// PrepareProposal is used to prepare a proposal for the next block in the blockchain. The application can re-order, remove
// or add transactions.
func (app *ForumApp) PrepareProposal(_ context.Context, req *abci.PrepareProposalRequest) (*abci.PrepareProposalResponse, error) {
app.logger.Info("Executing Application PrepareProposal")
// Get the curse words from for all vote extensions received at the end of last height.
voteExtensionCurseWords := app.getWordsFromVe(req.LocalLastCommit.Votes)
curseWords := strings.Split(voteExtensionCurseWords, "|")
if hasDuplicateWords(curseWords) {
return nil, errors.New("duplicate words found")
}
// Prepare req puts the BanTx first, then adds the other transactions
// ProcessProposal should verify this
proposedTxs := make([][]byte, 0)
finalProposal := make([][]byte, 0)
bannedUsersString := make(map[string]struct{})
for _, tx := range req.Txs {
msg, err := model.ParseMessage(tx)
if err != nil {
// this should never happen since the tx should have been validated by CheckTx
return nil, fmt.Errorf("failed to marshal tx in PrepareProposal: %w", err)
}
// Adding the curse words from vote extensions too
if !hasCurseWord(msg.Message, voteExtensionCurseWords) {
proposedTxs = append(proposedTxs, tx)
continue
}
// If the message contains curse words then ban the user by
// creating a "ban transaction" and adding it to the final proposal
banTx := model.BanTx{UserName: msg.Sender}
bannedUsersString[msg.Sender] = struct{}{}
resultBytes, err := json.Marshal(banTx)
if err != nil {
// this should never happen since the ban tx should have been validated by CheckTx
return nil, fmt.Errorf("failed to marshal ban tx in PrepareProposal: %w", err)
}
finalProposal = append(finalProposal, resultBytes)
}
// Need to loop again through the proposed Txs to make sure there is none left by a user that was banned
// after the tx was accepted
for _, tx := range proposedTxs {
// there should be no error here as these are just transactions we have checked and added
msg, err := model.ParseMessage(tx)
if err != nil {
// this should never happen since the tx should have been validated by CheckTx
return nil, fmt.Errorf("failed to marshal tx in PrepareProposal: %w", err)
}
// If the user is banned then include this transaction in the final proposal
if _, ok := bannedUsersString[msg.Sender]; !ok {
finalProposal = append(finalProposal, tx)
}
}
return &abci.PrepareProposalResponse{Txs: finalProposal}, nil
}
Explanation of code:
PrepareProposal
function first retrieves curse words from for all vote extensions received at the end of last height.
Then, it iterates over the transactions in the proposal and checks if each transaction contains curse words. If a
transaction does not contain curse words, it adds it to the proposedTxs
slice. If a transaction does contain curse words, it creates a ban transaction and adds
it to the finalProposal
slice.
After iterating over all the transactions, it loops through the proposedTxs
again to make sure there are no transactions
left from users who were banned after their transactions were accepted. The final set of transactions is stored in the
finalProposal
slice, which is then returned as part of the PrepareProposalResponse
response.
Tip: The function PrepareProposal
is used by state replication to indicate to the application to begin processing the tx.
Typically, the application is expected to order the tx and remove the tx from pool as defined by application logic.
Note: You will learn about different packages and functions like model.ParseMessage
in the upcoming sections.
In this section you will learn about app.go
file only.
The ProcessProposal
method is used to process a proposal for the next block in the blockchain. It is called by
CometBFT to request the application to validate and potentially execute the proposed block.
The ProcessProposal
method is responsible for performing any necessary validation checks on the proposed block, such
as verifying the validity of the included transactions, checking for double spending, or enforcing application-specific
rules (as defined by the application).
The method takes in a ProcessProposalRequest
object, which contains the proposed block contents, including the list of
transactions (req.Txs
) that are included in the block.
The ProcessProposal
method should return a ProcessProposalResponse
object, which includes a status if the the proposed block
was accepted (PROCESS_PROPOSAL_STATUS_ACCEPT
) or rejected (PROCESS_PROPOSAL_STATUS_REJECT
)
Following is the code for the ProcessProposal
function:
// ProcessProposal validates the proposed block and the transactions and return a status if it was accepted or rejected.
func (app *ForumApp) ProcessProposal(_ context.Context, req *abci.ProcessProposalRequest) (*abci.ProcessProposalResponse, error) {
app.logger.Info("Executing Application ProcessProposal")
bannedUsers := make(map[string]struct{}, 0)
finishedBanTxIdx := len(req.Txs)
for i, tx := range req.Txs {
if !isBanTx(tx) {
finishedBanTxIdx = i
break
}
var parsedBan model.BanTx
err := json.Unmarshal(tx, &parsedBan)
if err != nil {
return &abci.ProcessProposalResponse{Status: abci.PROCESS_PROPOSAL_STATUS_REJECT}, err
}
bannedUsers[parsedBan.UserName] = struct{}{}
}
for _, tx := range req.Txs[finishedBanTxIdx:] {
// From this point on, there should be no BanTxs anymore
// If there is one, ParseMessage will return an error as the
// format of the two transactions is different.
msg, err := model.ParseMessage(tx)
if err != nil {
return &abci.ProcessProposalResponse{Status: abci.PROCESS_PROPOSAL_STATUS_REJECT}, err
}
if _, ok := bannedUsers[msg.Sender]; ok {
// sending us a tx from a banned user
return &abci.ProcessProposalResponse{Status: abci.PROCESS_PROPOSAL_STATUS_REJECT}, nil
}
}
return &abci.ProcessProposalResponse{Status: abci.PROCESS_PROPOSAL_STATUS_ACCEPT}, nil
}
Explanation of code:
ProcessProposal
function initializes an empty map called bannedUsers
to keep track of banned user names.
Then, it iterates through the transactions and checks if each transaction is a ban transaction using the isBanTx
function.
If it is a ban transaction, it parses the transaction into a BanTx
struct and adds the banned user’s name to the bannedUsers
map.
If it is not a ban transaction, it breaks out of the loop and records the index of the last ban transaction.
After that, it iterates through the remaining transactions (starting from the index after the last ban transaction) and
parses each transaction using the model.ParseMessage
function. If any banned user attempts to send a transaction,
it rejects the proposal.
Finally, if there are no banned users found in the transactions, it accepts the proposal.
Tip: The function ProcessProposal
is used by state replication to indicate to the application to process the tx.
The application can process a tx in accordance to the logic defined by the application. Although the application can
perform ‘optimisitic execution’, the application is not mandated to do so.
Note: You will learn about different packages and functions like isBanTx
in the upcoming sections. In this section
you will learn about app.go
file only.
The FinalizeBlock
method is used to finalize a block in the blockchain. It is called by CometBFT after the validators have agreed on the
next block and it is ready to be added to the blockchain.
The FinalizeBlock
method takes in a FinalizeBlockRequest
object, which contains information about the block being finalized.
It performs any necessary processing or validation on the block, such as updating the application state, or performing
additional computations.
After processing the block, the method returns a FinalizeBlockResponse
object, which typically includes the results
of the block finalization process, such as the transaction results, validator set updates, consensus parameters updates
or the new hash of the application state.
Following is the code for the FinalizeBlock
function:
// FinalizeBlock Deliver the decided block to the Application.
func (app *ForumApp) FinalizeBlock(_ context.Context, req *abci.FinalizeBlockRequest) (*abci.FinalizeBlockResponse, error) {
app.logger.Info("Executing Application FinalizeBlock")
// Iterate over Tx in current block
app.onGoingBlock = app.state.DB.GetDB().NewTransaction(true)
respTxs := make([]*abci.ExecTxResult, len(req.Txs))
finishedBanTxIdx := len(req.Txs)
for i, tx := range req.Txs {
var err error
if !isBanTx(tx) {
finishedBanTxIdx = i
break
}
banTx := new(model.BanTx)
err = json.Unmarshal(tx, &banTx)
if err != nil {
// since we did this in ProcessProposal this should never happen here
return nil, err
}
err = UpdateOrSetUser(app.state.DB, banTx.UserName, true, app.onGoingBlock)
if err != nil {
return nil, err
}
respTxs[i] = &abci.ExecTxResult{Code: CodeTypeOK}
}
for idx, tx := range req.Txs[finishedBanTxIdx:] {
// From this point on, there should be no BanTxs anymore
// If there is one, ParseMessage will return an error as the
// format of the two transactions is different.
msg, err := model.ParseMessage(tx)
i := idx + finishedBanTxIdx
if err != nil {
// since we did this in ProcessProposal this should never happen here
return nil, err
}
// Check if this sender already existed; if not, add the user too
err = UpdateOrSetUser(app.state.DB, msg.Sender, false, app.onGoingBlock)
if err != nil {
return nil, err
}
// Add the message for this sender
message, err := model.AppendToExistingMessages(app.state.DB, *msg)
if err != nil {
return nil, err
}
err = app.onGoingBlock.Set([]byte(msg.Sender+"msg"), []byte(message))
if err != nil {
return nil, err
}
chatHistory, err := model.AppendToChat(app.state.DB, *msg)
if err != nil {
return nil, err
}
// Append messages to chat history
err = app.onGoingBlock.Set([]byte("history"), []byte(chatHistory))
if err != nil {
return nil, err
}
// This adds the user to the DB, but the data is not committed nor persisted until Commit is called
respTxs[i] = &abci.ExecTxResult{Code: abci.CodeTypeOK}
app.state.Size++
}
app.state.Height = req.Height
response := &abci.FinalizeBlockResponse{TxResults: respTxs, AppHash: app.state.Hash()}
return response, nil
}
Explanation of code:
FinalizeBlock
function iterates over the transactions (req.Txs
) in the block. If a transaction is a ban transaction (isBanTx(tx)
),
it updates or sets a user in the application’s database and assigns a response code. If it is not a ban transaction, the loop breaks.
After the ban transactions, the method parses the remaining transactions using model.ParseMessage(tx)
. It updates or sets
the sender as a user in the database, appends the message to the existing messages, appends the message to the chat history,
and assigns a response code. Finally, it updates the blockchain state and returns the response object.
Tip: The function FinalizeBlock
finalizes processing of a tx. All the state change that happen in this function are finalized.
However, they are not yet persisted to database. This is done in next step, i.e. Commit
Note: You will learn about different packages and functions like model.AppendToExistingMsgs
in the upcoming sections.
In this section you will learn about app.go
file only.
The Commit
method is used to persist the changes made to the application state during the FinalizeBlock method.
After calling FinalizeBlock
, the state changes are finalized but not yet persisted to the database. The Commit
method
is responsible for persisting these changes to the database, ensuring that they are durable and can be retrieved later.
Typically, the Commit
method updates the application’s blockchain state by saving the modified data to a persistent
storage system, such as a database or a file. This allows the application to maintain a consistent and reliable state
across different blocks in the blockchain.
The Commit method takes in a CommitRequest
object and returns a CommitResponse
object.
Following is the code for the Commit
function:
// Commit the application state.
func (app *ForumApp) Commit(_ context.Context, _ *abci.CommitRequest) (*abci.CommitResponse, error) {
app.logger.Info("Executing Application Commit")
if err := app.onGoingBlock.Commit(); err != nil {
return nil, err
}
err := saveState(&app.state)
if err != nil {
return nil, err
}
return &abci.CommitResponse{}, nil
}
Explanation of code:
The Commit
method takes in a context.Context
object and a *abci.CommitRequest
object as parameters. It returns
a *abci.CommitResponse
object and an error.
Inside the method, it calls the Commit method on app.onGoingBlock
, which is an instance of a block object. This commits
the state changes made during the FinalizeBlock
method to the underlying storage system. If there’s an error during
the commit
, it returns the error in the Commit
method.
After the state changes are committed, the saveState
function is called with a pointer to app.state.
This function
is responsible for persisting the updated state to a persistent storage system.
Finally, the method returns an empty *abci.CommitResponse
object and a nil
error.
The tutorial does not include the logic for verifying transaction signatures. However, in a real-world application, it
is important to validate transaction signatures to ensure that the users sending messages are legitimate and not trying
to exploit the system. If implemented, signature verification would typically be carried out using the CheckTx
, ProcessProposal
, and FinalizeBlock
methods to confirm the validity of transactions from a signature perspective. While our tutorial app is simplified for
educational purposes, a fully functional application should include signature verification as an essential part of its core logic.
An example signature verification code could be something like:
func isValidSignature(tx Transaction) bool {
pubKey := tx.PubKey
signature := tx.Signature
message := tx.Message
// Use the cryptographic library to verify the signature
return crypto.VerifySignature(pubKey, message, signature)
}
and then in CheckTx
the signature could be verified:
func (app *ForumApp) CheckTx(req types.CheckTxRequest) types.CheckTxResponse {
tx := req.Tx
// Extract the transaction fields, including the signature and the public key
// Verify the signature
if !isValidSignature(tx) {
return types.CheckTxResponse{
Code: code.InvalidSignature,
Log: "Invalid transaction signature",
}
}
// some other validation...
return types.CheckTxResponse{Code: code.OK}
}
In the next session, you will learn about how user can Query Message in the Forum Application.