0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Building Custom Blockchain Platforms: Smart Contract Design, Node Infrastructure, and Security

0
Posted at

A technical deep-dive for developers and engineers building production-grade blockchain systems

Why "Just Deploy on Mainnet" Is Not Always the Right Call
When someone says "let's build on blockchain," the default answer from most developers is Ethereum mainnet. And for a lot of use cases public DeFi protocols, NFT platforms, permissionless token launches that is a completely reasonable starting point.
But for enterprise-grade systems, supply chain networks, tokenized asset registries, settlement rails, healthcare data platforms, private financial infrastructure, a public general-purpose chain is often the wrong foundation. Throughput is constrained. Gas costs are unpredictable. Privacy at the protocol level does not exist. And you have zero control over the consensus rules governing your network.
Building a custom blockchain platform gives you something public chains cannot: full ownership of the protocol layer, the consensus mechanism, the node architecture, and the governance model. That control is valuable. It also means you own every mistake.
This article covers what building that platform actually looks like from smart contract patterns that hold under adversarial conditions, to node infrastructure that scales under real load, to security architecture that goes beyond running Slither and calling it an audit and how can taking help from Blockchain Development Company improve your results.

The Real Cost of Smart Contract Bugs
Most smart contract vulnerabilities are not exotic cryptographic edge cases. They are the same logic errors developers make in every other codebase — except that in smart contracts, there is no hotfix deployment, no database rollback, and no refund mechanism.

The Ronin Network hack (March 2022, 625M) traced back to a social engineering attack combined with a signature verification flaw in their bridge validator logic — nine validator keys with insufficient key distribution. The Cream Finance exploit (October 2021, $130M) was a reentrancy attack executed through a flash loan. Both were preventable with better architecture from day one.
Let's look at the patterns that prevent this class of failure.

Design Pattern 1: The Proxy Upgrade Pattern (EIP-1967)
Once a contract is deployed on-chain, its bytecode is immutable. For production systems that need bug fixes, parameter updates, or feature additions, this is a hard constraint.
The standard solution is the transparent proxy pattern defined in EIP-1967. Users and other contracts interact with a thin proxy contract that delegates all execution to a separate, swappable logic (implementation) contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**

  • @title TransparentProxy

  • @notice EIP-1967 compliant transparent upgradeable proxy

  • @dev Uses standardized storage slots to avoid storage collisions
    */
    contract TransparentProxy {
    // EIP-1967: keccak256("eip1967.proxy.implementation") - 1
    bytes32 private constant IMPLEMENTATION_SLOT =
    0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    // EIP-1967: keccak256("eip1967.proxy.admin") - 1
    bytes32 private constant ADMIN_SLOT =
    0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

    event Upgraded(address indexed implementation);
    event AdminChanged(address previousAdmin, address newAdmin);

    constructor(address _implementation, address _admin) {
    _setAddressSlot(IMPLEMENTATION_SLOT, _implementation);
    _setAddressSlot(ADMIN_SLOT, _admin);
    emit Upgraded(_implementation);
    }

    modifier ifAdmin() {
    if (msg.sender == _getAddressSlot(ADMIN_SLOT)) {
    _;
    } else {
    _fallback();
    }
    }

    function upgradeTo(address newImplementation) external ifAdmin {
    require(newImplementation.code.length > 0, "Not a contract");
    _setAddressSlot(IMPLEMENTATION_SLOT, newImplementation);
    emit Upgraded(newImplementation);
    }

    function changeAdmin(address newAdmin) external ifAdmin {
    emit AdminChanged(_getAddressSlot(ADMIN_SLOT), newAdmin);
    _setAddressSlot(ADMIN_SLOT, newAdmin);
    }

    fallback() external payable { _fallback(); }
    receive() external payable { _fallback(); }

    function _fallback() internal {
    address impl = _getAddressSlot(IMPLEMENTATION_SLOT);
    assembly {
    calldatacopy(0, 0, calldatasize())
    let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
    returndatacopy(0, 0, returndatasize())
    switch result
    case 0 { revert(0, returndatasize()) }
    default { return(0, returndatasize()) }
    }
    }

    function _setAddressSlot(bytes32 slot, address value) private {
    assembly { sstore(slot, value) }
    }

    function _getAddressSlot(bytes32 slot)
    private view returns (address value) {
    assembly { value := sload(slot) }
    }
    }

