Tutorial: The Scavenge Hunt Blockchain

Tutorial: The Scavenge Hunt Blockchain

Background

This tutorial was first presented as a workshop at GODays 2020 Berlin by Billy Rennekamp and later adopted to Ignite and edited for scale by Tobias Schwarz. To view slides from this workshop please see here.

The goal of this session is to get you thinking about what is possible when developing applications that have access to digital scarcity as a primitive. The easiest way to think of scarcity is as money; If money grew on trees it would stop being scarce and stop having value. We have a long history of software which deals with money, but it's never been a first class citizen in the programming environment. Instead, money has always been represented as a number or a float, and it has been up to a third party merchant service or some other process of exchange where the representation of money is swapped for actual cash. If money were a primitive in a software environment, it would allow for real economies to exist within games and applications, taking one step further in erasing the line between games, life and play.

We will be working today with a Golang framework called the Cosmos SDK. This framework makes it easy to build deterministic state machines. A state machine is simply an application that has a state and explicit functions for updating that state. You can think of a light bulb and a light switch as a kind of state machine: the state of the "application" is either light on or light off. There is one function in this state machine: flip switch. Every time you trigger flip switch the state of the application goes from light on to light off or vice versa. Simple, right?

light-bulb.png

A deterministic state machine is just a state machine in which an accumulation of actions, taken together and replayed, will have the same outcome. So if we were to take all the switch on and switch off actions of the entire month of January for some room and replay then in August, we should have the same final state of light on or light off. There should be nothing about January or August that changes the outcome - of course a real room might not be deterministic if there were things like power shortages or maintenance that took place during those periods.

What is nice about deterministic state machines is that you can track changes with cryptographic hashes of the state just like version control systems like git. If there is agreement about the hash of a certain state, it is unnecessary to replay every action from genesis to ensure that two repos are in sync. These properties are useful when dealing with software that is run by many different people in many different situations, just like git.

Another nice property of cryptographically hashing state is that it creates a system of reliable dependencies. I can build software that uses your library and reference a specific state in your software. That way if you change your code in a way that breaks my code, I don't have to use your new version but can continue to use the version that I reference. This same property of knowing exactly what the state of a system (as well as all the ways that state can update) makes it possible to have the necessary assurances that allow for digital scarcity within an application. If I say there is only one of some thing within a state machine and you know that there is no way for that state machine to create more than one, you can rely on there always being only one.

git.jpeg

You might have guessed by now that what I'm really talking about are Blockchains. These are deterministic state machines which have very specific rules about how state is updated. They checkpoint state with cryptographic hashes and use asymmetric cryptography to handle access control. There are different ways that different Blockchains decide who can make a checkpoint of state. These entities can be called Validators. Some of them are chosen by an electricity intensive game called proof-of-work in tandem with something called the longest chain rule or Nakamoto Consensus on Blockchains like Bitcoin or Ethereum 1.0.

The state machine we are building will use an implementation of proof-of-stake called Tendermint (or CometBFT lately), which is energy efficient and can consist of one or many validators, either trusted or byzantine. When building a system that handles real scarcity, the integrity of that system becomes very important. One way to ensure that integrity is by sharing the responsibility of maintaining it with a large group of independently motivated participants as validators.

So, now that we know a little more about why we might build an app like this, let's dive into the game itself.

The Game

The application we're building today can be used in many different ways but I'll be talking about it as scavenger hunt game. Scavenger hunts are all about someone setting up tasks or questions that challenge a participant to find solutions which come with some sort of a prize. The basic mechanics of the game are as follows:

  • Anyone can post a question with an encrypted answer.
  • This question comes paired with a bounty of coins.
  • Anyone can post an answer to this question, if they get it right, they receive the bounty of coins.

Something to note here is that when dealing with a public network with latency, it is possible that something like a man-in-the-middle attack could take place. Instead of pretending to be one of the parties, an attacker would take the sensitive information from one party and use it for their own benefit. This is actually called Front Running and happens as follows:

  1. You post the answer to some question with a bounty attached to it.
  2. Someone else sees you posting the answer and posts it themselves right before you.
  3. Since they posted the answer first, they receive the reward instead of you.

To prevent Front-Running, we will implement a commit-reveal scheme. A commit-reveal scheme converts a single exploitable interaction and turns it into two safe interactions.

The first interaction is the commit. This is where you "commit" to posting an answer in a follow-up interaction. This commit consists of a cryptographic hash of your name combined with the answer that you think is correct. The app saves that value which is a claim that you know the answer but that it hasn't been confirmed whether the answer is correct.

The next interaction is the reveal. This is where you post the answer in plaintext along with your name. The application will take your answer and your name and cryptographically hash them. If the result matches what you previously submitted during the commit stage, then it will be proof that it is in fact you who knows the answer, and not someone who is just front-running you.

front-run.jpg

A system like this could be used in tandem with any kind of gaming platform in a trustless way. Imagine you were playing the legend of Zelda and the game was compiled with all the answers to different scavenger hunts already included. When you beat a level the game could reveal the secret answer. Then either explicitly or behind the scenes, this answer could be combined with your name, hashed, submitted and subsequently revealed. Your name would be rewarded and you would have more points in the game.

Another way of achieving this would be to have an access control list where there was an admin account that the video game company controlled. This admin account could confirm that you beat the level and then give you points. The problem with this is that it creates a single point of failure and a single target for trying to attack the system. If there is one key that rules the castle then the whole system is broken if that key is compromised. It also creates a problem with coordination if that Admin account has to be online all the time in order for players to get their points. If you use a commit reveal system then you have a more trustless architecture where you don't need permission to play. This design decision has benefits and drawbacks, but paired with a careful implementation it can allow your game to scale without a single bottle neck or point of failure.

Now that we know what we're building we can get started.

Ignite

Requirements

For this tutorial we will be using Ignite CLI v28.7.0, an easy to use tool for building blockchains. To install ignite visit the docs and find your best way to install for your operating system:

Ignite Installation

Create a blockchain

Afterwards, you can enter in ignite in your terminal, and should see the following help text displayed:

$ ignite
Ignite CLI is a tool for creating sovereign blockchains built with Cosmos SDK, the world's
most popular modular blockchain framework. Ignite CLI offers everything you need to scaffold,
test, build, and launch your blockchain.

To get started, create a blockchain:

        ignite scaffold chain example

Usage:
  ignite [command]

Available Commands:
  scaffold    Create a new blockchain, module, message, query, and more
  chain       Build, init and start a blockchain node
  generate    Generate clients, API docs from source code
  node        Make requests to a live blockchain node
  account     Create, delete, and show Ignite accounts
  docs        Show Ignite CLI docs
  version     Print the current build information
  app         Create and manage Ignite Apps
  completion  Generates shell completion script.
  testnet     Start a testnet local
  network     Launch a blockchain in production
  relayer     Connect blockchains with an IBC relayer
  help        Help about any command

Flags:
  -h, --help   help for ignite

Use "ignite [command] --help" for more information about a command.

Now that the ignite command is available, you can scaffold an application by using the ignite scaffold chain scavenge command:

$ ignite scaffold chain scavenge --help
Scaffolding is a quick way to generate code for major pieces of your
application.

For details on each scaffolding target (chain, module, message, etc.) run the
corresponding command with a "--help" flag, for example, "ignite scaffold chain
--help".

The Ignite team strongly recommends committing the code to a version control
system before running scaffolding commands. This will make it easier to see the
changes to the source code as well as undo the command if you've decided to roll
back the changes.

