Module 6: Ledger and Contracts
Overview
This module teaches you ICN's economic layer: the mutual credit ledger for accounting and the Cooperative Contract Language (CCL) for programmable rules. Understanding these systems is essential for working on any feature involving value exchange or automated agreements.
Objectives
- Understand mutual credit and how it differs from traditional money
- Master double-entry accounting and the journal entry model
- Learn the Merkle-DAG structure for ledger integrity
- Understand CCL syntax, capabilities, and fuel metering
- Know how contracts interact with the ledger
Prerequisites
- Module 5 (Network & Gossip) - for understanding replication
- Module 3 (Runtime & Actors) - for understanding integration
Key Reading
icn/crates/icn-ledger/src/ledger.rs- Ledger implementationicn/crates/icn-ccl/src/lib.rs- Contract languagedocs/ARCHITECTURE.md- Sections 4 and 5
Core Concepts
1. What is Mutual Credit?
Mutual credit is an accounting system where credit is created through transactions themselves, rather than being issued by a central authority.
Traditional money:
Bank creates $100
Bank gives to Alice
Alice: $100, Bob: $0
Alice pays Bob $30
Alice: $70, Bob: $30
Total money in system: $100 (fixed supply)
Mutual credit:
System starts empty
Alice: 0, Bob: 0, Carol: 0
Alice buys from Bob (30 credits)
Alice: -30, Bob: +30, Carol: 0
Bob buys from Carol (20 credits)
Alice: -30, Bob: +10, Carol: +20
Carol buys from Alice (30 credits)
Alice: 0, Bob: +10, Carol: -10
Total: ALWAYS sums to ZERO
Key properties:
- No external money supply: Credit is created through transactions
- Zero-sum: Total balances always sum to zero
- Symmetric: Every credit creates an equal debit
- Interest-free: No inherent interest on balances
- Mutual obligation: Negative balances represent obligations to the community
2. Why Mutual Credit for Cooperatives?
| Traditional Money | Mutual Credit |
|---|---|
| Scarcity by design | Abundance by design |
| Accumulation incentivized | Circulation incentivized |
| External control (central bank) | Community control |
| Interest extracts value | No extraction |
| Requires capital to start | Starts from nothing |
Cooperatives benefit because:
- Members can trade without external capital
- Value stays within the cooperative
- No one needs to "have money" to participate
- Healthy circulation is economically optimal (balance near zero)
3. Credit Limits
To prevent unlimited negative balances, accounts have credit limits:
pub struct CreditLimit {
/// Maximum negative balance allowed
pub limit: i64,
/// Who set this limit
pub granted_by: Did,
/// When it expires (optional)
pub expires_at: Option<u64>,
}
How limits work:
- New members start with a small limit (e.g., -100)
- Limits increase with trust and participation
- Hitting limit prevents further spending
- Positive balances are unlimited
Alice's limit: -500
Alice's balance: -450
Alice tries to buy 100 credits:
New balance would be: -550
-550 < -500 (limit)
Transaction REJECTED
Double-Entry Accounting
4. The Fundamental Principle
Every transaction creates TWO entries:
- A debit (value leaving an account)
- A credit (value entering an account)
These MUST be equal. This is the double-entry invariant.
Alice pays Bob 50 credits:
┌──────────────────────────────────────────────────┐
│ DEBIT: Alice -50 (value leaves Alice) │
│ CREDIT: Bob +50 (value enters Bob) │
│ │
│ TOTAL: -50 + 50 = 0 ✓ │
└──────────────────────────────────────────────────┘
5. Journal Entries
A journal entry is the atomic unit of ledger change:
pub struct JournalEntry {
/// Unique identifier (hash-based)
pub id: Option<String>,
/// When this entry was created
pub timestamp: u64,
/// Who authored this entry
pub author: Did,
/// Optional contract reference
pub contract_ref: Option<ContractRef>,
/// Account changes (debits and credits)
pub accounts: Vec<AccountDelta>,
/// Parent entries (Merkle-DAG)
pub parents: Vec<String>,
/// Cryptographic signature
pub signature: Option<Signature>,
}
pub struct AccountDelta {
pub account: Did,
pub currency: String,
pub amount: i64, // Positive = credit, Negative = debit
}
Validation rules:
- Balance: Sum of all deltas must be zero per currency
- Positive amounts: Amounts must be > 0
- Valid accounts: All DIDs must be valid
- Signature: Must be signed by author
- Timestamp: Must be reasonable (not in future)
6. Building Journal Entries
// Creating a payment from Alice to Bob
let entry = JournalEntryBuilder::new(alice_did.clone())
.debit(alice_did.clone(), "ICN".to_string(), 50)
.credit(bob_did.clone(), "ICN".to_string(), 50)
.memo("Payment for services".to_string())
.build()?;
// Multi-party transaction
let entry = JournalEntryBuilder::new(contract_did.clone())
.debit(alice_did.clone(), "ICN".to_string(), 100)
.credit(bob_did.clone(), "ICN".to_string(), 60)
.credit(carol_did.clone(), "ICN".to_string(), 40)
.contract_ref(contract_ref)
.build()?;
The Merkle-DAG Structure
7. What is a Merkle-DAG?
A Merkle-DAG (Directed Acyclic Graph) links entries through cryptographic hashes, creating an immutable, verifiable history.
Entry A (Genesis)
│
├──► Entry B (parent: A)
│ │
│ └──► Entry D (parent: B)
│
└──► Entry C (parent: A)
│
└──► Entry E (parents: C, D)
↑
Merge point
Properties:
- Immutable: Changing an entry changes its hash, breaking all links
- Verifiable: Anyone can verify the chain by recomputing hashes
- Forkable: Different nodes can have different branches
- Mergeable: Branches can be reconciled
8. Entry Hashing
impl JournalEntry {
pub fn compute_hash(&mut self) -> Result<()> {
// Serialize entry fields (excluding id and signature)
let content = self.content_for_hash();
// SHA-256 hash
let hash = sha256(&content);
// Base58 encode for human-readable ID
self.id = Some(bs58::encode(hash).into_string());
Ok(())
}
}
Hash includes:
- Timestamp
- Author DID
- Account deltas
- Parent hashes
- Contract reference
Hash excludes:
- Entry ID (it IS the hash)
- Signature (signs the hash)
9. Fork Detection and Resolution
When nodes diverge (different parents for new entries), forks are detected:
pub struct ForkDetector {
/// Known entry hashes per author
entries_by_author: HashMap<Did, Vec<String>>,
/// Detected forks
forks: Vec<Fork>,
}
pub struct Fork {
/// Common ancestor
base: String,
/// Diverging branches
branches: Vec<Branch>,
/// Resolution status
resolved: bool,
}
Resolution strategies:
- Automatic: Merge if no conflicts (different accounts affected)
- Manual: Human intervention for conflicting changes
- Policy-based: Configured rules (e.g., prefer higher trust author)
The Ledger Actor
10. Ledger Structure
pub struct Ledger {
/// Persistent storage
store: Arc<dyn Store>,
/// Our identity
our_did: Did,
/// Cached balances (for fast reads)
balances: HashMap<(Did, String), i64>,
/// Credit limits
credit_limits: HashMap<Did, CreditLimit>,
/// Fork detector
fork_detector: ForkDetector,
/// Gossip handle for replication
gossip_handle: Option<GossipHandle>,
/// Trust graph for validation
trust_graph: Option<Arc<RwLock<TrustGraph>>>,
}
11. Appending Entries
impl Ledger {
pub async fn append_entry(&mut self, entry: JournalEntry) -> Result<String> {
// 1. Validate entry
self.validate_entry(&entry)?;
// 2. Check credit limits
self.check_credit_limits(&entry)?;
// 3. Persist to storage
let entry_id = entry.id.clone().unwrap();
self.store.put(
&format!("ledger:entry:{}", entry_id).as_bytes(),
&entry.serialize()?,
)?;
// 4. Update cached balances
for delta in &entry.accounts {
let key = (delta.account.clone(), delta.currency.clone());
*self.balances.entry(key).or_default() += delta.amount;
}
// 5. Gossip to peers
if let Some(ref gossip) = self.gossip_handle {
gossip.write().await.announce(
"ledger:entries",
Entry::Journal(entry.clone()),
).await?;
}
Ok(entry_id)
}
}
12. Querying Balances
impl Ledger {
/// Get current balance for an account
pub fn get_balance(&self, account: &Did, currency: &str) -> i64 {
self.balances
.get(&(account.clone(), currency.to_string()))
.copied()
.unwrap_or(0)
}
/// Get all balances for an account
pub fn get_all_balances(&self, account: &Did) -> HashMap<String, i64> {
self.balances
.iter()
.filter(|((did, _), _)| did == account)
.map(|((_, currency), &amount)| (currency.clone(), amount))
.collect()
}
/// Get transaction history
pub fn get_history(
&self,
account: &Did,
limit: usize,
) -> Result<Vec<JournalEntry>> {
// Query from storage with index
self.store.query_entries_by_account(account, limit)
}
}
The Cooperative Contract Language (CCL)
13. What is CCL?
CCL is a domain-specific language for expressing cooperative agreements:
| Feature | CCL | General-Purpose |
|---|---|---|
| Turing-complete | No | Yes |
| Deterministic | Yes | No (I/O, time) |
| Capability-gated | Yes | No |
| Fuel-metered | Yes | No |
| Purpose | Agreements | General |
Why not Turing-complete?
- Halting problem: Can't guarantee termination
- Security: Simpler model = easier to audit
- Determinism: Same inputs = same outputs everywhere
14. CCL Structure
/// A contract is a collection of rules
pub struct Contract {
/// Contract name
pub name: String,
/// Contract version
pub version: u32,
/// Parties involved
pub parties: Vec<Did>,
/// Rules that define behavior
pub rules: Vec<Rule>,
/// Current contract state
pub state: ContractState,
}
/// A rule defines when and what happens
pub struct Rule {
/// Rule name
pub name: String,
/// Conditions that must be true to trigger
pub conditions: Vec<Expr>,
/// Actions to take when triggered
pub effects: Vec<Stmt>,
}
/// Expressions evaluate to values
pub enum Expr {
Literal(Value),
Variable(String),
BinaryOp { left: Box<Expr>, op: BinaryOp, right: Box<Expr> },
FunctionCall { name: String, args: Vec<Expr> },
FieldAccess { object: Box<Expr>, field: String },
}
/// Statements produce effects
pub enum Stmt {
Transfer { from: Expr, to: Expr, amount: Expr, currency: Expr },
SetVariable { name: String, value: Expr },
Emit { event: String, data: Expr },
If { condition: Expr, then: Vec<Stmt>, else_: Vec<Stmt> },
}
15. Example Contract
contract MembershipDues {
version: 1
parties: [cooperative_did, member_did]
state {
last_payment: timestamp
amount_due: 100
}
rule PayDues {
when {
caller == member_did
time.now > last_payment + 30 days
}
then {
transfer(member_did, cooperative_did, amount_due, "ICN")
set last_payment = time.now
emit("DuesPaid", { member: member_did, amount: amount_due })
}
}
rule WaiveDues {
when {
caller == cooperative_did
member has role("hardship")
}
then {
set amount_due = 0
emit("DuesWaived", { member: member_did })
}
}
}
16. Capabilities
Contracts must declare what they're allowed to do:
pub enum Capability {
/// Read ledger balances
ReadLedger,
/// Transfer up to N credits
WriteLedger(i64),
/// Read trust scores
ReadTrust,
/// Send gossip messages to topic
SendMessage(String),
/// Read contract state
ReadState,
/// Modify contract state
WriteState,
}
pub struct ContractCapabilities {
/// Capabilities this contract has
capabilities: HashSet<Capability>,
}
impl ContractCapabilities {
pub fn can_transfer(&self, amount: i64) -> bool {
self.capabilities.iter().any(|cap| {
matches!(cap, Capability::WriteLedger(limit) if *limit >= amount)
})
}
}
Capability enforcement:
impl ContractRuntime {
fn execute_transfer(&self, transfer: &Transfer) -> Result<()> {
let amount = self.eval_expr(&transfer.amount)?;
// Check capability
if !self.capabilities.can_transfer(amount) {
bail!("Contract lacks WriteLedger({}) capability", amount);
}
// Execute transfer
self.ledger.transfer(
&transfer.from,
&transfer.to,
amount,
&transfer.currency,
)
}
}
17. Fuel Metering
To prevent infinite loops and resource exhaustion, CCL uses fuel:
pub struct FuelMeter {
/// Remaining fuel
remaining: u64,
/// Operations executed
operations: u64,
}
impl FuelMeter {
pub fn consume(&mut self, amount: u64) -> Result<()> {
if amount > self.remaining {
bail!("Out of fuel: needed {}, have {}", amount, self.remaining);
}
self.remaining -= amount;
self.operations += 1;
Ok(())
}
}
// Fuel costs
const FUEL_EXPR_EVAL: u64 = 1;
const FUEL_TRANSFER: u64 = 100;
const FUEL_STATE_READ: u64 = 5;
const FUEL_STATE_WRITE: u64 = 20;
Fuel allocation:
- Caller provides fuel with contract invocation
- Unused fuel is returned
- Running out of fuel reverts the transaction
Contract Execution Flow
18. Execution Pipeline
┌─────────────────────────────────────────────────────────────────┐
│ CONTRACT EXECUTION FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Request arrives (Gateway or Gossip) │
│ │ │
│ ▼ │
│ 2. Load contract from storage │
│ │ │
│ ▼ │
│ 3. Verify caller authorization │
│ │ │
│ ▼ │
│ 4. Initialize fuel meter │
│ │ │
│ ▼ │
│ 5. Evaluate rule conditions │
│ │ (consumes fuel) │
│ ▼ │
│ 6. Execute matching rule effects │
│ │ (consumes fuel, may produce LedgerOperations) │
│ ▼ │
│ 7. Apply LedgerOperations as JournalEntries │
│ │ │
│ ▼ │
│ 8. Return ExecutionResult │
│ │
└─────────────────────────────────────────────────────────────────┘
19. The Contract Runtime
pub struct ContractRuntime {
/// Contract being executed
contract: Contract,
/// Available capabilities
capabilities: ContractCapabilities,
/// Fuel meter
fuel: FuelMeter,
/// Ledger handle
ledger: Arc<RwLock<Ledger>>,
/// Accumulated ledger operations
pending_ops: Vec<LedgerOperation>,
}
impl ContractRuntime {
pub async fn execute_rule(&mut self, rule_name: &str, caller: &Did) -> Result<ExecutionResult> {
let rule = self.contract.get_rule(rule_name)?;
// 1. Evaluate conditions
for condition in &rule.conditions {
self.fuel.consume(FUEL_EXPR_EVAL)?;
if !self.eval_expr_as_bool(condition)? {
return Ok(ExecutionResult::ConditionNotMet);
}
}
// 2. Execute effects
for effect in &rule.effects {
self.execute_stmt(effect)?;
}
// 3. Apply pending operations to ledger
for op in &self.pending_ops {
self.apply_ledger_operation(op).await?;
}
Ok(ExecutionResult::Success {
fuel_used: self.fuel.operations,
operations: self.pending_ops.len(),
})
}
}
Integration with Gossip
20. Ledger Replication
Ledger entries replicate through gossip:
impl Ledger {
fn setup_gossip_subscription(&mut self, gossip: &GossipHandle) -> Result<()> {
let ledger = self.handle();
// Subscribe to incoming entries
gossip.write().await.subscribe(
"ledger:entries",
Box::new(move |entry: Entry| {
if let Entry::Journal(journal) = entry {
let ledger = ledger.clone();
tokio::spawn(async move {
if let Err(e) = ledger.write().await.apply_remote_entry(journal).await {
warn!("Failed to apply remote entry: {}", e);
}
});
}
}),
)?;
Ok(())
}
async fn apply_remote_entry(&mut self, entry: JournalEntry) -> Result<()> {
// 1. Verify signature
entry.verify_signature()?;
// 2. Check if we already have it
if self.has_entry(&entry.id.as_ref().unwrap())? {
return Ok(()); // Idempotent
}
// 3. Check parents exist (or request them)
for parent in &entry.parents {
if !self.has_entry(parent)? {
self.request_entry(parent).await?;
}
}
// 4. Apply entry
self.apply_entry_internal(entry)?;
Ok(())
}
}
Diagrams
Ledger Data Flow
flowchart TB
subgraph API[Gateway API]
Create[Create Payment]
Query[Query Balance]
end
subgraph Ledger[Ledger Actor]
Builder[JournalEntryBuilder]
Validator[Validator]
Storage[(Sled Storage)]
Cache[Balance Cache]
end
subgraph Contract[Contract Runtime]
Parser[CCL Parser]
Interpreter[Interpreter]
Fuel[Fuel Meter]
end
subgraph Sync[Replication]
Gossip[Gossip Actor]
Peers[Peer Nodes]
end
Create --> Builder
Builder --> Validator
Validator --> Storage
Validator --> Cache
Validator --> Gossip
Parser --> Interpreter
Interpreter --> Fuel
Interpreter --> Builder
Query --> Cache
Gossip <--> Peers
Contract Execution Sequence
sequenceDiagram
participant C as Caller
participant G as Gateway
participant R as ContractRuntime
participant L as Ledger
participant Go as Gossip
C->>G: Execute contract rule
G->>R: load_contract()
R->>R: verify_capabilities()
R->>R: init_fuel_meter()
loop For each condition
R->>R: eval_condition()
R->>R: consume_fuel()
end
loop For each effect
R->>R: execute_effect()
R->>R: consume_fuel()
R->>R: accumulate_ops()
end
R->>L: apply_operations()
L->>L: build_journal_entry()
L->>L: validate()
L->>L: persist()
L->>Go: announce()
R-->>G: ExecutionResult
G-->>C: Success
Exercises
Balance Calculation: Given these transactions, calculate final balances:
- Alice → Bob: 50
- Bob → Carol: 30
- Carol → Alice: 20
Double-Entry: Write the JournalEntry (debits/credits) for a transaction where Alice pays 100 to split between Bob (60) and Carol (40).
Contract Design: Design a CCL contract for a lending agreement:
- Lender provides credit
- Borrower repays over time
- Interest is forbidden (mutual credit)
Fuel Calculation: If a contract has 1000 fuel, and each condition costs 1 fuel and each transfer costs 100 fuel, how many transfers can it do with 5 conditions?
Fork Scenario: Two nodes create entries at the same time. Both have entry A as parent. How does the Merkle-DAG look? How is this resolved?
Checkpoints
- You can explain mutual credit vs. traditional money
- You understand the double-entry invariant
- You can trace how a journal entry is created and validated
- You understand the Merkle-DAG structure
- You know what capabilities a contract needs
- You understand fuel metering and why it exists
- You can describe how ledger entries replicate via gossip
Quick Reference
| Concept | Definition |
|---|---|
| Mutual Credit | Accounting system where credit is created through transactions |
| Double-Entry | Every transaction has equal debits and credits |
| Journal Entry | Atomic unit of ledger change |
| Merkle-DAG | Hash-linked graph of entries for integrity |
| CCL | Cooperative Contract Language |
| Capability | Permission grant for contract actions |
| Fuel | Resource limit for contract execution |
| Credit Limit | Maximum negative balance allowed |
Next Steps
Proceed to Module 7: Gateway & SDK to understand how external applications interact with ICN through REST APIs, WebSocket connections, and the TypeScript SDK.