What this gives you: You can push logic updates without redeploying the proxy or migrating user state. All external integrations keep pointing to the same address.

Critical caveat — storage layout collisions: When you deploy a new implementation, the storage layout must be backward-compatible. If you change the order or type of state variables in the new implementation, you silently corrupt existing storage. Always use append-only storage upgrades. Verify with hardhat-storage-layout diff before every upgrade deployment.
The alternative to transparent proxies is the UUPS pattern (EIP-1822), where the upgrade logic lives in the implementation contract rather than the proxy. UUPS is more gas-efficient (no admin check on every call) but requires the implementation itself to include the upgrade function — which must be guarded carefully.

For production, use OpenZeppelin's audited implementations (TransparentUpgradeableProxy or UUPSUpgradeable). The edge cases in proxy implementations — function selector clashes, initialization re-entrancy, uninitialized implementation contracts — are well-documented in OZ's codebase. Rolling your own proxy for a production system is almost never justified.

Design Pattern 2: Role-Based Access Control
The most common access control pattern in beginner smart contracts is onlyOwner — a single privileged address with full control. This is a single point of failure. One compromised key, one stolen private key file, and your entire protocol is in an attacker's hands.

Production systems need role-based access control (RBAC), where distinct roles govern distinct operations, and role assignment is itself governed by a separate admin role.

solidity// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**

  • @title AccessControl

  • @notice Hierarchical RBAC with role-admin chaining

  • @dev Each role has an admin role that controls who can grant/revoke it
    */
    abstract contract AccessControl {
    struct RoleData {
    mapping(address => bool) members;
    bytes32 adminRole;
    }

    mapping(bytes32 => RoleData) private _roles;

    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

    event RoleGranted(
    bytes32 indexed role,
    address indexed account,
    address indexed sender
    );
    event RoleRevoked(
    bytes32 indexed role,
    address indexed account,
    address indexed sender
    );
    event RoleAdminChanged(
    bytes32 indexed role,
    bytes32 indexed previousAdminRole,
    bytes32 indexed newAdminRole
    );

    modifier onlyRole(bytes32 role) {
    _checkRole(role, msg.sender);
    _;
    }

    function hasRole(bytes32 role, address account)
    public view returns (bool) {
    return _roles[role].members[account];
    }

    function getRoleAdmin(bytes32 role) public view returns (bytes32) {
    return _roles[role].adminRole;
    }

    function grantRole(bytes32 role, address account)
    public virtual onlyRole(getRoleAdmin(role)) {
    _grantRole(role, account);
    }

    function revokeRole(bytes32 role, address account)
    public virtual onlyRole(getRoleAdmin(role)) {
    _revokeRole(role, account);
    }

    // Allows an account to renounce its own role (safety mechanism)
    function renounceRole(bytes32 role, address account) public virtual {
    require(account == msg.sender, "Can only renounce for self");
    _revokeRole(role, account);
    }

    function _checkRole(bytes32 role, address account) internal view {
    if (!hasRole(role, account)) {
    revert(
    string(abi.encodePacked(
    "AccessControl: account ",
    _toHexString(uint160(account), 20),
    " missing role ",
    _toHexString(uint256(role), 32)
    ))
    );
    }
    }

    function _grantRole(bytes32 role, address account) internal {
    if (!hasRole(role, account)) {
    _roles[role].members[account] = true;
    emit RoleGranted(role, account, msg.sender);
    }
    }

    function _revokeRole(bytes32 role, address account) internal {
    if (hasRole(role, account)) {
    _roles[role].members[account] = false;
    emit RoleRevoked(role, account, msg.sender);
    }
    }

    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal {
    bytes32 previousAdminRole = getRoleAdmin(role);
    _roles[role].adminRole = adminRole;
    emit RoleAdminChanged(role, previousAdminRole, adminRole);
    }

    function _toHexString(uint256 value, uint256 length)
    private pure returns (string memory) {
    bytes memory buffer = new bytes(2 * length + 2);
    buffer[0] = "0"; buffer[1] = "x";
    for (uint256 i = 2 * length + 1; i > 1; --i) {
    buffer[i] = bytes1(uint8(48 + uint256(value & 0xf)));
    if (uint8(buffer[i]) > 57) buffer[i] = bytes1(uint8(buffer[i]) + 39);
    value >>= 4;
    }
    return string(buffer);
    }
    }

