Skip to content

Latest commit

 

History

History
314 lines (264 loc) · 10.1 KB

File metadata and controls

314 lines (264 loc) · 10.1 KB

Interchained Token Operations and Signature Verification

Introduction

Interchained Core extends the standard cryptocurrency feature set with a token system that allows custom tokens to be issued, transferred and managed directly on chain. Token operations are encoded as on-chain data and signed messages to provide a verifiable audit trail. This document describes the token mechanism, how operations are signed and validated, and gives practical examples for interacting with the system. The token subsystem is tightly integrated with the wallet and consensus layers. Each operation is a compact structure that can be hashed, signed offline and broadcast across the network. Balances, allowances and historical actions are automatically recorded in the ledger, while governance fees and byte-based costs provide a built-in incentive mechanism. Deterministic messages generated by BuildTokenMsg ensure that signatures are portable across implementations.

Token Operations

Token functionality is implemented in the wallet library. The set of supported operations is defined in the TokenOp enumeration:

enum class TokenOp : uint8_t {
    CREATE = 0,
    TRANSFER = 1,
    APPROVE = 2,
    TRANSFERFROM = 3,
    INCREASE_ALLOWANCE = 4,
    DECREASE_ALLOWANCE = 5,
    BURN = 6,
    MINT = 7
};

Each operation is represented by a TokenOperation structure that records the parties involved and the signed payload:

struct TokenOperation {
    TokenOp op{TokenOp::CREATE};
    std::string from;
    std::string to;
    std::string spender;
    std::string token;
    CAmount amount{0};
    std::string name;
    std::string symbol;
    uint8_t decimals{0};
    std::string signer;
    std::string signature;

};

The ledger keeps track of balances, allowances, token metadata and governance fees, as seen in TokenLedgerState:

struct TokenLedgerState {
    std::map<std::pair<std::string, std::string>, CAmount> balances;
    std::map<AllowanceKey, CAmount> allowances;
    std::map<std::string, CAmount> totalSupply;
    std::map<std::string, TokenMeta> token_meta;
    std::map<std::string, std::vector<TokenOperation>> history;
    CAmount governance_fees{0};
    CAmount fee_per_vbyte{10000};
    CAmount create_fee_per_vbyte{10000000};
    std::map<std::string, std::string> wallet_signers;
    int64_t tip_height{0};
    uint32_t version{TOKEN_DB_VERSION};
};

Operation Hashing and Broadcasting

Before signing, a deterministic message is generated using BuildTokenMsg:

std::string BuildTokenMsg(const TokenOperation& op)
{
    return strprintf(
        "op=%d|from=%s|to=%s|spender=%s|token=%s|amount=%lld|name=%s|symbol=%s|decimals=%d|timestamp=%d",
        static_cast<int>(op.op),
        op.from,
        op.to,
        op.spender,
        op.token,
        op.amount,
        op.name,
        op.symbol,
        op.decimals,
        op.timestamp);
}

Each token operation is hashed without the signature fields to produce a unique identifier:

uint256 TokenOperationHash(const TokenOperation& op)
{
    TokenOperation tmp = op;
    tmp.signature.clear();
    tmp.signer.clear();
    return SerializeHash(tmp);
}

Operations can be relayed to peers with BroadcastTokenOp:

void BroadcastTokenOp(const TokenOperation& op)
{
    if (!g_connman) return;
    g_connman->ForEachNode([&](CNode* pnode) {
        CNetMsgMaker msgMaker(pnode->GetCommonVersion());
        g_connman->PushMessage(pnode, msgMaker.Make(NetMsgType::TOKENTX, op));
    });
}

Signature Verification

The wallet verifies that every token operation was signed by the declared signer. Verification relies on standard message-signing utilities. The function MessageVerify from src/util/message.h is used by the ledger:

enum class MessageVerificationResult {
    ERR_INVALID_ADDRESS,
    ERR_ADDRESS_NO_KEY,
    ERR_MALFORMED_SIGNATURE,
    ERR_PUBKEY_NOT_RECOVERED,
    ERR_NOT_SIGNED,
    OK
};

MessageVerificationResult MessageVerify(
    const std::string& address,
    const std::string& signature,
    const std::string& message);

TokenLedger::VerifySignature ties these primitives together:

bool TokenLedger::VerifySignature(const TokenOperation& op) const
{
    LOCK(m_mutex);
    CTxDestination dest = DecodeDestination(op.signer);
    if (!IsValidDestination(dest)) {
        return false;
    }
    MessageVerificationResult result = MessageVerify(op.signer, op.signature, BuildTokenMsg(op));
    if (result != MessageVerificationResult::OK) {
        return false;
    }
    return true;
}

Only after a signature passes validation is the operation applied to the ledger and recorded on chain.

Unique Aspects of the Token Subsystem

Unlike many blockchains that rely on programmable contracts, Interchained treats tokens as built-in objects. Nodes interpret a fixed set of operations, resulting in deterministic validation and lower resource usage. Balances, allowances, supply records and metadata are stored in compact maps within the wallet database. Because every action is signed over the same BuildTokenMsg string, signatures can be created on hardware devices and verified by any node. This design reduces complexity compared to contract-based systems while still enabling rich features such as delegated transfers and on-chain fee collection.

