Sponsored Transactions Smart Contract#
The goal of this part of the tutorial is to understand how the sponsored transaction mechanism is implemented in the smart contract and how to create and verify an Ed25519 signature in the smart contract code.
Clone the sponsored transaction example
which you will modify in this tutorial. The recurse-submodules
flag is needed because the git repository contains submodules which need to be cloned along with the example.
$ git clone --recurse-submodules [email protected]:Concordium/concordium-rust-smart-contracts.git
Navigate to the correct example folder:
$ cd ./examples/cis3-nft-sponsored-txs
The cis3_nft
contract implements the CIS-3 standard (sponsored transactions). The standard defines that the contract has to expose the
permit
and the supportsPermit
functions.
permit
function#
The permit
function verifies an ed25519 signature from a sponsoree and authorizes the sponsor to execute the logic of
specific entrypoints (transfer/updateOperator
in the example code) on behalf of the sponsoree. The sponsored transaction mechanism replaces
the authorization checks conducted on the sender/invoker variable with signature verification.
That is, the sponsoree needs to sign a message (indicating its intended action), then
anyone can invoke the permit
function with this signed message to execute the sponsoree’s intended action.
The smart contract logic checks if the signature
was generated by the sponsoree with the check_account_signature
host function.
This function looks up the public key/keys associated with the signer address and verifies that the signer has generated the signature
by signing the message_hash
.
// Check signature.
let valid_signature = host.check_account_signature(param.signer, ¶m.signature, &message_hash)?;
ensure!(valid_signature, CustomContractError::WrongSignature.into());
Note
Ed25519 is an elliptic curve signing algorithm using EdDSA and Curve25519. It belongs to asymmetric cryptography, also known as public-key cryptography, where you generate a pair of related keys – one public key and one private key. The sponsoree signs a message with its private key and anyone can verify the sponsoree’s signature using its public key. The public key is shared with other entities (e.g., the smart contract) while the private key is only known to the sponsoree. The smart contract needs to know the public key to be able to verify that the sponsoree signed the message with the corresponding private key.
Note
The Concordium Wallet for Web prepends the message with the account address (signer
) and 8 zero bytes before signing it.
This ensures that signed messages (prepended with 8 zero bytes and the account address) can be distinguished from transactions (prepended with the account nonce and the account address).
The smart contract logic reverse engineers the Concordium Wallet for Web implementation by equally prepending the account address (signer) and 8 zero bytes to the message.
Note
Concordium accounts can be multi-sig and each account has at least one public key
and at least
one private key
associated with it.
For this sponsored transactions example, the accounts in the Concordium Wallet for Web (or Concordium Wallet for Mobile)
have exactly one public key
and exactly one private key
(no multi-sig accounts).
You use your private key
to sign the message in the wallet and your public key
is
used in the smart contract to verify that this signature was generated in the wallet with the associated private key
.
Never share your private key
.
supportsPermit
function#
This function can be queried with a list of given entrypoint names. The response contains corresponding results for each entrypoint, where the result is either “Entrypoint is not supported and can not be invoked via the permit function using the sponsored transaction mechanism” or “Entrypoint is supported and can be invoked via the permit function using the sponsored transaction mechanism”.
You can explore the function by invoking it with the concordium-client
as follows:
$ concordium-client contract invoke 6372 --entrypoint supportsPermit --parameter-json supportsPermit.json --grpc-port 20000 --grpc-ip node.testnet.concordium.com
6372
is the index of the smart contract. A smart contract address consists of an index and a subindex, in the current protocol the subindex will always be 0. So, in other words, the command above means invoke contract with index 6372 on the testnet.
For example, this supportsPermit.json
file results in the below screenshot.
{"queries":["aaaa","transfer","updateOperator"]}
Note
Comprehensive instructions on how to download and set up concordium-client
can be found in Setup the development environment.
Nonces
registry#
The smart contract has a registry to link an account to its next nonce.
The nonce is increased sequentially every time a signed message (corresponding to the account) is successfully executed in the permit
function.
The nonce is used to prevent replay attacks of the signed message because only a message signed with the next nonce in sequence can be executed as seen below:
// Check the nonce to prevent replay attacks.
ensure_eq!(message.nonce, nonce, CustomContractError::NonceMismatch.into());
Testing the signature verification#
In this section, you will learn how to create an Ed25519 signature that you can use for your test cases to confirm that the smart contract signature verification logic works as expected.
Note
The commands in this section have to be executed from the ./examples/cis3-nft-sponsored-txs folder where the Cargo.toml
file is defined for this project.
Run the test cases to confirm that they pass before you start modifying the code:
$ cargo test
or
$ cargo concordium test
You should see no errors and all the test cases are green indicating that they passed successfully.
Note
Comprehensive instructions on how to download and set up cargo concordium
can be found in Setup the development environment.
The next few commands explain how you can display the message_hash
that you would need to sign.
Add the following to the smart contract permit
function after the variable message_hash
is defined.
println!("Message hash: {}", HashSha2256(message_hash));
This prints the message_hash
when running the test cases as follows:
$ cargo test -- --nocapture
To learn more about the nocapture
flag you can follow this link
The test cases have two tests (test_permit_transfer/test_permit_update_operator) that execute the permit
function, and that is why you see two message_hashes
printed out in the console. You can comment out one of the tests to not get confused about
which message you are going to sign. You need to generate a public-private key pair to sign this message_hash
.
You can use an Ed25519 signature generation tool for testing to generate such keys and signatures.
Note
Only use these online tools that generate private keys for you in your test cases. Don’t use the key in a production setting, since the private key might be known to the online webpage provider and should not be considered safe.
Copy the message_hash
into the message field and switch the Msg Encoding
to hex
(not text
).
Click the Generate Random Key
button and click the Sign
button.
This action generates a signature in the last field in the above screenshot.
The signature
and public key
in the above screenshot can be copied into the below code snippets to decode them to bytes.
let signature:SignatureEd25519 = "FC87CE9497CBD9DDDFB6CED31914D4FB93DD158EEFE7AF927AB31BB47178E61A33BEA52568475C161EC5B7A5E86B9F5F0274274192665D83197C4CE9A24C7C06".parse().unwrap();
println!("Signature: {:?}", signature.0);
Add the above code snippet to the top of a test case and run the test cases again to output the signature in bytes.
$ cargo test -- --nocapture
The output should look similar to:
$ signature: [252, 135, 206, 148, 151, 203, 217, 221, 223, 182, 206, 211, 25, 20, 212, 251, 147, 221, 21, 142, 239, 231, 175, 146, 122, 179, 27, 180, 113, 120, 230, 26, 51, 190, 165, 37, 104, 71, 92, 22, 30, 197, 183, 165, 232, 107, 159, 95, 2, 116, 39, 65, 146, 102, 93, 131, 25, 124, 76, 233, 162, 76, 124, 6]
You can create the below signature constant. This signature can be used in test cases to check if your signature verification logic in the smart contract works.
const SIGNATURE: SignatureEd25519 = SignatureEd25519([252, 135, 206, 148, 151, 203, 217, 221, 223, 182, 206, 211, 25, 20, 212, 251, 147, 221, 21, 142, 239, 231, 175, 146, 122, 179, 27, 180, 113, 120, 230, 26, 51, 190, 165, 37, 104, 71, 92, 22, 30, 197, 183, 165, 232, 107, 159, 95, 2, 116, 39, 65, 146, 102, 93, 131, 25, 124, 76, 233, 162, 76, 124, 6]);
The public key
from the above screenshot can be converted in a similar fashion.
let public_key:PublicKeyEd25519 = "8728D5F139ABEF87188BBF18B9A75B4E27DF81BD6BC9CC1B7582A09D74BC3C88".parse().unwrap();
println!("Public key: {:?}", public_key.0);
Add the above code snippet to the top of a test case and run the test cases again to output the public key in bytes.
$ cargo test -- --nocapture
The output should look similar to:
$ public_key: [135, 40, 213, 241, 57, 171, 239, 135, 24, 139, 191, 24, 185, 167, 91, 78, 39, 223, 129, 189, 107, 201, 204, 27, 117, 130, 160, 157, 116, 188, 60, 136]
You can create the below public key constant. This public key can be used in test cases to check if your signature verification logic in the smart contract works.
const PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([135, 40, 213, 241, 57, 171, 239, 135, 24, 139, 191, 24, 185, 167, 91, 78, 39, 223, 129, 189, 107, 201, 204, 27, 117, 130, 160, 157, 116, 188, 60, 136]);