How to build a blockchain transaction for Partisia Consent
This article teaches you how to build a transaction for a private blockchain. A transaction is an instruction from a user containing information used to change the state of the blockchain.
When the transaction has been signed, the signed transaction can be delivered to a blockchain node to put it on the blockchain.
All transactions are targeted towards a specific smart contract. The smart contract contains a state and actions that we can target with our transactions to change the state.
Transactions are defined by the transaction binary format.
When creating a serialized signed transaction we recommend you create it using one of the available client libraries. The specification can help you if you want to make your own implementation, for instance if you are targeting another programming language.
When the transaction is sent to the blockchain, the transaction bytes are delivered as a base64 string. In this document we interpret the bytes as hexadecimal for all examples.
Blockchain address and private key
To send a transaction, you will need a private key. On the blockchain we use
the ECDSA signature scheme, using secp256k1 as the curve. An
example of a private key on this curve
is d6f4e13c010dbca23f647627d9b6085b816c933678f6026446f9a9596a2fb0d7
. You should however create a
new random key and keep it private.
Each private key has a blockchain address. This is similar to a public key and can thus be shared
with others. The blockchain address is the identifier of the account, and will among many things be
used to fetch the nonce. The blockchain address is created by
hashing the public
key and take the last 20 bytes (40 hex characters) and prepend it with 00
. For the private key
above, the corresponding blockchain address is
00cbb82f722528a63cc77abef0737ee693efecc995
Creating the transaction for the Consent contract
In this example a transaction is built according to the transaction binary format.
A private deployment does not require gas, so the gas cost will always be 0, so our transaction will
be of the following form, where {x}
denotes a variable.
{nonce}{validToTime}0000000000000000{contractId}{rpc}
Nonce
The nonce is specific to each account and can be retrieved from a blockchain node's REST API.
If the value of the nonce is 123456
, the corresponding 8 byte big endian is 000000000001e240
.
000000000001e240{validToTime}0000000000000000{contractId}{rpc}
Valid-to-time
The valid-to-time is a unix timestamp of when the transaction is valid to. If the transaction hasn't been included in a block before this time, it never will be, and the transaction will never take its effect on the blockchain. To create the valid-to-time you should get the current time and then add for how long you want the transaction to be valid (e.g. 60 seconds).
Example: If the current time is Tue Jan 16 2024 09:59:33 GMT+0100 (Central European Standard Time)
we first translate this to milliseconds which is 1705395573437
and then finally add the 60
seconds (60.000 in milliseconds) which gives us the valid-to-time of 1705395633437
. Representing
this as 8 byte big endian gives us 0000018d11803d1d
, which we put into the transaction:
{nonce}0000018d11803d1d0000000000000000{contractId}{rpc}
Contract ID / Address
The contract ID (sometimes also called contract address) is the ID of the smart contract that we are targeting with our transaction.
Example: For a transaction targeting the smart contract with
id 044e9078383674cb8ea9994f13fa1fbb6b1766503b
the transaction would look like:
{nonce}{validToTime}0000000000000000044e9078383674cb8ea9994f13fa1fbb6b1766503b{rpc}
Note: All ID's/addresses are already represented as hex strings throughout our system.
Creating the RPC
You can see how to create rpc for the consent management platform here.
Creating the signature
The signature for the transaction is an ECDSA signature, using secp256k1 as the curve, on a sha256 hash of the transaction and the chain ID of the blockchain.
First we look at the structure of the content that must be hashed:
<ToBeHashed> := {
transaction: Transaction
chainId: ChainId
}
<ChainId> := {
len: 0xnn*4 (big-endian)
utf8: 0xnn*len
}
The content to be hashed consists of the transaction followed by the chain ID. You can see above how to serialize the transaction.
The final step is to create the actual signature of the hash, you can do this by using one of the below two methods.
- Apply the signature scheme yourself. This requires that you hold the private key. This is recommended for service providers.
- Send the hash to the key management service which will provide the signature. This method requires that the sender of the transaction has been registered through the SSO. This is only intended for end users.
The signature is serialized with the following format:
<Signature> := {
recoveryId: 0xnn
valueR: 0xnn*32 (big-endian)
valueS: 0xnn*32 (big-endian)
}
and includes
- A recovery id between 0 and 3 used to recover the public key when verifying the signature
- The
r
value of the ECDSA signature - The
s
value of the ECDSA signature
Chain ID
The chain id is a unique identifier for the blockchain.
Example: The chain id for Partisia Demo Consent Platform is PARTISIA DEMO CONSENT CHAIN
, which
translates to
0000001e50415254495349412053414e44424f5820434f4e53454e5420434841494e
Full example
Now we look at a full example of how a transaction with signature would look like. In this example we want to give a consent, so we create a transaction from the following values
- Nonce:
5
- ValidToTime:
1705406564662
- GasCost:
0
- Address:
044e9078383674cb8ea9994f13fa1fbb6b1766503b
- Rpc: Give consent (
0000000100
when serialized) - ChainId:
PARTISIA SANDBOX CONSENT CHAIN
- The signature is created with private
key
d6f4e13c010dbca23f647627d9b6085b816c933678f6026446f9a9596a2fb0d7
which gives us the objects
<SignedTransaction> := {
signature: 00a55a983ec8d40f63b5debdde42577b02186e4f9ee898031369d55f3e6c9be4bc93a280a488b49be7834491067a8e4f38795aeacfef87e8a2cbc7a86bacc2de4e
transaction: Transaction
}
<Transaction> := {
nonce: 0000000000000005
validToTime: 0000018d12270936
gasCost: 0000000000000000
address: 044e9078383674cb8ea9994f13fa1fbb6b1766503b
rpc : 0000000100
}
<ToBeHashed> := {
transaction: Transaction
chainId: 0000001e50415254495349412053414e44424f5820434f4e53454e5420434841494e
}
So the signed transaction is
00a55a983ec8d40f63b5debdde42577b02186e4f9ee898031369d55f3e6c9be4bc93a280a488b49be7834491067a8e4f38795aeacfef87e8a2cbc7a86bacc2de4e00000000000000050000018d122709360000000000000000044e9078383674cb8ea9994f13fa1fbb6b1766503b0000000100
which translated to base64 is
AKVamD7I1A9jtd693kJXewIYbk+e6JgDE2nVXz5sm+S8k6KApIi0m+eDRJEGeo5POHla6s/vh+iiy8eoa6zC3k4AAAAAAAAABQAAAY0SJwk2AAAAAAAAAAAETpB4ODZ0y46pmU8T+h+7axdmUDsAAAABAA==