This blockchain you create with the chain scaffolding command uses the modular
Cosmos SDK framework and imports many standard modules for functionality like
proof of stake, token transfer, inter-blockchain connectivity, governance, and
more. Custom functionality is implemented in modules located by convention in
the "x/" directory. By default, your blockchain comes with an empty custom
module. Use the module scaffolding command to create an additional module.

An empty custom module doesn't do much, it's basically a container for logic
that is responsible for processing transactions and changing the application
state. Cosmos SDK blockchains work by processing user-submitted signed
transactions, which contain one or more messages. A message contains data that
describes a state transition. A module can be responsible for handling any
number of messages.

A message scaffolding command will generate the code for handling a new type of
Cosmos SDK message. Message fields describe the state transition that the
message is intended to produce if processed without errors.

Scaffolding messages is useful to create individual "actions" that your module
can perform. Sometimes, however, you want your blockchain to have the
functionality to create, read, update and delete (CRUD) instances of a
particular type. Depending on how you want to store the data there are three
commands that scaffold CRUD functionality for a type: list, map, and single.
These commands create four messages (one for each CRUD action), and the logic to
add, delete, and fetch the data from the store. If you want to scaffold only the
logic, for example, you've decided to scaffold messages separately, you can do
that as well with the "--no-message" flag.

Reading data from a blockchain happens with a help of queries. Similar to how
you can scaffold messages to write data, you can scaffold queries to read the
data back from your blockchain application.

You can also scaffold a type, which just produces a new protocol buffer file
with a proto message description. Note that proto messages produce (and
correspond with) Go types whereas Cosmos SDK messages correspond to proto "rpc"
in the "Msg" service.

If you're building an application with custom IBC logic, you might need to
scaffold IBC packets. An IBC packet represents the data sent from one blockchain
to another. You can only scaffold IBC packets in IBC-enabled modules scaffolded
with an "--ibc" flag. Note that the default module is not IBC-enabled.

Usage:
  ignite scaffold [command]

Aliases:
  scaffold, s

Available Commands:
  chain          New Cosmos SDK blockchain
  module         Custom Cosmos SDK module
  list           CRUD for data stored as an array
  map            CRUD for data stored as key-value pairs
  single         CRUD for data stored in a single location
  type           Type definition
  message        Message to perform state transition on the blockchain
  query          Query for fetching data from a blockchain
  packet         Message for sending an IBC packet
  chain-registry Configs for the chain registry

Flags:
  -h, --help   help for scaffold

Use "ignite scaffold [command] --help" for more information about a command.

Run the command

ignite scaffold chain scavenge --no-module

to create your Blockchain App.

You've successfully scaffolded a Cosmos SDK application using ignite! In the next step, we're going to run the application using the instructions provided.

Let's enter the new directory to continue working with our application. Open the terminal in that directory and optimally your IDE or code working environment.

$ cd scavenge

To finish our blockchain template, add the module that we will be working in, the scavenge module:

ignite scaffold module scavenge --dep bank,account

This will create the scavenge module inside the newly create scavenge blockchain. Out of convention, all modules are hosted in the containing x directory. The directory scavenge/x/scavenge should now exist and already be pre-wired with other modules like the bank and account module.

Starting our application

Follow these commands to run our application:

$ ignite chain serve
  Blockchain is running

  πŸ‘€ alice's account address: cosmos1sz9n7y4cgh9zvc435aczzxv2un7v7x8jr4llke
  πŸ‘€ bob's account address: cosmos1hc3hzm8me6aqaglvfh35vljys7y2m59lv27wru

  🌍 Tendermint node: http://0.0.0.0:26657
  🌍 Blockchain API: http://0.0.0.0:1317
  🌍 Token faucet: http://0.0.0.0:4500

  ⋆ Data directory: /Users/tobiasschwarz/.scavenge
  ⋆ App binary: /Users/tobiasschwarz/Desktop/go/bin/scavenged

  Press the 'q' key to stop serve

Breaking down the ignite chain serve command

From the output above, we can see the following has occurred:

  • Two accounts were created with the respective mnemonics. Later on in this tutorial, you'll use them to log in and interact with your application.
  • Our Tendermint consensus engine (think of it as a database) is running at http://localhost:26657
  • A web server is running at http://localhost:1317
  • A faucet for requesting tokens to new accounts is running at http://localhost:4500

Before starting up our application, the chain serve command runs a build for our Cosmos SDK application.

The build process executed by ignite chain serve is similar to running make install with a Makefile.

After building the application, the serve command initializes the application based on the information provided in the config.yml file:

ersion: 1
validation: sovereign
accounts:
- name: alice
  coins:
  - 20000token
  - 200000000stake
- name: bob
  coins:
  - 10000token
  - 100000000stake
client:
  openapi:
    path: docs/static/openapi.yml
faucet:
  name: bob
  coins:
  - 5token
  - 100000stake
validators:
- name: alice
  bonded: 100000000stake
- name: validator1
  bonded: 100000000stake
- name: validator2
  bonded: 200000000stake
- name: validator3
  bonded: 300000000stake

You can see we've defined two accounts to the genesis, alice and bob, and have set up alice as the validator for the node we're going to run.

This setup can also be performed manually using the scavanged command, which is available after the application is built.

If you want to run the application manually, you can run scavenged start to start your Cosmos SDK application.

$ scavenged start
I[2020-09-27|04:58:19.684] starting ABCI with Tendermint                module=main 
I[2020-09-27|04:58:24.900] Executed block                               module=state height=1 validTxs=0 invalidTxs=0
I[2020-09-27|04:58:24.909] Committed state                              module=state height=1 txs=0 appHash=26BB4D82E1E3BCB98EC9EAFE7139D1551B96F5AD98D3A3AE904F42AF39D16DA6
I[2020-09-27|04:58:29.940] Executed block                               module=state height=2 validTxs=0 invalidTxs=0
I[2020-09-27|04:58:29.947] Committed state                              module=state height=2 txs=0 appHash=26BB4D82E1E3BCB98EC9EAFE7139D1551B96F5AD98D3A3AE904F42AF39D16DA6

Application

In this section, we'll be explaining how to quickly scaffold types for your application using the ignite scaffold type command.

Scaffolding Types

Open a new terminal under project's folder and run the following ignite scaffold type command to generate our scavenge-question type:

ignite scaffold type scavenge-question creator question answer bounty:uint completed:bool winner:string

We also want to create a second type, Commit, in order to prevent frontrunning of our submitted solutions as mentioned earlier.

ignite scaffold type committed-answer creator question-id:uint solution

Here, ignite has already done the majority of the work by helping us scaffold the necessary files and functions.

In the next sections, we'll be modifying these to give our application the functionality we want, according to the game.

Messages

Messages are a great place to start when building a module because they define the actions that your application can make. Think of all the scenarios where a user would be able to update the state of the application in any way. These should be boiled down into basic interactions, similar to CRUD (Create, Read, Update, Delete).

Create Scavenge Message

We will need to scaffold messages for our scavenge application.

  • Message Create Scavenge
  • Message Commit Answer
  • Message Reveal Answer

Let's scaffold these messages with the following commands:

Scaffold Create Scavenge

ignite scaffold message create-question question answer bounty:uint

Scaffold Commit Answer

ignite scaffold message commit-answer question-id:uint answer

Scaffold Reveal Answer

ignite scaffold message reveal-answer question-id:uint answer

Keeper

Our keeper stores all our data for our module. Sometimes a module will import the keeper of another module. This will allow state to be shared and modified across modules. Since we are dealing with coins in our module as bounty rewards, we have defined access to the bank module's keeper.

