Skip to main content

Sign a transaction

In this section, we will learn about signing transactions, which involves verifying the authenticity of the user who is sending the transaction to the blockchain.

We will create an administration function for a bookkeeper responsible for managing and adding books available for review. Only a specific administrator will be authorized to sign the transactions required for this task..

Setup module arguments

We will use module arguments to ensure that only the administrator can add books. These arguments configure a module on the Chromia blockchain and enable access to variables from a context called chain_context. Here's how to set it up:

Create a keypair

First, generate a new key pair for the administrator, which will be used to sign transactions. Run the following command in your project folder:

chr keygen --file .chromia/config

This command generates a new key pair and stores it in a file called .chromia/config within your project folder.

Define a struct for module arguments

We need to define a struct to hold our module arguments. This struct should be added to the module where you want to access these arguments. For this example, add it to src/main/entities.rell:

src/main/entities.rell
struct module_args {
admin_pubkey: byte_array;
}

Configure module arguments in chromia.yml

Next, define the module arguments in the chromia.yml configuration file. Update your chromia.yml file as follows:

blockchains:
book-review:
module: main
moduleArgs:
main:
admin_pubkey: <pubkey from .chromia/config>

Explanation of example configuration:

  • book-review: The name of your blockchain configuration. This should correspond to the blockchain configuration you are working with.
  • module: main: Specifies that the main module is used for this blockchain configuration.
  • moduleArgs: Under this section, you define arguments specific to the module.
    • main: This should match the module name defined in your source code.
    • admin_pubkey: This is the argument name that the module will use to verify the administrator's public key.
    • <pubkey from .chromia/config>: Replace this placeholder with the actual public key extracted from the .chromia/config file. This key is used to authorize administrative actions.

For more details on the chromia.yml configuration file, visit the Chromia project settings documentation.

Require signed transactions

Now, update our create_book operation to require that the administrator signs the transaction. Modify src/main/operations.rell with the following code:

src/main/operations.rell
operation create_book(isbn: text, title: text, author: text) {
val adminPubkey = chain_context.args.admin_pubkey;
require(op_context.is_signer(adminPubkey), "Only admin can create books");

create book ( .isbn = isbn, .title = title, .author = author );
}

Here’s what happens in this operation:

  • Retrieve the administrator's public key: val adminPubkey = chain_context.args.admin_pubkey;
    • This fetches the public key from the module arguments defined in chain_context.
  • Check if the transaction is signed by the administrator: require(op_context.is_signer(adminPubkey), "Only admin can create books");
    • This checks if the transaction is signed with the administrator's public key. If not, it throws an error message: "Only admin can create books".

This ensures that only transactions signed by the administrator can create books.

Setup and test signing

To test the transaction signing, we will update existing test functions to verify the correct behaviour.

Step 1: Update test_add_book

First, add a local key pair for testing in your test file src/test/book_review_test.rell:

src/test/book_review_test.rell
val book_keeper = rell.test.keypair(
priv = x"DEE3B1414196653BF7FA621B2EEFC3146093B1932BA2ABFAEED830906D81972A",
pub = x"0359A8F2CE1BEF95F583169B7DF053AA227A93B2652B0A9C22975FEED638032610"
);

This creates a test key pair for book_keeper with the provided private and public keys.

Next, update your chromia.yml to include this new key pair:

chromia.yml
blockchains:
book-review:
module: main
moduleArgs:
main:
admin_pubkey: <pubkey from .chromia/config>
compile:
rellVersion: 0.13.5
database:
schema: schema_hello
test:
modules:
- test
moduleArgs:
main:
admin_pubkey: "0359A8F2CE1BEF95F583169B7DF053AA227A93B2652B0A9C22975FEED638032610"

In the test section, we set the admin_pubkey to the same public key used for book_keeper.

Now, update the existing test_add_book function to use the book_keeper key pair:

src/test/book_review_test.rell
function test_add_book() {
rell.test.tx()
.op(create_book("123", "Book1", "Author1"))
.op(create_book("124", "Book2", "Author2"))
.sign(book_keeper)
.run();

val all_books = book @* { };

assert_equals(all_books.size(), 2);
assert_equals(all_books[0].title, "Book1");
assert_equals(all_books[0].author, "Author1");
}

Step 2: Update test_add_book_review

Update the existing test_add_book_review function to use the book_keeper key pair:

src/test/book_review_test.rell
function test_add_book_review() {
rell.test.tx()
.op(create_book("123", "Book1", "Author1"))
.op(create_book_review("123", "Reviewer1", "ReviewText1", 5))
.op(create_book_review("123", "Reviewer2", "ReviewText2", 3))
.sign(book_keeper)
.run();

val reviews = book_review @* { };
val book = book @ { .isbn == "123" };

assert_equals(reviews.size(), 2);
assert_equals(book, reviews[0].book);
assert_equals(reviews[0].reviewer_name, "Reviewer1");
assert_equals(reviews[0].review, "ReviewText1");
assert_equals(reviews[0].rating, 5);
}

Step 3: Update test_get_books

Add the new test_get_books function to verify the creation of books:

src/test/book_review_test.rell
function test_get_books() {
rell.test.tx()
.op(create_book("123", "Book1", "Author1"))
.op(create_book("124", "Book2", "Author2"))
.sign(book_keeper)
.run();

val reviews = get_all_books();
assert_equals(reviews.size(), 2);
}

Step 4: Test non-administrator access

To further verify security, create another test where a key pair not configured as an administrator attempts to sign a transaction.

The available users/key pairs are:

bob, alice, trudy, charlie, dave, eve, frank, grace, heidi

Add a definition for the bob key pair in your test file:

src/test/book_review_test.rell
val bob = rell.test.keypairs.bob;

And use this key pair in a test to sign the create_book transaction:

src/test/book_review_test.rell
function test_add_book_as_non_admin() {
rell.test.tx()
.op(create_book("123", "Book1", "Author1"))
.sign(bob)
.run_must_fail();
}
note

This test is expected to fail, which means it is correctly passing the test condition. Bob's public key is not listed as an administrator in the module arguments, so the transaction should fail.

Run tests

After updating your tests, run them to verify everything is working as expected:

chr test

This command will execute all the test functions and ensure that the transaction signing and authorization are working correctly.