panurus

Network Service - Fabric Implementation

The Fabric network implementation (fabric.Network) provides integration with Hyperledger Fabric networks using the traditional chaincode-based endorsement model. It leverages the Fabric Smart Client (FSC) to interact with the underlying Hyperledger Fabric network.

Architecture Overview

The Fabric implementation uses a Token Chaincode deployed on Fabric peers to handle token operations. This chaincode validates token requests, manages token state, and enforces business logic.

graph TB
    subgraph "Application Node running FSC/Panurus stack"
        App[Application/TTX]
        FabricNet[Fabric Network Service]
    end

    subgraph "Hyperledger Fabric Network"
        Peer1[Peer 1<br/>Token Chaincode]
        Peer2[Peer 2<br/>Token Chaincode]
        Orderer[Ordering Service]
        Ledger[Committer]
    end

    App -->|1. Request Approval| FabricNet
    FabricNet -->|2. Endorse Proposal| Peer1
    FabricNet -->|2. Endorse Proposal| Peer2
    Peer1 -->|3. Endorsement| FabricNet
    Peer2 -->|3. Endorsement| FabricNet
    FabricNet -->|4. Broadcast Tx| Orderer
    Orderer -->|5. Order & Distribute| Ledger
    Ledger -->|6. Finality Event| FabricNet
    FabricNet -->|7. Notify| App

Token Chaincode

The Token Chaincode (tcc.TokenChaincode) is a Fabric chaincode that runs on peers and handles all token-related operations.

Chaincode Functions

The chaincode exposes the following functions:

Function Purpose Parameters Returns
invoke Process token requests (issue, transfer, redeem) Token request (transient) Transaction envelope
queryPublicParams Retrieve public parameters Public parameters bytes
queryTokens Query token state Token IDs Token data
areTokensSpent Check if tokens are spent Token IDs, metadata Boolean array
queryStates Query arbitrary state keys State keys State values

Chaincode Deployment

The Token Chaincode must be deployed to the Fabric network before Panurus can operate:

sequenceDiagram
    participant Admin as Network Admin
    participant Peer as Fabric Peer
    participant Orderer as Ordering Service
    participant Ledger as Blockchain

    Admin->>Peer: Package chaincode
    Admin->>Peer: Install chaincode
    Admin->>Peer: Approve chaincode definition
    Admin->>Orderer: Commit chaincode definition
    Orderer->>Ledger: Record chaincode metadata
    Ledger-->>Peer: Chaincode ready
    
    Note over Admin,Ledger: Chaincode is now available for invocation

Deployment Steps:

  1. Package: Create chaincode package with Token Chaincode implementation
  2. Install: Install package on all endorsing peers
  3. Approve: Each organization approves the chaincode definition
  4. Commit: Commit the chaincode definition to the channel
  5. Initialize: Initialize the chaincode so it writes the selected public parameters to the ledger setup key

Chaincode Initialization

At initialization time, the chaincode loads public parameters and persists them to the setup key on the ledger. The implementation in tcc.TokenChaincode.Init() calls tcc.TokenChaincode.Params(), which resolves the parameters using the following precedence:

  1. File-based override: if the PUBLIC_PARAMS_FILE_PATH environment variable is set, tcc.TokenChaincode.ReadParamsFromFile() reads the raw public-parameter bytes from that file and feeds them back as a base64 string.
  2. Built-in parameters: if no file is provided, tcc.Params is used. In the source tree this variable is empty by default, but packaging tools replace tcc/params.go with generated content that embeds a base64-encoded blob of the public parameters into the chaincode package itself.
  3. Failure: if neither source is available, initialization fails.

This means the token chaincode supports both models:

tokengen can also generate the token-chaincode package with the public parameters already embedded, by generating a replacement for tcc/params.go from the template in cc.DefaultParams as part of cc.GeneratePackage().

// Simplified initialization flow
func (cc *TokenChaincode) Init(stub shim.ChaincodeStubInterface) *pb.Response {
    // Resolve public parameters from file override or built-in Params
    ppRaw, err := cc.Params(Params)

    // Write the selected parameters to the ledger setup key
    w := translator.New(stub.GetTxID(), ...)
    w.Write(context.Background(), &SetupAction{SetupParameters: ppRaw})

    return shim.Success(nil)
}

Validator Initialization

Writing the public parameters to the ledger is only part of the setup: to answer invoke and areTokensSpent, the chaincode also needs a validator and a public-parameters manager built from those parameters. These are created lazily, on the first request that needs them, by tcc.TokenChaincode.GetValidator(), which resolves the parameters with the same precedence as Init() and then calls the TokenServicesFactory configured by the chaincode’s main.

Initialization runs at most once successfully:

GetValidator() never reports success with a nil validator, so a request that reaches validation is always served by an initialized chaincode.

Endorsement Service