// --- Concrete usage example ---
contract TokenProtocol is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

constructor(address admin) {
    // Grant deployer the top-level admin role
    _grantRole(DEFAULT_ADMIN_ROLE, admin);

    // Wire role admin chain
    _setRoleAdmin(MINTER_ROLE,   DEFAULT_ADMIN_ROLE);
    _setRoleAdmin(BURNER_ROLE,   DEFAULT_ADMIN_ROLE);
    _setRoleAdmin(PAUSER_ROLE,   DEFAULT_ADMIN_ROLE);
    _setRoleAdmin(UPGRADER_ROLE, DEFAULT_ADMIN_ROLE);
}

function mint(address to, uint256 amount)
    external onlyRole(MINTER_ROLE) { /* ... */ }

function burn(address from, uint256 amount)
    external onlyRole(BURNER_ROLE) { /* ... */ }

function pause()
    external onlyRole(PAUSER_ROLE) { /* ... */ }

}

In production, the DEFAULT_ADMIN_ROLE holder should be a Gnosis Safe multisig (or equivalent), not an EOA. This means any role modification requires N-of-M approvals from independent signers. A single compromised key cannot grant new roles or revoke existing ones.

Design Pattern 3: Reentrancy Guard and CEI Order
Reentrancy remains one of the most consistently exploited vulnerability classes in Solidity. The DAO hack (2016, ~$60M in ETH at the time), Cream Finance (2021, 130M), and several Curve Finance pools in 2023 were all reentrancy or reentrancy-adjacent exploits.
The fix operates at two levels.

Level 1 — Checks-Effects-Interactions (CEI) ordering:

solidity// VULNERABLE — interaction before effect
function withdrawBad(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
// Interaction happens BEFORE state is updated
// An attacker's fallback function can re-enter here
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "Transfer failed");
balances[msg.sender] -= amount; // Too late
}

// SAFE — effects before interactions
function withdrawSafe(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient balance"); // Check
balances[msg.sender] -= amount; // Effect
(bool ok, ) = msg.sender.call{value: amount}(""); // Interaction
require(ok, "Transfer failed");
}
Level 2 — Reentrancy guard as belt-and-suspenders:
solidityabstract contract ReentrancyGuard {
// Gas-optimized: use uint256 instead of bool
// Avoids the cold SLOAD cost on first access (2100 gas)
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;

constructor() { _status = _NOT_ENTERED; }

modifier nonReentrant() {
    require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
    _status = _ENTERED;
    _;
    _status = _NOT_ENTERED;
}

// Read-only reentrancy guard for view functions
// Prevents read-only reentrancy attacks (e.g., Curve read-only reentrancy)
modifier nonReentrantView() {
    require(_status != _ENTERED, "ReentrancyGuard: reentrant view");
    _;
}

}
Note the nonReentrantView modifier, this addresses the read-only reentrancy attack pattern exploited against several Curve integrations, where an attacker re-enters a view function during a state-mutating call to read stale state.

Solana Programs: A Different Mental Model
If you are building on Solana rather than an EVM chain, the security model is fundamentally different. Solana programs are stateless — they do not own their data. State lives in separate account objects that are passed into the program at call time.
The most common Solana vulnerability class is missing account ownership checks:

rustuse anchor_lang::prelude::*;

#[program]
pub mod secure_vault {
use super::*;

pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    let vault = &ctx.accounts.vault;

    // CRITICAL: Verify account ownership before trusting its data
    // Without this, an attacker passes a spoofed account they control
    require!(
        vault.owner == ctx.accounts.authority.key(),
        VaultError::UnauthorizedOwner
    );