Understanding the Keeper and Store Keys

In our implementation, you'll notice we use different key prefixes like ScavengeQuestionKey, ScavengeQuestionCountKey, and CommittedAnswerKey. These are defined in x/scavenge/types/keys.go and help organize our data storage in the keeper.

The keeper in Cosmos SDK acts as a key-value store, similar to a database. Each piece of data is stored under a unique key, which we need to carefully structure to avoid conflicts and enable efficient querying.
Here's how our keys are organized in keys.go:

const (
    // ModuleName defines the module name
    ModuleName = "scavenge"

    // StoreKey defines the primary module store key
    StoreKey = ModuleName

    // Key prefixes
    ScavengeQuestionKey    = "ScavengeQuestion/value/"
    ScavengeQuestionCountKey = "ScavengeQuestion/count/"
    CommittedAnswerKey       = "CommittedAnswer/value/"
)

For our scavenger hunt game, we store two main types of data:

ScavengeQuestions: Each question is stored with a unique ID (uint64), which we generate using a counter.
CommittedAnswers: Each commit is stored using a combination of the question ID and the creator's address.

Key Construction and Storage

When storing a ScavengeQuestion or CommittedAnswer, we use helper functions to construct the appropriate keys:

// Helper function for ScavengeQuestion IDs
func GetScavengeQuestionIDBytes(id uint64) []byte {
    bz := make([]byte, 8)
    binary.BigEndian.PutUint64(bz, id)
    return bz
}

// Helper function for CommittedAnswer keys
func GetCommittedAnswerKey(questionId uint64, creator string) []byte {
    questionIdBytes := make([]byte, 8)
    binary.BigEndian.PutUint64(questionIdBytes, questionId)
    
    var key []byte
    key = append(key, KeyPrefix(CommittedAnswerKey)...)
    key = append(key, questionIdBytes...)
    key = append(key, []byte(creator)...)
    
    return key
}

Now that you've seen the keys where paths for Commit and Scavenge are stored, we need to connect the messages to the storage. This process is called handling the messages and is done inside the Keeper.

Store Prefixes and Iteration

In Cosmos SDK v0.50.11 and higher, we use the store service pattern with prefixes to organize our data. When we need to access all questions or commits, we use the prefix store:

// Example of getting all ScavengeQuestions
func (k Keeper) GetAllScavengeQuestion(ctx sdk.Context) (list []types.ScavengeQuestion) {
    store := prefix.NewStore(k.getStore(ctx), types.KeyPrefix(types.ScavengeQuestionKey))
    iterator := store.Iterator(nil, nil)
    defer iterator.Close()

    for ; iterator.Valid(); iterator.Next() {
        var val types.ScavengeQuestion
        k.cdc.MustUnmarshal(iterator.Value(), &val)
        list = append(list, val)
    }
    return
}

This organization allows us to:

  • Store questions with auto-incrementing IDs
  • Store commits using a composite key of question ID and creator address
  • Efficiently query all questions or commits using prefix iteration
  • Prevent key collisions between different types of data

Expected Keeper

In our game we will want to provide bounties to the winners of a scavenge hunt. In order to securely deposit the game bounty, we use the "module account" - an account that can just be controlled by the module itself. In order to use them easily, it was necessary to scaffold our module with the --dep bank dependency. Update the expected keeper file accordingly and add functions like SendCoinsFromAccountToModule or the other way around:

package types

import (
	"context"

	sdk "github.com/cosmos/cosmos-sdk/types"
)

// AccountKeeper defines the expected interface for the Account module.
type AccountKeeper interface {
	GetAccount(context.Context, sdk.AccAddress) sdk.AccountI // only used for simulation
	// Methods imported from account should be defined here
	GetModuleAddress(moduleName string) sdk.AccAddress
}

// BankKeeper defines the expected interface for the Bank module.
type BankKeeper interface {
	SpendableCoins(context.Context, sdk.AccAddress) sdk.Coins
	// Methods imported from bank should be defined here
	SendCoinsFromAccountToModule(context.Context, sdk.AccAddress, string, sdk.Coins) error
	SendCoinsFromModuleToAccount(context.Context, string, sdk.AccAddress, sdk.Coins) error
}

// ParamSubspace defines the expected Subspace interface for parameters.
type ParamSubspace interface {
	Get(context.Context, []byte, interface{})
	Set(context.Context, []byte, interface{})
}

Message Handling

The message handling in our implementation is done through the keeper's message server methods. Each message type (CreateQuestion, CommitAnswer, RevealAnswer) has its own handler that interacts with the keeper's storage methods.
In the context of our scavenger hunt:

  • Questions are stored with their encrypted answers (hash of the solution)
  • Commits store a hash of both the solution and the committer's address (preventing front-running)
  • Reveals verify both the commit and the solution before awarding the bounty

This structured approach to data storage and message handling ensures our scavenger hunt game is both secure and efficient.

The message server acts as the controller layer between the incoming messages and the keeper's storage methods. If you're familiar with MVC architecture, the keeper is like the Model (data layer), and the message server is like the Controller (business logic layer). In React/Redux terms, the keeper is similar to the Store/Reducer, while the message server methods are like Action Handlers.
Our message server implementation is structured as follows:

  1. The message server is initialised in x/scavenge/keeper/msg_server.go
// NewMsgServerImpl returns an implementation of the MsgServer interface
// for the provided Keeper.
func NewMsgServerImpl(keeper Keeper) types.MsgServer {
	return &msgServer{Keeper: keeper}
}

var _ types.MsgServer = msgServer{}
  1. For every message that we scaffolded, we now have a new file msg_server_SCAFFOLDED_MESSAGE. In our case, we start with the create-question message. Now open the file msg_server_create_question.go and you will see a newly scaffolded message without the logic yet. Add the following code to create new scavenges:
package keeper

import (
	"context"

	"scavenge/x/scavenge/types"

	errorsmod "cosmossdk.io/errors"
	sdkmath "cosmossdk.io/math"
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

func (k msgServer) CreateQuestion(goCtx context.Context, msg *types.MsgCreateQuestion) (*types.MsgCreateQuestionResponse, error) {
	ctx := sdk.UnwrapSDKContext(goCtx)

	// Get the next question ID
	count := k.GetScavengeQuestionCount(ctx)

	// Create a new scavenge question
	question := types.ScavengeQuestion{
		Id:              count,
		Creator:         msg.Creator,
		Question:        msg.Question,
		Answer:          msg.Answer,
		Bounty:          msg.Bounty,
		Completed:       false,
		Winner:          "",
	}

	// Lock the bounty in the module account
	creator, err := sdk.AccAddressFromBech32(msg.Creator)
	if err != nil {
		return nil, errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "invalid creator address")
	}

	coins := sdk.NewCoins(sdk.NewCoin(types.ChainDenom, sdkmath.NewInt(int64(msg.Bounty))))
	if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, creator, types.ModuleName, coins); err != nil {
		return nil, errorsmod.Wrap(err, "failed to lock bounty")
	}

	k.SetScavengeQuestion(ctx, question)
	k.SetScavengeQuestionCount(ctx, count+1)

	return &types.MsgCreateQuestionResponse{
		Id: count,
	}, nil
}

We also need the helper function GetScavengeQuestionCount. Create a new go file for this where we can store helper function.
I call this scavenge_question.go within the keeper directory.

Add the two functions that we need above:

package keeper

