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ο
Login
curl -X POST http://localhost:5001/login \
-H "Content-Type: application/json" \
-d '{"username": "user1", "password": "pass1"}' \
-c cookies.txt
Transfer
curl -X POST http://localhost:5001/transfer \
-H "Content-Type: application/json" \
-d '{"receiver": "user2", "amount": 50}' \
-b cookies.txt
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ο
Transfer tokens
curl -X POST http://localhost:5001/transfer \
-H "Content-Type: application/json" \
-d '{
"sender": "0x...",
"receiver": "0x...",
"amount": 10,
"signature": "0x..."
}'
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:
Reconstructs the signed message
Verifies the signature
Checks sender balance
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 BLSEach 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.