Token as a Verifiable Service

This guide walks through transforming a simple token service into a decentralized and verifiable application using the Zellular Sequencer.

We follow a progressive enhancement model, starting from a basic session-based FastAPI service, and incrementally evolving it into a replicated and cryptographically verifiable system.

Each step corresponds to a real code implementation in the Zellular token directory.

Step 1: Centralized Token Service

In the first stage, we build a basic centralized token service using FastAPI. User sessions are tracked with server-side cookies, and balances are stored in-memory.

πŸ“„ File: token/01_centralized_token_service.py

Key Concepts

  • Framework: FastAPI

  • Session Management: Via SessionMiddleware

  • Authentication: Username/password stored in memory

  • Token State: Python dict holding balances

  • Transfer Logic: Requires an active session

Endpoints

  • POST /login: Authenticate with username/password

  • POST /transfer: Send tokens from the logged-in user

  • GET /balance?username=<username>: Query balance

Example Usage

  1. Login

curl -X POST http://localhost:5001/login \
     -H "Content-Type: application/json" \
     -d '{"username": "user1", "password": "pass1"}' \
     -c cookies.txt
  1. Transfer

curl -X POST http://localhost:5001/transfer \
     -H "Content-Type: application/json" \
     -d '{"receiver": "user2", "amount": 50}' \
     -b cookies.txt
  1. Check Balance

curl http://localhost:5001/balance?username=user1

Limitations

  • Single-node, centralized architecture

  • No cryptographic guarantees

  • Relies on server-side session for authentication

In the next step, we replace the session system with cryptographic signatures for stateless and verifiable authentication.

Step 2: Signature-Based Token Service

This version removes session-based auth and introduces Ethereum-style digital signatures. Users sign transfer messages off-chain using their private key. The backend verifies these signatures and recovers the sender address directly from the signed message.

πŸ“„ File: token/02_signature_based_token_service.py

Key Concepts

  • Stateless authentication using ECDSA signatures

  • Compatible with wallets like MetaMask or eth_account

  • Server no longer stores user credentials or sessions

Endpoints

  • POST /transfer: Send a signed transfer request

  • GET /balance?address=0x…: Query token balance

Signing Format

Users must sign a message in this format:

Transfer {amount} to {receiver}

For example:

Transfer 10 to 0xAbc123...

Client-Side Signing (Python Example)

message = f"Transfer {AMOUNT} to {RECEIVER_ADDRESS}"
message_hash = encode_defunct(text=message)
signed_message = Account.sign_message(message_hash, private_key=SENDER_PRIVATE_KEY)
signature = signed_message.signature.hex()

Backend Verification

On the server:

def verify_signature(sender: str, message: str, signature: str) -> bool:
    """Verifies if the provided signature is valid for the given sender address."""
    try:
        message_hash = encode_defunct(text=message)
        recovered_address = Account.recover_message(message_hash, signature=signature)
        return recovered_address.lower() == sender.lower()
    except Exception:
        return False  # Any error in signature recovery means invalid signature


class TransferRequest(BaseModel):
    sender: str
    receiver: str
    amount: int
    signature: str


@app.post("/transfer")
async def transfer(data: TransferRequest) -> JSONResponse:
    """Handles token transfers using signature-based authentication."""
    message = f"Transfer {data.amount} to {data.receiver}"

    if not verify_signature(data.sender, message, data.signature):
        raise HTTPException(status_code=401, detail="Invalid signature")

    if balances.get(data.sender, 0) < data.amount:
        raise HTTPException(status_code=400, detail="Insufficient balance")

    balances[data.sender] -= data.amount
    balances[data.receiver] = balances.get(data.receiver, 0) + data.amount
    return JSONResponse({"message": "Transfer successful"})


Test Script

To simplify development, a helper script is included:

πŸ“„ File: token/transfer.py

This script:

  • Loads a private key

  • Signs a message

  • Sends it to the /transfer endpoint

Run it with:

python examples/token/transfer.py

Example Usage

  1. Transfer tokens

curl -X POST http://localhost:5001/transfer \
     -H "Content-Type: application/json" \
     -d '{
           "sender": "0x...",
           "receiver": "0x...",
           "amount": 10,
           "signature": "0x..."
         }'
  1. Check balance