The Fabric implementation supports chaincode-based endorsement through the ChaincodeEndorsementService.

Endorsement Process

sequenceDiagram
    participant App as Application
    participant Net as Network Service
    participant ES as Endorsement Service
    participant FSC as Fabric Smart Client
    participant Peer as Fabric Peer

    App->>Net: RequestApproval(request)
    Net->>ES: Endorse(request, signer, txID)
    ES->>FSC: NewEndorseView(namespace, "invoke")
    ES->>FSC: WithTransient("token_request", request)
    ES->>FSC: WithSignerIdentity(signer)
    ES->>FSC: WithTxID(txID)
    FSC->>Peer: Send proposal
    Peer->>Peer: Execute chaincode
    Peer->>Peer: Generate RWSet
    Peer->>Peer: Sign endorsement
    Peer-->>FSC: Endorsement response
    FSC-->>ES: Transaction envelope
    ES-->>Net: Endorsed envelope
    Net-->>App: Endorsed envelope

Endorsement Policies

The chaincode endorsement follows Fabric’s standard endorsement policies:

Example policy: "OR('Org1MSP.peer', 'Org2MSP.peer')" - requires endorsement from either Org1 or Org2.

FSC Endorsement

As an alternative to chaincode-based endorsement, FSC nodes equipped with a proper endorsement key can endorse the token chaincode themselves (see services.network.fabric.fsc_endorsement in Configuration). The set of endorsers to contact is selected by fsc_endorsement.policy.type:

Public Parameters Setup/Update via FSC

Besides endorsing token requests, the same FSC endorsement machinery can be used to submit new or updated public parameters (PP) for a namespace, as an alternative to setting them through the chaincode Init lifecycle callback. A second initiator/responder pair handles this:

The two initiator/responder pairs are distinguished by chaincode function name and transient key: the token-request flow uses the invoke function, while the PP flow uses a dedicated setup function and carries the raw parameters under a dedicated public_params transient key (in addition to the tmsID key used by both). On re-setup of a namespace that already has a TMS, a third transient key, public_params_sig, carries a detached signature (JSON-encoded signer identity plus signature bytes) authorizing the change — see below.

Both responders share a common receive step that only checks that the proposal’s tmsID transient carries a non-empty network, channel, and namespace — it does not look up the TMS itself. Each responder looks up the TMS for that tmsID on its own, at validation time, and decides for itself whether an absent TMS is a problem:

Endorsing the proposal (common to both responders) needs the local endorser identity, resolved from fsc_endorsement.id in the namespace’s configuration. This is resolved directly from configuration rather than through a fully-built TMS, so it also works during first-time PP setup, before any TMS exists for the namespace.

The responder enforces the following before writing:

  1. If a TMS is found for the namespace, its ID must match the tmsID carried by the proposal.
  2. The raw PP must be deserializable (PublicParametersFromBytes) and pass PublicParameters.Validate().
  3. First-time setup (no TMS found for the namespace yet) stops here — an empty issuer list is allowed, matching the permissive “anyone can issue” default some drivers (e.g. fabtoken) use out of the box.
  4. Re-setup (a TMS already exists) additionally requires:
    • the current PP declares at least one issuer (otherwise no identity could ever be authorized to change it) — the submitted PP is not required to declare any issuers itself;
    • the proposal carries a public_params_sig transient: a detached signature over the raw PP bytes, produced by one of the current PP’s issuers;
    • that signer identity is verified against the current TMS’s Deserializer (SigService().IssuerVerifier).

    Driver name, version, and CertificationDriver() are all allowed to change freely on re-setup — the only gate is the current-issuer signature above, there is no equality check against the previous PP. There is likewise no monotonicity or replay protection beyond what Fabric’s own transaction anti-replay already provides.

The signature itself is produced entirely on the initiator side, without changing the public SetupPublicParams API: fsc.EndorsementService resolves the current TMS via its TokenManagementSystemProvider, iterates the current PP’s issuers, and signs the raw PP bytes with the first one it holds a local wallet/signer for. On first-time setup (no current TMS) it submits no signature, matching the responder’s tolerance above.

There is no separate “init” vs. “update” API: the write is an unconditional overwrite of the setup key, mirroring the semantics of the chaincode Init path. Endorser selection reuses the same fsc_endorsement.policy.type mechanism described above (1outn/all/namespace) — there is no separate policy configuration for PP setup.

As with token requests, the responder does not refresh the local TMS directly after writing; the existing ledger setup-key listener (see Public Parameters Management) detects the committed write and updates the TMS.

Reachability

SetupPublicParamsView is reachable from application code through the same layering as RequestApproval, but keyed by TMSID rather than *token.ManagementService — a namespace has no TMS yet before its first PP setup:

