Transaction Signing

This document describes how to manually sign transactions on the Constellation Network. Understanding the transaction signing process is crucial for developers implementing custom signing solutions in various programming languages or integrating with the Constellation Network.

Overview

Constellation Network supports two distinct methods for transaction signing:

  1. DAG/L0 Token Transactions: Uses Kryo serialization without compression

  2. Other Transaction Types: Uses Brotli compression for serialization (TokenLock, AllowSpend, DelegatedStake, etc.)

In the long term, DAG and L0 token transactions will likely be migrated to use the same brotli compression as the newer transaction types but for now, the Kryo serialization method is maintained for backwards compatibility.

Transaction Format

All transactions in the Constellation Network, regardless of type, are submitted to the network using a standard format:

{
  "value": {
    // Transaction body containing all transaction details
  },
  "proofs": [
    {
      "id": "<signer public key>",
      "signature": "<signature of transaction body>"
    }
  ]
}

Key components of this format:

  • value: Contains the transaction body with all relevant transaction details

  • proofs: An array of signatures that validate the transaction

    • Currently, all transactions require exactly one proof in the array

    • The array structure supports future multi-signature functionality

    • Each proof contains:

      • id: The public key of the signer

      • signature: The signature of the transaction body, created using the corresponding private key

The different transaction signing methods described in this document ultimately produce transaction data in this standard format, though the specific contents of the value field and the method of generating the signature in the proofs array will differ based on the transaction type.

Prerequisites

To sign transactions, you need:

  • A private key

  • A public key

  • Transaction details to be signed

  • Understanding of cryptographic operations: SHA-256, SHA-512, and secp256k1 ECDSA signing

1. DAG/L0 Token Transaction Signing

DAG or L0 token transactions follow this signing process:

1.1 Transaction Structure

For token transactions, the following fields are required:

  • source: Sender address

  • destination: Recipient address

  • amount: Transaction amount (in smallest unit, e.g., 1 DAG = 100,000,000 units)

  • fee: Transaction fee (in smallest unit)

  • parent: Last transaction reference (hash and ordinal)

  • salt: Random value to ensure unique transaction hashes

1.2 Transaction Preparation

  1. Create a transaction object with the required fields

  2. Format the transaction according to the network's expected structure (v2 format)

  3. Encode the transaction into a specific format that concatenates fields with their lengths

1.3 Kryo Serialization

The encoded transaction is serialized using Kryo serialization, which follows these steps:

  1. Create a prefix using the format: 03 (fixed) + UTF-8 encoded length

  2. Convert the encoded transaction to UTF-8 bytes

  3. Convert the UTF-8 bytes to hexadecimal format

  4. Concatenate the prefix with the hexadecimal transaction

Example pseudo-code for Kryo serialization:

function kryoSerialize(encodedTransaction):
    prefix = "03" + utf8EncodedLength(encodedTransaction.length + 1)
    hexTransaction = hexEncode(utf8Encode(encodedTransaction))
    return prefix + hexTransaction

The UTF-8 encoded length is variable-length encoded with specific bit patterns:

  • Bit 8 denotes UTF-8

  • Bit 7 denotes if another byte is present

1.4 Hash Computation

Compute the SHA-256 hash of the Kryo-serialized transaction bytes:

transactionHash = sha256(Buffer.from(serializedTransaction, 'hex'))

1.5 Signature Generation

Sign the hash using ECDSA with the secp256k1 curve and the private key:

  1. Compute SHA-512 hash of the transaction hash

  2. Sign the SHA-512 hash with the private key using secp256k1 ECDSA

  3. Format the signature according to the expected format (often DER encoded and converted to hexadecimal)

sha512Hash = sha512(transactionHash)
signature = secp256k1Sign(sha512Hash, privateKey)
hexSignature = signature.toHex()

1.6 Verification (Optional)

To verify the signature:

  1. Use the public key to verify the signature against the same SHA-512 hash

  2. Ensure the signature verification passes

isValid = secp256k1Verify(signature, sha512Hash, publicKey)

2. Other Transaction Types (TokenLock, AllowSpend, DelegatedStake, etc.)

For other transaction types, the signing process uses Brotli compression:

2.1 Transaction Normalization

Before serialization, normalize the transaction object:

  1. Sort object keys alphabetically

  2. Remove null/undefined values

  3. Convert the object to a consistent format

normalizedBody = normalizeObject(transactionBody)

2.2 Brotli Serialization

Serialize the normalized transaction using Brotli compression:

  1. Convert the normalized JSON to a string

  2. Encode the string as UTF-8 bytes

  3. Compress the bytes using Brotli compression (typically with compression level 2)

normalizedJson = JSON.stringify(normalizedBody)
utf8Bytes = utf8Encode(normalizedJson)
compressedData = brotliCompress(utf8Bytes, compressionLevel=2)

2.3 Hash Computation

Compute the SHA-256 hash of the Brotli-compressed data:

messageHash = sha256(compressedData)

2.4 Signature Generation

Sign the hash with the private key:

  1. Compute SHA-512 hash of the message hash

  2. Sign the SHA-512 hash with the private key using secp256k1 ECDSA

  3. Format the signature according to the expected format

sha512Hash = sha512(messageHash)
signature = secp256k1Sign(sha512Hash, privateKey)
hexSignature = signature.toHex()

2.5 Result

The final result should be structured as:

{
  "value": normalizedBody,
  "proofs": [
    {
      "id": publicKey,
      "signature": hexSignature
    }
  ]
}

Implementation Considerations

Key Format

  • Private keys should be in hexadecimal format without the "0x" prefix

  • Public keys for verification can be in compressed or uncompressed format

  • When using uncompressed public keys, they may need the "04" prefix

Transaction Amounts

  • Transaction amounts and fees should be represented in the smallest unit (datum)

  • Example: 1 DAG = 100,000,000 datum (8 decimal places)

  • Amounts should be integers after being multiplied by 10^8

Hash Functions

  • SHA-256 and SHA-512 implementations should follow the standard specifications

  • Input to hash functions should be byte arrays, not hexadecimal strings

ECDSA Signing

  • Use secp256k1 curve for ECDSA signing

  • Sign the SHA-512 hash of the message, not the message directly

  • DER encoding is commonly used for the signature format

Last updated

Was this helpful?