import (
	"encoding/binary"
	"scavenge/x/scavenge/types"

	"cosmossdk.io/store/prefix"
	sdk "github.com/cosmos/cosmos-sdk/types"
)

// GetScavengeQuestionCount get the total number of scavengeQuestion
func (k Keeper) GetScavengeQuestionCount(ctx sdk.Context) uint64 {
	store := k.getStore(ctx)
	byteKey := types.KeyPrefix(types.ScavengeCountKey)
	bz := store.Get(byteKey)
	if bz == nil {
		return 0
	}
	return binary.BigEndian.Uint64(bz)
}

// SetScavengeQuestion store a specific scavengeQuestion in the store
func (k Keeper) SetScavengeQuestion(ctx sdk.Context, scavengeQuestion types.ScavengeQuestion) uint64 {
	store := prefix.NewStore(k.getStore(ctx), types.KeyPrefix(types.ScavengeKey))
	appendedValue := k.cdc.MustMarshal(&scavengeQuestion)

	// Get the current count
	count := k.GetScavengeQuestionCount(ctx)

	// Store using the count as the ID
	store.Set(GetScavengeQuestionIDBytes(count), appendedValue)

	// Update the count
	k.SetScavengeQuestionCount(ctx, count+1)

	return count
}

Message Commit Answer

Now that the Keeper knows how to store the Question. Let's look at the Commit Answer part. Open x/scavenge/keeper/msg_server_commit_answer.go.
A user commits to answering a question.

package keeper

import (
	"context"
	"strconv"

	"scavenge/x/scavenge/types"

	errorsmod "cosmossdk.io/errors"
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

func (k msgServer) CommitAnswer(goCtx context.Context, msg *types.MsgCommitAnswer) (*types.MsgCommitAnswerResponse, error) {
	ctx := sdk.UnwrapSDKContext(goCtx)

	// Hash of solution and scavenger combined - this prevents front-running
	commit := types.CommittedAnswer{
		QuestionId: msg.QuestionId,
		HashAnswer: msg.HashAnswer, // This should be hash(solution + creator)
		Creator:    msg.Creator,
	}

	// Check if a commit already exists
	_, found := k.GetCommittedAnswer(ctx, msg.QuestionId, msg.Creator)
	if found {
		return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "commit already exists for this question and creator")
	}

	k.SetCommittedAnswer(ctx, commit)

	return &types.MsgCommitAnswerResponse{}, nil
}

Message Reveal Answer

After commiting, you are ready to reveal the answer. Open x/scavenge/keeper/msg_server_reveal_answer.go.

package keeper

import (
	"context"
	"strconv"

	"scavenge/x/scavenge/types"

	"crypto/sha256"
	"encoding/hex"

	errorsmod "cosmossdk.io/errors"
	sdkmath "cosmossdk.io/math"
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

func (k msgServer) RevealAnswer(goCtx context.Context, msg *types.MsgRevealAnswer) (*types.MsgRevealAnswerResponse, error) {
	ctx := sdk.UnwrapSDKContext(goCtx)

	// Get the question
	question, found := k.GetScavengeQuestion(ctx, msg.QuestionId)
	if !found {
		return nil, errorsmod.Wrap(sdkerrors.ErrKeyNotFound, "question not found")
	}

	if question.Completed {
		return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "question already completed")
	}

	// First verify the commit exists by recreating the hash of solution + creator
	plainTextSha := sha256.Sum256([]byte(msg.PlainText))
	encodedPlainText := hex.EncodeToString(plainTextSha[:])

	solutionScavengerBytes := []byte(encodedPlainText + msg.Creator)
	solutionScavengerHash := sha256.Sum256(solutionScavengerBytes)
	commitHash := hex.EncodeToString(solutionScavengerHash[:])

	// Get the commit
	commit, found := k.GetCommittedAnswer(ctx, msg.QuestionId, msg.Creator)
	if !found {
		return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "no commit found: must commit before reveal")
	}

	// Verify the hash matches their commit
	if commitHash != commit.HashAnswer {
		return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "committed hash does not match: "+commitHash)
	}

	// Verify the solution hash matches the question's encrypted answer
	if encodedPlainText != question.EncryptedAnswer {
		return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "incorrect answer "+msg.PlainText+": "+encodedPlainText+" vs "+question.EncryptedAnswer)
	}

	// Award the bounty
	winner, err := sdk.AccAddressFromBech32(msg.Creator)
	if err != nil {
		return nil, errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "invalid winner address")
	}

	coins := sdk.NewCoins(sdk.NewCoin(types.ChainDenom, sdkmath.NewInt(int64(question.Bounty))))
	if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, winner, coins); err != nil {
		return nil, errorsmod.Wrap(err, "failed to send bounty")
	}

	// Update the question
	question.Completed = true
	question.Winner = msg.Creator
	k.SetScavengeQuestion(ctx, question)

	return &types.MsgRevealAnswerResponse{}, nil
}

In this function we replicate the commit-reveal scheme and verify if it has been followed. This includes re-creating the commit hash and checking if it has been followed. Afterwards checking if the provided answer matches the solution by the Creator of the Question. If all checks go successful, the Bounty is awarded to the user and the challenge switches "Completed" to true and the Winner to the User.

In order for these commands to be userfriendly, we do not want them to do the hashing of the answers themselves. But they also should not end up on the blockchain in plain text. We need to interrupt between the user entering the solution and anything submitted on the blockchain. To do this, we need to create a new client.

The Client

Normally your application probably works totally fine with the AutoCLI feature that Ignite provides for you in x/scavenge/module/autocli.go.
For this application, we need to interrupt and hash the user input before it becomes written on the blockchain for everyone to see. Else the commit-reveal scheme would not make sense.

Using the standard tree for Cosmos blockchains, create a client and within cli directory in x/scavenge.
In the cli directory create a new file tx.go.

This file now is in x/scavenge/client/cli/tx.go and will be issuing Cobra commands to the user. This procedure will override the autocli commands.

Let's initiate cobra with the following code:

package cli

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"strconv"

	"github.com/spf13/cobra"

	"scavenge/x/scavenge/types"

	"github.com/cosmos/cosmos-sdk/client"
	"github.com/cosmos/cosmos-sdk/client/flags"
	"github.com/cosmos/cosmos-sdk/client/tx"
)

// GetTxCmd returns the transaction commands for the scavenge module
func GetTxCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:                        types.ModuleName,
		Short:                      fmt.Sprintf("%s transactions subcommands", types.ModuleName),
		DisableFlagParsing:         true,
		SuggestionsMinimumDistance: 2,
		RunE:                       client.ValidateCmd,
	}

	cmd.AddCommand(
		CmdCreateQuestion(),
		CmdCommitAnswer(),
		CmdRevealAnswer(),
	)

	return cmd
}

Client Create Question

That the Creator of a question can put the answer in plain text without having to sha256 hash on its own, we provide the hashing in the Client.
This command looks as follows:

func CmdCreateQuestion() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "create-question [question] [answer] [bounty]",
		Short: "Create a new scavenge question with a bounty",
		Args:  cobra.ExactArgs(3),
		RunE: func(cmd *cobra.Command, args []string) error {
			clientCtx, err := client.GetClientTxContext(cmd)
			if err != nil {
				return err
			}

			question := args[0]
			answer := args[1]

			// Hash the answer
			answerHash := sha256.Sum256([]byte(answer))
			answerHashString := hex.EncodeToString(answerHash[:])

			bounty, err := strconv.ParseUint(args[2], 10, 64)
			if err != nil {
				return err
			}

			msg := types.NewMsgCreateQuestion(
				clientCtx.GetFromAddress().String(),
				question,
				answerHashString,
				bounty,
			)

			return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
		},
	}

	flags.AddTxFlagsToCmd(cmd)
	return cmd
}