network.Network.SetupPublicParams(tmsID, ppRaw, signer, txID)
  -> driver.Network.SetupPublicParams
    -> fabric.Network.SetupPublicParams
      -> endorsement.Service.SetupPublicParams
        -> fsc.EndorsementService.SetupPublicParams
          -> NewSetupPublicParamsView

Chaincode-based endorsement (ChaincodeEndorsementService) does not support this call and returns an error — for that endorsement mode, PP setup/update remains exclusively through the chaincode Init lifecycle callback described above.

Finality Management

The Fabric implementation supports two modes for monitoring transaction finality:

Delivery Mode

Uses a block delivery stream from the peer for real-time finality tracking:

sequenceDiagram
    participant App as Application
    participant Net as Network Service
    participant FM as Finality Manager
    participant Peer as Fabric Peer
    participant Proc as Block Processor

    App->>Net: AddFinalityListener(txID, listener)
    Net->>FM: Register listener
    
    loop Block Delivery Stream
        Peer->>FM: Deliver block
        FM->>Proc: Process block (parallel)
        Proc->>Proc: Extract transactions
        Proc->>Proc: Check validation codes
        Proc->>Proc: Match registered listeners
        Proc->>Net: Notify listener(txID, status)
        Net->>App: OnStatus(txID, VALID/INVALID)
    end

Features:

Choosing the Starting Block

The scan resumes at the peer’s current ledger height, read via GetLedgerInfo (delivery.go). Because that one RPC decides the starting block, a failure is retried with an exponentially growing delay — 7 attempts spanning ~31.5s by default, aborting early if the context is cancelled. Both are configurable via token.finality.delivery.ledgerInfoAttempts and ledgerInfoRetryDelay (see Finality Configuration).

That budget is deliberately long. Nothing retries ScanBlock: FSC’s events.ListenerManager calls it once from a goroutine that only logs the result, so an error escaping the retry loop leaves the channel with no block-based finality until the process restarts. The retries therefore have to outlast a peer restart, not merely a dropped packet.

If the height is still unavailable after the last attempt, ScanBlock returns the error rather than defaulting to block 0. Starting at genesis would rescan the entire chain and replay finality notifications for every historical transaction, while the caller would have no way to tell a transient RPC failure from a genuinely fresh chain. Block 0 is used only when no ledger is configured at all.

The doubling has a ceiling of 30s, so raising ledgerInfoAttempts lengthens the budget without letting a single pause grow without bound. A ledgerInfoRetryDelay larger than the ceiling is honoured as configured — the cap limits growth, it does not shorten the delay you asked for. The backoff itself is utils.RetryRunner, shared with the rest of the SDK rather than reimplemented here.

The returned error is classifiable with errors.Is, so a caller does not have to match on its message. ErrLedgerHeightUnavailable is the single test for “the starting block could not be resolved, so no scan started” — it accompanies every such failure, including a cancelled one:

Sentinel Meaning
finality.ErrLedgerHeightUnavailable the height could not be read, so no scan started
finality.ErrNoLedgerInfo some attempt saw the ledger return neither info nor an error — a driver contract violation
context.Canceled / context.DeadlineExceeded a wait between attempts was cut short, or the scan itself was cancelled

Every attempt’s failure is reported, so ErrNoLedgerInfo is present whenever the contract was violated at least once, even intermittently. The context error is not a discriminator on its own: the same context governs the scan, so it does not say which phase ended — pair it with ErrLedgerHeightUnavailable to tell a cancelled height read from a cancelled scan.

The underlying ledger errors are preserved in every case. An error that does not match ErrLedgerHeightUnavailable comes from the block scan itself rather than from resolving the starting block.

There is no in-tree caller that inspects these sentinels yet — today the sole consumer logs the error and stops. Classifying the failure is what a future caller needs to react (fall back to query-based finality, retry, or restart the manager) rather than a description of current behaviour.

Notification Mode

Uses asynchronous event notifications from the FSC layer:

sequenceDiagram
    participant App as Application
    participant Net as Network Service
    participant FM as Finality Manager
    participant FSC as Fabric Smart Client

    App->>Net: AddFinalityListener(txID, listener)
    Net->>FM: Register listener
    
    FSC->>FSC: Monitor ledger events
    FSC->>FM: Transaction event(txID, status)
    FM->>Net: Notify listener
    Net->>App: OnStatus(txID, VALID/INVALID)

Public Parameters Management

The Fabric implementation monitors the ledger for public parameters updates:

sequenceDiagram
    participant Net as Network Service
    participant SL as Setup Listener
    participant Ledger as Blockchain
    participant TMS as TMS Provider
    participant DB as Tokens Database

    Note over Net,DB: Initialization
    Net->>SL: Register setup listener
    SL->>Ledger: Monitor setup key
    
    Note over Net,DB: Update Detection
    Ledger->>SL: Setup key modified
    SL->>SL: Fetch new parameters
    SL->>TMS: Update(new params)
    SL->>DB: Persist(new params)
    
    Note over Net,DB: SDK synchronized with new parameters