    // Verify the vault account is owned by THIS program
    // Prevents passing in an account owned by a different (malicious) program
    require!(
        vault.to_account_info().owner == ctx.program_id,
        VaultError::InvalidAccountOwner
    );

    // Proceed with withdrawal
    **ctx.accounts.vault.to_account_info()
        .try_borrow_mut_lamports()? -= amount;
    **ctx.accounts.user.to_account_info()
        .try_borrow_mut_lamports()? += amount;

    Ok(())
}

}

#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut, has_one = authority)] // Anchor auto-checks has_one constraint
pub vault: Account<'info, VaultState>,
pub authority: Signer<'info>,
/// CHECK: Verified via has_one constraint above
#[account(mut)]
pub user: UncheckedAccount<'info>,
}

#[account]
pub struct VaultState {
pub owner: Pubkey,
pub balance: u64,
}

#[error_code]
pub enum VaultError {
#[msg("Caller is not the vault owner")]
UnauthorizedOwner,
#[msg("Account not owned by this program")]
InvalidAccountOwner,
}
Using Anchor's constraint system (has_one, constraint, owner) handles many of these checks declaratively and makes the security model explicit in the account struct definition rather than scattered through function bodies.

Part 2: Node Infrastructure Architecture
What a Node Actually Does (and Why Mixing Roles Kills Performance)
Before designing infrastructure, it helps to be precise about what a blockchain node is responsible for:

Transaction validation — verifying signatures and protocol rules
Block production / propagation — creating or relaying new blocks to peers
State management — maintaining the current world state (account balances, contract storage)
Historical storage — keeping the full transaction history (archive nodes only)
RPC serving — responding to JSON-RPC or gRPC requests from clients

Each of these has a distinct resource profile. State management is memory-intensive. Block processing is CPU-intensive under load. RPC serving produces unpredictable traffic spikes. Historical storage grows without bound.

The most common infrastructure failure pattern: treating all of these as one workload and running a single node type for everything. Under load, RPC traffic starves the consensus process, or state growth exhausts memory, or a storage spike causes disk I/O saturation that delays block validation.

The fix is role separation.

Three-Tier Node Architecture
External Traffic (dApps, wallets, indexers)


┌───────────────────────┐
│ Load Balancer │ Rate limiting, TLS termination,
│ (NGINX / AWS ALB) │ API key auth, DDoS mitigation
└──────────┬────────────┘

┌──────┴──────┐
▼ ▼
┌────────┐ ┌────────┐ Tier 3: RPC Nodes
│ RPC-1 │ │ RPC-2 │ Stateless at app layer
│ │ │ │ Horizontally autoscalable
└───┬────┘ └───┬────┘ AWS c5.2xlarge / 16GB RAM
└──────┬─────┘

┌──────────────────┐ Tier 2: Full Nodes
│ Full Node(s) │ Full state + history
│ (Internal RPC) │ Feed RPC nodes, run indexers
│ │ AWS r5.4xlarge / 128GB RAM
└────────┬─────────┘ NVMe SSD (3000+ IOPS)

┌──────────────────┐ Tier 1: Validator Ring
│ Validator 1 │ Block production + consensus
│ Validator 2 │ Never publicly accessible
│ Validator 3 │ Dedicated bare metal or
│ (Private VPC) │ AWS c5n.4xlarge
└──────────────────┘ HSM-backed signing keys

Key architectural rules:

Validator nodes are never reachable from the public internet. Period. They live in a private VPC/subnet with explicit deny rules on public ingress.
Full nodes sync from validators internally and absorb all indexing and data workloads.
RPC nodes are stateless enough to autoscale — if traffic spikes, spin up more. Each connects to the internal full node cluster for state reads.

Consensus Mechanism Selection
The consensus mechanism is the most consequential architectural decision in a custom chain build. It determines finality time, validator requirements, throughput ceiling, and attack surface.

PoA — Clique
Finality is near-instant with a TPS range of 100–500. Best for dev/test environments and small enterprise chains. Trade-off: low Byzantine fault tolerance.

PoA — IBFT 2.0
Deterministic finality in 1–2 seconds with 500–2,000 TPS. Best for enterprise and consortium chains. Trade-off: requires 2/3+ honest validators to maintain liveness.

