Submitting Transactions

Transactions go through various stages during their lifespan. First, a transaction is created in an application or a FIO-integrated client by bundling actions into a transaction. Next, the transaction is sent to a locally connected node, which in turn relays it to the active producing nodes for validation and execution via the peer-to-peer network. Next, the validated transaction is pushed to a block by the active producer on schedule along with other transactions. Finally the block that contains the transaction is pushed to all other nodes for validation. When a supermajority of producers have validated the block, and the block becomes irreversible, the transaction gets permanently recorded in the blockchain and it is considered immutable.

Building and submitting a transaction

The process for creating a transaction digest (serializing the transaction) and signing the serialized transaction are depicted below.

Image

The chain ID identifies the actual FIO blockchain and consists of a hash of its genesis state, which depends on the blockchain’s initial configuration parameters. Before signing the transaction, the application first computes a digest of the transaction. The digest value is a SHA-256 hash of the chain ID, the transaction instance, and the context free data if the transaction has any context free actions. Any instance fields get serialized before computing any cryptographic hashes to avoid including reference fields (memory addresses) in the hash computation.

The steps to build and submit a transaction on the FIO chain are as follows:

Content Summary
Create transaction digest Structure and serialize a transaction.
Sign transaction Sign the seralized transaction.
Push transaction Push the transaction to a local API node.
Verify transaction Verification of the transaction by the node.

Create transaction digest

Transaction digests are created within an application by instantiating a transaction object and pushing the related action instances into a list within the transaction instance. An action instance contains the actual details about the receiver account to whom the action is intended, the name of the action, the list of actors and permission levels that must authorize the transaction via signatures and delays, and the actual message to be sent, if any.

Below is an example of fully contructed addaddress transaction using the FIO Transaction schema.

const fetch = require('node-fetch')
const httpEndpoint = 'http://testnet.fioprotocol.io'

// Create keypair, fund from the faucet, and register a FIO Address on the Testnet monitor (http://monitor.testnet.fioprotocol.io).
const user = {
    privateKey: '',
    publicKey: '',
    account: '',
    address: ''
}

const contract = 'fio.address'
const action = 'addaddress'

// Example data for addaddress
const data = {
    fio_address: user.address,
    public_addresses: [
        {
        chain_code: 'BCH',
        token_code: 'BCH',
        public_address: 'bitcoincash:somebitcoincashpublicaddress'
        },
        {
        chain_code: 'DASH',
        token_code: 'DASH',
        public_address: 'somedashpublicaddress'
        }
    ],
    max_fee: 600000000,
    tpid: '[email protected]',
    actor: user.account
}

info = await (await fetch(httpEndpoint + '/v1/chain/get_info')).json();
blockInfo = await (await fetch(httpEndpoint + '/v1/chain/get_block', { body: `{"block_num_or_id": ${info.last_irreversible_block_num}}`, method: 'POST' })).json()
chainId = info.chain_id;
currentDate = new Date();
timePlusTen = currentDate.getTime() + 10000;
timeInISOString = (new Date(timePlusTen)).toISOString();
expiration = timeInISOString.substr(0, timeInISOString.length - 1);

transaction = {
    expiration,
    ref_block_num: blockInfo.block_num & 0xffff,
    ref_block_prefix: blockInfo.ref_block_prefix,
    actions: [{
        account: contract,
        name: action,
        authorization: [{
        actor: user.account,
        permission: 'active'
        }],
        data: data
    }]
};

Next, the completed transaction is serialized. Serializing a transaction takes two steps:

1. Serialize the data in the actions field

For this example we use the fiojs serializer which is the same as that used by eosjs.

const { TextEncoder, TextDecoder } = require('text-encoding');
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
const { base64ToBinary, arrayToHex } = require('@fioprotocol/fiojs/dist/chain-numeric');
var ser = require("@fioprotocol/fiojs/dist/chain-serialize");

// Retrieve the fio.address ABI
abiFioAddress = await (await fetch(httpEndpoint + '/v1/chain/get_abi', { body: `{"account_name": "fio.address"}`, method: 'POST' })).json();
rawAbi = await (await fetch(httpEndpoint + '/v1/chain/get_raw_abi', { body: `{"account_name": "fio.address"}`, method: 'POST' })).json()
const abi = base64ToBinary(rawAbi.abi);

// Get a Map of all the types from fio.address
var typesFioAddress = ser.getTypesFromAbi(ser.createInitialTypes(), abiFioAddress.abi);

// Get the addaddress action type
const actionAddaddress = typesFioAddress.get('addaddress');