Setup Key Monitoring

The setup listener watches for changes to a specific ledger key that stores public parameters. This mechanism is based on delivery as the finality path, because setup-key updates are detected from committed ledger events delivered by the peer:

  1. Key Format: Derived from namespace and setup identifier
  2. Update Trigger: Any transaction that writes to the setup key
  3. Validation: Parameters are validated before being applied
  4. Persistence: New parameters are stored in the local database

The same delivery-based mechanism is reused by the lookup service to detect transfer action metadata writes: it derives the transfer action metadata key prefix from KeyTranslator.TransferActionMetadataKeyPrefix and prefix-matches rwset writes against it, without needing to know each write’s specific subkey ahead of time.

When resolving a batch of keys, the lookup service groups them by namespace and issues one QueryStates chaincode call per namespace (deliveryqs.go). Each namespace is resolved independently: if its query cannot be built, does not reach the chaincode, or returns a response that cannot be decoded, only that namespace falls back to the block scan. The keys of the other namespaces in the same batch are still resolved and delivered.

The block scan itself is the fallback shared by the lookup service and the finality service: it rewinds NumberPastBlocks blocks behind the last known block and scans forward from there. The starting block is computed by blockscan.StartingBlock, which clamps the result to FirstBlock so that a chain shorter than the rewind window is scanned from its first block rather than from an underflowed block number.

State Queries

The Fabric implementation provides efficient state querying through the chaincode:

Token Queries

sequenceDiagram
    participant App as Application
    participant Net as Network Service
    participant Ledger as Ledger Service
    participant CC as Token Chaincode
    participant State as World State

    App->>Net: QueryTokens(tokenIDs)
    Net->>Ledger: GetStates(namespace, keys)
    Ledger->>CC: Query("queryTokens", tokenIDs)
    CC->>State: GetState(tokenID)
    State-->>CC: Token data
    CC-->>Ledger: Token data array
    Ledger-->>Net: Token data
    Net-->>App: Token data

Spent Status Checks

sequenceDiagram
    participant App as Application
    participant Net as Network Service
    participant CC as Token Chaincode
    participant State as World State

    App->>Net: AreTokensSpent(tokenIDs)
    Net->>CC: Query("areTokensSpent", tokenIDs)
    CC->>State: GetState(spentKey)
    State-->>CC: Spent markers
    CC->>CC: Check each token
    CC-->>Net: Boolean array
    Net-->>App: [true, false, true, ...]

Configuration

Basic Configuration

token:
  enabled: true
  tms:
    my-fabric-tms:
      network: fabric-network-name  # Matches fsc.networks configuration
      channel: my-channel
      namespace: my-chaincode-id    # Token chaincode name

Finality Configuration

token:
  finality:
    type: delivery  # "delivery" or "notification"
    committer:
      maxRetries: 3
      retryWaitDuration: 5s
    delivery:
      mapperParallelism: 10        # Parallel transaction mappers
      blockProcessParallelism: 10  # Parallel block processors
      lruSize: 30                  # Cache size for recent transactions
      listenerTimeout: 10s         # Timeout for listener notifications
      ledgerInfoAttempts: 7        # Attempts at reading the starting ledger height
      ledgerInfoRetryDelay: 500ms  # First retry pause; doubles each attempt (~31.5s total)

ledgerInfoAttempts and ledgerInfoRetryDelay bound the ledger-height read that decides where a block scan starts — see Choosing the Starting Block. Non-positive values are ignored in favour of the defaults: zero attempts would refuse every scan, and a non-positive delay would busy loop.

Endorsement Configuration

# Chaincode-based endorsement (default)
# No additional configuration needed - uses Fabric's endorsement policies

Implementation Details

Key Components

  1. Network (fabric.Network)
    • Main network service implementation
    • Coordinates endorsement, ordering, and finality
  2. Ledger (fabric.ledger)
    • Provides state query capabilities
    • Wraps Fabric Smart Client ledger interface
  3. Endorsement Service (endorsement.ChaincodeEndorsementService)
    • Handles chaincode invocation for endorsement
    • Manages transient data and transaction IDs
  4. Finality Manager (finality.ListenerManager)
    • Tracks transaction finality
    • Notifies registered listeners
  5. Token Chaincode (tcc.TokenChaincode)
    • Validates token requests
    • Manages token state on-chain

Transaction ID Calculation

// Fabric uses SHA256(nonce || creator) for transaction IDs
func (n *Network) ComputeTxID(id *driver.TxID) string {
    temp := &fabric.TxID{
        Nonce:   id.Nonce,
        Creator: id.Creator,
    }
    return n.n.TransactionManager().ComputeTxID(temp)
}

See Also