Tendermint BFT
Instant finality (~1s) with 1,000–10,000 TPS. Best for Cosmos SDK chains and high-value settlement systems. Trade-off: performance degrades noticeably beyond 100 validators.

DPoS (Delegated Proof of Stake)
Finality in 1–3 seconds with 1,000–5,000+ TPS. Best for public chains with token-based governance. Trade-off: prone to voter apathy and validator stake concentration over time.

Gasper (Ethereum PoS)
Single-slot finality at ~12–15 seconds with 15–30 TPS at L1 base layer. Best for permissionless public chains. Trade-off: complex implementation and too slow for most custom chain builds.

PoH + Tower BFT (Solana)
Finality at ~400ms with 50,000+ TPS. Best for ultra-high throughput applications. Trade-off: infrastructure requirements are extreme — not practical for most custom chain deployments.

For most enterprise custom chains where the validator set is known and controlled, IBFT 2.0 (available in Hyperledger Besu) gives you deterministic finality, high throughput, and 1/3 Byzantine fault tolerance. This is the right trade-off when decentralization is not the primary goal.

For public chains requiring permissionless validator participation, Tendermint-based consensus (Cosmos SDK) provides a more battle-tested foundation for custom builds than implementing PoS from scratch.

Storage and Pruning Strategy
Blockchain state grows continuously. As of early 2026:

Ethereum full node (snap sync): ~1.2TB
Ethereum archive node: ~15TB+
Solana ledger (full): ~100TB+

For a custom chain running at 100 TPS with average 200-byte transactions, you can estimate raw storage growth at approximately 1.7GB/day (100 tx/s × 200 bytes × 86,400 s). State growth depends heavily on how many new accounts and contract storage slots are created.

Implement pruning strategy in your node configuration from genesis:

Transaction Privacy
Ethereum exposes everything publicly. L2 Rollups and Custom PoA offer limited app-layer privacy. Hyperledger Fabric has it natively at the protocol level.
Permissioned Access
Ethereum has none. L2 Rollups support it only at the app layer. Custom PoA enforces it at the protocol level. Fabric has it natively.
Custom Consensus
Ethereum and L2 give you no control — you inherit the base layer. Custom PoA gives you full control. Fabric offers a pluggable consensus model.
Decentralization
Ethereum is maximally decentralized. L2 inherits that security. Custom PoA and Fabric are both centralized by design — the validator set is known and controlled.
Regulatory Audit Trail
Ethereum and L2 have partial auditability but no data access controls. Custom PoA and Fabric support structured audit trails with controlled access — better for compliance.
Smart Contract Ecosystem
Ethereum has the largest ecosystem. L2 is fully EVM-compatible. Custom PoA is also EVM-compatible in most builds. Fabric uses Go or JavaScript chaincode — the Solidity ecosystem does not apply.
Deployment Complexity
Ethereum is the simplest — deploy and go. L2 adds moderate complexity. Custom PoA requires significant engineering effort. Fabric is the most complex to set up correctly.
Operational Overhead
Ethereum needs almost no infrastructure management. L2 adds moderate overhead. Custom PoA demands ongoing validator management and monitoring. Fabric carries the highest burden — certificate authorities, channel config, and peer lifecycle are all continuous responsibilities.

For Geth-compatible nodes on EVM-compatible custom chains:

Running Geth with Path-Based State Scheme
Introduced in go-ethereum 1.13. Significantly reduces state storage compared to the legacy hash-based trie.
Sync & State
--syncmode snap — uses snap sync to download current state instead of replaying all history.
--state.scheme path — enables the path-based state scheme for leaner storage.
--gcmode full — runs full garbage collection on state data.
Cache Allocation (total 4096 MB split across)
--cache 4096 — sets total cache size to 4GB.
--cache.database 50 — allocates 50% of cache to the database layer.
--cache.trie 30 — allocates 30% to trie caching.
--cache.gc 20 — allocates 20% to garbage collection routines.
Data & RPC
--datadir /data/geth — sets the data directory for chain storage.
--http — enables the HTTP-RPC server.
--http.api eth,net,web3,txpool — exposes these namespaces over HTTP.
--http.vhosts "*" — allows requests from any virtual host.
--ws — enables the WebSocket-RPC server.
--ws.api eth,net,web3 — exposes these namespaces over WebSocket.

