This document provides a comprehensive guide to upgrading components within a Panurus application. Upgradability is essential for long-term maintenance, allowing for security patches, feature additions, and protocol migrations.
Panurus manages upgradability at three distinct layers:
Panurus manages token upgrades using two distinct mechanisms: the Atomic “Burn and Re-issue” protocol for across-format migrations and In-place Upgrades for backward-compatible transitions.
In-place upgrades allow Panurus to spend tokens from a previous driver version or format (e.g., Fabtoken) directly as if they were native to the current driver (e.g., ZKAT-DLOG), without requiring an explicit ledger transaction first.
The current driver determines compatibility based on several criteria:
SupportedTokenFormats().When in-place upgrade is not possible (e.g., moving to a completely incompatible cryptographic curve or increasing precision beyond limits), Panurus implements an atomic “Burn and Re-issue” protocol.
[!NOTE] Eligibility is the complement of in-place support, by design. For Fabtoken-to-DLog upgrades, a token’s precision is either
<= maxPrecision(in-place support, criterion 2 above — no issuer needed) or> maxPrecision(this path — issuer sign-off required because the value cannot be safely reinterpreted in a lower-precision format). These two ranges must never overlap: a driver that also accepted<= maxPrecisiontokens into the Burn-and-Re-issue eligibility list would leave no token that ever needs this path, since every upgrade-eligible token would already be directly spendable. See theServicedoc comment intoken/core/zkatdlog/nogh/v1/crypto/upgrade/service.gofor the implementation-level invariant.
Developers can use the tokens service to find tokens that require an upgrade.
// Get the tokens service for a specific TMS
tms, _ := token.GetManagementService(context, token.WithTMSID(myTMSID))
tokensService, _ := tokens.GetService(context, tms.ID())
// Iterate over tokens of type "USD" that the current driver cannot spend
it, err := tokensService.UnsupportedTokensIteratorBy(
context.Context(),
myWalletID,
"USD",
)
if err != nil {
return err
}
defer it.Close()
var toUpgrade []token.LedgerToken
for {
tok, _ := it.Next()
if tok == nil { break }
toUpgrade = append(toUpgrade, *tok)
}
The issuer uses the ttx package to wrap the upgrade logic.
// Inside a Responder View
tx, err := ttx.NewTransaction(context, nil, ttx.WithTMSID(upgradeRequest.TMSID))
// The Upgrade call consumes old tokens and issues new ones in one atomic step
err = tx.Upgrade(
context.Context(),
issuerWallet,
upgradeRequest.RecipientIdentity,
upgradeRequest.Challenge,
upgradeRequest.Tokens, // Old tokens from ledger
upgradeRequest.Proof, // ZK-Proof or Signature
)
PublicParameters of the new driver before initiating a mass upgrade to ensure the target format is correct.Panurus handles driver transitions gracefully during its startup sequence.
When the Token Management Service (TMS) initializes, it performs a PostInit sequence. It compares the formats of all tokens in the local database against the SupportedTokenFormats() reported by the currently loaded driver.
spendable = false in the local DB.spendable = true.Drivers like fabtoken or zkatdlog derive their format string from their PublicParameters (e.g., precision, identity types).
// Example of how a driver might calculate its format
func SupportedTokenFormat(precision uint64) (token.Format, error) {
hasher := sha256.New()
hasher.Write([]byte("zkatdlog"))
hasher.Write([]byte(fmt.Sprintf("%d", precision)))
return token.Format(hex.EncodeToString(hasher.Sum(nil))), nil
}
For more information on how token formats are used in Panurus’s token service, see the Tokens Service documentation.
Panurus provides a structured approach for upgrading public parameters across the network. This process ensures that all participants can smoothly transition to new cryptographic parameters while maintaining compatibility.
sequenceDiagram
autonumber
actor Admin as Administrator<br/>(or Issuer)
participant Fabric as Ledger<br/>(Fabric/FabricX/etc)
box darkgreen Panurus Stack
participant Network as Network Service
participant Provider as TMS Provider
participant TMS as Token Management<br/>Service
participant Driver as Driver API
end
Admin->>+Fabric: Generate & Publish<br/>New Public Parameters
Fabric->>+Network: Ledger Update<br/>Notification
deactivate Fabric
Network->>+Provider: GetManagementServiceProvider
Provider->>+TMS: Update/Create TMS<br/>(with new PP)
TMS->>+Driver: Set Public Parameters
Driver-->>-TMS: Ready for Token Operations
TMS-->>-Provider: TMS Instance
Provider-->>-Network: TMS Response
Network-->>-Admin: Update Complete<br/>(Optional Notification)
PublicParamsFetcher interfaceTokenManagerServiceProvider compares new vs existing parameters and updates the TMS if changedAfter the upgrade process completes, administrators can verify that all nodes are synchronized by retrieving the public parameters using the Token API’s PublicParametersManager. This ensures that the new parameters have been successfully propagated throughout the network.
// Get the TMS for verification
tms, err := token.GetManagementService(context, token.WithTMSID(myTMSID))
if err != nil {
return err
}
// Get the Public Parameters Manager
ppm := tms.PublicParametersManager()
// Retrieve the current public parameters
currentPP := ppm.PublicParameters()
if currentPP == nil {
return errors.New("public parameters not available")
}
// Verify the parameters match the expected values
// (Implementation-specific validation would go here)
Balance API to monitor the ratio of spendable vs. unspendable tokens. A sudden drop in spendable balance indicates a driver mismatch.The local storage (SQL) uses a “Lazy Creation” strategy.
Panurus uses CREATE TABLE IF NOT EXISTS. This handles fresh installs perfectly but does not manage ALTER TABLE operations for existing databases.
-- Panurus executes this on startup
CREATE TABLE IF NOT EXISTS fsc_tokens (
tx_id TEXT NOT NULL,
idx INT NOT NULL,
-- ... other columns
ledger_type TEXT DEFAULT '', -- New columns added in SDK updates
PRIMARY KEY (tx_id, idx)
);
If a new version of Panurus adds a column (e.g., ledger_metadata or spent_at), the IF NOT EXISTS clause will prevent the new schema from being applied to an existing table.
ALTER TABLE fsc_tokens ADD COLUMN IF NOT EXISTS ledger_metadata BYTEA;
vault.db). Panurus’s Vault service can re-sync its state by scanning the ledger, though this may take time depending on the ledger size.ALTER statements.[!WARNING] Breaking change: the
Tokenstable (fsc_tokens) gained a newredeemed BOOL NOT NULL DEFAULT falsecolumn, and allINSERT/SELECTstatements against this table were updated accordingly (seeIssuedBalance/RedeemedBalancein Issuer Wallet). Existing deployments must apply this migration before upgrading, otherwise the SDK’sINSERT INTO fsc_tokens (..., redeemed)statements will fail against the old schema:ALTER TABLE fsc_tokens ADD COLUMN IF NOT EXISTS redeemed BOOL NOT NULL DEFAULT false;Any node that also stores tokens locally (e.g. an issuer that is also an owner or auditor) needs this migration applied to its
TokenDBbefore upgrading.
Wallets / IdentityConfigurations linkagePanurus links every Wallets row to the IdentityConfigurations row it originated
from via a conf_id column: IdentityConfigurations.conf_id is a deterministic hash
of (id, type, url) (see IdentityConfiguration.UniqueID()), declared UNIQUE NOT
NULL, and Wallets.conf_id is a hard FOREIGN KEY reference to it, also NOT NULL.
Because Panurus only ever runs CREATE TABLE IF NOT EXISTS, existing databases
created before this change will not get these columns automatically. Before starting
an updated SDK against an existing database, migrate manually:
-- 1. Add the column to IdentityConfigurations first (FK target).
ALTER TABLE fsc_identity_configurations ADD COLUMN IF NOT EXISTS conf_id TEXT;
-- Backfill: conf_id = base64(sha256(id || '@' || type || '@' || url)), matching
-- IdentityConfiguration.UniqueID(). Run this from application code (or a one-off
-- script using the same hash function) rather than in pure SQL.
ALTER TABLE fsc_identity_configurations ALTER COLUMN conf_id SET NOT NULL;
ALTER TABLE fsc_identity_configurations ADD CONSTRAINT uq_identity_configurations_conf_id UNIQUE (conf_id);
-- 2. Add the column to Wallets and backfill it from the matching configuration
-- (join on however the wallet's owning configuration is known, e.g. wallet_id/id).
ALTER TABLE fsc_wallets ADD COLUMN IF NOT EXISTS conf_id TEXT;
-- Backfill fsc_wallets.conf_id from fsc_identity_configurations.conf_id here.
ALTER TABLE fsc_wallets ALTER COLUMN conf_id SET NOT NULL;
ALTER TABLE fsc_wallets ADD CONSTRAINT fk_wallets_conf_id
FOREIGN KEY (conf_id) REFERENCES fsc_identity_configurations (conf_id);
On SQLite, use the ALTER TABLE ... ADD COLUMN form (SQLite cannot add a NOT NULL
column without a default or a FK constraint in one statement) — add the columns
nullable, backfill, then rebuild the table (CREATE TABLE ... AS SELECT + rename) to
add the NOT NULL and FOREIGN KEY constraints, since SQLite does not support adding
constraints via ALTER TABLE after the fact.
Requests recovery-claim columnsThe Transaction Recovery Service added two nullable
columns to the Requests table (fsc_requests), used to lease pending transactions
to a claiming instance: recovery_claimed_by TEXT and recovery_claim_expires_at
TIMESTAMP, plus a partial index on (status, recovery_claim_expires_at, stored_at)
WHERE status = 1. Since both columns are nullable, this is a non-breaking change —
existing rows are simply treated as unclaimed — but CREATE TABLE IF NOT EXISTS still
will not add them to a pre-existing table, so recovery will fail to claim transactions
until the columns exist. Migrate manually before upgrading:
ALTER TABLE fsc_requests ADD COLUMN IF NOT EXISTS recovery_claimed_by TEXT;
ALTER TABLE fsc_requests ADD COLUMN IF NOT EXISTS recovery_claim_expires_at TIMESTAMP;
CREATE INDEX IF NOT EXISTS idx_recovery_claim_fsc_requests
ON fsc_requests (status, recovery_claim_expires_at, stored_at) WHERE status = 1;
This table is shared by TTXDB, AuditDB, and TokenDB (each defines the same
CREATE TABLE IF NOT EXISTS for Requests), so the migration only needs to run once
against the physical table they share.
Panurus relies heavily on Protocol Buffers (Protobuf) for serializing all core objects, including Public Parameters, Token Requests, and individual Actions. This choice is fundamental to Panurus’s ability to evolve over time while maintaining compatibility between nodes running different software versions.
Protobuf provides a binary serialization format that is both efficient and highly extensible. Panurus leverages several Protobuf features to ensure long-term stability:
Priority field to a TokenRequest). Older nodes receiving these messages will simply ignore the unknown fields and continue processing the data they recognize.0 for integers, "" for strings), allowing the new logic to handle them gracefully.PublicParameters message at the driver API level looks like this:
message PublicParameters {
string identifier = 1; // e.g., "zkatdlognogh/v1"
bytes raw = 2; // Opaque driver-specific bytes
}
This allows the core SDK to handle the delivery and storage of public parameters without needing to understand their internal structure. The raw bytes are only unmarshalled by the specific driver version identified by the identifier.
For more details on the specific Protobuf messages used by each driver, see:
IssueMetadata or PublicParameters) include a map<string, bytes> extra_data or application field. This allows developers to attach arbitrary information to transactions or configurations without modifying the underlying .proto definitions, avoiding the need for a full protocol migration for application-specific changes..proto file, it must never be reassigned to a different field, even if the original field is deprecated.proto3 defaults or explicitly check for presence to ensure that missing fields from older clients don’t cause crashes.package zkatdlognogh.v2;). This allows both the old and new unmarshallers to coexist in the same codebase.| Component | Responsibility | Mechanism |
|---|---|---|
| Tokens | Owner / Issuer | ttx.Transaction.Upgrade (Burn & Re-issue) |
| Driver | Admin / SDK | PostInit (Automatic Spendability Toggle) |
| Schema | Developer / Admin | Manual SQL ALTER or Database Re-sync |