Sending Messages

In this section you will learn how a user can send a message on the Forum Application.

CheckTx

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.

PrepareProposal

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.

ProcessProposal

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 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.

FinalizeBlock

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.

Commit

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.

Signature verification

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.

Decorative Orb