As you can see, we have three arguments being entered, the question, answer and bounty.

The answer becomes a sha256 hash of the answer before it get's forwarded to the Message and the Keeper.

Client Commit Answer

func CmdCommitAnswer() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "commit-answer [question-id] [solution]",
		Short: "Commit a answer to a scavenge question",
		Args:  cobra.ExactArgs(2),
		RunE: func(cmd *cobra.Command, args []string) error {
			clientCtx, err := client.GetClientTxContext(cmd)
			if err != nil {
				return err
			}

			questionID, err := strconv.ParseUint(args[0], 10, 64)
			if err != nil {
				return err
			}

			// Get the solution from args
			solution := args[1]

			// Get creator address
			creator := clientCtx.GetFromAddress().String()

			plainTextSha := sha256.Sum256([]byte(solution))
			encodedPlainText := hex.EncodeToString(plainTextSha[:])

			// Create the hash from solution and creator address
			hash := sha256.Sum256([]byte(encodedPlainText + creator))
			hashString := hex.EncodeToString(hash[:])

			msg := types.NewMsgCommitAnswer(
				creator,
				questionID,
				hashString,
			)

			return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
		},
	}

	flags.AddTxFlagsToCmd(cmd)
	return cmd
}

For the Commit-Solution scheme we want to create a new hash as described before, a combination of the answer and the person submitting the answer.

This is enabled with the line hash := sha256.Sum256([]byte(encodedPlainText + creator)). Later with submitting the final answer we can check if with the answer and the creator of the solution this holds true. Let's look at the final part revealing the answer.

Client Reveal Answer

func CmdRevealAnswer() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "reveal-answer [question-id] [solution]",
		Short: "Reveal the answer for a committed answer",
		Args:  cobra.ExactArgs(2),
		RunE: func(cmd *cobra.Command, args []string) error {
			clientCtx, err := client.GetClientTxContext(cmd)
			if err != nil {
				return err
			}

			questionID, err := strconv.ParseUint(args[0], 10, 64)
			if err != nil {
				return err
			}

			answer := args[1]

			// Hash the answer
			answerHash := sha256.Sum256([]byte(answer))
			answerHashString := hex.EncodeToString(answerHash[:])

			msg := types.NewMsgRevealAnswer(
				clientCtx.GetFromAddress().String(),
				questionID,
				answerHashString,
				args[1],
			)

			return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
		},
	}

	flags.AddTxFlagsToCmd(cmd)
	return cmd
}

As you can see in the code, we submit now both, the hashed answer and plain text to the chain for everyone to see. The bounty is only payed if all the hashes turn out to be correct.

Events

At the end of each message is an EventManager which will create logs within the transaction that reveals information about what occurred during the handling of this message. This is useful for client side software that wants to know exactly what happened as a result of this state transition.

Let's add events to our message handlers.

Event Create Question

We put the event right before returning, after all checks were successful and the data written to the blockchain. Let's create the event within msg_server_create_question.go:

	k.SetScavengeQuestion(ctx, question)
	k.SetScavengeQuestionCount(ctx, count+1)

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			sdk.EventTypeMessage,
			sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
			sdk.NewAttribute(sdk.AttributeKeyAction, "create_question"),
			sdk.NewAttribute(types.AttributeKeyQuestionId, strconv.FormatUint(count, 10)),
			sdk.NewAttribute(types.AttributeKeyCreator, msg.Creator),
			sdk.NewAttribute(types.AttributeKeyBounty, strconv.FormatUint(msg.Bounty, 10)),
		),
	)

	return &types.MsgCreateQuestionResponse{
		Id: count,
	}, nil

Event Commit Answer

k.SetCommittedAnswer(ctx, commit)

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			sdk.EventTypeMessage,
			sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
			sdk.NewAttribute(sdk.AttributeKeyAction, "commit_answer"),
			sdk.NewAttribute(types.AttributeKeyQuestionId, strconv.FormatUint(msg.QuestionId, 10)),
			sdk.NewAttribute(types.AttributeKeyCommitter, msg.Creator),
			sdk.NewAttribute(types.AttributeKeyCommitHash, msg.HashAnswer),
		),
	)

	return &types.MsgCommitAnswerResponse{}, nil

Event Reveal Answer

k.SetScavengeQuestion(ctx, question)

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			sdk.EventTypeMessage,
			sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
			sdk.NewAttribute(sdk.AttributeKeyAction, "reveal_answer"),
			sdk.NewAttribute(types.AttributeKeyQuestionId, strconv.FormatUint(msg.QuestionId, 10)),
			sdk.NewAttribute(types.AttributeKeyWinner, msg.Creator),
			sdk.NewAttribute(types.AttributeKeyBounty, strconv.FormatUint(question.Bounty, 10)),
		),
	)

	return &types.MsgRevealAnswerResponse{}, nil

Event Types

With these events we are adding a few new types, let's define them in the keys.go file within our types directory:

	AttributeKeyQuestionId = "question_id"
	AttributeKeyCreator    = "creator"
	AttributeKeyBounty     = "bounty"
	AttributeKeyCommitter  = "committer"
	AttributeKeyCommitHash = "commit_hash"
	AttributeKeyWinner     = "winner"

Done - you now have the Events specified,.

Querier

In order to query the data of our app we need to make it accessible using Queries.

Ignite helps us build the skeleton of the queries and we can implement their logic.

Let's first scaffold our queries, we want:

  • List Questions
  • List Commits
  • Show Question
  • Show Commit

Starting with the List Questions Query. It will list all the questions every added to the chain scaffold it using:

ignite scaffold query list-questions

Next, let's look at querying a single question with:

ignite scaffold query show-question question-id:uint

Now let's continue with the Commits which look very similar.

ignite scaffold query list-commits

and for the individual commit:

ignite scaffold query show-commit question-id:uint creator

Let's add to our x/scavenge/keeper/scavenge_question.go file helper function for getting these data.


// GetScavengeQuestion returns a scavengeQuestion from its id
func (k Keeper) GetScavengeQuestion(ctx sdk.Context, id uint64) (val types.ScavengeQuestion, found bool) {
	store := prefix.NewStore(k.getStore(ctx), types.KeyPrefix(types.ScavengeKey))
	b := store.Get(GetScavengeQuestionIDBytes(id))
	if b == nil {
		return val, false
	}
	k.cdc.MustUnmarshal(b, &val)
	return val, true
}



// SetScavengeQuestionCount set the total number of scavengeQuestion
func (k Keeper) SetScavengeQuestionCount(ctx sdk.Context, count uint64) {
	store := k.getStore(ctx)
	byteKey := types.KeyPrefix(types.ScavengeCountKey)
	bz := make([]byte, 8)
	binary.BigEndian.PutUint64(bz, count)
	store.Set(byteKey, bz)
}

// Helper function to convert ID to bytes
func GetScavengeQuestionIDBytes(id uint64) []byte {
	bz := make([]byte, 8)
	binary.BigEndian.PutUint64(bz, id)
	return bz
}

// GetAllScavengeQuestion returns all scavenge questions
func (k Keeper) GetAllScavengeQuestion(ctx sdk.Context) (list []types.ScavengeQuestion) {
	store := prefix.NewStore(k.getStore(ctx), types.KeyPrefix(types.ScavengeKey))
	iterator := store.Iterator(nil, nil)
	defer iterator.Close()

	for ; iterator.Valid(); iterator.Next() {
		var val types.ScavengeQuestion
		k.cdc.MustUnmarshal(iterator.Value(), &val)
		list = append(list, val)
	}

	return list
}