Separate your archive nodes from your operational nodes. Archive nodes (needed for block explorers, forensics, compliance queries) have fundamentally different storage requirements. Do not let archive requirements drive your validator or RPC node provisioning.

Part 3: Security Architecture
The Four Layers You Cannot Ignore
Security on a custom blockchain platform operates at four distinct layers. Failure at any one of them can compromise the whole system, regardless of how well the others are built.

Protocol layer — Consensus integrity, eclipse/Sybil resistance, network-level attack surface
Smart contract layer — Logic correctness, access control, economic attack resistance
Infrastructure layer — Node hardening, key management, DDoS resilience
Operational layer — Key ceremony procedures, upgrade governance, incident response

Most teams invest heavily in the smart contract layer and underinvest in the other three. Here is what each requires.

Protocol Layer: Eclipse Attack Resistance
Tendermint P2P Configuration — Eclipse Attack Resistance
Persistent Peers
Define known-good peers that the node reconnects to automatically after every restart. These are your trusted anchors on the network.
Unconditional Peer IDs
These peer connections are always maintained regardless of your max peer limits. The node will never drop them to make room for others.
Inbound and Outbound Limits
Max inbound peers is set to 40. Max outbound peers is set to 10. This prevents connection flooding from unknown or malicious nodes trying to saturate your peer slots.
Peer Exchange (PEX)
Disabled entirely for permissioned chains. With PEX off, the node only connects to manually specified peers — it will not discover or accept connections from unknown nodes automatically.
Private Peer IDs
Validator node IDs listed here are never shared in peer exchange gossip. This keeps validator identities and IP addresses hidden from the broader network, reducing targeted attack surface.

Additionally, enforce IP subnet diversity at the application level. No more than two or three peers should share the same /24 subnet. Most P2P libraries support this via a peer scoring configuration.

Smart Contract Layer: Static Analysis and Formal Verification
Static analysis should be part of your CI pipeline, not a pre-deployment afterthought.
Installation
Install Slither directly via pip using pip install slither-analyzer.
Full Detector Suite
Run against your contracts folder with JSON output for CI pipeline integration. The following vulnerability classes are checked:

reentrancy-eth and reentrancy-no-eth — both ETH and token reentrancy patterns
uninitialized-state — state variables used before being set
arbitrary-send-eth — functions that can send ETH to an arbitrary address
controlled-delegatecall — delegatecall targets controlled by user input
tx-origin — unsafe use of tx.origin for authorization
locked-ether — contracts that can receive ETH but have no withdraw path
unchecked-lowlevel — low-level calls whose return value is not checked
tautology — conditions that are always true or always false

--exclude-dependencies skips third-party libraries like OpenZeppelin so results stay focused on your own code. --print human-summary gives a clean readable overview at the end.
Inheritance Graph
Run with --print inheritance-graph on complex contract hierarchies to visualize how contracts inherit from each other. Useful for spotting unexpected permission or function override paths.
Function Visibility Summary
Run with --print function-summary to audit every function's visibility, modifiers, and read/write access to state. Catches public functions that should be internal or external.

For high-value contracts — token bridges, treasury contracts, AMM core logic — formal verification is not optional. Certora Prover lets you write formal specifications (invariants and rules) and verify they hold under all possible inputs:

// Certora specification example for an ERC-20 token
// Invariant: total supply must always equal sum of all balances
invariant totalSupplyIsSumOfBalances()
totalSupply() == sum(balanceOf)
filtered { f -> f.selector != sig:transfer(address,uint256).selector }

// Rule: transfer must not increase the sender's balance
rule transferDecreasesOrMaintainsSenderBalance(
address sender, address recipient, uint256 amount
) {
uint256 balanceBefore = balanceOf(sender);
transfer(recipient, amount);
uint256 balanceAfter = balanceOf(sender);
assert balanceAfter <= balanceBefore;
}

