This guide describes how to implement a Network Service driver for Ethereum-based networks. Panurus’s driver architecture enables integration with Ethereum and EVM-compatible blockchains through two distinct approaches, each with different trade-offs.
Ethereum integration requires adapting Panurus’s transaction model to Ethereum’s account-based ledger and smart contract execution environment. Unlike Fabric’s channel-based architecture, Ethereum uses a global state model with smart contracts for business logic execution.
graph TB
subgraph "Panurus"
App[Application/TTX]
Driver[Ethereum Network Driver]
end
subgraph "Ethereum Network Driver"
Node[Ethereum Node]
Contract[Token Smart Contract]
State[(Blockchain State)]
end
App -->|Token Request| Driver
Driver -->|Transaction| Node
Node -->|Execute| Contract
Contract -->|Update| State
State -->|Events| Driver
Driver -->|Finality| App
Panurus supports two architectural approaches for Ethereum integration, each suited to different requirements:
The smart contract performs full validation logic, similar to Fabric’s token chaincode model.
Architecture:
graph LR
subgraph "FSC Node"
App[Application]
Driver[Ethereum Network Driver]
end
subgraph "Ethereum Network Driver"
Node[Ethereum Node]
SC[Token Smart Contract]
State[(State)]
end
App -->|1. Token Request| Driver
Driver -->|2. Submit Transaction| Node
Node -->|3. Execute Contract| SC
SC -->|4. Validate Request| SC
SC -->|5. Check Double Spend| State
SC -->|6. Update State| State
State -->|7. Event| Driver
Driver -->|8. Finality| App
Characteristics:
FSC nodes perform validation off-chain and endorse state updates, similar to FabricX model.
Architecture:
graph TB
subgraph "Initiator FSC Node"
App[Application]
DriverI[Ethereum Network Driver]
end
subgraph "Endorser FSC Nodes"
E1[Endorser 1]
E2[Endorser 2]
Val1[Validator]
Val2[Validator]
end
subgraph "Ethereum Network"
Node[Ethereum Node]
SC[Token Smart Contract]
State[(State)]
end
App -->|1. Request| DriverI
DriverI -->|2. Request Endorsement| E1
DriverI -->|2. Request Endorsement| E2
E1 -->|3. Validate| Val1
E2 -->|3. Validate| Val2
Val1 -->|4. Sign State Update| DriverI
Val2 -->|4. Sign State Update| DriverI
DriverI -->|5. Submit Tx + Signatures| Node
Node -->|6. Execute| SC
SC -->|7. Verify Signatures| SC
SC -->|8. Double Spending Check</br>Apply State Update| State
State -->|9. Event| DriverI
DriverI -->|10. Finality| App
Characteristics:
| Aspect | Approach 1: Smart Contract Validation | Approach 2: Pre-Order Execution |
|---|---|---|
| Validation Location | On-chain (in smart contract) | Off-chain (in FSC nodes) |
| Gas Costs | Higher (full validation on-chain) | Lower (only signature verification) |
| Complexity | Simpler (single-tier) | More complex (two-tier) |
| Flexibility | Limited by EVM constraints | High (validation in Go) |
| Endorsement | Not required | Required from FSC endorsers |
| Best For | Simple deployments, public tokens | Complex validation, privacy needs |
Both approaches must implement the driver.Network interface:
type EthereumNetwork struct {
client EthereumClient
contract TokenContract
chainID *big.Int
// Approach 2 specific
endorsers []Endorser // Only for Approach 2
}
// Core interface methods
func (n *EthereumNetwork) Name() string
func (n *EthereumNetwork) Channel() string
func (n *EthereumNetwork) Broadcast(ctx context.Context, blob interface{}) error
func (n *EthereumNetwork) RequestApproval(...) (driver.Envelope, error)
func (n *EthereumNetwork) ComputeTxID(id *driver.TxID) string
func (n *EthereumNetwork) FetchPublicParameters(namespace string) ([]byte, error)
func (n *EthereumNetwork) AddFinalityListener(...) error
sequenceDiagram
participant App as Application
participant Driver as Ethereum Network Driver
participant Node as Ethereum Node
participant Contract as Token Contract
participant State as Blockchain State
Note over App,State: Token Transfer Example
App->>Driver: Transfer(tokens, recipient)
Driver->>Driver: Create transaction data
Driver->>Driver: Sign transaction
Driver->>Node: eth_sendRawTransaction
Node->>Contract: Execute transfer()
Contract->>Contract: Decode token request
Contract->>Contract: Validate signatures
Contract->>State: Check input tokens exist
Contract->>State: Check not double-spent
Contract->>Contract: Validate business logic
Contract->>State: Mark inputs as spent
Contract->>State: Create output tokens
Contract->>Contract: Emit TransferEvent
Node->>Node: Include in block
Node->>Node: Mine block
State-->>Driver: Event notification
Driver->>Driver: Confirm finality
Driver-->>App: OnStatus(VALID)
// Conceptual interface - not production code
interface ITokenContract {
// Process a token request
function processRequest(
bytes calldata tokenRequest,
bytes[] calldata signatures
) external returns (bool);
// Query functions
function getToken(bytes32 tokenId) external view returns (bytes memory);
function isSpent(bytes32 tokenId) external view returns (bool);
function getPublicParameters() external view returns (bytes memory);
// Events
event TokenRequest(bytes32 indexed txId, bool success);
event TokenCreated(bytes32 indexed tokenId, address owner);
event TokenSpent(bytes32 indexed tokenId);
}
Transaction Construction:
func (n *EthereumNetwork) RequestApproval(
ctx view.Context,
tms *token.ManagementService,
requestRaw []byte,
signer view.Identity,
txID driver.TxID,
) (driver.Envelope, error) {
// 1. Encode token request for contract call
data := encodeContractCall("processRequest", requestRaw)
// 2. Create Ethereum transaction
tx := types.NewTransaction(
nonce,
contractAddress,
value,
gasLimit,
gasPrice,
data,
)
// 3. Sign transaction
signedTx, err := types.SignTx(tx, signer, chainID)
// 4. Return as envelope
return &EthereumEnvelope{tx: signedTx}, nil
}
Finality Tracking:
func (n *EthereumNetwork) AddFinalityListener(
namespace string,
txID string,
listener driver.FinalityListener,
) error {
// Subscribe to contract events
eventChan := make(chan *TokenRequestEvent)
sub, err := n.contract.WatchTokenRequest(eventChan, txID)
// Monitor for finality
go func() {
event := <-eventChan
status := driver.Valid
if !event.Success {
status = driver.Invalid
}
listener.OnStatus(ctx, txID, status, "", nil)
}()
return nil
}
sequenceDiagram
participant App as Application
participant Driver as Ethereum Network Driver
participant E1 as Endorser 1
participant E2 as Endorser 2
participant Node as Ethereum Node
participant Contract as Token Contract
participant State as Blockchain State
Note over App,State: Token Transfer with Pre-Order Execution
App->>Driver: Transfer(tokens, recipient)
Driver->>Driver: Create token request
Note over Driver,E2: Off-Chain Validation Phase
par Collect Endorsements
Driver->>E1: Request endorsement
E1->>E1: Validate token request
E1->>E1: Compute state delta
E1->>E1: Sign state delta
E1-->>Driver: Signature 1
and
Driver->>E2: Request endorsement
E2->>E2: Validate token request
E2->>E2: Compute state delta
E2->>E2: Sign state delta
E2-->>Driver: Signature 2
end
Driver->>Driver: Assemble state update + signatures
Note over Driver,State: On-Chain Execution Phase
Driver->>Node: eth_sendRawTransaction
Node->>Contract: Execute applyStateUpdate()
Contract->>Contract: Verify endorser signatures
Contract->>Contract: Check signature threshold
Contract->>Contract: Double Spending Check
Contract->>State: Apply state delta
Contract->>Contract: Emit StateUpdateEvent
Node->>Node: Include in block
Node->>Node: Mine block
State-->>Driver: Event notification
Driver-->>App: OnStatus(VALID)
// Conceptual interface - not production code
interface ITokenContractWithEndorsement {
// Apply a pre-validated state update
function applyStateUpdate(
bytes32 stateRoot,
bytes calldata stateDelta,
bytes[] calldata endorserSignatures
) external returns (bool);
// Endorser management
function addEndorser(address endorser) external;
function removeEndorser(address endorser) external;
function setThreshold(uint256 threshold) external;
// Query functions
function getToken(bytes32 tokenId) external view returns (bytes memory);
function isSpent(bytes32 tokenId) external view returns (bool);
function getEndorsers() external view returns (address[] memory);
// Events
event StateUpdate(bytes32 indexed stateRoot, bool success);
event EndorserAdded(address indexed endorser);
event EndorserRemoved(address indexed endorser);
}
Endorser Service:
type EthereumEndorserService struct {
validator TokenValidator
signer crypto.Signer
stateManager StateManager
}
func (e *EthereumEndorserService) Endorse(
ctx context.Context,
request []byte,
) (*Endorsement, error) {
// 1. Validate token request
if err := e.validator.Validate(request); err != nil {
return nil, err
}
// 2. Compute state delta
delta, err := e.stateManager.ComputeDelta(request)
if err != nil {
return nil, err
}
// 3. Sign state delta
signature, err := e.signer.Sign(delta)
if err != nil {
return nil, err
}
return &Endorsement{
Delta: delta,
Signature: signature,
}, nil
}
Driver Implementation:
func (n *EthereumNetwork) RequestApproval(
ctx view.Context,
tms *token.ManagementService,
requestRaw []byte,
signer view.Identity,
txID driver.TxID,
) (driver.Envelope, error) {
// 1. Collect endorsements from FSC nodes
endorsements := make([]*Endorsement, 0)
for _, endorser := range n.endorsers {
endorsement, err := endorser.Endorse(ctx, requestRaw)
if err != nil {
return nil, err
}
endorsements = append(endorsements, endorsement)
}
// 2. Aggregate state deltas (should be identical)
stateDelta := endorsements[0].Delta
// 3. Collect signatures
signatures := make([][]byte, len(endorsements))
for i, e := range endorsements {
signatures[i] = e.Signature
}
// 4. Create Ethereum transaction
data := encodeContractCall(
"applyStateUpdate",
stateRoot,
stateDelta,
signatures,
)
tx := types.NewTransaction(nonce, contractAddress, value, gasLimit, gasPrice, data)
signedTx, err := types.SignTx(tx, signer, chainID)
return &EthereumEnvelope{tx: signedTx}, nil
}
Choose Approach 1 (Smart Contract Validation) when:
Choose Approach 2 (Pre-Order Execution) when: