Blockchain Payment Architectures
Blockchain Payment Architectures
Blockchain networks are payment systems. Strip away the ideology and speculation, and you’re left with a distributed ledger that tracks balances, validates transfers, and achieves consensus on transaction ordering without a central authority. Whether this architecture is superior to traditional payment rails depends entirely on the specific use case — and understanding why requires dissecting the mechanics.
The diagram above contrasts the two dominant models for tracking balances in blockchain systems. The choice between UTXO and Account models has profound implications for parallelism, privacy, and smart contract expressiveness.
Bitcoin: Payment by Proof
Bitcoin’s design philosophy is radical: every payment is verified by every full node from first principles. There’s no “trust the bank’s database” — the transaction carries its own proof of validity.
Transaction Structure
A Bitcoin transaction consumes previous outputs (spending them) and creates new outputs (paying recipients):
from dataclasses import dataclass, field
import hashlib
@dataclass
class TransactionInput:
"""
References a previous transaction output being spent.
"""
prev_tx_hash: bytes # 32 bytes — hash of the transaction containing the output
output_index: int # Which output of that transaction
script_sig: bytes # Unlocking script (proves ownership)
sequence: int = 0xFFFFFFFF
@dataclass
class TransactionOutput:
"""
Creates a new spendable output.
"""
value_satoshis: int # Amount in satoshis (1 BTC = 100,000,000 satoshis)
script_pubkey: bytes # Locking script (defines spending conditions)
@dataclass
class Transaction:
version: int = 2
inputs: list[TransactionInput] = field(default_factory=list)
outputs: list[TransactionOutput] = field(default_factory=list)
locktime: int = 0
def txid(self) -> bytes:
"""
Transaction ID = double SHA-256 of the serialized transaction.
Note: the txid does NOT include witness data (SegWit).
This separation was introduced to fix transaction malleability,
which caused issues for payment channel protocols.
"""
serialized = self.serialize()
return hashlib.sha256(hashlib.sha256(serialized).digest()).digest()
def serialize(self) -> bytes:
"""Serialize the transaction to its wire format."""
result = bytearray()
# Version (4 bytes, little-endian)
result.extend(self.version.to_bytes(4, 'little'))
# Input count (varint)
result.extend(self._varint(len(self.inputs)))
for inp in self.inputs:
result.extend(inp.prev_tx_hash[::-1]) # Reversed byte order
result.extend(inp.output_index.to_bytes(4, 'little'))
result.extend(self._varint(len(inp.script_sig)))
result.extend(inp.script_sig)
result.extend(inp.sequence.to_bytes(4, 'little'))
# Output count (varint)
result.extend(self._varint(len(self.outputs)))
for out in self.outputs:
result.extend(out.value_satoshis.to_bytes(8, 'little'))
result.extend(self._varint(len(out.script_pubkey)))
result.extend(out.script_pubkey)
# Locktime (4 bytes)
result.extend(self.locktime.to_bytes(4, 'little'))
return bytes(result)
@staticmethod
def _varint(n: int) -> bytes:
if n < 0xFD:
return bytes([n])
elif n <= 0xFFFF:
return b'\xFD' + n.to_bytes(2, 'little')
elif n <= 0xFFFFFFFF:
return b'\xFE' + n.to_bytes(4, 'little')
else:
return b'\xFF' + n.to_bytes(8, 'little')
@property
def fee(self) -> int:
"""
Transaction fee = sum(inputs) - sum(outputs).
The fee is implicit — it's whatever value is left over
after all outputs are accounted for. The miner who includes
this transaction in a block collects the fee.
This design means there's no "fee field" that can be forged.
The fee is a mathematical consequence of the inputs and outputs.
"""
# In practice, you need to look up input values from the UTXO set
# This is a simplification
input_total = sum(getattr(inp, 'value', 0) for inp in self.inputs)
output_total = sum(out.value_satoshis for out in self.outputs)
return input_total - output_total
Script: Bitcoin’s Verification Language
Bitcoin Script is a stack-based language deliberately designed to be non-Turing-complete. It can express conditions like “this output can be spent by whoever provides a valid signature for public key X” but cannot express loops, recursion, or arbitrary computation.
class ScriptVM:
"""
Simplified Bitcoin Script virtual machine.
The VM executes the unlocking script (scriptSig) followed by
the locking script (scriptPubKey). If the stack's top element
is truthy after execution, the spend is valid.
"""
def __init__(self):
self.stack: list[bytes] = []
self.alt_stack: list[bytes] = []
def execute(self, script_sig: bytes, script_pubkey: bytes) -> bool:
"""
Execute scriptSig + scriptPubKey and return True if valid.
"""
# Execute scriptSig (pushes data onto stack)
self._execute_script(script_sig)
# Save stack state (for P2SH verification)
stack_copy = list(self.stack)
# Execute scriptPubKey (verifies conditions)
self._execute_script(script_pubkey)
# Transaction is valid if top of stack is truthy
if not self.stack or self.stack[-1] == b'':
return False
return True
def _execute_script(self, script: bytes):
pos = 0
while pos < len(script):
opcode = script[pos]
pos += 1
# Data push opcodes (1-75 bytes)
if 1 <= opcode <= 75:
self.stack.append(script[pos:pos + opcode])
pos += opcode
elif opcode == 0x76: # OP_DUP
self.stack.append(self.stack[-1])
elif opcode == 0xA9: # OP_HASH160
data = self.stack.pop()
sha = hashlib.sha256(data).digest()
h160 = hashlib.new('ripemd160', sha).digest()
self.stack.append(h160)
elif opcode == 0x88: # OP_EQUALVERIFY
a = self.stack.pop()
b = self.stack.pop()
if a != b:
raise ScriptError("OP_EQUALVERIFY failed")
elif opcode == 0xAC: # OP_CHECKSIG
pubkey = self.stack.pop()
signature = self.stack.pop()
# Verify ECDSA signature (simplified)
valid = self._verify_signature(signature, pubkey)
self.stack.append(b'\x01' if valid else b'')
def _verify_signature(self, sig: bytes, pubkey: bytes) -> bool:
"""Verify ECDSA signature against the transaction hash."""
# In production: hash the transaction with SIGHASH flags,
# then verify the ECDSA signature on secp256k1
return True # Placeholder
class ScriptError(Exception):
pass
The most common script pattern is P2PKH (Pay-to-Public-Key-Hash):
Locking script (scriptPubKey):
OP_DUP OP_HASH160 <pubkey_hash> OP_EQUALVERIFY OP_CHECKSIG
Unlocking script (scriptSig):
<signature> <pubkey>
Execution:
Stack: [signature, pubkey] ← scriptSig pushed these
OP_DUP: [signature, pubkey, pubkey]
OP_HASH160: [signature, pubkey, hash(pubkey)]
<push>: [signature, pubkey, hash(pubkey), expected_hash]
OP_EQUALVERIFY: [signature, pubkey] ← hashes match, continue
OP_CHECKSIG: [true] ← signature valid for this pubkey
Ethereum: Programmable Payments
Ethereum replaces Bitcoin’s UTXO model with an account model and its limited Script with a Turing-complete virtual machine (EVM). This enables payment logic that Bitcoin cannot express: escrow contracts, streaming payments, multi-party splits, and conditional releases.
from dataclasses import dataclass
from web3 import Web3
@dataclass
class EthereumTransaction:
"""
An Ethereum transaction modifies world state by:
1. Transferring ETH from sender to recipient
2. Executing smart contract code
3. Updating contract storage
Unlike Bitcoin, Ethereum transactions have explicit gas pricing:
- gasLimit: maximum compute units this transaction can consume
- maxFeePerGas: maximum total fee per gas unit (EIP-1559)
- maxPriorityFeePerGas: tip to the block proposer
"""
to: str # Recipient address (or contract)
value_wei: int # Amount in wei (1 ETH = 10^18 wei)
nonce: int # Sender's transaction count (replay protection)
gas_limit: int # Maximum gas units
max_fee_per_gas: int # Max fee per gas (wei)
max_priority_fee_per_gas: int # Tip per gas (wei)
data: bytes = b'' # Contract call data (ABI-encoded)
chain_id: int = 1 # 1 = mainnet, prevents cross-chain replay
def build_payment_transaction(
web3: Web3,
sender: str,
recipient: str,
amount_eth: float,
private_key: str
) -> str:
"""
Build and sign an ETH transfer transaction.
Key differences from traditional payment:
- Nonce provides replay protection (like ATC in EMV)
- Gas price is a market-driven fee (like interchange, but dynamic)
- Transaction is final after ~12 seconds (1 block) with high confidence
- No intermediary: sender signs, network validates, recipient credits
"""
nonce = web3.eth.get_transaction_count(sender)
# Get current gas prices from the network
latest_block = web3.eth.get_block('latest')
base_fee = latest_block['baseFeePerGas']
transaction = {
'to': recipient,
'value': web3.to_wei(amount_eth, 'ether'),
'nonce': nonce,
'gas': 21000, # Standard ETH transfer gas cost
'maxFeePerGas': base_fee * 2, # 2x base fee for inclusion buffer
'maxPriorityFeePerGas': web3.to_wei(2, 'gwei'), # Tip
'chainId': 1,
'type': 2, # EIP-1559 transaction
}
signed = web3.eth.account.sign_transaction(transaction, private_key)
tx_hash = web3.eth.send_raw_transaction(signed.raw_transaction)
return tx_hash.hex()
Performance Comparison with Traditional Rails
| Metric | Visa Network | Bitcoin | Ethereum | FedNow |
|---|---|---|---|---|
| TPS (peak) | 65,000 | 7 | 30 | 500+ |
| Finality | T+1 day (settlement) | ~60 min (6 blocks) | ~12 sec (1 block) | < 20 sec |
| Fee per txn | $0.05-$0.30 | $1-$50 (variable) | $0.50-$100 (variable) | $0.01 |
| Energy per txn | ~0.001 kWh | ~700 kWh (PoW) | ~0.03 kWh (PoS) | Negligible |
| Reversibility | Chargebacks possible | Irreversible | Irreversible | Irreversible |
| Privacy | Pseudonymous (acquirer sees all) | Pseudonymous (chain analysis) | Pseudonymous | Full identity (KYC) |
The throughput gap is the most visible limitation, but the finality model is more architecturally significant. Card networks offer fast authorization (2 seconds) but slow settlement (1-2 days). Bitcoin offers slow confirmation (60 minutes) but that IS the settlement — there’s no separate clearing and settlement phase. Ethereum’s ~12-second block time with single-slot finality provides a middle ground.
For payment system architects, the question isn’t “which is better?” but “which finality model matches my use case?” Cross-border B2B payments that currently take 3-5 days through correspondent banking can benefit from blockchain’s 12-second finality. Point-of-sale retail payments need the 2-second authorization that card networks provide — waiting 12 seconds for a block confirmation is unacceptable.