Introduction to basic hashing and adding unit tests

Hash the solution, add basic unit tests

In the previous section, we stored the crossword solution as plain text as a String type on the smart contract. If we’re trying to hide the solution from the users, this isn’t a great approach as it’ll be public to anyone looking at the state. Let’s instead hash our crossword solution and store that instead. There are different ways to hash data, but let’s use sha256 which is one of the hashing algorithms available in the Rust SDK.

Helper unit test during rapid iteration

As mentioned in the first section of this Basics chapter, our smart contract is technically a library as defined in the manifest file. For our purposes, a consequence of writing a library in Rust is not having a “main” function that runs. You may find many online tutorials where the command cargo run is used during development. We don’t have this luxury, but we can use unit tests to interact with our smart contract. This is likely more convenient than building the contract, deploying to a blockchain network, and calling a method.

We’ll add a dependency to the hex crate to make things easier. As you may remember, dependencies live in the manifest file.

Let’s write a unit test that acts as a helper during development. This unit test will sha256 hash the input “near nomicon ref finance” and print it in a human-readable, hex format. (We’ll typically put unit tests at the bottom of the lib.rs file.)

Run the unit tests with the command:

cargo test -- --nocapture

You’ll see this output:

…
running 1 test
Let's debug: "69c2feb084439956193f4c21936025f14a5a5a78979d67ae34762e18a7206a0f"
test tests::debug_get_hash ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

This means when you sha256 the input “near nomicon ref finance” it produces the hash:
69c2feb084439956193f4c21936025f14a5a5a78979d67ae34762e18a7206a0f

The unit test above is meant for debugging and quickly running snippets of code. Some may find this a useful technique when getting familiar with Rust and writing smart contracts. Next we’ll write a real unit test that applies to this early version of our crossword puzzle contract.

Write a regular unit test

Let’s add this unit test (inside the mod tests {} block, under our previous unit test) and analyze it:

The first few lines of code will be used commonly when writing unit tests. It uses the VMContextBuilder to create some basic context for a transaction, then sets up the testing environment.

Next, an object is created representing the contract and the set_solution function is called. After that, the guess_solution function is called twice: first with the incorrect solution and then the correct one. We can check the logs to determine that the function is acting as expected.

Again, we can run all the unit tests with:

cargo test -- --nocapture

Modifying set_solution

The overview section of this chapter tells us we want to have a single crossword puzzle and the user solving the puzzle should not be able to know the solution. Using a hash addresses this, and we can keep crossword_solution‘s field type, as String will work just fine. The overview also indicates we only want the author of the crossword puzzle to be able to set the solution. As it stands, our function set_solution can be called by anyone with a full-access key. It’s trivial for someone to create a NEAR account and call this function, changing the solution. Let’s fix that.

Let’s have the solution be set once, right after deploying the smart contract.

Here we’ll use the #[near] macro on a function called new, which is a common pattern.

Let’s call this method on a fresh contract.

Go into the directory containing the Rust smart contract and build it:

cd contract

# Build
cargo near build

Create fresh account if you wish, which is good practice:


“`bash
# Delete an account
near delete-account crossword.friend.testnet friend.testnet –networkId testnet

# Create an account again
near create-account crossword.friend.testnet –use-account friend.testnet –initial-balance 1 –network-id testnet
“`

“`bash
# Delete an account
near account delete-account crossword.friend.testnet beneficiary friend.testnet network-config testnet sign-with-keychain send

# Create an account again
near account create-account fund-myself crossword.friend.testnet ‘1 NEAR’ autogenerate-new-keypair save-to-keychain sign-as friend.testnet network-config testnet sign-with-keychain send
“`

Deploy the contract:

cargo near deploy crossword.friend.testnet without-init-call network-config testnet sign-with-keychain send

Call the “new” method:


“`bash
near call crossword.friend.testnet new ‘{“solution”: “69c2feb084439956193f4c21936025f14a5a5a78979d67ae34762e18a7206a0f”}’ –gas 100000000000000 –accountId crossword.friend.testnet
“`

“`bash
near contract call-function as-transaction crossword.friend.testnet new json-args ‘{“solution”: “69c2feb084439956193f4c21936025f14a5a5a78979d67ae34762e18a7206a0f”}’ prepaid-gas ‘100.0 Tgas’ attached-deposit ‘0 NEAR’ sign-as crossword.friend.testnet network-config testnet sign-with-keychain send
“`

Now the crossword solution, as a hash, is stored instead. If you try calling the last command again, you’ll get the error message, thanks to the #[init] macro:
The contract has already been initialized

First use of Batch Actions

This is close to what we want, but what if a person deploys their smart contract and someone else quickly calls the new function before them? We want to make sure the same person who deployed the contract sets the solution, and we can do this using Batch Actions. Besides, why send two transactions when we can do it in one? (Technical details covered in the spec for a batch transaction here.)

Cookie sheet representing a transaction, where cookies are Deploy and FunctionCall Actions. Art created by dobulyo.near.
Art by dobulyo.near
Scroll to Top