This documentation provides a high-level overview of how the protocol works. While the core concepts and flows are accurate, certain implementation details and design choices may evolve over time as the system matures. Please treat this as a conceptual guide rather than a source of exact specifications.

Constructing and broadcasting a Token Transaction takes three steps for transfers and two steps for minting. For a full breakdown of the communication flow, refer to the Swimlane Diagram.

Step 1: StartTokenTransaction()

StartTokenTransaction is the first step where an issuer (via a wallet) signals to a group of Spark Operators its intent to Mint or Transfer funds. This step is non-binding—no funds are moved yet. Instead, it sets the stage for the Issuer and Spark Operators to agree on a Final Token Transaction by determining key transaction parameters, including revocation keys.

At this stage, a Sender or Issuer constructs a Partial Token Transaction, specifying the outputs they intend to spend and the public keys of the intended recipients. To prove authorization, they sign this partial transaction for each output they wish to spend, allowing the Spark Entity to verify their permission to proceed.

message StartTokenTransactionRequest {
    bytes identity_public_key = 1;
    TokenTransaction partial_token_transaction = 2;
    // List of signatures authorizing movement of tokens from the token input.
    TokenTransactionSignatures token_transaction_signatures = 3;
}

message StartTokenTransactionResponse {
    // This is the same token transaction sent by the wallet with output revocation public keys
    // filled. This is the final transaction that is published and gossiped among LRC-20 nodes.
    TokenTransaction final_token_transaction = 1;
    // Information for fetching and resolving the revocation keyshare on a transfer operation.
    // Contains the threshold of keyshares needed and the SO owners of those keyshares.
    SigningKeyshare keyshare_info = 2;
}

Sample Partial Token Transaction

transferTokenTransaction := &pb.TokenTransaction{
    TokenInputs: &pb.TokenTransaction_TransferInput{
        TransferInput: &pb.TransferInput{
            OutputsToSpend: []*pb.TokenOutputToSpend{
                {
                    PrevTokenTransactionHash: abcdef12345,
                    PrevTokenTransactionOutputVout: 0,
                },
                {
                    PrevTokenTransactionHash: abcdef12345,
                    PrevTokenTransactionOutputVout: 1,
                },
            },
        },
    },
    TokenOutputs: []*pb.TokenOutput{
        {
            OwnerPublicKey: receiver1PublicKey,
            TokenPublicKey: sUSDPublicKey,
            TokenAmount:    50,
        },
        {
            OwnerPublicKey: receiver2PublicKey,
            TokenPublicKey: sUSDPublicKey,
            TokenAmount:    50,
        },
    },
    SparkOperatorIdentityPublicKeys: []*bytes{sparkOperatorIdentityKey1,sparkOperatorIdentityKey2,…}
}

Upon receiving the request, the Spark Operators validates and process the transaction. After validating, the Spark Operator fills empty fields in the Partial Token Transaction to create the Final Token Transaction, which includes additional parameters for each output:

OutputId: 012131414153,
WithdrawalBondSats: bondRequiredForWithdrawal,
WithdrawalRelativeBlockLocktime: timelockForSpendingOfWithdrawedFunds,
RevocationCommitment: outputUniqueRevocationComitment,

Key Parameters

  • Output ID: A shared reference that allows both the Wallet and Spark Operator to track the TTXO created by this operation.
  • Revocation Commitment: An immutable public key corresponding to a unique shared secret owned collectively by the Spark Oper3ators. Enables secure spending of outputs in transfer transactions.
  • Withdrawal Bond & Withdrawal Locktime: Immutable parameters that define the conditions for a valid unilateral exit transaction of this output to Bitcoin L1.

Step 2: SignTokenTransaction()

Next, the wallet uses the FinalTokenTransaction from the previous step and retrieves a unique signature from each SO.

message SignTokenTransactionRequest {
    TokenTransaction final_token_transaction = 1;
    repeated OperatorSpecificTokenTransactionSignature operator_specific_signatures = 2;
}

message SignTokenTransactionResponse {
    bytes spark_operator_signature = 1;
    repeated bytes token_transaction_revocation_keyshares = 2;
}

Each Spark Operator validates the request and, if valid, responds with the revocation keyshares for each spent TTXO. Additionally, the SO signs the transaction and sends this signature to an LRC-20 node, which propagates it across the LRC-20 Gossip Network.

Once a threshold of SOs have signed and responded, the issuer can now derive the Revocation Key.

Step 3: FinalizeTokenTransaction()

The final step is for the issuer to send the Revocation Key it derived to each Spark Operator involved in the transaction. (For Mint operations, this step is unnecessary since mint inputs are not tied to a Revocation Key.)

message FinalizeTokenTransactionRequest {
    TokenTransaction final_token_transaction = 1;
    // List of ordered revocation keys that map 1:1 with outputs being spent in the
    // token transaction.
    repeated bytes output_to_spend_revocation_keys = 2;
}

At this point, the input outputs are officially revoked once the SOs communicate this update to their watchtowers. This make sure that the receiver of the funds can be confident that the sender cannot later attempt to double-spend the revoked outputs by broadcasting an outdated transaction on-chain.

Swimlane