func (k Keeper) GetAllCommittedAnswer(ctx sdk.Context) (list []types.CommittedAnswer) {
	store := prefix.NewStore(k.getStore(ctx), types.KeyPrefix(types.CommitKeyPrefix))
	iterator := store.Iterator(nil, nil)
	defer iterator.Close()

	for ; iterator.Valid(); iterator.Next() {
		var val types.CommittedAnswer
		k.cdc.MustUnmarshal(iterator.Value(), &val)
		list = append(list, val)
	}

	return list
}

Now we can easily add these functionality to our queries. Let's start with the query for List all Questions:

Open the file x/scavenge/keeper/query_list_questions.go

package keeper

import (
	"context"

	"scavenge/x/scavenge/types"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func (k Keeper) ListQuestions(goCtx context.Context, req *types.QueryListQuestionsRequest) (*types.QueryListQuestionsResponse, error) {
	if req == nil {
		return nil, status.Error(codes.InvalidArgument, "invalid request")
	}

	sdkCtx := sdk.UnwrapSDKContext(goCtx)
	questions := k.GetAllScavengeQuestion(sdkCtx)

	return &types.QueryListQuestionsResponse{
		ScavengeQuestion: questions,
	}, nil
}

This is quite self-explanatory since we're using the before defined helper functions. Let's continue with the next files.

In x/scavenge/keeper/query_show_question.go

package keeper

import (
	"context"

	"scavenge/x/scavenge/types"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func (k Keeper) ShowQuestion(goCtx context.Context, req *types.QueryShowQuestionRequest) (*types.QueryShowQuestionResponse, error) {
	if req == nil {
		return nil, status.Error(codes.InvalidArgument, "invalid request")
	}

	ctx := sdk.UnwrapSDKContext(goCtx)

	question, found := k.GetScavengeQuestion(ctx, req.QuestionId)
	if !found {
		return nil, status.Error(codes.NotFound, "question not found")
	}

	return &types.QueryShowQuestionResponse{
		ScavengeQuestion: question,
	}, nil
}

In x/scavenge/keeper/query_list_commits.go

package keeper

import (
	"context"

	"scavenge/x/scavenge/types"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func (k Keeper) ListCommits(goCtx context.Context, req *types.QueryListCommitsRequest) (*types.QueryListCommitsResponse, error) {
	if req == nil {
		return nil, status.Error(codes.InvalidArgument, "invalid request")
	}

	ctx := sdk.UnwrapSDKContext(goCtx)

	commits := k.GetAllCommittedAnswer(ctx)

	return &types.QueryListCommitsResponse{
		CommittedAnswer: commits,
	}, nil
}

and in x/scavenge/keeper/query_show_commit.go

package keeper

import (
	"context"

	"scavenge/x/scavenge/types"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func (k Keeper) ShowCommit(goCtx context.Context, req *types.QueryShowCommitRequest) (*types.QueryShowCommitResponse, error) {
	if req == nil {
		return nil, status.Error(codes.InvalidArgument, "invalid request")
	}

	ctx := sdk.UnwrapSDKContext(goCtx)

	commit, found := k.GetCommittedAnswer(ctx, req.QuestionId, req.Creator)
	if !found {
		return nil, status.Error(codes.NotFound, "commit not found")
	}

	return &types.QueryShowCommitResponse{
		CommittedAnswer: commit,
	}, nil
}

That's it! Now we want to start the chain with the command:

ignite chain serve --reset-once

Play

Your application is running! That's great but who cares unless you can play with it. The first command you will want to try is creating a new scavenge. Since our user alice has way more stake token than the user bob, let's create the scavenge from their account.

You can begin by running scavenged tx scavenge --help to see all the commands we created for your new module. You should see the following options:

$ scavenged tx scavenge --help
scavenge transactions subcommands

Usage:
  scavenged tx scavenge [flags]
  scavenged tx scavenge [command]

Available Commands:
  commit-answer     Commit a answer to a scavenge question
  complete-question Execute the CompleteQuestion RPC method
  create-question   Create a new scavenge question with a bounty
  reveal-answer     Reveal the answer for a committed answer

Flags:
  -h, --help   help for scavenge

Global Flags:
      --chain-id string     The network chain ID (default "scavenge")
      --home string         directory for config and data (default "/Users/tobiasschwarz/.scavenge")
      --log_format string   The logging format (json|plain) (default "plain")
      --log_level string    The logging level (trace|debug|info|warn|error|fatal|panic|disabled or '*:<level>,<key>:<level>') (default "info")
      --log_no_color        Disable colored logs
      --trace               print out full stack trace on errors

Use "scavenged tx scavenge [command] --help" for more information about a command.

We want to use the create-question command so let's check the help screen for it as well like scavenged tx scavenge create-question --help. It should look like:

$ scavenged tx scavenge create-question --help
Create a new scavenge question with a bounty

Usage:
  scavenged tx scavenge create-question [question] [answer] [bounty] [flags]

Let's follow the instructions and create a new scavenge. The first parameter we need is the question.

Next we should list our solution, but probably we should also know what the actual quesiton is that our solution solves (our description).
How about our challenge question be something family friendly like: I have cities, but no houses. I have mountains, but no trees. I have water, but no fish. I have roads, but no cars. What am I?. Of course the solution to this question is: map.

Let's give away 69token as a reward for solving our scavenge (nice).

Now we have all the pieces needed to create our Message. Let's piece them all together, adding the flag --from so the CLI knows who is sending it:

scavenged tx scavenge create-question "I have cities, but no houses. I have mountains, but no trees. I have water, but no fish. I have roads, but no cars. What am I?" "map" 69 --from alice --chain-id scavenge

After confirming the message looks correct and signing it, you should see something like the following:

auth_info:
  fee:
    amount: []
    gas_limit: "200000"
    granter: ""
    payer: ""
  signer_infos: []
  tip: null
body:
  extension_options: []
  memo: ""
  messages:
  - '@type': /scavenge.scavenge.MsgCreateQuestion
    bounty: "69"
    creator: cosmos13ynhy7670sre5pw97kx6pnu4hl6yhn7f3m0zl0
    encryptedAnswer: 60be9861750facbfad8758254a2f76c0cfe78d54459a3bc187d49b1401fcd8e8
    question: I have cities, but no houses. I have mountains, but no trees. I have
      water, but no fish. I have roads, but no cars. What am I?
  non_critical_extension_options: []
  timeout_height: "0"
signatures: []
confirm transaction before signing and broadcasting [y/N]: y
code: 0
codespace: ""
data: ""
events: []
gas_used: "0"
gas_wanted: "0"
height: "0"
info: ""
logs: []
raw_log: ""
timestamp: ""
tx: null
txhash: 1A36A8C594544DA0E86AAD5C6CA4F0238644F15D715288D8637DDF009D3944DD

This tells you that the message was accepted into the app. Whether the message failed afterwards can not be told from this screen. However, the section under txhash is like a receipt for this interaction. To see if it was successfully processed after being successfully included you can run the following command:

scavenged q tx <txhash>

But replace the <txhash> with your own. You should see something similar to this afterwards:

code: 0
codespace: ""
data: 122E0A2C2F73636176656E67652E73636176656E67652E4D73674372656174655175657374696F6E526573706F6E7365
events:
- attributes:
  - index: true
    key: fee
    value: ""
  - index: true
    key: fee_payer
    value: cosmos13ynhy7670sre5pw97kx6pnu4hl6yhn7f3m0zl0
  type: tx
- attributes:
  - index: true
    key: acc_seq
    value: cosmos13ynhy7670sre5pw97kx6pnu4hl6yhn7f3m0zl0/1
  type: tx
- attributes:
  - index: true
    key: signature
    value: 6HDhd5rv8JpSorEkywS+Zt5l0DSc6jM0hx4YcgY/NA0v8HU9+cYvA0wYQxsJVXPRjfEBT+yOaYNHXrlB4TOBoA==
  type: tx
- attributes:
  - index: true
    key: action
    value: /scavenge.scavenge.MsgCreateQuestion
  - index: true
    key: sender
    value: cosmos13ynhy7670sre5pw97kx6pnu4hl6yhn7f3m0zl0
  - index: true
    key: msg_index
    value: "0"
  type: message
- attributes:
  - index: true
    key: spender
    value: cosmos13ynhy7670sre5pw97kx6pnu4hl6yhn7f3m0zl0
  - index: true
    key: amount
    value: 69stake
  - index: true
    key: msg_index
    value: "0"
  type: coin_spent
- attributes:
  - index: true
    key: receiver
    value: cosmos13aupkh5020l9u6qquf7lvtcxhtr5jjama2kwyg
  - index: true
    key: amount
    value: 69stake
  - index: true
    key: msg_index
    value: "0"
  type: coin_received
- attributes:
  - index: true
    key: recipient
    value: cosmos13aupkh5020l9u6qquf7lvtcxhtr5jjama2kwyg
  - index: true
    key: sender
    value: cosmos13ynhy7670sre5pw97kx6pnu4hl6yhn7f3m0zl0
  - index: true
    key: amount
    value: 69stake
  - index: true
    key: msg_index
    value: "0"
  type: transfer
- attributes:
  - index: true
    key: sender
    value: cosmos13ynhy7670sre5pw97kx6pnu4hl6yhn7f3m0zl0
  - index: true
    key: msg_index
    value: "0"
  type: message
- attributes:
  - index: true
    key: module
    value: scavenge
  - index: true
    key: action
    value: create_question
  - index: true
    key: question_id
    value: "0"
  - index: true
    key: creator
    value: cosmos13ynhy7670sre5pw97kx6pnu4hl6yhn7f3m0zl0
  - index: true
    key: bounty
    value: "69"
  - index: true
    key: msg_index
    value: "0"
  type: message
gas_used: "81136"
gas_wanted: "200000"
height: "114"
info: ""
logs: []
raw_log: ""
timestamp: "2025-01-28T16:07:16Z"
tx:
  '@type': /cosmos.tx.v1beta1.Tx
  auth_info:
    fee:
      amount: []
      gas_limit: "200000"
      granter: ""
      payer: ""
    signer_infos:
    - mode_info:
        single:
          mode: SIGN_MODE_DIRECT
      public_key:
        '@type': /cosmos.crypto.secp256k1.PubKey
        key: Al8ngYEK4fGX2VooGy9VItcp8r53ntdFpHNpnnwyWDNv
      sequence: "1"
    tip: null
  body:
    extension_options: []
    memo: ""
    messages:
    - '@type': /scavenge.scavenge.MsgCreateQuestion
      bounty: "69"
      creator: cosmos13ynhy7670sre5pw97kx6pnu4hl6yhn7f3m0zl0
      encryptedAnswer: 60be9861750facbfad8758254a2f76c0cfe78d54459a3bc187d49b1401fcd8e8
      question: I have cities, but no houses. I have mountains, but no trees. I have
        water, but no fish. I have roads, but no cars. What am I?
    non_critical_extension_options: []
    timeout_height: "0"
  signatures:
  - 6HDhd5rv8JpSorEkywS+Zt5l0DSc6jM0hx4YcgY/NA0v8HU9+cYvA0wYQxsJVXPRjfEBT+yOaYNHXrlB4TOBoA==
txhash: 1A36A8C594544DA0E86AAD5C6CA4F0238644F15D715288D8637DDF009D3944DD

Here you can see all the events we defined within our Handler that describes exactly what happened when this message was processed. Since our message was formatted correctly and since the user alice had enough stake to pay the bounty, our Scavenge was accepted. You can also see what the solution looks like now that it has been hashed:

encryptedAnswer: 60be9861750facbfad8758254a2f76c0cfe78d54459a3bc187d49b1401fcd8e8

Let's query the question for more details:

$ scavenged q scavenge list-questions

The ourput is

scavengeQuestion:
- bounty: "69"
  creator: cosmos13ynhy7670sre5pw97kx6pnu4hl6yhn7f3m0zl0
  encryptedAnswer: 60be9861750facbfad8758254a2f76c0cfe78d54459a3bc187d49b1401fcd8e8
  question: I have cities, but no houses. I have mountains, but no trees. I have water,
    but no fish. I have roads, but no cars. What am I?

Since we know the solution to this question and since we have another user at hand that can submit it, let's begin the process of committing and revealing that solution.

First we should check the CLI command for commit-answer by running scavenged tx scavenge commit-answer --help in order to see:

$ scavenged tx scavenge commit-answer --help
Commit a answer to a scavenge question

Usage:
  scavenged tx scavenge commit-answer [question-id] [solution] [flags]

Let's follow the instructions and submit the answer as a commit on behalf of bob:

scavenged tx scavenge commit-answer 0 "map" --from bob --chain-id scavenge -y

This time we're passing the -y to auto-confirm the transaction. Afterwards, we should see our txhash again. To confirm the txhash let's look at it again with scavenged q tx <txhash>. This time you should see something like:

code: 0
codespace: ""
data: 122C0A2A2F73636176656E67652E73636176656E67652E4D7367436F6D6D6974416E73776572526573706F6E7365
events:
- attributes:
  - index: true
    key: fee
    value: ""
  - index: true
    key: fee_payer
    value: cosmos1wlchtxjg8vkznh5qz0t30t3kdyjhgnzyj5xrs5
  type: tx
- attributes:
  - index: true
    key: acc_seq
    value: cosmos1wlchtxjg8vkznh5qz0t30t3kdyjhgnzyj5xrs5/0
  type: tx
- attributes:
  - index: true
    key: signature
    value: jOAEXAqQ7REC61wTFyr/mU9MvQ0+DhssnXG9D8ziuaBa4Wh5WU5Nm7ukW3D9X/Nt/R+uVoCvGyk31ZB5R49GjQ==
  type: tx
- attributes:
  - index: true
    key: action
    value: /scavenge.scavenge.MsgCommitAnswer
  - index: true
    key: sender
    value: cosmos1wlchtxjg8vkznh5qz0t30t3kdyjhgnzyj5xrs5
  - index: true
    key: msg_index
    value: "0"
  type: message
- attributes:
  - index: true
    key: module
    value: scavenge
  - index: true
    key: action
    value: commit_answer
  - index: true
    key: question_id
    value: "0"
  - index: true
    key: committer
    value: cosmos1wlchtxjg8vkznh5qz0t30t3kdyjhgnzyj5xrs5
  - index: true
    key: commit_hash
    value: 35384ce20f46773ab0ca7e363a1785479986de00c4c8a53b3a2f9f0f75d8e811
  - index: true
    key: msg_index
    value: "0"
  type: message
gas_used: "52523"
gas_wanted: "200000"
height: "628"
info: ""
logs: []
raw_log: ""
timestamp: "2025-01-28T16:16:36Z"
tx:
  '@type': /cosmos.tx.v1beta1.Tx
  auth_info:
    fee:
      amount: []
      gas_limit: "200000"
      granter: ""
      payer: ""
    signer_infos:
    - mode_info:
        single:
          mode: SIGN_MODE_DIRECT
      public_key:
        '@type': /cosmos.crypto.secp256k1.PubKey
        key: AvfeuRZsoTJS6CdbRbCXLva5iN1hSSgzUIHbL1Q/wXW0
      sequence: "0"
    tip: null
  body:
    extension_options: []
    memo: ""
    messages:
    - '@type': /scavenge.scavenge.MsgCommitAnswer
      creator: cosmos1wlchtxjg8vkznh5qz0t30t3kdyjhgnzyj5xrs5
      hashAnswer: 35384ce20f46773ab0ca7e363a1785479986de00c4c8a53b3a2f9f0f75d8e811
      questionId: "0"
    non_critical_extension_options: []
    timeout_height: "0"
  signatures:
  - jOAEXAqQ7REC61wTFyr/mU9MvQ0+DhssnXG9D8ziuaBa4Wh5WU5Nm7ukW3D9X/Nt/R+uVoCvGyk31ZB5R49GjQ==
txhash: FC98E639BA1094093A582255ADE211577A78BCC04BBB280341E234FCFC5585BF

You'll notice that the solutionHash matches the one before. We've also created a new hash for the solutionScavengerHash which is the combination of the solution and our account address. We can make sure the commit has been made by querying it directly as well:

scavenged q scavenge list-commits

Hopefully you should see something like:

committedAnswer:
- creator: cosmos1wlchtxjg8vkznh5qz0t30t3kdyjhgnzyj5xrs5
  hashAnswer: 35384ce20f46773ab0ca7e363a1785479986de00c4c8a53b3a2f9f0f75d8e811

This confirms that your commit was successfully submitted and is awaiting the follow-up reveal. To make that command let's first check the --help command using scavenged tx scavenge reveal-answer --help. This should show the following screen:

$ scavenged tx scavenge reveal-answer --help
Reveal the answer for a committed answer

Usage:
  scavenged tx scavenge reveal-answer [question-id] [solution] [flags]

Since all we need is the solution again let's send and confirm our final message:

scavenged tx scavenge reveal-answer 0 "map" --from bob --chain-id scavenge

We can gather the txhash and query it again using scavenged q tx <txhash> to reveal:

code: 0
codespace: ""
data: 122C0A2A2F73636176656E67652E73636176656E67652E4D736752657665616C416E73776572526573706F6E7365
events:
- attributes:
  - index: true
    key: fee
    value: ""
  - index: true
    key: fee_payer
    value: cosmos1y4ydygwmfv5uyq8aeu0htfzyjeecakgpnu99zg
  type: tx
- attributes:
  - index: true
    key: acc_seq
    value: cosmos1y4ydygwmfv5uyq8aeu0htfzyjeecakgpnu99zg/1
  type: tx
- attributes:
  - index: true
    key: signature
    value: JwJwimTOvwfFO4f0MgoWCQFfXVZxhFDS0KEDfOV5cy0mvxVF39KQNqPMZ809Ilcyq2+xaYxkNWUuTi+OoUFHPA==
  type: tx
- attributes:
  - index: true
    key: action
    value: /scavenge.scavenge.MsgRevealAnswer
  - index: true
    key: sender
    value: cosmos1y4ydygwmfv5uyq8aeu0htfzyjeecakgpnu99zg
  - index: true
    key: msg_index
    value: "0"
  type: message
- attributes:
  - index: true
    key: spender
    value: cosmos13aupkh5020l9u6qquf7lvtcxhtr5jjama2kwyg
  - index: true
    key: amount
    value: 69stake
  - index: true
    key: msg_index
    value: "0"
  type: coin_spent
- attributes:
  - index: true
    key: receiver
    value: cosmos1y4ydygwmfv5uyq8aeu0htfzyjeecakgpnu99zg
  - index: true
    key: amount
    value: 69stake
  - index: true
    key: msg_index
    value: "0"
  type: coin_received
- attributes:
  - index: true
    key: recipient
    value: cosmos1y4ydygwmfv5uyq8aeu0htfzyjeecakgpnu99zg
  - index: true
    key: sender
    value: cosmos13aupkh5020l9u6qquf7lvtcxhtr5jjama2kwyg
  - index: true
    key: amount
    value: 69stake
  - index: true
    key: msg_index
    value: "0"
  type: transfer
- attributes:
  - index: true
    key: sender
    value: cosmos13aupkh5020l9u6qquf7lvtcxhtr5jjama2kwyg
  - index: true
    key: msg_index
    value: "0"
  type: message
- attributes:
  - index: true
    key: module
    value: scavenge
  - index: true
    key: action
    value: reveal_answer
  - index: true
    key: question_id
    value: "0"
  - index: true
    key: winner
    value: cosmos1y4ydygwmfv5uyq8aeu0htfzyjeecakgpnu99zg
  - index: true
    key: bounty
    value: "69"
  - index: true
    key: msg_index
    value: "0"
  type: message
gas_used: "61309"
gas_wanted: "200000"
height: "12"
info: ""
logs: []
raw_log: ""
timestamp: "2025-01-28T16:21:32Z"
tx:
  '@type': /cosmos.tx.v1beta1.Tx
  auth_info:
    fee:
      amount: []
      gas_limit: "200000"
      granter: ""
      payer: ""
    signer_infos:
    - mode_info:
        single:
          mode: SIGN_MODE_DIRECT
      public_key:
        '@type': /cosmos.crypto.secp256k1.PubKey
        key: A1uB1GOU2zjInoowIl3rrTtBuqfkTZ6sGskopYUuaFLk
      sequence: "1"
    tip: null
  body:
    extension_options: []
    memo: ""
    messages:
    - '@type': /scavenge.scavenge.MsgRevealAnswer
      answer: 60be9861750facbfad8758254a2f76c0cfe78d54459a3bc187d49b1401fcd8e8
      creator: cosmos1y4ydygwmfv5uyq8aeu0htfzyjeecakgpnu99zg
      plainText: map
      questionId: "0"
    non_critical_extension_options: []
    timeout_height: "0"
  signatures:
  - JwJwimTOvwfFO4f0MgoWCQFfXVZxhFDS0KEDfOV5cy0mvxVF39KQNqPMZ809Ilcyq2+xaYxkNWUuTi+OoUFHPA==
txhash: EC0E4DE761751E4A8F5ACEAA1ADFA67B7D206E333543D8191F265EE3DD3C54C1

You'll notice that the final event that was submitted was a transfer. This shows the movement of the reward into the account of the user user1. To confirm user2 now has 69token more you can query their account balance as follows:

scavenged q bank balances $(scavenged keys show bob -a)

This should show a healthy account balance of 100000069 stake since bob began with 100000000 stake:

balances:
- amount: "100000069"
  denom: stake
- amount: "10000"
  denom: token

Thanks for joining me in building a deterministic state machine and using it as a game. I hope you can see that even such a simple app can be extremely powerful as it contains digital scarcity.

If you'd like to keep going, consider trying to expand on the capabilities of this application by doing one of the following:

  • Allow the Creator of a Scavenge to edit or delete a scavenge.
  • Create a query that lists more details about a scavenge.

If you're interested in learning more about the Cosmos SDK check out the rest of our docs or join our forum.

If you want to learn more about Ignite, visit our documentation at: Ignite Docs.