Transaction signing

Let privateKey is 32 bytes EdDSA private key

Concatenate all bytes of transaction fields (for strings use utf8 encoding, for numbers such as nonce and timestamp - 8 bytes big-endian unsigned representation):

tags_bytes = concat(sort(tags))
data = concat(
    space
    key,
    bytes(nonce), // 8 bytes big-endian
    bytes(timestamp), // 8 bytes big-endian
    memo,
    tags_bytes, // concatenated array of tags
    value
)

Calculate hash = sha256(data)

Sign message signature = EdDSA.sign(privateKey, hash)

Create JSON object:

signedTransaction = {
    'space': space,
    'key': key,
    'nonce': nonce,
    'timestamp': timestamp,
    'tags': tags,
    'memo': memo,
    'value': value,
    'hash': encodeHex(tx_hash),
    'signature': encodeBase64(signature),
    'public_key': encodeBase64(public_key)
}

Create encoded transaction encodedTransaction=encodeBase64(signedTransaction)

Call API method /api/events/add?event={encodedTransaction}

If transaction is constructed and signed properly this endpoint will return 200 OK HTTP response. It doesn't mean transaction is immediately add to blockchain as processing is asynchronous. In order to check if transaction succeeded use provided in documentation API methods.

Here is the reference implementation of transaction signing in NodeJS

const axios = require('axios');
const nacl = require('tweetnacl');
const crypto = require('crypto');

const UTF8 = 'utf8';

const EVENT_PATH = '/api/events/add';

const LAST_ID_PATH = '/api/events/last-id';

async function getLastId(nodeUrl, space, key) {
    const params = { space, key };
    const res = await axios.get(nodeUrl + LAST_ID_PATH, { params });
    return res.data;
}

async function sendSignedTransaction(nodeUrl, event) {
    const params = { event };
    const res = await axios.get(nodeUrl + EVENT_PATH, { params });
    return res.data;
}

function signTransaction(privateKey, space, key, nonce, timestamp, value, memo = '', tags = []) {

    const secretKeyBytes = Buffer.from(privateKey, 'base64');

    const keyPair = nacl.sign.keyPair.fromSeed(secretKeyBytes);

    const tagsBuffersList = [];

    tags.forEach(tag => {
        tagsBuffersList.push(Buffer.from(tag, UTF8));
    });

    const tagsBuffer = Buffer.concat(tagsBuffersList);

    const nonceBuffer = Buffer.alloc(8);
    nonceBuffer.writeBigInt64BE(BigInt(nonce));

    const tsBuffer = Buffer.alloc(8);
    tsBuffer.writeBigInt64BE(BigInt(timestamp));

    const messageBuffer = Buffer.concat([
        Buffer.from(space, UTF8),
        Buffer.from(key, UTF8),
        nonceBuffer,
        tsBuffer,
        Buffer.from(memo, UTF8),
        tagsBuffer,
        Buffer.from(value, UTF8)
    ]);

    const hash = crypto.createHash('sha256').update(messageBuffer).digest();

    const signature = nacl.sign.detached(hash, keyPair.secretKey);

    const encodedSignature =  Buffer.from(signature).toString('base64');

    const signedTransaction = {
        'space': space,
        'key': key,
        'nonce': nonce,
        'timestamp': timestamp,
        'tags': tags,
        'memo': memo,
        'value': value,
        'hash': Buffer.from(hash).toString('hex').toUpperCase(),
        'signature': encodedSignature,
        'public_key': Buffer.from(keyPair.publicKey).toString('base64')
    };

    return signedTransaction;

}

function encodeTransaction(signedTransaction) {
    const json = JSON.stringify(signedTransaction);

    return Buffer.from(json).toString('base64')
}

module.exports = {
    signTransaction,
    encodeTransaction,
    getLastId,
    sendSignedTransaction
};

Last updated