curl http://localhost:5001/balance?address=0xYourAddress

Why This Matters

  • Cryptographic authentication without storing secrets

  • Stateless backend logic

  • Ready for replication in decentralized networks

In Step 3, we integrate the Zellular Sequencer to distribute and replicate transfer updates across nodes.

Step 3: Replicated Token Service

In this step, we integrate the Zellular Sequencer to replicate the token state across multiple nodes. Transfer requests are no longer applied directly when submitted β€” instead, they are sent to the Zellular Sequencer, which sequences them and broadcasts them to all participating replicas.

Each replica node independently fetches the same ordered batch of transfers and applies them locally. This ensures all nodes remain consistent, even in the presence of faults or restarts.

πŸ“„ File: token/03_replicated_token_service.py

Key Concepts

  • Uses the Zellular Python SDK (Zellular(…))

  • Transfers are submitted via zellular.send(…)

  • Replica nodes pull and apply batches using zellular.batches()

  • Transfers are still signed and verified using the same logic from Step 2

Transfer Submission

Transfers are submitted via the /transfer route, verified as before, and then sent to the Zellular Sequencer:

    txs = [
        {
            "sender": data.sender,
            "receiver": data.receiver,
            "amount": data.amount,
            "signature": data.signature,
        }
    ]

    zellular.send(txs, blocking=False)

This appends the transfer to the global sequence shared by all replicas.

Processing Batches from Zellular

Each replica runs a background loop using the SDK to process batches:

def process_loop() -> None:
    """Continuously processes incoming batches from Zellular."""
    for batch, index in zellular.batches():
        txs = json.loads(batch)
        for tx in txs:
            apply_transfer(tx)


The apply_transfer(tx) function:

  1. Reconstructs the signed message

  2. Verifies the signature

  3. Checks sender balance

  4. Applies the transfer if valid

This ensures all replicas apply transfers in the same order and reach the same balances.

Full Transfer Verification Logic

def apply_transfer(data: dict[str, Any]) -> None:
    """Executes a transfer after batch processing."""
    sender, receiver, amount, signature = (
        data["sender"],
        data["receiver"],
        data["amount"],
        data["signature"],
    )

    message = f"Transfer {amount} to {receiver}"
    if not verify_signature(sender, message, signature):
        logger.error(f"Invalid signature: {data}")
        return

    if balances.get(sender, 0) < amount:
        logger.error(f"Insufficient balance: {data}")
        return

    balances[sender] -= amount
    balances[receiver] = balances.get(receiver, 0) + amount
    logger.info(f"Transfer successful: {data}")


Why This Matters

  • Ensures all nodes apply transfers in the same global order

  • Enables fault-tolerant, deterministic replication

  • Balances remain consistent even if nodes crash or restart

In Step 4, we’ll introduce verifiable reads: users can query balances and verify the response using aggregated BLS signatures from the token replicas.

Step 4: Signed Balance Token Service

In this step, we introduce cryptographic signatures for balance responses. Each replica node now signs its own /balance response using a BLS signature, which allows external clients to confirm that the node is attesting to a specific value.

πŸ“„ File: token/04_signed_balance_token_service.py

Key Concepts

  • /balance responses are now individually signed using BLS

  • Each node attests to the correctness of its response

  • Clients can optionally verify the individual signature using the node’s public key

Why Signed Responses?

In a decentralized environment, it’s important to ensure that values returned from public APIs can be cryptographically authenticated.

By signing the balance response:

  • The node proves it is accountable for the value it returned

  • Clients can verify the signature independently

  • This enables detection of tampered or inconsistent responses

This step lays the groundwork for verifiable reads, where multiple nodes agree on the same value.

Balance Endpoint

Each node signs its /balance response using BLS:

@app.get("/balance")
async def balance(address: str) -> dict[str, Any]:
    """Retrieves the balance of a given address and returns a BLS-signed message."""
    balance = balances.get(address, 0)
    message = f"Address: {address}, Balance: {balance}".encode("utf-8")
    signature = PopSchemeMPL.sign(sk, message)
    return {"address": address, "balance": balance, "signature": str(signature)}