Beyond code correctness, audit your economic attack surface explicitly. Flash loan attacks, oracle manipulation, and sandwich attacks are not code bugs — they are incentive-structure bugs. Ask the following questions before launch:

  • If an attacker borrows $500M in a single transaction, can they manipulate any price feed your contracts rely on?
  • Do you use spot prices from an AMM as an oracle? (You should not — use TWAPs with sufficient window length)
  • Can a validator or MEV searcher extract value by reordering transactions around your protocol's state-changing calls?

Infrastructure Layer: Validator Key Management
Validator signing keys are the most sensitive assets in a PoA or PoS network. A compromised validator key allows an attacker to sign malicious blocks, equivocate (sign conflicting blocks for double-spend), or selectively censor transactions.

Production key management requirements:
Hardware Security Modules (HSMs) for all validator signing keys. The private key material must never exist in software memory on the host machine. Options by use case:

Cloud deployments: AWS CloudHSM, Azure Dedicated HSM, GCP Cloud HSM
On-premises: Thales Luna, Utimaco, YubiHSM 2 (lower cost, sufficient for smaller validator sets)

Threshold signature schemes for high-stakes operations. For protocol-level actions like emergency pauses, contract upgrades, or validator set changes, no single key should be sufficient. Use threshold ECDSA (GG20 or FROST protocol) where N-of-M validators must cooperate to produce a valid signature:

Threshold Signing — FROST (3-of-5 example):

Round 1: Each participant generates a commitment (nonce + public commitment)
Participants broadcast commitments to all others

Round 2: Each participant generates a partial signature using:
- Their secret share
- The aggregated commitment from Round 1
- The message to be signed

Aggregation: Any participant (or coordinator) combines 3+ partial
signatures to produce the final valid group signature.

Result: A single valid ECDSA signature — no trusted coordinator,
no single point of key compromise.

Key ceremony for genesis validators. The initial validator key set must be generated under a formal key ceremony: air-gapped machines, multiple independent participants, verifiable randomness (e.g., NIST Randomness Beacon or hardware RNG), and documented chain-of-custody for each key shard.

Hyperledger Fabric: Privacy Architecture for Permissioned Chains
If your use case requires transaction-level privacy — where participants should not see each other's transaction data — Hyperledger Fabric's channel and private data collection architecture is worth understanding.
In Fabric, channels are private sub-networks where only channel members can see transactions. Private data collections take this further — only a defined subset of channel members receive the actual data, while the hash of that data is committed on the channel ledger for auditability.

#Fabric private data collection definition (collections_config.json)
[
{
"name": "settlementDetails",
"policy": "OR('Org1MSP.member', 'Org2MSP.member')",
"requiredPeerCount": 1,
"maxPeerCount": 3,
"blockToLive": 0, # 0 = never purge
"memberOnlyRead": true,
"memberOnlyWrite": false,
"endorsementPolicy": {
"signaturePolicy": "OR('Org1MSP.member', 'Org2MSP.member')"
}
}
]
go// Fabric chaincode: write to private data collection
func (s *SmartContract) RecordSettlement(
ctx contractapi.TransactionContextInterface,
settlementID string,
amount string,
counterparty string,
) error {
data := Settlement{
ID: settlementID,
Amount: amount,
Counterparty: counterparty,
Timestamp: time.Now().Unix(),
}

dataJSON, err := json.Marshal(data)
if err != nil { return err }

// Write to private collection — only Org1 and Org2 peers receive this data
// All channel members see only the hash commitment
return ctx.GetStub().PutPrivateData(
    "settlementDetails",  // Collection name
    settlementID,
    dataJSON,
)

}

This architecture is the right answer for use cases like inter-bank settlement, healthcare record sharing, or supply chain provenance where transaction details must remain confidential between parties while the ledger itself remains auditable.