// Serialize the actions[] "data" field (This example assumes a single action, though transactions may hold an array of actions.)
const buffer = new ser.SerialBuffer({ textEncoder, textDecoder });
actionAddaddress.serialize(buffer, transaction.actions[0].data);
serializedData = arrayToHex(buffer.asUint8Array())

// Get the actions parameter from the transaction and replace the data field with the serialized data field
serializedAction = transaction.actions[0]
serializedAction = {
    ...serializedAction,
    data: serializedData
};

2. Serialize the entire transaction

abiMsig = await (await fetch(httpEndpoint + '/v1/chain/get_abi', { body: `{"account_name": "eosio.msig"}`, method: 'POST' })).json()

var typesTransaction = ser.getTypesFromAbi(ser.createInitialTypes(), abiMsig.abi)

// Get the transaction action type
const txnaction = typesTransaction.get('transaction');

rawTransaction = {
    ...transaction,
    max_net_usage_words: 0,
    max_cpu_usage_ms: 0,
    delay_sec: 0,
    context_free_actions: [],
    actions: [serializedAction],     //Actions have to be an array
    transaction_extensions: [],
}

// Serialize the transaction
const buffer2 = new ser.SerialBuffer({ textEncoder, textDecoder });
txnaction.serialize(buffer2, rawTransaction);
serializedTransaction = buffer2.asUint8Array()

Sign transaction

After the transaction digest is computed, the digest is signed with the private key associated with the signing account’s public key. The public-private key pair is usually stored within the local machine that connects to the local node. The signing process is performed within the wallet manager associated with the signing account, which is typically the same user that deploys the application. The wallet manager provides a virtual secure enclave to perform the digital signing, so a message signature is generated without the private key ever leaving the wallet.

The transaction must be signed by a set of keys sufficient to satisfy the accumulated set of explicit actor:permission pairs specified in all the actions enclosed within the transaction. This linkage is done through the authority table for the given permission (see Accounts & Permissions). The actual FIO Private key used for signing is obtained by querying the wallet on the client where the application is run.

The transaction signing process takes three parameters:

  1. The chain ID
  2. The transaction instance to sign
  3. The set of public keys from which the associated private keys within the application wallet are retrieved

For this example we use the fiojs signature provider which is the same as that used by eosjs.

const { JsSignatureProvider } = require('@fioprotocol/fiojs/dist/chain-jssig');
const signatureProvider = new JsSignatureProvider([user.privateKey]);

requiredKeys = [user.publicKey]
serializedContextFreeData = null;

signedTxn = await signatureProvider.sign({
    chainId: chainId,
    requiredKeys: requiredKeys,
    serializedTransaction: serializedTransaction,
    serializedContextFreeData: serializedContextFreeData,
    abis: abi,
});

Push transaction

After the transaction is signed, a packed transaction instance is created from the signed transaction instance and pushed from the application to a local FIO node, which in turn relays the transaction to the active producing nodes for signature verification, execution, and validation.

const txn = {
    signatures: signedTxn.signatures,
    compression: 0,
    packed_context_free_data: arrayToHex(serializedContextFreeData || new Uint8Array(0)),
    packed_trx: arrayToHex(serializedTransaction)
}

pushResult = await fetch(httpEndpoint + '/v1/chain/push_transaction', {
    body: JSON.stringify(txn),
    method: 'POST',
});

jsonResult = await pushResult.json()

if (jsonResult.transaction_id) {
    console.log('Success. \nTransaction: ', jsonResult);
} else if (jsonResult.code) {
    console.log('Error: ', jsonResult.error);
} else {
    console.log('Error: ', jsonResult)
}

Every producing node that receives a transaction will attempt to execute and validate it in their local context before relaying it to the next producing node. Hence, valid transactions are relayed while invalid ones are dropped. The idea behind this is to prevent bad actors from spamming the network with bogus transactions. The expectation is for bad transactions to get filtered and dropped before reaching the active producer on schedule. When a transaction is received, no assumption is made on its validity. All transactions are validated again by the next producing node, regardless of whether it is producing blocks. The only difference is that the producer on schedule attempts to produce blocks by pushing the transactions it validates into a pending block before pushing the finalized block to its own local chain and relaying it to other nodes.

Verify transaction

The process to verify a transaction is twofold. First, the public keys associated with the accounts that signed the transaction are recovered from the set of signatures provided in the transaction. Such a recovery is cryptographically possible for ECDSA, the elliptic curve digital signature algorithm used in FIO. Second, the public key of each actor specified in the list of action authorizations (actor:permission) from each action included in the transaction is checked against the set of recovered keys to see if it is satisfied. Third, each satisfied actor:permission is checked against the associated minimum permission required for that actor:contract::action pair to see if it meets or exceeds that minimum. This last check is performed at the action level before any action is executed.