Usage Examples

Creating and Signing a Token

The RPC method createtoken (defined in rpcwallet.cpp) constructs a token creation operation and signs it before applying it:

std::string msg = BuildTokenMsg(op);
if (!MessageSign(key, msg, op.signature)) {
    throw JSONRPCError(RPC_WALLET_ERROR, "Token signing failed");
}
if (!g_token_ledger.ApplyOperation(op)) {
    throw JSONRPCError(RPC_WALLET_ERROR, "Token creation failed");
}

CLI example:

$ interchained-cli createtoken "1000" "MyToken" "MTK" "8"

Approving a Spender

Another RPC, tokenapprove, demonstrates signing and broadcasting an approval operation:

TokenOperation op;
op.op = TokenOp::APPROVE;
op.from = walletName;
op.spender = spender;
op.token = token_id;
op.amount = amount;
op.signer = signer;
std::string msg = BuildTokenMsg(op);
if (!MessageSign(key, msg, op.signature)) {
    throw JSONRPCError(RPC_WALLET_ERROR, "Signing failed");
}
if (!g_token_ledger.ApplyOperation(op)) {
    throw JSONRPCError(RPC_WALLET_ERROR, "Failed to apply token approve operation");
}

CLI example:

$ interchained-cli tokenapprove bob 0xtokenid "5.0"

Transferring Tokens

tokentransfer moves tokens from the caller to another wallet:

TokenOperation op;
op.op = TokenOp::TRANSFER;
op.from = walletName;
op.to = recipient;
op.token = token_id;
op.amount = amount;
op.signer = signer;
std::string msg = BuildTokenMsg(op);
MessageSign(key, msg, op.signature);
g_token_ledger.ApplyOperation(op);

RPC usage:

$ interchained-cli tokentransfer bob 0xtokenid "5.0"

Spending Allowances

tokentransferfrom allows a spender to transfer on behalf of another wallet:

TokenOperation op;
op.op = TokenOp::TRANSFERFROM;
op.from = owner;
op.to = recipient;
op.spender = walletName;
op.token = token_id;
op.amount = amount;
op.signer = signer;
std::string msg = BuildTokenMsg(op);
MessageSign(key, msg, op.signature);
g_token_ledger.ApplyOperation(op);

CLI example:

$ interchained-cli tokentransferfrom alice bob 0xtokenid "1.0"

Updating Allowances

Increase or decrease allowances with tokenincreaseallowance and tokendecreaseallowance:

TokenOperation op;
op.op = TokenOp::INCREASE_ALLOWANCE; // or DECREASE_ALLOWANCE
op.from = walletName;
op.spender = spender;
op.token = token_id;
op.amount = delta;
op.signer = signer;
std::string msg = BuildTokenMsg(op);
MessageSign(key, msg, op.signature);
g_token_ledger.ApplyOperation(op);

Example calls:

$ interchained-cli tokenincreaseallowance bob 0xtokenid "2.0"
$ interchained-cli tokendecreaseallowance bob 0xtokenid "1.0"

Burning Tokens

To permanently remove tokens, call tokenburn:

TokenOperation op;
op.op = TokenOp::BURN;
op.from = walletName;
op.token = token_id;
op.amount = amount;
op.signer = signer;
std::string msg = BuildTokenMsg(op);
MessageSign(key, msg, op.signature);
g_token_ledger.ApplyOperation(op);

CLI usage:

$ interchained-cli tokenburn 0xtokenid "10"

Querying Balances and Metadata

Several RPCs return information without needing a signature:

$ interchained-cli gettokenbalance 0xtokenid false
$ interchained-cli tokenallowance alice bob 0xtokenid
$ interchained-cli tokentotalsupply 0xtokenid
$ interchained-cli token_meta 0xtokenid
$ interchained-cli my_tokens false
$ interchained-cli all_tokens
$ interchained-cli token_history 0xtokenid
$ interchained-cli rescan_tokentx 3000
$ interchained-cli getgovernancebalance

Verifying a Message via RPC

An external system can verify a signed message using the verifymessage RPC. Example (Python):

from bitcoinrpc.authproxy import AuthServiceProxy
rpc = AuthServiceProxy("http://user:password@127.0.0.1:8332")
address = "interchainedAddress"
signature = "base64sig"
message = "op=1 token=tokenid from=alice signer=alice"
result = rpc.verifymessage(address, signature, message)
print("Signature valid:", result)

Conclusion

Interchained's token layer builds on the existing cryptocurrency infrastructure to provide flexible asset issuance and transfer. By hashing each operation and verifying signatures using standard message verification functions, the system ensures integrity and auditability. Developers interact with tokens through RPCs that handle signing and broadcasting, while clients can independently verify signatures for increased trust.

Compared with general-purpose smart-contract platforms, Interchained offers a leaner approach: tokens are handled via deterministic operations rather than contract bytecode. This eliminates the need for a virtual machine while still supporting delegated transfers, supply adjustments and on-chain fees.

As hardware wallets adopt BuildTokenMsg, signatures remain portable and verifiable by any node. The result is a lightweight yet powerful mechanism for asset management that stands apart from other projects.