Operational Security: Incident Response You Actually Rehearse
Every team has a deployment runbook. Almost none have an incident response plan they have actually tested.
Emergency pause capability must be built into every financial contract from day one. It should not require a governance vote — it needs to execute in minutes:
solidityabstract contract Pausable is AccessControl {
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

bool private _paused;

event Paused(address account);
event Unpaused(address account);

modifier whenNotPaused() {
    require(!_paused, "Pausable: paused");
    _;
}

modifier whenPaused() {
    require(_paused, "Pausable: not paused");
    _;
}

// Fast quorum multisig (2-of-3) should hold PAUSER_ROLE
// NOT a 5-of-9 governance multisig — that takes too long
function pause() external onlyRole(PAUSER_ROLE) {
    _paused = true;
    emit Paused(msg.sender);
}

function unpause() external onlyRole(PAUSER_ROLE) {
    _paused = false;
    emit Unpaused(msg.sender);
}

}
Chain halt procedure for PoA networks under active attack. For IBFT 2.0 (Besu), you can stop block production by dropping below the 2/3 validator quorum — intentionally taking validators offline. Document who calls whom, who authorizes the decision, and how you verify the threat before halting. This should be a written runbook, rehearsed at least quarterly.

Alert on block production gaps longer than 2× your expected block time. Alert on validator peer count dropping below 2/3 of your validator set. Alert on memory usage exceeding 80% of available RAM on state-heavy nodes.

Architecture Decision Reference: Custom Chain vs Public Chain vs L2

Transaction Privacy
Ethereum — none, all transactions are fully public. L2 Rollups and Custom PoA — limited, only at the application layer. Hyperledger Fabric — native protocol-level privacy.
Permissioned Access
Ethereum — no permissioning at any layer. L2 Rollups — app-layer only, not enforced at protocol level. Custom PoA — protocol-level enforcement. Hyperledger Fabric — native and fine-grained.
Custom Consensus
Ethereum and L2 Rollups — no control, you inherit the base layer rules. Custom PoA — full control over consensus design. Hyperledger Fabric — pluggable consensus, swappable per use case.
Decentralization
Ethereum — maximum decentralization. L2 Rollups — inherit Ethereum's security guarantees. Custom PoA and Hyperledger Fabric — centralized by design, validator set is known and controlled.
Regulatory Audit Trail
Ethereum and L2 Rollups — partial auditability, no data access controls. Custom PoA and Hyperledger Fabric — full structured audit trails with controlled access, better suited for compliance requirements.
Smart Contract Ecosystem
Ethereum — largest and most mature ecosystem. L2 Rollups — fully EVM-compatible, entire Ethereum toolchain applies. Custom PoA — also EVM-compatible in most implementations. Hyperledger Fabric — uses Go or JavaScript chaincode, Solidity ecosystem does not apply.
Deployment Complexity
Ethereum — low, deploy a contract and you are live. L2 Rollups — medium, bridge and sequencer configuration required. Custom PoA — high, significant engineering across consensus, nodes, and tooling. Hyperledger Fabric — very high, most complex to set up correctly.
Operational Overhead
Ethereum — low, minimal infrastructure management needed. L2 Rollups — medium, sequencer and prover operations add ongoing work. Custom PoA — high, continuous validator management and monitoring required. Hyperledger Fabric — very high, certificate authorities, channel configuration, and peer lifecycle are all permanent responsibilities.

If you need decentralization and a rich ecosystem — deploy on L1 or an EVM-compatible L2 (Arbitrum, Optimism, Base). If you need custom consensus, transaction privacy, or protocol-level permissioning — build the custom chain and accept the operational cost that comes with it.

Final Thoughts
Building a custom blockchain platform is a systems engineering problem across at least five disciplines simultaneously: distributed consensus, cryptography, smart contract security, cloud infrastructure, and DevSecOps. The decisions you make at the architecture phase — proxy patterns, key management, node role separation, consensus mechanism — are expensive to reverse later.
The patterns and code examples in this article are starting points, not copy-paste production code. Every system has specific requirements that will change specific decisions. Audit your contracts with independent reviewers before deploying anything that holds real value. Rehearse your operational procedures before you need them under pressure.
The most important mindset shift for blockchain infrastructure is this: assume adversarial conditions from day one. Design access control as if your keys will be compromised. Build your node infrastructure as if your cloud provider will fail. Write your contracts as if the most motivated attacker in the world has unlimited time to find a path through them.
In production blockchain systems, that attacker almost certainly exists — and they are patient.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?