In Step 5, we’ll show how to aggregate these signed responses from multiple nodes to produce a single verifiable proof that a quorum attested to the same value.

Step 5: Verifiable Token Service

In this final step, we introduce a new endpoint that aggregates signed balance responses from multiple nodes and returns a single BLS signature as proof.

πŸ“„ File: token/05_verifiable_token_service.py

Key Concepts

  • Aggregator node queries multiple replicas for their signed balances

  • Only responses that match the expected value are included in the quorum

  • The resulting BLS signatures are aggregated into a single proof

  • Clients can verify the aggregated signature using the aggregated public key (with excluded non-signers)

Why Signature Aggregation?

In a decentralized token system, it’s not enough for individual nodes to return signed balances. What matters is whether a majority of them agree on the same value.

Step 5 introduces signature aggregation, allowing clients to:

  • Collect individual BLS-signed balance responses

  • Aggregate them into a single compact proof

  • Verify that a quorum of nodes attested to the same balance

This is especially important for external independent services β€” such as cross-chain bridges or decentralized exchanges β€” that consume balance data from the token service. These services need cryptographic assurance that a balance was not only signed, but also agreed upon by a majority of nodes, without trusting any single replica.

By verifying the aggregated signature, clients can confirm not only the value itself but that a threshold of honest nodes agrees with it β€” enabling trustless interoperability across decentralized infrastructure.

Aggregation Logic

The aggregator queries all replicas and collects signed balance responses:

async def fetch_balance(
    session: aiohttp.ClientSession, node_id: str, node: dict[str, str], address: str
):
    try:
        async with session.get(
            f"{node['url']}/balance", params={"address": address}, timeout=3
        ) as response:
            data = await response.json()
            return node_id, data["balance"], data["signature"]
    except Exception:
        return node_id, None, None


async def query_nodes_for_balance(address: str):
    async with aiohttp.ClientSession() as session:
        tasks = [
            fetch_balance(session, node_id, node, address)
            for node_id, node in NODES.items()
            if node_id != SELF_NODE_ID
        ]
        return await asyncio.gather(*tasks)


Valid signatures that match the expected balance are combined:

def aggregate_signatures(
    message: bytes, expected_value: int, results: list[tuple[str, int, str]]
):
    valid_sigs = []
    non_signers = []

    for node_id, value, sig_hex in results:
        if value != expected_value or sig_hex is None:
            non_signers.append(node_id)
            continue

        try:
            pubkey = G1Element.from_bytes(bytes.fromhex(NODES[node_id]["pubkey"]))
            sig = G2Element.from_bytes(bytes.fromhex(sig_hex))
            if PopSchemeMPL.verify(pubkey, message, sig):
                valid_sigs.append(sig)
            else:
                non_signers.append(node_id)
        except Exception:
            non_signers.append(node_id)

    if len(valid_sigs) < 2 * len(NODES) / 3:
        raise ValueError("Not enough valid signatures to reach threshold")

    return PopSchemeMPL.aggregate(valid_sigs), non_signers


The final response includes:

  • The agreed-upon balance

  • The aggregated BLS signature

  • The list of non-signing nodes

Verification Example

To verify the aggregated signature, clients subtract non-signers’ public keys from the aggregate key and verify the result.

πŸ“„ File: token/verify_aggregated_signature.py

aggregate_pubkey = G1Element.from_bytes(bytes.fromhex(AGGREGATE_PUBLIC_KEY_HEX))

for node_id in AGGREGATED_RESPONSE["non_signing_nodes"]:
    pubkey = G1Element.from_bytes(bytes.fromhex(NODE_CONFIG[node_id]["pubkey"]))
    aggregate_pubkey += pubkey.negate()
aggregated_signature = G2Element.from_bytes(
    bytes.fromhex(AGGREGATED_RESPONSE["aggregated_signature"])
)

is_valid = PopSchemeMPL.verify(aggregate_pubkey, message, aggregated_signature)
print("Aggregated Signature Valid:", is_valid)

Why This Matters

  • Enables auditable consensus from decentralized nodes

  • Promotes interoperability with other offchain or onchain systems

  • Reduces trust assumptions to cryptographic validation

You now have a fully replicated, consistent, and deterministic orderbook service built with Zellular.