# Introduction Source: https://docs.kontor.network/docs/consensus/introduction Kontor is the first metaprotocol to transcend Bitcoin's 10-minute block time and transaction throughput limits, offering confirmations in only 1–2 seconds, dramatically lower fees, and over 1,000 transactions per second—all while relying entirely on Bitcoin consensus for transaction finality. Like all metaprotocols, every Kontor transaction is simply extra data stored inside regular Bitcoin transaction. **Embedded consensus** is the foundation: Kontor indexers scan Bitcoin blocks, find Kontor transactions, and execute them according to the Kontor protocol rules. Because Bitcoin provides an immutable log and Kontor's rules are deterministic, every indexer arrives at the same state. Kontor, however, includes three sets of innovations which greatly improve the performance, scalability and functionality of the Bitcoin network without compromising its security guarantees: [Transaction Encoding](/docs/consensus/transaction-encoding) — Kontor offers the best of both worlds when it comes to token transfer semantics: Kontor tokens can be "attached" to Bitcoin UTXOs for trading with standard Bitcoin tools (PSBTs, atomic swaps), or "detached" for direct use in smart contracts. [Optimistic Consensus](/docs/consensus/optimistic-consensus) — Kontor includes a novel consensus layer that offers real-time transaction finality with a Byzantine fault–tolerant prediction market on Bitcoin's mempool. [Scalability](/docs/consensus/scalability) — Kontor's transaction encoding scheme is heavily optimized for space efficiency, with trustless BLS signature aggregation, compact registry IDs, efficient binary encoding, and data compression, allowing Kontor to achieve 1,000+ TPS at less than 10 WU per transaction. # Optimistic Consensus Source: https://docs.kontor.network/docs/consensus/optimistic-consensus Bitcoin blocks take ten minutes on average, and prudent recipients wait for multiple confirmations before considering a transaction final. For many applications—exchanges, point-of-sale, real-time trading—this latency is prohibitive. Kontor's optimistic consensus provides intra-block confirmations while preserving Bitcoin as the ultimate source of truth. This is possible because stateful metaprotocols have an extra degree of freedom that Nakamoto consensus leaves unspecified: the ordering of transactions within a single Bitcoin block. Two transactions that modify the same contract state produce different results depending on which executes first. Since all transactions in a block appear to the network simultaneously, this ordering is semantically arbitrary—it must be resolved by some rule, but the choice of rule is up to the metaprotocol. Rather than waiting for Bitcoin confirmation to determine ordering, a network of KOR stakers runs BFT consensus to produce signed transaction batches that assign deterministic positions *before* Bitcoin confirmation. Users can transact with confidence after batch inclusion—typically within 1–2 seconds—because the ordering is already fixed. When the underlying Bitcoin transaction eventually confirms, execution proceeds at the pre-assigned position. ## A Virtual Prediction Market When stakers sign a batch, they are participating in a virtual prediction market. Each signature is a prediction backed by slashable collateral: "these transactions will confirm on Bitcoin without conflict before this batch expires." The staker is betting their capital on this outcome. If the prediction holds—the batched transactions confirm normally on Bitcoin—stakers earn fees and protocol emissions. If the prediction fails—a conflicting transaction confirms on Bitcoin instead—the batched transactions are rolled back and stakers are slashed. This creates strong economic incentives for stakers to only sign batches they believe will succeed. The market structure provides several properties: **Economic finality.** When a quorum (>2/3 of total stake) signs a batch, the transaction has economic finality proportional to the total stake at risk. Recipients can calibrate their acceptance threshold based on transaction value—small payments may accept minimal stake backing, while large transfers may require the full quorum. **Measurable risk.** Unlike vague "pre-confirmation" signals, optimistic consensus makes the remaining risk explicit and quantifiable. The stake backing each batch is public. The slashing conditions are deterministic. Recipients know exactly what is at risk and can make informed decisions. **Graceful degradation.** If stakers fail to reach consensus, or if the staker network goes offline entirely, transactions still confirm on Bitcoin normally. Unbatched transactions execute at block-end using standard lexicographic ordering. The staker layer accelerates finality but never blocks it. ## Protocol Flow ```mermaid theme={null} sequenceDiagram participant User participant Stakers participant Bitcoin participant Recipient User->>Stakers: Submit transaction Stakers->>Stakers: Validate & batch via BFT Stakers->>User: Signed batch (position assigned) Stakers->>Recipient: Signed batch Note over Recipient: Optimistic confirmation
(~1-2 seconds) User->>Bitcoin: Broadcast transaction Bitcoin->>Bitcoin: Mine in block Note over Recipient: Batch-Confirmed
(~10+ minutes) ``` 1. **User submits transaction.** The user broadcasts their Kontor transaction to the staker network (and to Bitcoin). 2. **Stakers validate and batch.** Stakers validate the transaction and include it in the next batch via BFT consensus. 3. **Quorum signs batch.** A quorum of stakers (>2/3 of total stake) signs the batch, each committing slashable stake on the batch's success. 4. **User receives confirmation.** The signed batch assigns the transaction a deterministic position. The recipient can now treat the transaction as confirmed, backed by the bonded stake. 5. **Bitcoin confirmation.** When the transaction confirms on Bitcoin, it becomes *Batch-Confirmed*—both batched by stakers and confirmed by Bitcoin. The transaction executes at its batch-assigned position. ## Ordering Each transaction in a batch receives a position: `(batch_id, index)`. Positions are totally ordered—earlier batches come first, and within a batch, earlier indices come first. Execution proceeds strictly by position, ensuring deterministic execution order even as transactions confirm on Bitcoin out of order. **Unbatched transactions** (those that confirm on Bitcoin without being batched) execute at "block-end"—after all batched transactions, ordered by their position within the Bitcoin block. ```mermaid theme={null} flowchart TD subgraph P["Temporal Order"] direction LR Bn["Batchh,n
t₁, t₅"] Bn1["Batchh,n+1
t₃"] dots["..."] Bh["Blockh+i
t₁, t₂, t₃, t₄, t₅"] Bn --> Bn1 --> dots --> Bh end subgraph E["Execution order"] direction LR t1["t₁"] --> t5["t₅"] --> t3["t₃"] --> t2["t₂"] --> t4["t₄"] end Bn -.-> t1 Bn1 -.-> t3 Bh -.-> t2 Bh -.-> t4 style Bn fill:#4a90d9,color:#fff style Bn1 fill:#50b87d,color:#fff style t1 fill:#4a90d9,color:#fff style t5 fill:#4a90d9,color:#fff style t3 fill:#50b87d,color:#fff style t2 fill:#777,color:#fff style t4 fill:#777,color:#fff ``` ## Conflict Resolution Bitcoin is the ultimate arbiter. When a batched transaction confirms on Bitcoin, it becomes Batch-Confirmed and executes at its assigned position. If a *conflicting* transaction confirms instead (one that spends the same inputs), the batched transaction is rolled back and stakers who signed the batch are slashed. ## Security Guarantees The protocol's core properties have been formally verified: **Safety.** No two valid batches contain conflicting transactions, under honest supermajority (>2/3 stake). This follows from quorum intersection: any two quorums overlap in at least 1/3 stake, and honest stakers refuse to sign conflicting batches. **Liveness.** Valid transactions are eventually included in batches. If stakers fail to reach consensus, transactions still confirm on Bitcoin and execute at block-end. **Accountability.** Any safety violation is attributable to at least 1/3 of total stake with cryptographic evidence—the violating stakers' signatures on conflicting batches serve as proof. *** For the complete specification, see [Optimistic Consensus Specification](/docs/resources/optimistic-consensus). # Scalability Source: https://docs.kontor.network/docs/consensus/scalability As a metaprotocol, Kontor inherits Bitcoin's constraints: 4,000,000 weight units per block and a ten-minute block time. A naive implementation would be limited to roughly the same throughput as Bitcoin itself—about 10 transactions per second. Kontor reaches 1,000+ operations per second by bundling many operations into single Bitcoin transactions and minimizing per-operation payload overhead. Depending on batch size, this reduces weight per operation by 35–120×. ### Transaction Bundling The most significant optimization is trustless BLS signature aggregation. Vanilla Bitcoin signatures require one 64-byte Schnorr signature per input, and these cannot be aggregated across different keys. BLS signatures can be aggregated: N signatures from N different signers compress into a single 48-byte aggregate signature. Bundling packs multiple signed operations into a single Bitcoin transaction, amortizing the fixed transaction overhead (inputs, outputs, witness structure) across the batch. Kontor users can choose to use their regular Bitcoin addresses, or they can use Kontor BLS addresses for lower fees and greater scalability. 1. **Users sign operations.** Each user signs their Kontor operation using a BLS key derived from their seed phrase. The signature commits to the operation details: target contract, function, arguments. 2. **Bundlers collect operations.** Bundlers are infrastructure providers who collect signed operations from users. Any party can run a bundler—there is no permissioning or staking requirement. 3. **Bundlers aggregate and publish.** The bundler combines all signatures into a single BLS aggregate signature, constructs a Bitcoin transaction containing all operations, and broadcasts it to the Bitcoin network. 4. **Indexers verify and execute.** When the transaction confirms, indexers verify the aggregate signature against all (public key, message) pairs and execute each operation with its designated signer as caller. Users do not need to trust bundlers. The BLS signature scheme has a key property: given signed operations, *anyone* can produce a valid aggregate. ## Payload-size optimizations Kontor reduces per-operation payload size via: ### Compact Registry IDs Kontor maintains a deterministic registry that maps long identifiers (public keys, contract addresses, etc.) to compact 4-byte numeric IDs. When a new identifier first appears (a new public key signs an operation, or a new contract is deployed), the indexer assigns it the next sequential ID. This is deterministic—all indexers processing the same blocks assign the same IDs. **Space savings:** 4 bytes instead of 32+ bytes per identifier—an 8–16× reduction in identifier overhead. ### Binary Encoding Operations are encoded in a compact binary format to remove syntactic overhead and improve compressibility: ``` BinaryCall: signer_id: 4 bytes (registry ID) contract_id: 4 bytes (registry ID) function_index: 2 bytes (index into contract exports) args: variable (compact serialization) ``` Combined with registry IDs, this produces compact payloads that compress well. ### Zstd Compression The final layer is Zstd compression. After operations are serialized in binary format with registry IDs, compression exploits redundancy across the batch: repeated contract IDs, common function indices, and similar argument patterns. ## Benchmarks The following table shows how each optimization contributes to throughput at batch size N=100: | Configuration | WU/op | TPS | Improvement | | ---------------------- | ------- | ------- | ----------- | | Vanilla Bitcoin P2TR | 615 | 11 | baseline | | Naive Kontor (N=1) | 770–824 | 8 | 0.7× | | + BLS batching (N=100) | 33–38 | 172–199 | 16–19× | | + Registry IDs | 21–25 | 264–305 | 25–29× | | + Binary + Zstd | 15–18 | 366–443 | 35–42× | Throughput scales with batch size: | Batch Size | WU/op | TPS | Improvement | | ---------- | ------- | --------- | ----------- | | 1 | 696–698 | 9 | 1× | | 10 | 80–82 | 79–81 | 8× | | 100 | 15–18 | 366–443 | 35–42× | | 1,000 | 7–10 | 683–986 | 65–93× | | 10,000 | 5–8 | 771–1,295 | 73–123× | ## Fee model ### Fee components Users pay two types of fees: **Bundling fee.** Compensates the bundler for Bitcoin block space. This is the user's share of the total Bitcoin transaction fee, amortized across all operations in the bundle. **Ordering fee (optional).** Paid to consensus stakers for optimistic ordering. This is independent of bundling. ### Cost model The bundler's cost per operation is: ``` cost = (BTC_fee / N) + operational_overhead ``` Where: * `BTC_fee` is the total Bitcoin transaction fee for the bundle * `N` is the number of operations in the bundle * `operational_overhead` covers bandwidth, computation, and infrastructure As `N` increases, the per-operation share of the Bitcoin transaction fee decreases proportionally to (1/N). For example, a bundler processing 1,000 operations pays roughly the same total Bitcoin fee as a bundler processing 10 operations, but spreads it across 100× more users. ### Fee Burn A fraction of bundling fees can be burned. The remainder goes to the bundler: ``` bundler_revenue = (1 - burn_rate) × bundling_fee ``` The burn creates deflationary pressure on KOR while ensuring bundlers remain profitable. ### Market Structure The bundling market is designed to be competitive: **Permissionless entry.** Anyone can run a bundler. There is no staking requirement, no approval process, no geographic restriction. **User choice.** Users can submit to any bundler, self-bundle, or submit to multiple bundlers simultaneously. There is no lock-in. **Price transparency.** Bundling fees are visible on-chain. Users can compare bundler prices and switch freely. These dynamics drive fees toward marginal cost. Bundlers compete on price, latency, and reliability. Over time, the market tends toward efficient pricing where bundlers earn normal returns on capital. *** For the complete specification and detailed benchmark results, see [Scalability Specification](/docs/resources/scalability). # Transaction Encoding Source: https://docs.kontor.network/docs/consensus/transaction-encoding Kontor uses a hybrid-UTXO model, in which assets can exist one of two states, and move fluidly between them, even within a single block. **Attached state:** Assets are "attached" to Bitcoin UTXOs, making them tradeable using standard Bitcoin tools: * PSBTs (Partially Signed Bitcoin Transactions) * Bitcoin Script interactions * Native Bitcoin wallet support * Atomic swaps with other Bitcoin metaprotocol tokens **Detached state:** Assets are "detached" for use in smart contracts: * Held in contract state * Subject to programmable logic * Can be transferred between contracts * Can cross-contract call with type safety This hybrid model gives Kontor unique interoperability—assets are first-class Bitcoin citizens while also supporting rich programmable logic. Users can perform single-confirmation atomic swaps between BTC, KOR, and other Bitcoin metaprotocol assets (Ordinals, Runes, Counterparty), or chain a swap directly into a contract call within a single Bitcoin transaction. **Example flow:** 1. User holds BTC, wants to call a Kontor contract (which requires KOR for gas) 2. User constructs a Bitcoin transaction that atomically: swaps BTC → KOR, then calls the contract with KOR 3. All happens in one transaction, one confirmation 4. No pre-funding, no separate accounts, no bridges # Dual-Token Model Source: https://docs.kontor.network/docs/economics/dual-token-model Bitcoin was designed as sound, deflationary digital money with predictable scarcity. This makes BTC excellent as a store of value and medium of exchange—digital gold. However, using BTC for application-layer services creates fundamental problems. Some projects attempt to use BTC as "gas" for computational services or try to fund long-term data storage with one-time BTC payments. These approaches misunderstand what BTC is and create economic problems. Kontor addresses this by introducing KOR, a token with economic properties purpose-built for application-layer services—gas metering and storage incentives. This allows BTC to remain what it is (digital gold) while enabling the functionality that applications need. ## BTC is Digital Gold, KOR is Digital Gasoline Gold's value comes from scarcity and durability—you hold it, you don't burn it. Gasoline's value comes from consumption—it powers activity, and its price reflects supply-and-demand balance. If gold were used as fuel, either it would be too expensive to use (making services uneconomical), or its consumption would be too high (destroying its monetary properties). The solution is to separate the store-of-value function from the utility function. **How it works in practice:** * BTC remains the unit of account—assets are priced in BTC, exchanges quote pairs against BTC, settlement happens in BTC * KOR is a metered input—users acquire what they need for immediate use through atomic swaps * Users pay both BTC fees (to miners for inclusion) and KOR fees (to the protocol for execution and storage) * The dual-token model ensures neither fee cannibalizes the other ## Economic Alignment with Bitcoin Every Kontor operation requires a Bitcoin transaction that pays Bitcoin miners. This is direct and unavoidable. The more successful Kontor becomes, the more valuable Bitcoin blockspace becomes. This stands in contrast to sidechains and rollups, which explicitly aim to move transaction volume off Bitcoin. Kontor's architecture is not parasitic—it strengthens Bitcoin's fee market and contributes to long-term security. *** For the perpetual emissions model that funds storage, see [Perpetual Economics](/docs/economics/perpetual). # Economic Security Source: https://docs.kontor.network/docs/economics/economic-security Cryptographic security ensures that only authorized parties can perform certain operations—you cannot spend someone else's Bitcoin without their private key. Economic security goes further: it ensures that even authorized parties who *could* misbehave choose not to, because doing so is unprofitable. In Kontor's storage protocol, nodes have possession of file data and could theoretically delete it, store only partial copies, or collude with others to manipulate rewards. The protocol cannot cryptographically prevent these actions. Instead, it makes them economically irrational. This approach relies on a fundamental assumption: storage nodes are profit-maximizing agents who respond to incentives. They will take any action that increases expected profit and avoid any action that decreases it. The protocol's security derives from structuring costs and rewards such that honest behavior—actually storing files, responding to challenges, and maintaining availability—yields higher expected returns than any alternative strategy. When this condition holds, rational operators naturally behave honestly even without surveillance or enforcement beyond what the protocol itself provides. The security analysis examines potential attacks not by asking whether they are technically possible, but whether they are economically viable. For each attack vector, the protocol must ensure that expected costs exceed expected benefits. This often involves making capital requirements dominate operational costs, ensuring penalties exceed gains, or structuring rewards so that cooperative behavior pays better than defection. The result is a system where security emerges from aligned incentives rather than imposed restrictions. ## Capital Cost Dominance The protocol's security fundamentally depends on capital costs exceeding storage costs. Each node must stake KOR proportional to its commitments: $k_{req}(n) = (\sum_f k_f) \cdot \lambda_{stake}(n)$, where the sum is over all stored files and $\lambda_{stake}$ is the dynamic stake factor. This staked capital cannot be used for other purposes, creating an opportunity cost $k_{req} \cdot \rho$ where $\rho$ is the operator's discount rate. With realistic parameters, this opportunity cost dwarfs the physical cost of storing data. This asymmetry is what enables economic security. Many attacks attempt to reduce costs while maintaining revenues: storing partial files and gambling on challenges, running many identity nodes on shared storage, or claiming to store files without actually possessing the data. All these attacks save on storage costs, which are linear in data size and relatively cheap (fractions of a cent per gigabyte-month). However, they cannot avoid capital costs. To earn rewards, a node must maintain adequate stake regardless of whether it actually stores the data. Since capital costs dominate by orders of magnitude, the savings from not storing data are negligible compared to the capital that must still be committed. The dominance relationship must hold across the full range of file sizes the protocol supports. For small files, physical storage costs approach zero, so capital costs obviously dominate. For large files, physical costs rise but remain sublinear relative to rewards due to logarithmic emission scaling. The protocol enforces a maximum file size $s_{max}$ partly to ensure this relationship remains valid—extremely large files could potentially reverse the dominance if storage costs grew large enough relative to emission-driven rewards. Within the bounded size range, honest storage remains the most capital-efficient strategy. ## Sybil Resistance via Dynamic Staking A Sybil attack involves creating multiple node identities to game the protocol. The naive version attempts to collect multiple reward streams while storing only one physical copy of the data—creating, say, ten node identities that all claim to store a file, earning ten times the rewards while paying only once for storage. This fails immediately due to capital cost dominance: the attacker must post full stake for each identity, paying ten times the capital cost to save on storage costs that are already negligible. The attack is obviously unprofitable. A more sophisticated version uses Sybil identities not to save storage costs but to compartmentalize risk. Large operators face correlated failure risks—a data center fire, network outage, or software bug could cause them to fail challenges on many files simultaneously. If all files are stored under one identity, such an event could lead to complete stake forfeiture via the cascading insufficient-stake mechanism. By fragmenting their portfolio across many small identities, operators can limit the damage from any single failure event, preserving most of their stake even if some identities are slashed. The protocol counters this through the dynamic stake factor $\lambda_{stake}(n) = 1 + \lambda_{slash} / \ln(2 + |F_n|)$, which imposes a capital penalty on nodes storing few files. A node storing a single file must stake $k_f \cdot \lambda_{stake}$ where $\lambda_{stake}$ can be 20-30× higher than for a large diversified node. To fragment a 100,000-file portfolio into individual single-file nodes would require roughly 20-30× more total capital than operating as a single large node. This makes risk compartmentalization prohibitively expensive—operators save more capital through consolidation than they gain through risk isolation. The logarithmic scaling of $\lambda_{stake}$ means the penalty decreases smoothly as portfolio size grows. A node with ten files faces a moderate penalty; a node with a hundred files faces a small penalty; a node with thousands faces essentially no penalty. This creates the right incentive gradient: small-scale operators can still participate profitably (the penalty isn't infinite), but large-scale operators are strongly incentivized to consolidate rather than fragment. The system achieves Sybil resistance without completely excluding small participants. ## Wash-Trading Prevention Wash-trading involves storing your own data to farm rewards: create a file agreement, run storage nodes to store it, collect emissions, and hope the rewards exceed the fees paid. This attack would allow users to mint KOR through self-dealing rather than providing genuine storage services to others. The protocol prevents this through the relationship between user fees and emission values. The user fee is $\upsilon_f = \chi_{fee} \cdot k_f$, where $k_f$ is the per-node base stake requirement and $\chi_{fee} \approx 0.003$ is a protocol parameter. This fee is paid once and burned immediately. The attacker's revenue comes from emissions $\varepsilon_f(t)$ distributed over time and divided among all nodes storing the file. For the attack to be profitable, the net present value of future emissions must exceed the upfront fee. The key parameter that prevents profitability is $\chi_{fee}$. Consider the simplified case where an attacker controls all nodes storing their own file. The file's emissions are $\varepsilon_f(t) = \varepsilon(t) \cdot (\omega_f / \Omega(t))$, and the attacker captures all of this. Over a planning horizon $h$ with discount rate $\rho$, the NPV is approximately $\varepsilon_f \cdot (1 - (1+\rho)^{-h}) / \rho$. For the attack to fail, we need $\upsilon_f > \text{NPV}$, which after substitution and simplification yields a condition on $\chi_{fee}$ relative to the emission-to-stake ratio and discount rate. With $\chi_{fee} = 0.003$ and realistic parameters (20% annual discount rate, typical emission levels), the upfront fee exceeds the recoverable emissions even over multi-year horizons. The attacker loses money on each wash-traded file. Furthermore, the attacker still faces ongoing costs—proving costs when challenged, opportunity costs on staked capital—that further reduce profitability. The mechanism doesn't rely on detecting wash-trading; it simply makes the economics unfavorable regardless of the attacker's intent. ## Collusion and Redistribution Storage nodes might collude to manipulate reward distributions. One scenario involves intentional slashing: multiple nodes storing the same file agree that one will deliberately fail a challenge, getting slashed, with the redistributed stake benefiting the co-conspirators. The question is whether the group profits collectively from this coordinated action. When a node is slashed an amount $S = k_f \cdot \lambda_{slash}$, a fraction $\beta_{slash}$ is burned and the remainder $(1 - \beta_{slash}) \cdot S$ is distributed equally among the other $(|N_f| - 1)$ nodes storing that file. For a colluding group of size $k$ out of $n$ total nodes, the group's net change is $-S + (k-1)/(n-1) \cdot (1-\beta_{slash}) \cdot S$. This is positive—the group profits—only if $(k-1)/(n-1) \cdot (1-\beta_{slash}) > 1$. Since both $(k-1)/(n-1) \leq 1$ and $(1-\beta_{slash}) < 1$, their product is always less than 1. The group always loses money on net. The burn fraction ensures that redistributed amounts are always smaller than slashed amounts, and the equal division among many nodes means even a large colluding group captures only a fraction of the redistribution. The collusion is strictly unprofitable—the group would be better off not executing the attack and avoiding the slashing entirely. Other collusion scenarios face similar structural barriers. Attempts to coordinate exits to trigger leave fees fail because fees are burned, not redistributed. Attempts to form cartels to monopolize files and extract rents fail because the sponsorship market allows any member to profitably defect. The protocol systematically avoids creating situations where coordinated misbehavior yields higher returns than independent honest behavior. ## Detection Economics The challenge mechanism provides probabilistic guarantees for detecting data loss. Each file expects $C_{target} = 12$ challenges per year regardless of network size. When challenged, a node must prove knowledge of $s_{chal} = 100$ randomly selected sectors from the file. The probability of detecting missing data depends only on the fraction lost, not the absolute file size. For a node that has lost fraction $\mu$ of its data, the probability of detection per challenge is approximately $1 - (1-\mu)^{s_{chal}}$. With $s_{chal} = 100$, even 10% data loss yields 99.997% detection probability per challenge. Given 12 challenges per year, the expected time to detection is measured in months, not years. For complete data loss ($\mu = 1$), detection is nearly certain on the first challenge—the annual detection probability is $1 - e^{-C_{target}} \approx 99.9994\%$. The economic implication is that selective storage—keeping only part of a file to reduce costs—carries enormous risk. The node saves marginal storage costs (already small relative to capital costs) but faces near-certain detection within a short timeframe. When detected, the node loses its stake $k_f \cdot \lambda_{slash}$ plus all future earning potential from that file. The expected value calculation is decisively negative: tiny savings versus large probable losses. This detection probability is what makes the capital requirements effective. Without reliable detection, nodes could safely store nothing and gamble on not being checked. With high detection probability, storing less than the full file is a losing bet. The challenge rate $C_{target}$ is calibrated to make the risk unacceptable—12 challenges per year provides multiple opportunities for detection within reasonable timeframes, ensuring that data loss is caught before files degrade beyond recovery thresholds (the protocol assumes erasure coding provides 10% fault tolerance). # Introduction Source: https://docs.kontor.network/docs/economics/introduction Bitcoin was designed as sound, deflationary digital money with predictable scarcity. This makes BTC excellent as a store of value and medium of exchange—digital gold. However, as developers have attempted to build applications on Bitcoin, they have often tried to use BTC itself for purposes it was never intended to serve. Some projects attempt to use BTC as "gas" for computational services, paying miners in BTC proportional to execution costs. Others try to fund long-term data storage with one-time BTC payments. These approaches fundamentally misunderstand what BTC is and create economic problems that undermine both the application layers and Bitcoin itself, not to mention fueling deep discord within the greater Bitcoin community. Kontor addresses this by introducing a token for its metaprotocol, KOR, with economic properties purpose-built for application-layer services—gas metering and storage incentives. This allows BTC to remain what it is (digital gold) while enabling the functionality that applications need. The challenge is structuring this dual-token model so that KOR complements rather than competes with Bitcoin, creating a synergistic relationship where both tokens' value propositions are strengthened. ## Economic Alignment with Bitcoin When users interact with Kontor—deploying contracts, calling functions, storing files—they must submit Bitcoin transactions that pay Bitcoin miners. This is not optional or mediated through bridges; it is direct and unavoidable. Every Kontor operation increases demand for Bitcoin blockspace, which increases fee revenue for miners, which strengthens Bitcoin's economic model. The more successful Kontor becomes, the more valuable Bitcoin blockspace becomes. This stands in contrast to sidechains and rollups, which explicitly aim to move transaction volume off Bitcoin. Their value proposition is "do more while using Bitcoin less"—execute thousands of transactions off-chain, then settle the results with a single Bitcoin transaction. This reduces the per-transaction cost to users, but it also reduces fee revenue to miners. When a rollup succeeds, Bitcoin miners earn less, not more. This creates misaligned incentives: rollup operators benefit from transaction volume that Bitcoin miners never see. The relationship is fundamentally extractive. Kontor's architecture is not parasitic. The protocol cannot move transactions "off-chain" because it has no separate chain. Scaling happens through efficiency in how data is encoded and how state is computed, but every state transition still requires a Bitcoin transaction, and there is a constant-factor relationship between Kontor data payload size and Bitcoin transaction fees. Users pay both BTC fees (to miners for inclusion) and KOR fees (to the protocol for execution and storage). The dual-token model ensures that neither fee cannibalizes the other—BTC compensates miners for the global service of transaction ordering and finality, while KOR compensates storage nodes for the application-specific service of maintaining protocol state and data. This alignment extends to Bitcoin's long-term sustainability. Bitcoin's security budget depends on transaction fees replacing diminishing block subsidies as issuance approaches zero. Kontor contributes to this fee market by adding a category of transactions that have inelastic demand—if you want to interact with Kontor applications, you must pay Bitcoin fees, regardless of the fee rate. This provides steady demand that supports miner revenue even as block subsidies decline. Kontor's success directly translates to Bitcoin's long-term security. ## Gas and Smart Contract Execution Attempting to use BTC as gas for smart contract execution creates fundamental problems. Gas systems require tokens whose supply can adjust to meet operational needs—users pay fees that compensate network operators for ongoing costs, and these operators need predictable revenue in terms that match their expenses. Bitcoin's deflationary model, by design, does not support this. BTC's fixed supply and diminishing issuance make it excellent as a store of value but problematic as a metered resource for computational services. The analogy is straightforward: BTC is digital gold, KOR is digital gasoline. Gold's value comes from scarcity and durability—you hold it, you don't burn it. Gasoline's value comes from consumption—it powers activity, and its price reflects supply-and-demand balance in the service market. If gold were used as fuel, either it would be too expensive to use (making the service uneconomical), or its consumption would be too high (destroying its monetary properties). The solution is to separate the store-of-value function from the utility function, using different tokens with different economic properties for different purposes. KOR has controlled inflation through emissions that reward storage nodes and other protocol participants. Users pay KOR for smart contract execution, and these fees are burned, creating deflationary pressure. The balance between emissions and burns determines net inflation, which adjusts based on protocol usage and network conditions. This is fundamentally different from BTC's fixed-supply model—KOR's supply expands and contracts in response to protocol needs, providing flexibility that enables sustainable service delivery. This dual-token model respects BTC's monetary properties while adding the flexibility needed for application-layer services. BTC remains the unit of account—assets are priced in BTC, exchanges quote pairs against BTC, and settlement happens in BTC. KOR is a metered input—users acquire what they need for immediate use, similar to how one buys gasoline. The relationship is complementary: demand for Kontor services increases demand for both KOR (to pay gas) and BTC (to pay miners), and neither token undermines the other's value proposition. ## Perpetual Storage Incentives Attempting to fund perpetual storage with one-time BTC payments faces an impossibility: a finite payment cannot fund infinite service. Even if you collect a large upfront fee, it will eventually be depleted—you cannot pay for decades of ongoing storage costs with a fixed amount paid today. Storage nodes incur continuous expenses for equipment, bandwidth, power, and facilities. These costs must be met year after year, indefinitely, yet the initial payment is spent once and is gone. Kontor addresses this through perpetual emissions: storage nodes earn KOR each block proportional to the files they store. This creates an ongoing revenue stream that lasts as long as the protocol exists. The system is designed so that for any rational operator, the net present value of future emissions exceeds storage costs, making it profitable to maintain files indefinitely. The user's one-time fee is burned rather than escrowed—it serves to prevent spam and create deflationary pressure, but it does not directly fund the storage. The actual funding comes from inflation, which can adjust dynamically based on network conditions. This emission-based model enables responsive incentives that fixed payments cannot provide. If replication falls below safe levels, emissions can increase, making storage more profitable and attracting additional nodes. If costs change—due to technology improvements, energy price fluctuations, or capital market conditions—nodes respond by entering or exiting based on profitability, and the protocol adjusts emissions to maintain adequate participation. The system self-regulates without requiring governance decisions or manual parameter updates. The long-term sustainability comes from balancing emissions against burns. In the early network, emissions dominate—the protocol is inflationary by design to bootstrap participation. As the network matures, burn mechanisms (file creation fees, smart contract gas, slashing, exit fees) remove KOR from circulation. Eventually, as smart contract usage grows and file creation slows, burns can offset emissions, stabilizing or even reversing inflation. The dual-token model enables this: BTC remains deflationary and appreciates, while KOR inflates or deflates based on protocol dynamics, providing the economic flexibility needed to guarantee perpetual storage. # Long-Term Economic Dynamics Source: https://docs.kontor.network/docs/economics/long-term-dynamics ## Dual-Token Equilibrium The protocol uses two tokens with fundamentally different purposes. BTC serves as the unit of account, the settlement asset, and the currency for pricing and trading. Users denominate value in BTC: a file costs $X$ satoshis to store, an asset trades at $Y$ satoshis, a service charges $Z$ satoshis. KOR serves as a metered resource for computational work (gas) and as the incentive mechanism for storage provision (emissions and stake requirements). This separation allows each token to have economics appropriate to its role. BTC is deflationary and scarce—properties that make it effective money but problematic as fuel. If smart contract gas were paid in BTC, the incentive structure for long-term storage would be difficult to sustain. Emissions in a deflationary currency create different dynamics than in an inflationary one: operators would need to forecast BTC appreciation when evaluating storage profitability, and the protocol would struggle to maintain adequate security budget as BTC becomes more valuable. Using BTC as fuel would also require Bitcoin users to spend their store-of-value asset for routine operations, creating psychological friction. KOR, on the other hand, is inflationary with dynamic adjustments—properties that make it not particularly well suited to serve as a store of value but which make it very effective as fuel and incentive mechanism. Users acquire KOR as needed for specific operations rather than holding it long-term. The atomic swap mechanism allows BTC holders to exchange for KOR and immediately use it for contract calls or storage fees within a single Bitcoin transaction confirmation. This removes friction from the user experience while preserving economic separation. The KOR/BTC exchange rate is market-determined, reflecting supply and demand dynamics. The dual-token structure provides an adjustment mechanism for protocol economics. If KOR inflation rises, market forces respond through the exchange rate: KOR becomes less valuable relative to BTC, making storage and smart contracts cheaper in BTC terms while maintaining the same KOR-denominated prices. This transfers inflation costs to KOR holders rather than to users who transact in BTC. The protocol's real security budget, measured in BTC terms, adjusts to market conditions rather than being rigidly fixed. ## Network Maturity and Inflation The inflation rate at any time is determined by the balance between emissions and burns: $\mu(t) = (\varepsilon(t) - \Phi_{burned}(t)) / KOR_{total}(t)$ where $\varepsilon(t)$ represents total emissions and $\Phi_{burned}(t)$ represents total burns during that period. In the early network, emissions dominate and $\mu(t)$ remains positive at roughly 5-10% annually (before dynamic health adjustments). As the network evolves, several factors reduce inflation. First, emission tapering means new files contribute less to total emissions than early files. The growth rate of $\varepsilon(t)$ slows even as file count increases. Second, the user fee mechanism burns a fraction of each file's effective emission value upfront—each new file pays $\upsilon_f = \chi_{fee} \cdot k_f$, calibrated to represent multiple years of that file's emission allocation. This creates immediate deflation with each file creation that partially offsets ongoing inflation from existing files. Third, smart contract gas burns create increasingly significant deflationary pressure as application usage grows. The protocol is designed to remain moderately inflationary even in steady state, providing ongoing security budget for storage. The economic model projects that under reasonable growth assumptions—file creation growing then stabilizing, smart contract usage increasing, network health maintained—net inflation gradually declines from initial rates toward lower single digits over a period of years. The exact trajectory depends on exogenous factors including market conditions, application adoption rates, and competing platforms. The dynamic emission multiplier $\alpha(t)$ can disrupt this trajectory. If network health deteriorates and $\alpha(t)$ remains elevated or reaches $\alpha_{max}$ for extended periods, emissions increase and inflation rises. The protocol prioritizes maintaining file availability over controlling inflation, up to a point. Stress conditions may moderately increase inflation in exchange for preserving data availability guarantees. ## Emission Tapering Mechanics A protocol promising perpetual storage faces an inflation problem: if emissions remain constant while file count grows without bound, either total emissions must increase proportionally (causing runaway inflation) or per-file emissions must decrease (eventually making storage unprofitable). Kontor addresses this through rank-based tapering, where each file's perpetual emission weight is fixed at creation time based on when it enters the network. The emission weight formula $\omega_f = \ln(s_{bytes}) / \ln(1 + rank_f)$ includes the file's creation rank in the denominator. Files created when the network is small receive permanently higher emission weights than identical files created later. A file created at rank 1,000 receives higher emissions than an identical file created at rank 1,000,000. This tapering occurs automatically without protocol changes or governance interventions. As the network grows and $\Omega(t) = \sum_f \omega_f$ increases, new files contribute proportionally less to the total. The growth rate of $\Omega(t)$ is sublinear—approximately $O(N \cdot \ln(\ln(N)))$ where $N$ is the total file count. Even as the network scales to billions of files, total emission weight grows slowly enough that per-file emissions for new files remain viable while total network emissions stay bounded relative to supply. Early storage nodes benefit by being "grandfathered" into higher reward tiers. A node joining when the network is small locks in higher emission weights on those early files permanently. This creates bootstrap incentives: early participants take on greater risk with unproven protocols and limited network effects, compensated through permanently higher returns. The mechanism aligns individual incentives with network growth—early nodes benefit as the network expands because their fixed high-weight files become relatively more valuable as new files arrive with lower weights. ## Smart Contract Fees The protocol's economic model anticipates a structural shift as the network matures. Initially, file storage dominates activity: users create file agreements, storage nodes earn emissions, and burn mechanisms (user fees, leave fees, slashing) remain modest compared to emissions. The network is intentionally inflationary during this growth phase to attract storage providers and bootstrap data availability. Emissions rise with file creation, but rank-based tapering keeps growth rate manageable. Over time, smart contract activity becomes increasingly significant. The file storage layer reaches a mature state where new file creation rate stabilizes—not because the protocol limits it, but because addressable demand plateaus. Meanwhile, smart contract usage continues growing as applications are built and adopted. DeFi protocols, asset management systems, and on-chain markets generate transaction volume that scales with economic activity rather than data storage needs. Each smart contract call consumes gas, and all gas fees are burned. This transition is critical for long-term sustainability. Storage emissions must continue indefinitely to maintain file availability, but unbounded emission rate growth would lead to unbounded inflation. The solution is not capping emissions, which would compromise storage guarantees, but developing sufficient burn rate from smart contracts to offset storage emissions. The economic model projects that mature network state involves storage emissions providing ongoing incentives for file availability while smart contract burns create comparable or greater deflationary pressure. Ultimately the users who burn fees for smart contract execution will be paying for perpetual storage indirectly. # Network Health and Stability Source: https://docs.kontor.network/docs/economics/network-health ## Measuring Network Health The file storage protocol makes a perpetual commitment: once a user pays to store a file, the network must maintain that file forever. This creates a coordination problem across thousands of independent storage nodes making individual economic decisions. The protocol cannot directly control whether nodes join or leave—it can only shape incentives. The challenge is to design an incentive structure that keeps files safely replicated even as node operators respond to changing profitability, market conditions, and operational costs. To manage this, the protocol needs a quantitative measure of whether the network is fulfilling its commitments. The most obvious metric would be counting how many files are at or near minimum replication, but this is reactive—it measures failure rather than health. A better approach is to track the aggregate replication level across the network and use this as a forward-looking indicator. When aggregate replication is high, the system has buffer against shocks; when it falls, the system needs to respond before individual files become critically under-replicated. Kontor uses emission-weighted replication $R(t) = \sum(\omega_f \cdot |N_f|) / \Omega(t)$, where each file's replication count is weighted by its emission weight $\omega_f$. This means economically important files—those created early or those that are larger—contribute proportionally more to the metric. A file representing 10% of network emissions contributes 10% to the health measurement. This weighting ensures the protocol prioritizes maintaining replication on high-value files, where "value" is defined by the file's ongoing cost to the network in terms of inflation. The metric is smoothed over a trailing window using a weighted moving average, giving more weight to recent observations. This filtering prevents the system from overreacting to temporary fluctuations while still responding to genuine trends. The critical reference point is $n_{min}$, the minimum required replication. The health buffer $m(t) = R(t) - n_{min}$ represents how much room exists above this threshold. A large positive buffer indicates a healthy network; a shrinking buffer signals growing risk. ## Dynamic Emission Response The protocol's ability to guarantee perpetual storage depends on maintaining adequate economic incentives for storage nodes. If rewards fall too low relative to costs, rational operators will exit, reducing replication and potentially causing files to drop below minimum thresholds. The protocol cannot force nodes to participate, but it can adjust the reward structure to make participation more attractive when needed. This is achieved through a dynamic emission multiplier $\alpha(t)$ that modulates the baseline inflation rate based on network health. Total emissions each block are $\varepsilon(t) = \text{KOR}_{total}(t-1) \cdot \alpha(t-1) \cdot \mu_0 / B$, where $\mu_0$ is the baseline annual inflation rate and $\alpha(t)$ adjusts in response to the health buffer. When the buffer is large, $\alpha(t)$ stays near 1, keeping inflation at the baseline. As the buffer shrinks, $\alpha(t)$ increases, raising emissions and making storage more profitable. If the buffer reaches zero—meaning weighted average replication has fallen to the minimum threshold—$\alpha(t)$ immediately jumps to its maximum value $\alpha_{max}$. This counter-cyclical policy is analogous to monetary policy in traditional economies: expand the money supply during stress, contract during stability. The mathematical form is $\alpha(t) = \min(\alpha_{max}, 1 + \kappa_\alpha / m(t))$ when $m(t) > 0$, creating an inverse relationship where the emission multiplier accelerates as problems worsen. A small deterioration produces a modest increase; a severe deterioration produces a strong response. The parameter $\kappa_\alpha$ controls sensitivity—how aggressively the system responds to changes in health. The adjustment mechanism is deterministic and transparent. Every participant can observe the health metric and calculate the resulting emission rate. This predictability allows nodes to forecast expected rewards and make informed decisions about participation. The protocol updates the multiplier at the end of each block based on current conditions, and this updated value determines emissions for the subsequent block. ## Feedback Loops and Equilibrium Understanding system stability requires examining the feedback loops that connect network health, emissions, and node behavior. The intended stabilizing mechanism works as follows: declining replication triggers higher emissions, which increases per-node profitability, which attracts new participants or retains existing ones, which raises replication, which eventually reduces emissions back toward baseline. This negative feedback loop creates a self-regulating system that tends toward equilibrium without requiring governance intervention. However, the system also contains a potentially destabilizing positive feedback loop operating through external market prices. If the KOR/USD exchange rate falls, storage nodes' fiat-denominated profitability declines even if KOR-denominated rewards remain constant. This may cause exits, reducing replication and triggering emission increases. Higher emissions expand KOR supply, which can put additional downward pressure on the exchange rate, amplifying the initial shock. This pro-cyclical dynamic represents a systemic vulnerability, particularly during market stress or loss of confidence. The overall stability depends on which feedback loop dominates. When the health buffer is positive and the emission multiplier is below its maximum, the stabilizing monetary policy can effectively counteract deteriorating conditions by making storage more profitable. The system self-corrects through purely economic mechanisms. However, if conditions worsen to the point where $\alpha(t) = \alpha_{max}$ for extended periods, the stabilizing mechanism saturates—it has no more room to increase incentives. At this point, recovery depends on exogenous factors: improved market sentiment, reduced operational costs, or increased demand for storage services. This design accepts that no purely endogenous mechanism can guarantee stability under all conditions. Instead, it provides strong self-correction within a bounded range of stress, while acknowledging that extreme shocks may require time and favorable external conditions to resolve. The emission cap $\alpha_{max}$ represents an explicit trade-off: it prevents unlimited inflation but also limits the protocol's corrective power during severe crises. ## The Vulnerability Penalty Beyond adjusting global emissions, the protocol uses targeted mechanisms to prevent cascading failures. The leave fee $\varphi_{leave}(f,t) = k_f \cdot (n_{min} / |N_f|)^2$ creates a direct cost for exits that scales with file vulnerability. When a file has replication far above the minimum, the fee is negligible—nodes can leave freely when storage becomes unprofitable. As replication approaches $n_{min}$, the fee grows quadratically, becoming prohibitively expensive precisely when additional exits would be most harmful. This structure creates hysteresis in node behavior. Nodes join when expected profit exceeds zero, but they require much larger losses before exiting becomes rational due to the leave fee. The gap between entry and exit thresholds stabilizes replication by preventing churn—nodes do not repeatedly join and leave in response to small profit fluctuations. For files near minimum replication, the high exit barrier means nodes must experience substantial sustained losses before departure makes economic sense, providing a cushion against cascading exits during temporary market stress. The fee is burned rather than redistributed to remaining nodes. This design prevents strategic behavior where nodes might engineer exits to capture redistributed fees, or where colluding groups might profit from coordinated departures. Burning ensures the decision calculus is simple: exiting costs you money with no compensating benefit to anyone. This makes leaves a pure deadweight loss that rational operators prefer to avoid. ## Self-Correction Mechanisms The protocol's self-correcting behavior emerges from the interaction of multiple simultaneous mechanisms. When a file becomes under-replicated, the emission-weighted health metric $R(t)$ falls, triggering increased total emissions through $\alpha(t)$. These higher emissions distribute across all files proportionally to their weights, but under-replicated files see larger per-node increases because the same total is divided among fewer participants. The leave fee increases quadratically, raising the cost of further exits. If nodes fail challenges and get slashed, a portion of their slashed stake redistributes to remaining honest nodes, directly improving their profitability. These forces compound to create progressively stronger correction as problems worsen. Small deviations from equilibrium produce gentle nudges; large deviations produce powerful responses. No central coordination is required. Each node makes independent decisions based on local profitability calculations, but the aggregate behavior stabilizes because individual incentives are designed to align with collective network needs. A rational operator seeking to maximize profit will naturally choose actions that support network health. The system's dynamic characteristics—response speed, stability margins, tendency to overshoot or oscillate—depend on protocol parameters like the health averaging window $W_R$ and emission sensitivity $\kappa_\alpha$. These represent fundamental trade-offs. Longer averaging windows make the system more stable against noise but slower to detect genuine problems. Higher sensitivity produces faster corrections but risks overreacting to transient conditions. The chosen parameters reflect a balance informed by economic modeling and simulation, designed to provide adequate responsiveness without inducing volatility or instability in the emission rate. # Storage Node Economics Source: https://docs.kontor.network/docs/economics/node-economics Storage nodes are modeled as rational economic actors seeking to maximize profit. The protocol does not rely on altruism or assume operators will act against their financial interests. Instead, it structures incentives so that profit-maximizing behavior aligns with protocol goals: storing files reliably and responding to challenges honestly. A node's decision to join, remain in, or exit a file agreement is determined by comparing the net present value of expected revenues against costs over some time horizon. If this calculation is positive, the node participates; if negative, it exits (when permitted) or declines to join. The protocol must ensure that for any file with sufficient replication, honest storage is more profitable than any alternative strategy—including storing nothing, storing partial data, or colluding with other nodes. This is achieved through a combination of ongoing rewards, capital lock-up requirements, and penalties for detectable misbehavior. The economic parameters are calibrated so that deviating from honest behavior either reduces expected revenue or increases risk beyond what rational operators will accept. ## Revenue Model: Emissions and Redistribution A storage node's primary revenue is KOR emissions. Each block, a node storing file $f$ receives $r_{storage}(n,f,t) = \varepsilon_f(t) / |N_f|$, where $\varepsilon_f(t)$ is the file's emission allocation and $|N_f|$ is the number of nodes storing it. This base reward is modified by sponsorship agreements—if the node is sponsored, it pays a commission to its sponsor; if it sponsors others, it earns commissions from them. The total revenue scales inversely with replication: fewer nodes storing a file means higher per-node rewards, creating a natural incentive for nodes to join under-replicated files. Secondary revenue comes from slashing redistribution. When a node fails a challenge, it is slashed an amount $k_f \cdot \lambda_{slash}$, of which a fraction $(1 - \beta_{slash})$ is distributed equally to the other nodes storing that file. The expected value of this revenue is $E[r_{slash}] = (p_f / |N_f|) \cdot p_{fail} \cdot (1 - \beta_{slash}) \cdot k_f \cdot \lambda_{slash}$, where $p_f$ is the per-file challenge probability and $p_{fail}$ is the probability any given node fails when challenged. This creates an additional incentive for honest behavior—by maintaining their data and responding to challenges, nodes not only avoid being slashed themselves but also benefit when others fail. Sponsorship provides a third revenue stream. When a new node (the "entrant") needs file data and cannot obtain it off-chain, it can enter a time-limited agreement with an existing storer (the "sponsor"). The sponsor provides the data in exchange for a commission rate $\gamma_{rate}$ on the entrant's rewards for duration $\gamma_{duration}$. These terms are negotiated off-chain and recorded on-chain. For the sponsor, the NPV of the commission stream must exceed the bandwidth cost of transferring the file. For the entrant, paying the commission must be cheaper than alternative methods of obtaining the data or more profitable than not storing the file at all. ## Cost Structure and Capital Requirements Storage nodes face three categories of costs. Physical storage costs—the cost of disk space, bandwidth for data repair and replication—are typically small and scale linearly with file size. Proving costs arise from the need to periodically submit storage proofs on-chain. Nodes can aggregate multiple challenges into a single Bitcoin transaction during the $W_{proof}$ window (\~2 weeks), amortizing the transaction fee across many proofs. With typical parameters, proving costs are negligible for operators with diversified portfolios. Capital costs dominate. Each node must maintain staked KOR proportional to its commitments: $k_{req}(n) = (\sum_f k_f) \cdot \lambda_{stake}(n)$, where the sum is over all files the node stores and $\lambda_{stake} = 1 + \lambda_{slash} / \ln(2 + |F_n|)$ is a dynamic stake factor that penalizes fragmentation. This staked KOR cannot be used for other purposes, creating an opportunity cost $k_{req} \cdot \rho$ where $\rho$ is the operator's discount rate. For a file with base stake requirement $k_f$, emission rate $\varepsilon_f$, and replication $|N_f|$, the break-even condition is approximately $(\varepsilon_f / |N_f|) \approx k_f \cdot \lambda_{stake} \cdot \rho$, with physical storage and proving costs as second-order terms. The design deliberately makes capital costs dominate storage costs. This asymmetry is what enables security: attacks that attempt to collect rewards without actually storing data (such as storing only partial files and gambling on challenges, or running many Sybil identities on shared storage) must still bear full capital costs. The protocol's security assumption is that capital is more expensive than storage, which holds true across realistic parameter ranges and makes honest storage the most profitable strategy. ## Node Decision Framework Nodes evaluate decisions by computing the net present value of expected profit over a planning horizon $h$. To decide whether to join a file agreement, a node calculates $\text{NPV}(\{E[\pi(n, t+i | F_n \cup \{f\})] - E[\pi(n, t+i | F_n)]\}, \rho, h)$ and joins if positive. This marginal analysis accounts for the additional revenue from the new file, the increased stake requirement (which may change $\lambda_{stake}$ if the portfolio is small), changes to proving costs, and physical storage costs. Nodes forecast future parameters—replication levels, exchange rates, Bitcoin fee rates—based on current conditions. To decide whether to leave, a node compares the NPV of future profit with and without the file, minus the leave fee $\varphi_{leave}(f,t) = k_f \cdot (n_{min} / |N_f|)^2$. The leave fee increases quadratically as replication approaches the minimum threshold $n_{min}$, making exits increasingly expensive precisely when files are most vulnerable. Leaving is only permitted when $|N_f| > n_{min}$, and even then, the node must have sufficient liquid balance to pay the fee. This creates hysteresis: nodes require a substantial profit deterioration before exiting becomes rational. When challenged, a node must decide whether to generate and submit a proof. The decision is straightforward: if the cost of proving (in BTC transaction fees) is less than the total loss from being slashed—which includes not just the immediate stake loss $k_f \cdot \lambda_{slash}$ but also all future earnings from that file—the node proves. The protocol ensures this inequality holds for honest nodes: proving costs are designed to be far smaller than slashing penalties. For dishonest nodes that deleted the data, generating a valid proof is impossible regardless of cost. ## The Sponsorship Market The sponsorship mechanism solves a bootstrapping problem: how do new nodes obtain file data when they don't already have it? Nodes will not join a file agreement without being able to respond to challenges, which requires possessing the actual data. If all existing storers refuse to share, they create a permanent monopoly with no possibility of new entrants. The sponsorship market breaks this by making it profitable for existing storers to provide data to newcomers. A sponsorship agreement is an on-chain contract specifying the entrant, sponsor, file, commission rate $\gamma_{rate}$, and duration $\gamma_{duration}$. During the agreement, the entrant pays $\gamma_{rate}$ fraction of its file rewards to the sponsor. This creates a competitive market: multiple existing storers can offer to sponsor, competing on commission rates and terms. The equilibrium commission rate $\gamma_{eq}$ balances the sponsor's bandwidth cost against the NPV of the commission stream. For typical files and parameters, $\gamma_{eq}$ is low enough that entrants find sponsorship attractive while sponsors earn meaningful returns. This market structure prevents gatekeeping. Even if a cartel of existing nodes attempts to monopolize a file by refusing to share data, any single cartel member has an incentive to defect and capture the entire sponsorship revenue for themselves. The protocol does not mandate sponsorship—nodes can join files directly if they obtain data through other means—but it provides a permissionless fallback that ensures no file can be permanently locked to a closed group. This keeps the network open and maintains the property that storage rewards naturally attract new participants when existing nodes are earning excess profits. # Perpetual Storage Economics Source: https://docs.kontor.network/docs/economics/perpetual Traditional cloud storage operates on a subscription model: users pay monthly or annually, and if payments stop, data disappears. This model is incompatible with blockchain use cases, where assets and applications need to reference data that must remain available indefinitely. Storing data directly on Bitcoin is one solution—it's permanently available in the blockchain—but it's prohibitively expensive and imposes costs on the entire network. A file stored on-chain must be downloaded and validated by every full node forever. Kontor addresses this by separating data availability from blockchain storage: file metadata and cryptographic proofs anchor on Bitcoin, while the data itself lives off-chain with storage nodes who are economically incentivized to maintain it. The fundamental challenge is sustainability: how can a one-time upfront payment from a user fund perpetual storage? The naive approach would be to collect a large enough fee to cover expected storage costs forever, but this requires predicting future costs decades in advance and creates perverse incentives—nodes might prefer the protocol to fail early so they can claim the remaining endowment. Kontor instead treats storage as an ongoing service funded by continuous token emissions, with economic mechanisms designed to ensure rational operators find it profitable to store data long-term. ## Emission-Based Incentives Storage nodes earn KOR through block emissions tied to the files they store. Each block, the protocol distributes newly minted KOR to storage nodes in proportion to the emission weights of the files they're storing. This creates a perpetual revenue stream: as long as the protocol exists and emits KOR, storage nodes have an economic reason to maintain their data. The system is designed so that for any rational operator, the net present value of future emissions exceeds the costs of storage, making it profitable to continue storing files indefinitely. Users pay a one-time fee when creating a file agreement, but this fee is entirely burned—it doesn't fund the storage directly. Instead, the fee serves two purposes: it prevents spam by imposing a cost barrier, and it creates deflationary pressure to offset the inflationary emissions. The actual incentive for storage comes from emissions, which scale with the total KOR supply and adjust dynamically based on network health. This separation of concerns—users pay to create scarcity, emissions fund the service—is what allows the system to function without predicting future economics. ## Logarithmic Scaling and Emission Weights Each file receives an emission weight $\omega_f = \ln(s_{bytes}) / \ln(1 + rank)$, where $s_{bytes}$ is the file size in bytes and $rank$ is its creation order. This logarithmic relationship is a core design principle: the economic value of data is largely independent of its physical size. A 1 KB file containing a private key may be far more valuable than a 1 GB video file. Linear scaling would create a structural bias where large files dominate emissions, making small but critical files economically unattractive to store. Logarithmic scaling compresses the size difference: a 1 GB file has only about twice the emission weight of a 1 MB file, ensuring that storage nodes find both worth maintaining. The emission weight determines a file's share of total network emissions. Each block, total emissions $\varepsilon(t)$ are calculated based on current supply and network health, then distributed to files in proportion to their weights: $\varepsilon_f(t) = \varepsilon(t) \cdot (\omega_f / \Omega(t))$, where $\Omega(t)$ is the sum of all file weights. These file-level emissions are then split equally among the nodes storing that file. This proportional allocation means that a file's ongoing "cost" to the network (in terms of inflation) corresponds to its relative importance, not its absolute size. ## Emission Tapering Through Rank The rank term in the emission weight formula—$\ln(1 + rank)$—creates a "grandfathering" effect where earlier files receive permanently higher rewards than later ones. A file created when the network has 1,000 files gets a higher weight than an identical file created when the network has 1,000,000 files. This is deliberate: as the network grows and total emissions increase, new files receive proportionally smaller shares, preventing runaway inflation. The system naturally tapers without requiring protocol upgrades or governance decisions. This rank-based tapering ensures that total emissions grow sublinearly with network size. Even as the network scales to billions of files, the aggregate emission rate remains bounded because each new file contributes less to $\Omega(t)$ than the previous one. Early storage nodes are rewarded for bootstrap risk, and the emission weight of their files becomes increasingly valuable relative to newer files. Meanwhile, new files remain profitable to store because their stake requirements (which are also based on emission weights) scale down proportionally with their emission potential. ## Deflationary Mechanisms Emissions create inflation; burns create deflation. Kontor implements several burn mechanisms that remove KOR from circulation. When a user creates a file agreement, the entire storage fee $\upsilon_f$ is burned. When nodes voluntarily exit agreements, they pay a leave fee that is burned. When nodes are slashed for failing challenges, a portion ($\beta_{slash}$) of the slashed amount is burned, with the remainder redistributed to honest nodes. Most significantly, all gas fees from smart contract execution are burned. The interplay between emissions and burns determines long-term inflation. In the early network, storage-related burns (file creation, exits, slashing) dominate but remain relatively modest—the network is inflationary by design to incentivize growth. As the network matures and file creation slows, storage emissions stabilize while smart contract activity (and therefore gas burns) continues growing. The system is architected so that eventually, gas burns from a thriving application ecosystem offset storage emissions, bringing net inflation down even as the protocol continues to guarantee perpetual storage through ongoing rewards. # Welcome to Kontor! Source: https://docs.kontor.network/docs/introduction/home
Kontor is a next-generation Bitcoin metaprotocol that brings programmable assets, scalable file storage, high-throughput and low-latency execution directly to the Bitcoin blockchain.
**Current Status:** Kontor is under active development with Bitcoin `signet` support. For the latest updates, please see the [Kontor website](https://kontor.network). ## Fast and Scalable Kontor's optimistic consensus layer provides transaction confirmations in 1–2 seconds while Bitcoin remains the ultimate source of finality. Bundling amortizes Bitcoin transaction fees across thousands of operations, bringing per-operation costs down significantly. Combined with BLS signature aggregation and compact data encoding, Kontor processes over 1,000 transactions per second—all entirely on the Bitcoin blockchain. [Learn about how Kontor extends Bitcoin's consensus system →](/docs/consensus/introduction) ## Smart Contracts with Sigil Write, deploy and run rich smart contract applications directly on the Bitcoin blockchain. The Sigil framework uses the WebAssembly Component Model to make writing contracts easier than ever before, with strong type safety, native cross-contract calls and best-in-class developer ergonomics. [Start building with the Sigil framework →](/docs/sigil/introduction) ## Perpetual File Storage Pay once, store forever. Kontor's storage protocol provides scalable, permanent file storage where the data lives off-chain but the storage *system* itself is entirely on-chain. Kontor supports files from kilobytes to gigabytes with logarithmic cost scaling and strong security guarantees. [Learn about Kontor's scalable storage system →](/docs/storage/introduction) ## Kontor Economics The Kontor economic model was designed from first principles to support data storage and smart contract execution. BTC remains the unit of account while KOR meters execution and incentivizes storage through perpetual emissions. Dynamic mechanisms ensure network health and long-term sustainability. [Explore the economics of the Kontor ecosystem →](/docs/economics/introduction) ## Resources Access technical specifications, comprehensive FAQs, and developer resources. [View all resources →](/docs/resources/) # How Does Kontor Work? Source: https://docs.kontor.network/docs/introduction/how-it-works **Kontor is a next-generation metaprotocol on Bitcoin.** A metaprotocol is a set of rules for interpreting Bitcoin transactions in a way that Bitcoin itself doesn't recognize—adding functionality on top of Bitcoin without changing Bitcoin's consensus rules. Other metaprotocols, like Colored Coins, Counterparty, Ordinals, Runes, BRC-20 and Alkanes all work the same way: they use Bitcoin for transaction ordering and finality, but add meaning to transactions that Bitcoin miners don't need to understand. Kontor distinguishes itself by providing rich smart contracts with expressive languages, native composability between contracts, and deep integration with Bitcoin's transaction model. The aim is to support the kind of complex financial applications—intricate asset logic, composable DeFi protocols, and programmable primitives—that have historically required purpose-built blockchains, while preserving the security and value alignment that make Bitcoin metaprotocols distinctive. **Kontor transactions embed data in Bitcoin witnesses.** Kontor uses Taproot to encode its transactions. Each Kontor operation is a commit-reveal pair: you create a Bitcoin transaction (the "commit") with a special Taproot output, then spend it immediately (the "reveal") with a script that contains your actual Kontor data. The data sits in Bitcoin's witness section—the part of a transaction that stores signatures and other information that doesn't affect the core transaction graph. Bitcoin nodes relay these transactions, miners include them in blocks, and the data is preserved in the blockchain forever, but Bitcoin itself doesn't execute any Kontor logic. To Bitcoin, these are just normal transactions paying normal fees. **Indexers interpret the protocol.** The key innovation of metaprotocols is that protocol state is derived, not enforced by miners. Anyone can run a Kontor indexer—software that scans Bitcoin blocks, finds Kontor transactions, and processes them according to Kontor's rules. If you deploy a smart contract, the indexer stores the contract's WebAssembly bytecode. If you call a contract function, the indexer executes that function in a sandboxed environment and updates the contract's state. Crucially, every indexer that processes the same Bitcoin history will arrive at the same state, because the rules are deterministic. You don't need to trust a specific indexer; you can verify the state yourself by running your own. **Metaprotocols vs sidechains and rollups.** The metaprotocol architecture is fundamentally different from Layer-2 approaches like sidechains or rollups. Sidechains have their own consensus mechanisms and validators—you're trusting a separate network to correctly process transactions and honestly report state back to Bitcoin. Rollups use cryptographic proofs to reduce trust, but still operate as separate systems that must bridge assets on and off Bitcoin, creating economic friction and competing with Bitcoin for transaction fees. Metaprotocols, by contrast, have the same security model as Bitcoin itself: every transaction is a Bitcoin transaction, secured by Bitcoin's full hashpower. The only difference is that users must run or trust an additional codebase (the indexer) to interpret protocol state. This makes metaprotocols fundamentally synergistic with Bitcoin—they increase network activity, pay miners more fees, and extend Bitcoin's functionality without requiring changes to Bitcoin or creating alternative consensus mechanisms. *** For details on staker security and slashing conditions, see [Optimistic Consensus](/docs/consensus/optimistic-consensus). For smart contract security, see [Contract Security](/docs/sigil/security). For storage network security, see [Storage Security](/docs/storage/storage-security). # What is Kontor? Source: https://docs.kontor.network/docs/introduction/what-is-kontor Kontor is a Bitcoin metaprotocol that brings programmable assets and verifiable long-term file storage directly to Bitcoin. Unlike sidechains or rollups that create separate networks, Kontor operates entirely on Bitcoin itself—every Kontor transaction is a Bitcoin transaction, secured by Bitcoin's full hashpower and finality guarantees. This means you can build and deploy sophisticated applications on Bitcoin without leaving the Bitcoin network or trusting additional consensus mechanisms. At its core, Kontor addresses what blockchains are actually used for: creating, managing, and transferring digital assets. It provides two key capabilities that Bitcoin alone cannot offer. First, **Sigil**, a next-generation WebAssembly-based smart contract framework that lets developers write expressive, type-safe contracts in familiar languages like Rust. Second, a **file persistence protocol** that stores data off-chain with cryptographic proofs and economic incentives anchored on Bitcoin, solving the problem of expensive on-chain storage without sacrificing persistence or availability. Kontor introduces **KOR**, a native token designed specifically for metering smart contract execution and incentivizing perpetual file storage. KOR complements rather than competes with BTC: Bitcoin remains the settlement layer and unit of account, while KOR acts as "gas" for computation and storage rewards. This dual-token model respects BTC's role as digital gold while providing the economic infrastructure needed for a thriving application ecosystem. While Kontor settles on Bitcoin, it doesn't inherit Bitcoin's confirmation latency. Through **optimistic consensus**, a network of KOR stakers coordinates transaction ordering before Bitcoin confirmation, providing 1–2 second finality backed by slashable collateral. Combined with BLS signature aggregation and compression, Kontor achieves over 1,000 transactions per second—all recorded on Bitcoin. Importantly, Kontor is fully interoperable with Bitcoin and other Bitcoin metaprotocols like Ordinals, Runes, and Counterparty. Through a novel hybrid UTXO model, assets can be "attached" to Bitcoin UTXOs for atomic swaps and native Bitcoin Script interactions, then "detached" for use in smart contracts—all in a single confirmation. This means any Bitcoin user can interact with Kontor immediately, using standard Bitcoin addresses and wallets, without bridging assets or managing separate accounts. Kontor extends Bitcoin's functionality without changing its security model or value proposition. It's a practical path toward decentralized finance on Bitcoin that miners are incentivized to support, developers can build on today, and the broader Bitcoin community can embrace as genuinely Bitcoin-native infrastructure. # Why Build on Kontor? Source: https://docs.kontor.network/docs/introduction/why-kontor **Build where the value is.** Bitcoin is the deepest, most liquid, and most trusted blockchain network in the world. By building on Kontor, you're tapping directly into Bitcoin's ecosystem—its users, its capital, its security guarantees—without the friction of bridges, wrapped tokens, or convincing people to move to yet another chain. Your applications settle on Bitcoin itself, with the same finality guarantees as BTC transactions. There's no separate network to bootstrap, no additional consensus to trust, and no parasitic relationship with the underlying chain. **Fast and scalable.** Kontor's optimistic consensus layer provides 1–2 second transaction confirmations backed by staker collateral, while Bitcoin remains the ultimate source of finality. Through BLS signature aggregation and compression, Kontor achieves over 1,000 TPS, with dramatically reduced transaction fees—all settling directly on Bitcoin. You get modern application speed without leaving Bitcoin. **Developer experience.** Kontor's Sigil framework is built on WebAssembly and designed to make writing smart contracts feel like writing normal software. If you know Rust, you can start building immediately—no learning obscure DSLs or fighting with baroque tooling. Sigil offers strong compile-time type safety, native cross-contract calls, and an expressive storage interface that elegantly encodes rich application logic. **True interoperability.** Kontor offers single-confirmation atomic swaps with Bitcoin and all major Bitcoin metaprotocols—Ordinals, Runes, Counterparty. You can chain a BTC-to-KOR swap directly with a contract call, meaning any Bitcoin user can interact with your application immediately without leaving their wallet or pre-funding a separate account. The hybrid UTXO model treats assets as first-class citizens: they attach to UTXOs for PSBT-based trading and detach for programmable logic. This level of interoperability isn't possible on sidechains or rollups; it requires being genuinely on-chain. **Aligned with Bitcoin.** Unlike Layer-2s that siphon fees and activity away from Bitcoin, Kontor is synergistic: every Kontor transaction pays Bitcoin miners, every storage agreement increases on-chain activity, and the protocol operates to expand BTC's utility without competing for its monetary premium. KOR is a metered resource for execution and storage, complementary by design. Miners are incentivized to support Kontor because it makes their blocks more valuable without challenging Bitcoin's core value proposition. # Kontor vs. Alkanes Source: https://docs.kontor.network/docs/resources/alkanes Conceptually, Alkanes and Kontor are very much aligned. Both are metaprotocols that bring programmability to Bitcoin with a WASM-based smart contract system. In practice, however, they are quite different to work with. Most significantly, Alkanes is: 1. Fully reliant on Bitcoin’s UTXO model 2. Does not have a gas currency 3. Builds on top of Runes infrastructure Kontor, by contrast, has a **hybrid accounts/UTXO model** (first implemented in Counterparty), that supports interoperability with Bitcoin via PSBTs and atomic swaps. Kontor has a sophisticated tokenomics for its native currency, which is the basis for its gas system and provides incentives for perpetual Bitcoin-anchored file storage. Kontor’s Sigil smart contract framework emphasizes **clarity, ergonomics, and guardrails** with typed entrypoints, structured state, a testing-friendly design. # Smart Contracts ## Programming Model and ABI With Alkanes, each contract implements a single WASM entrypoint and **manually dispatches on numeric opcodes**, parsing raw input bytes, and populating a response buffer. For each contract, you have to implement `AlkaneResponder::execute`, a required `__execute` export, and a `match` over integer opcodes, plus a **DIY key-value storage** layout built from “pointers” (string keypaths). This is a direct consequence of over-reliance on the “Protorunes” specification: method invocations are encoded as Protorunes messages in `OP_RETURN`, with inputs packed into “cellpacks.” That means all decoding, validation, and error reporting live in the custom opcode handlers. Sigil’s core idea—*first-class, typed contract methods*—makes contracts read like ordinary Rust services: multiple named entrypoints, explicit parameter and return types, and interface boundaries that the compiler can check. That yields self-documenting interfaces/ABIs, clear call sites, and predictable error surfaces. It also unlocks straightforward scaffolding (client SDKs, mocks, auto-generated bindings) without bespoke conventions, and Sigil makes malformed calls impossible to even *construct* in most client code. ## Storage Alkanes' **string-keyed key-value storage** pushes a lot of design burden onto developers: you invent your own keyspace, document it, and keep it consistent across versions and tooling. A ledger becomes `"/balance/{addr}"`; a cap becomes `"/cap"`, etc. This is fine for small demos, but entropy accumulates in real projects, especially those with multiple authors. Sigil's storage model uses **typed, structured storage with derive macros** (`StorageRoot`, `Storage`) that generate ORM-like interfaces at compile time. Instead of manually constructing string keys and calling generic `get_value::()` / `set_value::()` functions, you declare Rust structs and the framework generates type-safe accessor methods. For example, a `TokenStorage` struct with a `ledger: Map` field automatically gets `storage(ctx).ledger().get(ctx, key)` and `storage(ctx).ledger().set(ctx, key, value)` methods—no string keys to type, no risk of typos or collisions, and full IDE autocomplete support. This approach provides several advantages: **compile-time type checking** catches storage access errors before deployment; **nested field access** (e.g., `account.balance(ctx)`) reads specific fields without deserializing entire structures; and **refactoring safety**—renaming a field updates all access points automatically. The framework also generates storage "wrappers" for nested structs, allowing you to read or update sub-fields in complex data structures without manual keyspace management. In Alkanes, equivalent functionality requires careful string interpolation, manual (de)serialization, and vigilant documentation to avoid conflicts between similarly-named keys. ## Safety Alkanes contracts have **huge surface area**, so there’s a significant risk of reentrancy attacks, callee-controlled control flow, and the need for manual checks-effects-interactions discipline. Alkanes does provide a “call without side effects” mode, but that’s an opt-in pattern; the default is general side-effecting calls. Sigil’s typed, multi-entry interface encourages **capability scoping**—each method can enforce narrow invariants and accept strongly typed inputs. It also makes it natural to *ban or constrain risky cross-contract patterns* (e.g., reentrancy-enabling call cycles, which are simply disabled, or storage-context borrowing) at the framework layer. ## Tooling, Testing, and DX A Sigil-style contract is just **Rust with a clean contract boundary.** The natural dev loop is `cargo test` against in-memory harnesses and method-level unit tests; your IDE and linter do the heavy lifting. Mocking callers and state is trivial; running hundreds of fast, pure tests per second is the norm. This is exactly what you want when tightening invariants, refactoring storage layouts, or reproducing edge cases. With Alkanes, you can test pure helper code natively, but **end-to-end testing typically requires a regtest stack** (Bitcoin Core + Metashrew + a key-value store). This makes it much harder to develop and test Alkanes contracts. Many failure modes only manifest with fully constructed Protorunes messages, UTXO selection, and witness envelopes. On the *interface* side, Sigil-style typed methods are **immediately discoverable** in code and in client SDKs; Alkanes’ numeric opcodes require you to shepherd a separate, human-maintained mapping (e.g., “77 = mint”), keep encoders/decoders in sync across languages, and document versioning conventions for opcodes and their byte layouts. ## Performance and Resource Contention While Alkanes’ adoption of the Protorunes specification and its UTXO model nominally allows for parallel execution of contract calls, **every mutating call must be a real Bitcoin transaction,** each of which consumes at least one UTXO. In UTXO land, *each output can be spent exactly once* and conflicting spends must serialize; that’s a hard concurrency ceiling and a source of contention in busy workflows. This makes Alkanes unsuitable for building standard DeFi applications such as AMMs. The fact is that Bitcoin has \~10-minute blocks and **single-digit TPS** on-chain; the bottleneck for state-changing calls is the Bitcoin protocol, not CPU time inside an indexer. ## Side-by-Side Comparison Below are two minimal “counter” contracts that expose the same three capabilities: * initialize the counter to 0 * increment by `n` * read the current value *** ### Sigil — Counter Contract ```wit theme={null} // counter.wit package kontor:contract; world contract { include kontor:built-in/built-in; use kontor:built-in/context.{view-context, proc-context}; use kontor:built-in/numbers.{integer}; export init: func(ctx: borrow); export inc: func(ctx: borrow, n: integer); export get: func(ctx: borrow) -> integer; } ``` ```rust theme={null} // counter.rs use stdlib::*; contract!(name = "counter"); #[derive(Clone, Default, StorageRoot)] struct CounterStorage { pub value: Integer, } impl Guest for Counter { fn init(ctx: &ProcContext) { // Initialize storage root and set value = 0 CounterStorage::default().init(ctx); storage(ctx).value().set(ctx, Integer::from(0)); } fn inc(ctx: &ProcContext, n: Integer) { let s = storage(ctx); let cur = s.value().get(ctx).unwrap_or_default(); s.value().set(ctx, cur + n); } fn get(ctx: &ViewContext) -> Integer { storage(ctx).value().get(ctx).unwrap_or_default() } } ``` *** ### Alkanes — Counter Contract ```rust theme={null} use anyhow::{anyhow, Result}; use alkanes_runtime::runtime::AlkaneResponder; use alkanes_support::response::CallResponse; use alkanes_support::storage::StoragePointer; use metashrew_support::compat::{to_arraybuffer_layout, to_ptr}; #[derive(Default)] pub struct Counter; impl Counter { fn ptr(&self) -> StoragePointer { StoragePointer::from_keyword("/counter") } } impl AlkaneResponder for Counter { fn execute(&self) -> Result { let context = self.context().ok_or_else(|| anyhow!("no context"))?; let mut inputs = context.inputs.clone(); let mut resp = CallResponse::forward(&context.incoming_alkanes); // 0 = init, 1 = inc(n), 2 = get() match shift_or_err(&mut inputs)? { 0u128 => { // init: set to 0 self.ptr().set_value::(0); Ok(resp) } 1u128 => { // inc(n): parse n and add (u128 is the canonical doc type) let n: u128 = shift_or_err(&mut inputs)?; let cur: u128 = self.ptr().get_value::(); self.ptr().set_value::(cur.checked_add(n).ok_or_else(|| anyhow!("overflow"))?); Ok(resp) } 2u128 => { // get(): return current value in little-endian bytes let cur: u128 = self.ptr().get_value::(); resp.data = cur.to_le_bytes().to_vec(); Ok(resp) } _ => Err(anyhow!("unrecognized opcode")), } } } #[no_mangle] pub extern "C" fn __execute() -> i32 { let mut bytes = to_arraybuffer_layout(&Counter::default().run()); to_ptr(&mut bytes) + 4 } ``` *** Even with an extremely simple contract, the greater ergonomics of the Sigil version are readily apparent. The Sigil contract reads like normal Rust: you update fields on a struct and return typed values. There’s no manual packing/unpacking, no global keyspace to maintain, and far fewer opportunities to drift between the contract and its clients. Alkanes requires developers to invent and document an opcode table, hand-roll a storage key convention (e.g., `"/counter"`), and write custom (de)serialization for arguments and return values. # Kontor vs. BRC2.0 Source: https://docs.kontor.network/docs/resources/brc2-0 Both BRC2.0 and Kontor both aim to bring programmability to Bitcoin with metaprotocol-based smart contract execution. Unlike Kontor, however, BRC-2.0: 1. Uses a port of the EVM for its contract runtime 2. Does not have a native gas currency BRC2.0's choice of using the EVM execution environment aims to leverage Ethereum's large developer ecosystem and smart contract libraries. However, the constraints of running EVM on Bitcoin create significant compatibility limitations. The core issue is address translation: BRC2.0 maps Bitcoin addresses to EVM addresses via `keccak256(bitcoin_pkscript).slice(-40)`, which creates addresses that have no corresponding private keys. These addresses look like standard Ethereum addresses but cannot produce signatures verifiable by `ecrecover`. This affects: * Multisig wallets and contract-based accounts * Meta-transactions and gasless transaction patterns * EIP-712 typed signatures (used throughout DeFi) * Account abstraction schemes (ERC-4337) * Standard `permit()` functions on tokens BRC2.0 provides a BIP-322 signature verification precompile at address `0xfe`, but this requires rewriting any contract that performs signature verification. Existing Ethereum contracts, including widely-used protocols like Uniswap, Aave, and Safe, therefore need substantial modification to work in BRC2.0. The pattern of using `ecrecover` to verify signatures must be replaced with calls to the custom precompile. This means the benefit of leveraging existing Ethereum codebases is difficult to realize. Wallet integration also requires different infrastructure. Users cannot use MetaMask or standard Web3 wallets; they need Bitcoin wallets supporting BIP-322 signing, but this standard has very limited support. Beyond the compatibility issues, the EVM's 256-bit stack-based architecture and Solidity's design choices create different trade-offs than WebAssembly-based systems. The EVM was designed for Ethereum's specific constraints in 2015, while WebAssembly emerged only later as a portable compilation target for broader industry adoption. # Kontor File Storage Source: https://docs.kontor.network/docs/resources/file-storage The file storage specification provides comprehensive technical details on file persistence, challenge-response mechanisms, cryptographic proofs, and economic incentives. Read the complete technical specification for the file persistence protocol (PDF) ## Key Topics Covered * File preparation and Merkle commitment structure * Challenge-response mechanism and detection guarantees * Nova IVC proof system and multi-file aggregation * Sponsorship markets and trustless data distribution * Security analysis and attack resistance * Protocol parameters and cryptographic primitives # Kontor vs. Move Source: https://docs.kontor.network/docs/resources/move The biggest difference between the Sigil and Move smart contract systems is that Sigil is an embedded DSL in Rust designed to make writing contracts feel like writing regular Rust code. Sigil's emphasis is on simplicity, readability, and overall developer experience. Move, by contrast, is a novel DSL with Rust-like syntax that implements an idiosyncratic resource-oriented programming model designed to support greater theoretical performance and safety guarantees but requires learning a significant number of new concepts and implementing complex workflows to fit the Move model. Because of precisely these limitations, Move has split into at least two different main versions: Aptos Move and Sui Move, each of which has its own (significant) variations in programming model, and incompatible tooling. As such, the Move ecosystem is fragmented, and the tooling is even less mature than it would be otherwise. A significant fraction of Move's design quirks were introduced to support formal verification. Formal verification, while a very interesting technology, is not the silver bullet for code correctness that it is often made out to be. Verification of any meaningful amount of code is labor-intensive and highly technical, which explains its limited adoption up to the present day. Only a few Aptos Move dApps have used formal verification so far, and [Sui Move had no support until 2025](https://blog.sui.io/asymptotic-move-prover-formal-verification/). Even when formal verification is employed, it is usually practical to verify only a small core set of functions and simple properties they should exhibit. Along similar lines, while Move’s resource model makes it impossible to copy or implicitly drop assets—which does remove a class of bugs—it does not verify business intent in general. Mis‑scoped authorities, mispriced fees, incorrect caps, or time/sequence errors still compile and must be guarded explicitly. You also pay ceremony—abilities, `acquires`, and resource plumbing that ripple through refactors. In Move, explicitly declaring `acquires` shapes is a benefit for auditability and scheduling, but it adds significant friction to development. Overall, Sigil is much easier to write: it lets one author contracts as idiomatic Rust modules with a typed, multi‑method ABI defined in WIT. You work with familiar constructs (structs, methods, maps) and call signatures like `mint(ctx, n: integer)` or `balance(ctx, acc: string) -> option`. Testing feels like ordinary Rust: spin up an in‑memory runtime, call methods directly, assert results. Most mistakes surface as compiler errors or method‑signature mismatches rather than as byte‑level bugs at runtime. # Kontor Optimistic Consensus Source: https://docs.kontor.network/docs/resources/optimistic-consensus The Optimistic Consensus specification provides a formal description of the protocol for sub-block transaction confirmation. Read the complete consensus protocol specification (PDF) ## Key Topics Covered * Protocol model with state-machine replication and threat model * Transaction submission, validation, and batch formation * State execution ordering and finalization criteria * Stake requirements, rewards, and slashing conditions * Security analysis with safety, liveness, and accountability proofs * Formal verification in Lean 4 # Kontor Proof-of-Retrievability Source: https://docs.kontor.network/docs/resources/proof-of-retrievability The Proof-of-Retrievability specification details the cryptographic system underlying the storage protocol's challenge-response mechanism. Read the complete cryptographic specification for storage proofs (PDF) ## Key Topics Covered * Multi-file proof aggregation with File Ledger construction * Challenge derivation from Bitcoin block entropy * Nova IVC circuit design with Poseidon hashing * Spartan SNARK compression for compact proofs * Security analysis and computational assumptions # Kontor Scalability Source: https://docs.kontor.network/docs/resources/scalability The Scalability specification provides a technical description of how Kontor achieves high throughput within Bitcoin's constraints. Read the complete scalability analysis and benchmarks (PDF) ## Key Topics Covered * Transaction format and Bitcoin embedding * Optimization stack: BLS aggregation, registry IDs, binary encoding, Zstd compression * Performance benchmarks across workload scenarios * Bundler economics and fee market dynamics # Kontor Whitepaper Source: https://docs.kontor.network/docs/resources/whitepaper The comprehensive Kontor whitepaper provides in-depth technical details about the protocol design, architecture, and implementation. Read the full technical specification and protocol design (PDF) ## Key Topics Covered * Metaprotocol architecture and embedded consensus * Sigil smart contract framework design * File persistence protocol specifications * KOR tokenomics and economic model * Security considerations and attack resistance * Implementation details and technical specifications # Architecture Source: https://docs.kontor.network/docs/sigil/architecture ## WebAssembly Runtime Like many other smart contract frameworks, Sigil is built on WebAssembly (WASM), which provides several key benefits for blockchain smart contracts: * **Deterministic Execution**: Contracts can be executed and interrupted based on gas limits and memory constraints, ensuring predictable behavior. * **Efficient Bytecode**: WASM offers a portable and efficient bytecode format for the Sigil runtime. Unlike other WASM-based blockchain languages, Sigil is deeply integrated with the modern **WebAssembly Component Model**, providing a structured and interoperable approach to contract development. Sigil's runtime primitives and userspace abstractions enable secure, gas-efficient contracts with a familiar development experience. Storage is isolated, cross-contract calls are convenient to perform (while re-entrances are strictly prohibited), and a `fallback` hook enables support for contract versioning via proxying. ## WebAssembly Component Model Traditional WASM modules compiled from other languages produce self-contained but unstructured files, making their contents opaque to other code modules. The WASM Component Model introduces a language-agnostic structure, enhancing reusability and transparency. In Sigil: * **WIT Signatures**: Every Sigil contract includes a WebAssembly Interface Type (WIT) "header" file that explicitly exports all public functions in a standardized, high-level type language. This makes contracts more accessible to developers and tooling. * **Runtime Validation**: The WIT file is compiled into the WASM code, allowing the Sigil runtime to extract and validate the contract's interface at runtime and for tooling integration. * **Future Component Imports**: The Component Model lays the foundation for importing other components for code reuse, though cross-contract calls currently use a foreign call primitive in an isolated execution context. * **WAVE Format**: Sigil uses the WebAssembly Value Expression (WAVE) format for passing arguments and returning results, standardizing value types across languages compiled to WASM. * **Exported Function Conventions**: Exported functions in WIT files follow standard WIT types, with two additional conventions: * **Hook-Based Execution**: Functions with specific names serve as hooks and are called under certain conditions and not explicitly by a user. * **Execution Context**: The first argument of every function is an execution context, defining its behavior and access rights. # Key Features ### Hooks Sigil defines a minimal but full-featured set of hooks to handle specific contract behaviors: * **`init` Hook**: Runs when a contract is published and is primarily for setting initial storage values or perform data migrations for contract upgrades. It currently supports arbitrary contract code execution. * **`fallback` Hook**: Accepts WAVE arguments and returns WAVE results. This hook is called when no other function matches the call, and it is primarily used to implement proxy contracts for versioning in userspace, similar to how proxying is implemented in Solidity. ### Execution Contexts Public functions in Sigil are "decorated" with one of three execution contexts, controlling their behavior and access: * **`proc` (Procedure) Context**: * Can read and write to storage * Callable when submitting blockchain transactions * **`view` Context**: * Read-only access to storage * Callable via the API exposed by Kontor nodes * **`fall` (Fallback) Context**: * Used exclusively for the `fallback` hook * Can be converted into other context types Both `proc` and `view` functions can be called within a single contract or across contracts via foreign calls, with static type safety in the SDK and runtime checks ensuring compliance with read-only or read-write rules. Unlike some blockchain languages, Sigil does not implicitly expose storage contents. Developers must explicitly create `view` functions to expose data, enabling fine-grained control for future migrations or upgrades. These functions can include arbitrary code, such as userspace authentication or authorization. ### Primitives Sigil's runtime primitives keep the virtual machine simple and secure, encouraging userspace abstractions: 1. **Signer Access**: Available only in `proc` contexts, allows access to the signer of the current transaction 2. **Cross-Contract Calls**: A function for calling other contracts, contributing to the gas expenditure of the transaction 3. **Storage Functions**: * `get` and `set` operations for primitive types (e.g., strings, u64), parameterized by a key or path * Checking if a key exists in storage * Regex-based path matching The Sigil Rust SDK abstracts these primitives into higher-level, ORM-like interfaces, reducing the need for direct interaction. ### Library Code and SDK Sigil emphasizes userspace development through Rust traits and macros: * **Traits for Runtime Primitives**: The runtime implements traits for all primitives, allowing pure Rust libraries (including Sigil's standard library) to operate independently of the runtime. Developers can implement in-memory versions of these traits for testing without relying on the Sigil runtime. * **Ordinary Rust Programming**: Rust macros make Sigil programming feel simple and familiar. * **Private Helper Functions**: Non-exported functions (without WIT signatures) compile to regular WASM instructions, behaving like standard Rust functions. ### Storage and Versioning Sigil's storage system is designed for simplicity, isolation, and predictable gas usage: * **Primitive Operations**: Storage uses path-keyed `get` and `set` operations for basic types (e.g., integers, booleans, strings). * **ORM-like Macros**: The SDK provides macros for declaring persistent data types, including records, enums, non-recursive sum types, and built-in `Map` types. Nested fields can be accessed (e.g., `foo.bar.baz`) without retrieving entire structures. * **Isolation**: Each contract's storage is isolated, with no equivalent to Ethereum's `delegatecall`. This simplifies the mental model, treating foreign contract calls like web API calls. * **Gas Tracking**: The design avoids complex operations (e.g., SQL-like joins) to make gas consumption intuitive. * **Versioning**: Sigil supports userspace versioning through the `fallback` hook and proxy contracts. Developers can implement advanced upgrade mechanisms, such as DAO-like voting for safe upgrades, avoiding reliance on key-based upgrades that risk rug pulls. External calls to proxy contracts appear as standard contract calls, enhancing transparency. ### Error Handling Sigil provides a robust error handling mechanism: * **Error Types**: Errors are represented as a custom `Error` type, which can be returned from functions. * **Error Propagation**: Errors can be propagated up the call stack using the `?` operator. * **Error Handling**: `Result`s can be handled using `match` expressions over `Ok` and `Err` variants or with the many other error handling strategies available in Rust. When a function returns an `Err` or `panic`s, all storage changes are rolled back. For nested calls, the entire call chain is rolled back if any function errors. This means if Contract A calls Contract B and B returns an error, ALL storage changes in both A and B are rolled back—the entire transaction has no effect. A transaction either completes fully with all storage changes persisted, or fails entirely with no storage changes. ### Safety Sigil simplifies reasoning about safety compared to languages like Solidity by eliminating features like `delegatecall` and completely disallowing cross-contract calling cycles. However, developers should still follow the **Checks-Effects-Interactions (CEI)** pattern: * **Check**: Verify conditions (e.g., sufficient funds) * **Effect**: Update storage (e.g., deduct funds) * **Interaction**: Perform external operations (e.g., cross-contract calls) For example: ```rust theme={null} check_sufficient_funds(); // Check transfer_funds_in_storage(); // Effect do_external_operations(); // Interaction ``` Leveraging this pattern makes code easier to reason about and adds an additional defensive layer. # 6. Automated Market-Maker (AMM) Source: https://docs.kontor.network/docs/sigil/examples/amm **Full working code**: The complete source code for this example is available in [`test-contracts/amm/`](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts/amm) This example builds on top of the Token contract and demonstrates swaps and liquidity provision on an AMM. It demonstrates transfers of balances using the `token-dyn` interface for cross-contract calls to the Token contract, and complex integer arithmetic using the 256-bit `Integer` built-in type. **Standards Pattern**: This AMM accepts any token contract address that implements the transfer interface. The compiler ensures the AMM makes valid calls to that interface, but cannot verify at compile time that user-provided addresses actually implement it. ## WIT Interface Imports types: * `signer`: Transaction sender (identity) * `error`: Failure handling * `integer`: Amounts and exports functions: * `init`: Initializes the contract * `create`: Create a liquidity pool for a new token pair * `fee`: Get the fee (in basis points of input amount) for trading in a pair * `balance`: Queries a liquidity provider (LP) share balance * `token-balance`: Queries the balance of tokens in a liquidity pool * `deposit`: Deposit both tokens of a pair and receive LP shares * `withdraw`: Burn LP shares and withdraw the tokens they represent * `swap`: Swap an amount of one token of a pair for the other token * `quote-deposit`: Receive a quote of cost of shares received when depositing * `quote-withdraw`: Receive a quote of tokens received for shares when withdrawing * `quote-swap`: Receive a quote of out-tokens received when swapping ```wit theme={null} package root:component; world root { include kontor:built-in/built-in; use kontor:built-in/context.{view-context, proc-context, signer}; use kontor:built-in/error.{error}; use kontor:built-in/foreign.{contract-address}; use kontor:built-in/numbers.{integer, decimal}; record token-pair { a: contract-address, b: contract-address, } record deposit-result { lp-shares: integer, deposit-a: integer, deposit-b: integer, } record withdraw-result { amount-a: integer, amount-b: integer, } export init: func(ctx: borrow); export create: func(ctx: borrow, pair: token-pair, amount-a: integer, amount-b: integer, fee-bps: integer) -> result; export fee: func(ctx: borrow, pair: token-pair) -> result; export balance: func(ctx: borrow, pair: token-pair, acc: string) -> option; export token-balance: func(ctx: borrow, pair: token-pair, token: contract-address) -> result; export quote-deposit: func(ctx: borrow, pair: token-pair, amount-a: integer, amount-b: integer) -> result; export deposit: func(ctx: borrow, pair: token-pair, amount-a: integer, amount-b: integer) -> result; export quote-withdraw: func(ctx: borrow, pair: token-pair, shares: integer) -> result; export withdraw: func(ctx: borrow, pair: token-pair, shares: integer) -> result; export swap: func(ctx: borrow, pair: token-pair, token-in: contract-address, amount-in: integer, min-out: integer) -> result; export quote-swap: func(ctx: borrow, pair: token-pair, token-in: contract-address, amount-in: integer) -> result; } ``` ## Rust Implementation * `AMMStorage` represents the contract’s persistent storage. The `StorageRoot` derive macro enables storage capabilities for the type and marks it as the "root" storage type. The root storage is accessible via a generated function: `storage`, which provides an ORM-like interface to contract functions. Every contract with storage must have exactly one type that derives `StorageRoot`. * `Map` is a storage-enabled mapping type that can hold values of any type that also derive `Storage`. Holds mappings from token pairs to `Pool`. * `Pool` contains the information about the state of the pool for a token pair. * The `ctx` parameter must be passed to every operation that performs a database call. These functions take an `impl ReadContext` or an `impl WriteContext`, depending on their behavior. `ProcContext` implements both, while `ViewContext` implements only `ReadContext`. This ensures state mutations occur only as the result of a transaction while allowing the same functions and methods to be used across procedures and views. ```rust theme={null} use stdlib::*; contract!(name = "amm"); interface!(name = "token_dyn", path = "../../token/contract/wit"); #[derive(Clone, Storage)] struct Pool { pub token_a: ContractAddress, pub balance_a: Integer, pub token_b: ContractAddress, pub balance_b: Integer, pub fee_bps: Integer, pub lp_total_supply: Integer, pub lp_ledger: Map, } #[derive(Clone, StorageRoot)] struct AMMStorage { pub pools: Map, pub custodian: String, } fn pair_id(pair: &TokenPair) -> String { format!("{}::{}", pair.a, pair.b) } fn pair_other_token( pair: &TokenPair, token_in: &ContractAddress, ) -> Result { if token_in == &pair.a { Ok(pair.b.clone()) } else if token_in == &pair.b { Ok(pair.a.clone()) } else { Err(Error::Message(format!("token {} not in pair", token_in))) } } fn validate_pair(pair: &TokenPair) -> Result<(), Error> { if pair.a.name.is_empty() || pair.b.name.is_empty() { return Err(Error::Message( "Token addresses must not be empty".to_string(), )); } if pair.a.to_string() >= pair.b.to_string() { return Err(Error::Message( "Token pair must be ordered A < B".to_string(), )); } Ok(()) } fn validate_amount(amount: Integer) -> Result<(), Error> { // 0 < amount < sqrt(MAX_INT) if amount <= Integer::default() || amount > "340_282_366_920_938_463_463_374_607_431".into() { return Err(Error::Message("bad amount".to_string())); } Ok(()) } fn calc_swap_result( amount_in: Integer, bal_in: Integer, bal_out: Integer, fee_bps: Integer, ) -> Result { validate_amount(amount_in)?; validate_amount(bal_in)?; validate_amount(bal_out)?; // input amount less fee, round down let bps_in_100pct = 10000.into(); let in_less_fee = amount_in * (bps_in_100pct - fee_bps) / bps_in_100pct; let new_bal_in = bal_in + in_less_fee; validate_amount(new_bal_in)?; // calculate output amount from delta in output-token balance, round down let k = bal_in * bal_out; Ok((bal_out * new_bal_in - k) / new_bal_in) } fn get_pool(ctx: &impl ReadContext, pair: &TokenPair) -> Result { let id = pair_id(pair); let pools = ctx.model().pools(); pools .get(&id) .ok_or_else(|| Error::Message("Pool not found".to_string())) } fn quote_swap( ctx: &impl ReadContext, pair: &TokenPair, token_in: &ContractAddress, amount_in: Integer, ) -> Result { let pool = get_pool(ctx, pair)?; let (bal_in, bal_out) = if token_in == &pair.a { (pool.balance_a(), pool.balance_b()) } else { (pool.balance_b(), pool.balance_a()) }; calc_swap_result(amount_in, bal_in, bal_out, pool.fee_bps()) } fn quote_deposit( ctx: &impl ReadContext, pool: &PoolWrapper, amount_a: Integer, amount_b: Integer, ) -> Result { validate_amount(amount_a)?; validate_amount(amount_b)?; let lp_supply = pool.lp_total_supply(); let balance_a = pool.balance_a(); let balance_b = pool.balance_b(); let lp_shares = if amount_a * balance_b < amount_b * balance_a { amount_a * lp_supply / balance_a } else { amount_b * lp_supply / balance_b }; let supply_minus_one = lp_supply - 1.into(); Ok(DepositResult { deposit_a: (lp_shares * balance_a + supply_minus_one) / lp_supply, // round up deposit_b: (lp_shares * balance_b + supply_minus_one) / lp_supply, // round up lp_shares, }) } fn quote_withdraw( ctx: &impl ReadContext, pool: &PoolWrapper, shares: Integer, ) -> Result { validate_amount(shares)?; let lp_total_supply = pool.lp_total_supply(); Ok(WithdrawResult { amount_a: shares * pool.balance_a() / lp_total_supply, amount_b: shares * pool.balance_b() / lp_total_supply, }) } impl Guest for Amm { fn init(ctx: &ProcContext) { let custodian = ctx.contract_signer().to_string(); AMMStorage { pools: Map::default(), custodian, } .init(ctx) } fn create( ctx: &ProcContext, pair: TokenPair, amount_a: Integer, amount_b: Integer, fee_bps: Integer, ) -> Result { validate_pair(&pair)?; validate_amount(amount_a)?; validate_amount(amount_b)?; match get_pool(ctx, &pair) { Ok(_) => Err(Error::Message( "pool for this pair already exists".to_string(), )), Err(_) => Ok(()), }?; let lp_shares = (amount_a * amount_b).sqrt()?; let admin = ctx.signer().to_string(); ctx.model().pools().set( pair_id(&pair), Pool { token_a: pair.a.clone(), balance_a: amount_a, token_b: pair.b.clone(), balance_b: amount_b, fee_bps, lp_total_supply: lp_shares, lp_ledger: Map::new(&[(admin, lp_shares)]), }, ); let custodian = ctx.contract_signer().to_string(); token_dyn::transfer(&pair.a, ctx.signer(), &custodian, amount_a)?; token_dyn::transfer(&pair.b, ctx.signer(), &custodian, amount_b)?; Ok(lp_shares) } fn fee(ctx: &ViewContext, pair: TokenPair) -> Result { Ok(get_pool(ctx, &pair)?.fee_bps()) } fn balance(ctx: &ViewContext, pair: TokenPair, acc: String) -> Option { get_pool(ctx, &pair) .ok() .and_then(|p| p.lp_ledger().get(acc)) } fn token_balance( ctx: &ViewContext, pair: TokenPair, token: ContractAddress, ) -> Result { pair_other_token(&pair, &token)?; let pool = get_pool(ctx, &pair)?; if token == pair.a { Ok(pool.balance_a()) } else { Ok(pool.balance_b()) } } fn quote_deposit( ctx: &ViewContext, pair: TokenPair, amount_a: Integer, amount_b: Integer, ) -> Result { quote_deposit(ctx, &get_pool(ctx, &pair)?, amount_a, amount_b) } fn deposit( ctx: &ProcContext, pair: TokenPair, amount_a: Integer, amount_b: Integer, ) -> Result { let pool = get_pool(ctx, &pair)?; let res = quote_deposit(ctx, &pool, amount_a, amount_b)?; let ledger = pool.lp_ledger(); let addr = ctx.model().custodian(); pool.update_balance_a(|b| b + res.deposit_a); pool.update_balance_b(|b| b + res.deposit_b); let user = ctx.signer().to_string(); let bal = ledger.get(&user).unwrap_or_default(); ledger.set(user, bal + res.lp_shares); pool.set_lp_total_supply(pool.lp_total_supply() + res.lp_shares); token_dyn::transfer(&pair.a, ctx.signer(), &addr, res.deposit_a)?; token_dyn::transfer(&pair.b, ctx.signer(), &addr, res.deposit_b)?; Ok(res) } fn quote_withdraw( ctx: &ViewContext, pair: TokenPair, shares: Integer, ) -> Result { quote_withdraw(ctx, &get_pool(ctx, &pair)?, shares) } fn withdraw( ctx: &ProcContext, pair: TokenPair, shares: Integer, ) -> Result { let pool = get_pool(ctx, &pair)?; let res = quote_withdraw(ctx, &pool, shares)?; let ledger = pool.lp_ledger(); let user = ctx.signer().to_string(); let total = pool.lp_total_supply(); let bal = ledger.get(&user).unwrap_or_default(); if total < shares { return Err(Error::Message("insufficient total supply".to_string())); } if bal < shares { return Err(Error::Message("insufficient share balance".to_string())); } ledger.set(user.clone(), bal - shares); pool.set_lp_total_supply(total - shares); pool.update_balance_a(|b| b - res.amount_a); pool.update_balance_b(|b| b - res.amount_b); token_dyn::transfer(&pair.a, ctx.contract_signer(), &user, res.amount_a)?; token_dyn::transfer(&pair.b, ctx.contract_signer(), &user, res.amount_b)?; Ok(res) } fn quote_swap( ctx: &ViewContext, pair: TokenPair, token_in: ContractAddress, amount_in: Integer, ) -> Result { quote_swap(ctx, &pair, &token_in, amount_in) } fn swap( ctx: &ProcContext, pair: TokenPair, token_in: ContractAddress, amount_in: Integer, min_out: Integer, ) -> Result { let token_out = pair_other_token(&pair, &token_in)?; let amount_out = quote_swap(ctx, &pair, &token_in, amount_in)?; if amount_out < min_out { return Err(Error::Message(format!( "amount out ({}) below minimum", amount_out ))); } let pool = get_pool(ctx, &pair)?; if token_in == pair.a { pool.update_balance_a(|b| b + amount_in); pool.update_balance_b(|b| b - amount_out); } else { pool.update_balance_a(|b| b - amount_out); pool.update_balance_b(|b| b + amount_in); } token_dyn::transfer( &token_in, ctx.signer(), &ctx.model().custodian(), amount_in, )?; token_dyn::transfer( &token_out, ctx.contract_signer(), &ctx.signer().to_string(), amount_out, )?; Ok(amount_out) } } ``` ## Testing Similar to the tests for other contracts, this one uses the `interface!` macro to generate an interface for calling the contract. It also includes assertions for error handling. For all calls, the first `?` operator checks whether the runtime threw an error during execution. For contract functions that explicitly return a `Result`, the result can be "unwrapped" with an additional `?` operator or left omitted to make an assertion on an error. The test publishes three contracts: the `amm` contract and two separate instances of the `token` contract to serve as the trading pair. When working with numbers in Sigil, either the `Integer` or `Decimal` types should be used. `From` instances have been implemented for many of the primitive types which is why there are many `.into()`s in the test. One can also write: `Integer::from(100)`, or `Decimal::from("1.5")`, or even `let x: Decimal = 1.5.into()`, etc. ```rust theme={null} #[cfg(test)] mod tests { use testlib::*; interface!(name = "amm"); interface!(name = "token_a", path = "../../token/contract/wit"); interface!(name = "token_b", path = "../../token/contract/wit"); #[testlib::test(contracts_dir = "../../")] async fn test_contract() -> Result<()> { let admin = runtime.identity().await?; let minter = runtime.identity().await?; let amm = runtime.publish(&admin, "amm").await?; let token_a = runtime.publish(&admin, "token").await?; let token_b = runtime.publish(&admin, "token").await?; token_a::mint(runtime, &token_a, &minter, 1000.into()).await??; token_b::mint(runtime, &token_b, &minter, 1000.into()).await??; token_a::transfer(runtime, &token_a, &minter, &admin, 100.into()).await??; token_b::transfer(runtime, &token_b, &minter, &admin, 500.into()).await??; let pair = amm::TokenPair { a: token_a.clone(), b: token_b.clone(), }; let res = amm::create( runtime, &amm, &admin, pair.clone(), 100.into(), 500.into(), 0.into(), ) .await?; assert_eq!(res, Ok(223.into())); let bal_a = amm::token_balance(runtime, &amm, pair.clone(), token_a.clone()).await?; assert_eq!(bal_a, Ok(100.into())); let bal_b = amm::token_balance(runtime, &amm, pair.clone(), token_b.clone()).await?; assert_eq!(bal_b, Ok(500.into())); let k1 = bal_a.unwrap() * bal_b.unwrap(); let res = amm::quote_swap(runtime, &amm, pair.clone(), token_a.clone(), 10.into()).await?; assert_eq!(res, Ok(45.into())); let res = amm::quote_swap(runtime, &amm, pair.clone(), token_a.clone(), 100.into()).await?; assert_eq!(res, Ok(250.into())); let res = amm::quote_swap(runtime, &amm, pair.clone(), token_a.clone(), 1000.into()).await?; assert_eq!(res, Ok(454.into())); let res = amm::swap( runtime, &amm, &minter, pair.clone(), token_a.clone(), 10.into(), 46.into(), ) .await?; assert!(res.is_err()); // below minimum let res = amm::swap( runtime, &amm, &minter, pair.clone(), token_a.clone(), 10.into(), 45.into(), ) .await?; assert_eq!(res, Ok(45.into())); let bal_a = amm::token_balance(runtime, &amm, pair.clone(), token_a.clone()).await?; let bal_b = amm::token_balance(runtime, &amm, pair.clone(), token_b.clone()).await?; let k2 = bal_a.unwrap() * bal_b.unwrap(); assert!(k2 >= k1); let res = amm::quote_swap(runtime, &amm, pair.clone(), token_b.clone(), 45.into()).await?; assert_eq!(res, Ok(9.into())); let res = amm::swap( runtime, &amm, &minter, pair.clone(), token_b.clone(), 45.into(), 0.into(), ) .await?; assert_eq!(res, Ok(9.into())); let bal_a = amm::token_balance(runtime, &amm, pair.clone(), token_a.clone()).await?; let bal_b = amm::token_balance(runtime, &amm, pair.clone(), token_b.clone()).await?; let k3 = bal_a.unwrap() * bal_b.unwrap(); assert!(k3 >= k2); Ok(()) } } ``` # 1. Hello World Source: https://docs.kontor.network/docs/sigil/examples/hello-world **Full working code**: The complete source code for this example is shown on this page. More complete example contracts are available in [`test-contracts/`](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts) This example walks you through creating your first Sigil contract—a simple "Hello World" that demonstrates the fundamental structure and workflow. ## Project Structure ``` hello-world/ ├── Cargo.toml # Workspace configuration ├── contract/ │ ├── Cargo.toml # Contract dependencies │ ├── wit/ │ │ └── contract.wit │ └── src/ │ └── lib.rs └── test/ ├── Cargo.toml # Test dependencies └── src/ └── lib.rs ``` ## Step 1: Workspace Configuration **Root `Cargo.toml`:** ```toml theme={null} [workspace] members = ["contract", "test"] resolver = "2" [workspace.dependencies] stdlib = { path = "../../Kontor/core/stdlib" } testlib = { path = "../../Kontor/core/testlib" } ``` The paths shown assume the `Documentation` and `Kontor` repos are siblings. If you cloned them to different locations, adjust these paths to point to your Kontor installation. See [Getting Started](/docs/sigil/quickstart#working-with-example-contracts) for setup details. ## Step 2: Contract Configuration **`contract/Cargo.toml`:** ```toml theme={null} [package] name = "hello-world" version = "0.1.0" edition = "2024" [dependencies] stdlib = { workspace = true } [lib] crate-type = ["cdylib"] # Required for WASM compilation ``` **Key points:** * `crate-type = ["cdylib"]` - Builds a dynamic library for WASM * `stdlib` provides the `contract!` macro and runtime primitives ## Step 3: Define the Contract Interface (WIT) Create `contract/wit/contract.wit`: ```wit theme={null} package root:component; world root { include kontor:built-in/built-in; use kontor:built-in/context.{view-context, proc-context}; export init: func(ctx: borrow); export hello-world: func(ctx: borrow) -> string; } ``` **What this defines:** * `init` - Called once when contract is deployed * `hello-world` - Returns a greeting string (read-only) The `kontor:built-in` types are available via a symlink at `wit/deps/built-in.wit`. ## Step 4: Implement the Contract Create `contract/src/lib.rs`: ```rust contract/src/lib.rs theme={null} use stdlib::*; contract!(name = "hello-world"); impl Guest for HelloWorld { fn init(_ctx: &ProcContext) {} fn hello_world(_ctx: &ViewContext) -> String { "Hello, World!".to_string() } } ``` **How it works:** * `contract!(name = "hello-world")` - Generates the `Guest` trait and WASM bindings * `impl Guest for HelloWorld` - The contract name is converted to PascalCase * `init()` - Empty because we don't need storage * `hello_world()` - Returns a static string ## Step 5: Build and Test The contract is automatically built when you run tests. The test directory's `build.rs` handles compilation, optimization, and compression: ```bash theme={null} cd test cargo test ``` The `build.rs` automatically: 1. Compiles the contract to WASM 2. Optimizes with `wasm-opt -Oz --enable-bulk-memory --enable-sign-ext` 3. Compresses with `brotli` to create `.wasm.br` **To build manually without running tests:** ```bash theme={null} cd contract cargo build --release --target wasm32-unknown-unknown wasm-opt -Oz --enable-bulk-memory --enable-sign-ext \ target/wasm32-unknown-unknown/release/hello_world.wasm \ -o hello_world_opt.wasm brotli -Zf hello_world_opt.wasm -o hello_world.wasm.br ``` ## Step 6: Write Tests Create `test/src/lib.rs`: ```rust test/src/lib.rs theme={null} #[cfg(test)] mod tests { use testlib::*; interface!(name = "hello-world"); #[testlib::test] async fn test_contract() -> Result<()> { let alice = runtime.identity().await?; let hello_world = runtime.publish(&alice, "hello-world").await?; let result = hello_world::hello_world(runtime, &hello_world).await?; assert_eq!(result, "Hello, World!"); Ok(()) } } ``` **What this does:** * `interface!` - Generates type-safe bindings from the WIT file * `#[testlib::test]` - Auto-injects the `runtime` variable * `runtime.identity()` - Creates a test user with gas funding * `runtime.publish()` - Deploys the contract and returns its address * `hello_world::hello_world()` - Calls the contract function **Run tests:** ```bash theme={null} cd test cargo test ``` ## Common First-Time Issues **`contract!` macro not found** ``` error: cannot find macro `contract` in this scope ``` **Solution:** Add `use stdlib::*;` at the top of your file **Missing Guest trait implementation** ``` error: not all trait items implemented, missing: `hello_world` ``` **Solution:** Ensure you've implemented all functions listed in your WIT file. Function names with hyphens in WIT become underscores in Rust. **WIT file parse error** ``` error: expected `include`, `use`, `import`, `export`, or `type` ``` **Solution:** Ensure you have `include kontor:built-in/built-in;` in your WIT world **Build target not installed** ``` error: can't find crate for `core` ``` **Solution:** Install the WASM target: `rustup target add wasm32-unknown-unknown` # Examples Overview Source: https://docs.kontor.network/docs/sigil/examples/index This section covers the example contracts in `example-contracts/`. Each is a self-contained example that builds on concepts introduced in previous ones, progressing from a minimal contract to production-grade DeFi applications. The examples demonstrate how to develop blockchain smart contracts with seamless Rust-like testing and rapid feedback in a local development environment. ## Suggested Learning Order 1. **hello-world** → Minimal contract structure, basic functions 2. **token** → Map-based ledger, Integer type, error handling 3. **shared-account** → Nested Maps, authorization patterns 4. **proxy** → Fallback function, delegation patterns 5. **pool** → DeFi math, liquidity pools 6. **amm** → Multi-pool AMM, production patterns ## Example Contracts ### hello-world Minimal contract demonstrating basic contract structure, view functions, and simple state management. [View Example →](/docs/sigil/examples/hello-world) *** ### token (59 lines) Fungible token with Map-based ledger, arbitrary precision Integer arithmetic, balance transfers, and error handling for insufficient funds. [View Example →](/docs/sigil/examples/token) *** ### shared-account Multi-tenant account management with two-level nested Maps, authorization patterns (owner and tenants), helper functions for access control, and contract receiving tokens via `contract_signer()`. [View Example →](/docs/sigil/examples/shared-account) *** ### proxy Generic contract delegation using the fallback function, FallContext, and `foreign::call` for dynamic forwarding. Demonstrates proxy/upgrade patterns. [View Example →](/docs/sigil/examples/proxy) *** ### pool (200+ lines) Single liquidity pool implementation with constant product AMM math, LP token management, swap calculations, and slippage protection using Integer arithmetic. [View Example →](/docs/sigil/examples/pool) *** ### amm (350+ lines) Multi-pool automated market maker managing multiple token pairs with Map of Pool structs, dynamic token handling, pool creation, and production-grade validation logic. [View Example →](/docs/sigil/examples/amm) *** ## Project Structure Each contract directory contains: * **`contract/`** - Rust code (`src/lib.rs`), WIT interface (`wit/contract.wit`), and `wit/deps/` (symlink to built-in types) * **`test/`** - Tests (`src/lib.rs`), `build.rs` (compiles, optimizes, compresses contract), and dependencies * **Root `Cargo.toml`** - Workspace configuration ## Running Examples To test any example contract: ```bash theme={null} cd example-contracts/token # or any other example cargo test ``` Tests use the Kontor runtime for simulation, loading contract bytes from `target/` after compilation. This provides fast iteration like standard Rust development. Each example assumes familiarity with prior ones, focusing on new concepts. ## Finding the Code All example contracts are in the repository under `example-contracts/`: ``` example-contracts/ ├── crypto/ ├── token/ ├── arith/ ├── fib/ ├── proxy/ ├── shared-account/ ├── pool/ └── amm/ ``` # 7. Liquidity Pool Source: https://docs.kontor.network/docs/sigil/examples/pool **Full working code**: The complete source code for this example is available in [`test-contracts/pool/`](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts/pool) This example closely mirrors the AMM contract's logic but supports only one token pair. This design allows the pool to implement a token interface, where the pool's token represents shares in the liquidity pool, enabling interaction through standard token methods (demonstrated at the test's end). In a production environment, a middleware application would manage pool creation for users and maintain a mapping of token pairs to pool contract IDs. ## WIT Interface ```wit theme={null} package root:component; world root { include kontor:built-in/built-in; use kontor:built-in/context.{view-context, proc-context, signer}; use kontor:built-in/error.{error}; use kontor:built-in/foreign.{contract-address}; use kontor:built-in/numbers.{integer, decimal}; record deposit-result { lp-shares: integer, deposit-a: integer, deposit-b: integer, } record withdraw-result { amount-a: integer, amount-b: integer, } export init: func(ctx: borrow); export re-init: func(ctx: borrow, token-a: contract-address, amount-a: integer, token-b: contract-address, amount-b: integer, fee: integer) -> result; export fee: func(ctx: borrow) -> integer; export balance: func(ctx: borrow, acc: string) -> option; export transfer: func(ctx: borrow, to: string, n: integer) -> result<_, error>; export token-balance: func(ctx: borrow, token: contract-address) -> result; export quote-deposit: func(ctx: borrow, amount-a: integer, amount-b: integer) -> result; export deposit: func(ctx: borrow, amount-a: integer, amount-b: integer) -> result; export quote-withdraw: func(ctx: borrow, shares: integer) -> result; export withdraw: func(ctx: borrow, shares: integer) -> result; export swap: func(ctx: borrow, token-in: contract-address, amount-in: integer, min-out: integer) -> result; export quote-swap: func(ctx: borrow, token-in: contract-address, amount-in: integer) -> result; } ``` ## Rust Implementation ```rust theme={null} use stdlib::*; contract!(name = "pool"); interface!(name = "token_dyn", path = "../../token/contract/wit"); #[derive(Clone, StorageRoot)] struct PoolStorage { pub token_a: ContractAddress, pub token_b: ContractAddress, pub fee_bps: Integer, pub lp_total_supply: Integer, pub lp_ledger: Map, pub custodian: String, } impl PoolStorage { pub fn new( ctx: &ProcContext, token_a: ContractAddress, amount_a: Integer, token_b: ContractAddress, amount_b: Integer, fee_bps: Integer, ) -> Result { validate_amount(amount_a)?; validate_amount(amount_b)?; let lp_shares = (amount_a * amount_b).sqrt()?; let custodian = ctx.contract_signer().to_string(); let pool = PoolStorage { token_a: token_a.clone(), token_b: token_b.clone(), fee_bps, lp_total_supply: lp_shares, lp_ledger: Map::new(&[(ctx.signer().to_string(), lp_shares)]), custodian: custodian.clone(), }; token_dyn::transfer(&token_a, ctx.signer(), &custodian, amount_a)?; token_dyn::transfer(&token_b, ctx.signer(), &custodian, amount_b)?; Ok(pool) } } fn token_out(ctx: &impl ReadContext, token_in: &ContractAddress) -> Result { let token_a = ctx.model().token_a(); let token_b = ctx.model().token_b(); if token_in == &token_a { Ok(token_b) } else if token_in == &token_b { Ok(token_a) } else { Err(Error::Message(format!("token {} not in pair", token_in))) } } fn validate_amount(amount: Integer) -> Result<(), Error> { // 0 < amount < sqrt(MAX_INT) if amount <= Integer::default() || amount > "340_282_366_920_938_463_463_374_607_431".into() { return Err(Error::Message("bad amount".to_string())); } Ok(()) } fn calc_swap_result( amount_in: Integer, bal_in: Integer, bal_out: Integer, fee_bps: Integer, ) -> Result { validate_amount(amount_in)?; validate_amount(bal_in)?; validate_amount(bal_out)?; // input amount less fee, round down let bps_in_100pct = 10000.into(); let in_less_fee = amount_in * (bps_in_100pct - fee_bps) / bps_in_100pct; let new_bal_in = bal_in + in_less_fee; validate_amount(new_bal_in)?; // calculate output amount from delta in output-token balance, round down let k = bal_in * bal_out; Ok((bal_out * new_bal_in - k) / new_bal_in) } fn quote_swap( ctx: &impl ReadContext, token_in: &ContractAddress, amount_in: Integer, ) -> Result { let custodian = ctx.model().custodian(); let bal_in = token_dyn::balance(token_in, &custodian).unwrap_or_default(); let bal_out = token_dyn::balance(&token_out(ctx, token_in)?, &custodian).unwrap_or_default(); calc_swap_result(amount_in, bal_in, bal_out, ctx.model().fee_bps()) } fn quote_deposit( ctx: &impl ReadContext, amount_a: Integer, amount_b: Integer, ) -> Result { validate_amount(amount_a)?; validate_amount(amount_b)?; let token_a = ctx.model().token_a(); let token_b = ctx.model().token_b(); let lp_supply = ctx.model().lp_total_supply(); let custodian = ctx.model().custodian(); let bal_a = token_dyn::balance(&token_a, &custodian).unwrap_or_default(); let bal_b = token_dyn::balance(&token_b, &custodian).unwrap_or_default(); let lp_shares = if amount_a * bal_b < amount_b * bal_a { amount_a * lp_supply / bal_a } else { amount_b * lp_supply / bal_b }; let supply_minus_one = lp_supply - 1.into(); Ok(DepositResult { deposit_a: (lp_shares * bal_a + supply_minus_one) / lp_supply, // round up deposit_b: (lp_shares * bal_b + supply_minus_one) / lp_supply, // round up lp_shares, }) } fn quote_withdraw(ctx: &impl ReadContext, shares: Integer) -> Result { validate_amount(shares)?; let token_a = ctx.model().token_a(); let token_b = ctx.model().token_b(); let lp_supply = ctx.model().lp_total_supply(); let custodian = ctx.model().custodian(); let bal_a = token_dyn::balance(&token_a, &custodian).unwrap_or_default(); let bal_b = token_dyn::balance(&token_b, &custodian).unwrap_or_default(); Ok(WithdrawResult { amount_a: shares * bal_a / lp_supply, amount_b: shares * bal_b / lp_supply, }) } impl Guest for Pool { // Dummy implementation for testing purposes. fn init(ctx: &ProcContext) { PoolStorage { token_a: ContractAddress { name: "".to_string(), height: 0, tx_index: 0, }, token_b: ContractAddress { name: "".to_string(), height: 0, tx_index: 0, }, lp_ledger: Map::default(), lp_total_supply: 0.into(), fee_bps: 0.into(), custodian: "".to_string(), } .init(ctx); } // This represents the production init function. // Only for local testing purposes. fn re_init( ctx: &ProcContext, token_a: ContractAddress, amount_a: Integer, token_b: ContractAddress, amount_b: Integer, fee: Integer, ) -> Result { PoolStorage::new(ctx, token_a, amount_a, token_b, amount_b, fee)?.init(ctx); Ok(ctx.model().lp_total_supply()) } fn fee(ctx: &ViewContext) -> Integer { ctx.model().fee_bps() } fn balance(ctx: &ViewContext, acc: String) -> Option { ctx.model().lp_ledger().get(acc) } fn transfer(ctx: &ProcContext, to: String, n: Integer) -> Result<(), Error> { let from = ctx.signer().to_string(); let ledger = ctx.model().lp_ledger(); let from_balance = ledger.get(&from).unwrap_or_default(); let to_balance = ledger.get(&to).unwrap_or_default(); if from_balance < n { return Err(Error::Message("insufficient funds".to_string())); } ledger.set(from, from_balance - n); ledger.set(to, to_balance + n); Ok(()) } fn token_balance(ctx: &ViewContext, token: ContractAddress) -> Result { token_out(ctx, &token)?; Ok(token_dyn::balance(&token, &ctx.model().custodian()).unwrap_or_default()) } fn quote_deposit( ctx: &ViewContext, amount_a: Integer, amount_b: Integer, ) -> Result { quote_deposit(ctx, amount_a, amount_b) } fn deposit( ctx: &ProcContext, amount_a: Integer, amount_b: Integer, ) -> Result { let res = quote_deposit(ctx, amount_a, amount_b)?; let ledger = ctx.model().lp_ledger(); let custodian = ctx.model().custodian(); let user = ctx.signer().to_string(); let bal = ledger.get(&user).unwrap_or_default(); ledger.set(user, bal + res.lp_shares); ctx.model().set_lp_total_supply(ctx.model().lp_total_supply() + res.lp_shares); token_dyn::transfer( &ctx.model().token_a(), ctx.signer(), &custodian, res.deposit_a, )?; token_dyn::transfer( &ctx.model().token_b(), ctx.signer(), &custodian, res.deposit_b, )?; Ok(res) } fn quote_withdraw(ctx: &ViewContext, shares: Integer) -> Result { quote_withdraw(ctx, shares) } fn withdraw(ctx: &ProcContext, shares: Integer) -> Result { let res = quote_withdraw(ctx, shares)?; let ledger = ctx.model().lp_ledger(); let user = ctx.signer().to_string(); let total = ctx.model().lp_total_supply(); let bal = ledger.get(&user).unwrap_or_default(); if total < shares { return Err(Error::Message("insufficient total supply".to_string())); } if bal < shares { return Err(Error::Message("insufficient share balance".to_string())); } ledger.set(user.clone(), bal - shares); ctx.model().set_lp_total_supply(total - shares); token_dyn::transfer( &ctx.model().token_a(), ctx.contract_signer(), &user, res.amount_a, )?; token_dyn::transfer( &ctx.model().token_b(), ctx.contract_signer(), &user, res.amount_b, )?; Ok(res) } fn quote_swap( ctx: &ViewContext, token_in: ContractAddress, amount_in: Integer, ) -> Result { quote_swap(ctx, &token_in, amount_in) } fn swap( ctx: &ProcContext, token_in: ContractAddress, amount_in: Integer, min_out: Integer, ) -> Result { let token_out = token_out(ctx, &token_in)?; let amount_out = quote_swap(ctx, &token_in, amount_in)?; if amount_out < min_out { return Err(Error::Message(format!( "amount out ({}) below minimum", amount_out ))); } token_dyn::transfer( &token_in, ctx.signer(), &ctx.model().custodian(), amount_in, )?; token_dyn::transfer( &token_out, ctx.contract_signer(), &ctx.signer().to_string(), amount_out, )?; Ok(amount_out) } } ``` ## Testing ```rust theme={null} #[cfg(test)] mod tests { use testlib::*; interface!(name = "pool"); interface!(name = "token_a", path = "../../token/contract/wit"); interface!(name = "token_b", path = "../../token/contract/wit"); #[testlib::test(contracts_dir = "../../")] async fn test_contract() -> Result<()> { let admin = runtime.identity().await?; let minter = runtime.identity().await?; let pool = runtime.publish(&admin, "pool").await?; let token_a = runtime.publish(&admin, "token").await?; let token_b = runtime.publish(&admin, "token").await?; token_a::mint(runtime, &token_a, &minter, 1000.into()).await??; token_b::mint(runtime, &token_b, &minter, 1000.into()).await??; token_a::transfer(runtime, &token_a, &minter, &admin, 100.into()).await??; token_b::transfer(runtime, &token_b, &minter, &admin, 500.into()).await??; let res = pool::re_init( runtime, &pool, &admin, token_a.clone(), 100.into(), token_b.clone(), 500.into(), 0.into(), ) .await?; assert_eq!(res, Ok(223.into())); let bal_a = pool::token_balance(runtime, &pool, token_a.clone()).await?; assert_eq!(bal_a, Ok(100.into())); let bal_b = pool::token_balance(runtime, &pool, token_b.clone()).await?; assert_eq!(bal_b, Ok(500.into())); let k1 = bal_a.unwrap() * bal_b.unwrap(); let res = pool::quote_swap(runtime, &pool, token_a.clone(), 10.into()).await?; assert_eq!(res, Ok(45.into())); let res = pool::quote_swap(runtime, &pool, token_a.clone(), 100.into()).await?; assert_eq!(res, Ok(250.into())); let res = pool::quote_swap(runtime, &pool, token_a.clone(), 1000.into()).await?; assert_eq!(res, Ok(454.into())); let res = pool::swap(runtime, &pool, &minter, token_a.clone(), 10.into(), 46.into()).await?; assert!(res.is_err()); // below minimum let res = pool::swap(runtime, &pool, &minter, token_a.clone(), 10.into(), 45.into()).await?; assert_eq!(res, Ok(45.into())); let bal_a = pool::token_balance(runtime, &pool, token_a.clone()).await?; let bal_b = pool::token_balance(runtime, &pool, token_b.clone()).await?; let k2 = bal_a.unwrap() * bal_b.unwrap(); assert!(k2 >= k1); let res = pool::quote_swap(runtime, &pool, token_b.clone(), 45.into()).await?; assert_eq!(res, Ok(9.into())); let res = pool::swap(runtime, &pool, &minter, token_b.clone(), 45.into(), 0.into()).await?; assert_eq!(res, Ok(9.into())); let bal_a = pool::token_balance(runtime, &pool, token_a.clone()).await?; let bal_b = pool::token_balance(runtime, &pool, token_b.clone()).await?; let k3 = bal_a.unwrap() * bal_b.unwrap(); assert!(k3 >= k2); // use token interface to transfer shares let res = token::balance(runtime, &pool, &admin).await?; assert_eq!(res, Some(223.into())); let res = token::balance(runtime, &pool, &minter).await?; assert_eq!(res, None); token::transfer(runtime, &pool, &admin, &minter, 23.into()).await??; let res = token::balance(runtime, &pool, &admin).await?; assert_eq!(res, Some(200.into())); let res = token::balance(runtime, &pool, &minter).await?; assert_eq!(res, Some(23.into())); Ok(()) } } ``` # 5. Proxy Source: https://docs.kontor.network/docs/sigil/examples/proxy **Full working code**: The complete source code for this example is available in [`test-contracts/proxy/`](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts/proxy) This example demonstrates proxying (a common pattern used in contract upgrades), utilizing the `fallback` hook and introducing the `foreign::call` built-in and aliased imports. ## WIT Interface The proxy contract exports functions for delegation and configuration: ```wit theme={null} package root:component; world root { include kontor:built-in/built-in; use kontor:built-in/context.{fall-context, proc-context, view-context, signer}; use kontor:built-in/foreign.{contract-address}; export fallback: func(ctx: borrow, expr: string) -> string; export init: func(ctx: borrow); export get-contract-address: func(ctx: borrow) -> option; export set-contract-address: func(ctx: borrow, contract-address: contract-address); } ``` ## Rust Implementation The proxy contract stores a target contract address and forwards all calls to it using the `fallback` hook: ```rust theme={null} use stdlib::*; contract!(name = "proxy"); #[derive(Clone, StorageRoot, Default)] struct ProxyStorage { contract_address: Option, } impl Guest for Proxy { fn fallback(ctx: &FallContext, expr: String) -> String { let ctx_ = &ctx.view_context(); if let Some(contract_address) = ctx_.model().contract_address() { foreign::call(ctx.signer(), &contract_address, &expr) } else { "".to_string() } } fn init(ctx: &ProcContext) { ProxyStorage::default().init(ctx) } fn get_contract_address(ctx: &ViewContext) -> Option { ctx.model().contract_address() } fn set_contract_address(ctx: &ProcContext, contract_address: ContractAddress) { ctx.model().set_contract_address(Some(contract_address)); } } ``` **Key points:** * Storage accessed via `ctx.model()` * `fallback` handles missing function calls by forwarding to the target contract * Uses `if let Some(...)` to safely handle the `Option` * `FallContext` can be converted to ViewContext for read-only access * `foreign::call` is the low-level API used by `interface!` and `import!` macros ## Testing The test demonstrates configuring the proxy and forwarding calls to a shared-account contract: ```rust theme={null} #[cfg(test)] mod tests { use testlib::*; interface!(name = "proxy"); interface!(name = "token", path = "../../token/contract/wit"); interface!(name = "shared-account", path = "../../shared-account/contract/wit"); #[testlib::test(contracts_dir = "../../")] async fn test_shared_account_contract() -> Result<()> { let alice = runtime.identity().await?; let bob = runtime.identity().await?; let claire = runtime.identity().await?; let dara = runtime.identity().await?; let proxy = runtime.publish(&alice, "proxy").await?; let token = runtime.publish(&alice, "token").await?; let shared_account = runtime.publish(&alice, "shared-account").await?; // Configure proxy to forward to shared-account proxy::set_contract_address(runtime, &proxy, &alice, shared_account.clone()).await?; let result = proxy::get_contract_address(runtime, &proxy).await?; assert_eq!(result, Some(shared_account.clone())); // Use token contract token::mint(runtime, &token, &alice, 100.into()).await??; // Call shared-account THROUGH the proxy using the fallback function let account_id = shared_account::open( runtime, &proxy, &alice, token.clone(), 50.into(), vec![&bob, &dara], ) .await??; let result = shared_account::balance(runtime, &proxy, &account_id).await?; assert_eq!(result, Some(50.into())); // Further deposits and withdrawals through proxy shared_account::deposit(runtime, &proxy, &alice, token.clone(), &account_id, 25.into()).await??; let result = shared_account::balance(runtime, &proxy, &account_id).await?; assert_eq!(result, Some(75.into())); // Test withdrawals shared_account::withdraw(runtime, &proxy, &bob, token.clone(), &account_id, 25.into()).await??; let result = shared_account::balance(runtime, &proxy, &account_id).await?; assert_eq!(result, Some(50.into())); // Test error handling let result = shared_account::withdraw(runtime, &proxy, &claire, token.clone(), &account_id, 1.into()).await?; assert_eq!(result, Err(Error::Message("unauthorized".to_string()))); Ok(()) } } ``` **Key points:** * Uses `#[testlib::test]` with auto-injected `runtime` * Publishes all three contracts (proxy, token, shared-account) * Configures proxy to point to shared-account * Calls shared-account functions through the proxy address * The proxy's `fallback` function automatically forwards calls # 3. Shared Account Source: https://docs.kontor.network/docs/sigil/examples/shared-account **Full working code**: The complete source code for this example is available in [`test-contracts/shared-account/`](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts/shared-account) This example demonstrates a simple multi-tenant shared account introducing static contract imports, complex storage structs, authorization logic, ID generation, and cross-contract calls. ## WIT Interface Exports functions for multi-tenant account management with token deposits and withdrawals: ```wit theme={null} package root:component; world root { include kontor:built-in/built-in; use kontor:built-in/context.{view-context, proc-context, signer}; use kontor:built-in/error.{error}; use kontor:built-in/foreign.{contract-address}; use kontor:built-in/numbers.{integer, decimal}; export init: func(ctx: borrow); export open: func(ctx: borrow, token: contract-address, n: integer, other-tenants: list) -> result; export deposit: func(ctx: borrow, token: contract-address, account-id: string, n: integer) -> result<_, error>; export withdraw: func(ctx: borrow, token: contract-address, account-id: string, n: integer) -> result<_, error>; export balance: func(ctx: borrow, account-id: string) -> option; export token-balance: func(ctx: borrow, token: contract-address, holder: string) -> option; export tenants: func(ctx: borrow, account-id: string) -> option>; } ``` ## Rust Implementation * The `interface!` macro generates an interface for cross-contract calls to the `token` contract with a dynamic address (passed as a parameter). Use `import!` when the dependency contract has been previously published and the contract address is known at compile time. * The `StorageRoot` macro, used for the root storage type, and the `Storage` macro, used for nested storage types in the `Account` struct, enable persistent storage. * `other_tenants` uses `Map` because the storage layer does not currently support list types, and `Map` provides a limited interface with the `keys` method for iteration. Even with a `List` type using a `Map` here could make sense. Instead of `bool` an `enum` or `struct` that defines the their "role" in the account could be written. * `authorized` Verifies procedure permissions * `open`: Verifies token balance, generates an ID using `crypto::generate_id()`, sets the account, and transfers tokens to `ctx.contract_signer()` * `deposit`/`withdraw`: Authorize the caller, verify balances, update storage, and call the token contract following CEI pattern * `balance`/`tenants`: Query the storage for account details ```rust theme={null} use stdlib::*; contract!(name = "shared-account"); interface!(name = "token", path = "../../token/contract/wit"); #[derive(Clone, Default, Storage)] struct Account { pub other_tenants: Map, pub balance: Integer, pub owner: String, } #[derive(Clone, Default, StorageRoot)] struct SharedAccountStorage { pub accounts: Map, } fn authorized(signer: &Signer, account: &AccountModel) -> bool { account.owner() == signer.to_string() || account .other_tenants() .get(signer.to_string()) .is_some_and(|b| b) } fn insufficient_balance_error() -> Error { Error::Message("insufficient balance".to_string()) } fn unauthorized_error() -> Error { Error::Message("unauthorized".to_string()) } fn unknown_error() -> Error { Error::Message("unknown account".to_string()) } impl Guest for SharedAccount { fn init(ctx: &ProcContext) { SharedAccountStorage::default().init(ctx); } fn open( ctx: &ProcContext, token: ContractAddress, n: Integer, other_tenants: Vec, ) -> Result { let signer = ctx.signer(); let balance = token::balance(&token, &signer.to_string()).ok_or(insufficient_balance_error())?; if balance < n { return Err(insufficient_balance_error()); } let account_id = ctx.generate_id(); ctx.model().accounts().set( account_id.clone(), Account { balance: n, owner: ctx.signer().to_string(), other_tenants: Map::new( &other_tenants .into_iter() .map(|t| (t, true)) .collect::>(), ), }, ); token::transfer(&token, signer, &ctx.contract_signer().to_string(), n)?; Ok(account_id) } fn deposit( ctx: &ProcContext, token: ContractAddress, account_id: String, n: Integer, ) -> Result<(), Error> { let signer = ctx.signer(); let balance = token::balance(&token, &signer.to_string()).ok_or(insufficient_balance_error())?; if balance < n { return Err(insufficient_balance_error()); } let account = ctx .model() .accounts() .get(account_id) .ok_or(unknown_error())?; if !authorized(&signer, &account) { return Err(unauthorized_error()); } account.update_balance(|b| b + n); token::transfer(&token, signer, &ctx.contract_signer().to_string(), n) } fn withdraw( ctx: &ProcContext, token: ContractAddress, account_id: String, n: Integer, ) -> Result<(), Error> { let signer = ctx.signer(); let account = ctx .model() .accounts() .get(account_id) .ok_or(unknown_error())?; if !authorized(&signer, &account) { return Err(unauthorized_error()); } let balance = account.balance(); if balance < n { return Err(insufficient_balance_error()); } account.set_balance(balance - n); token::transfer(&token, ctx.contract_signer(), &signer.to_string(), n) } fn balance(ctx: &ViewContext, account_id: String) -> Option { ctx.model().accounts().get(account_id).map(|a| a.balance()) } fn token_balance( _ctx: &ViewContext, token: ContractAddress, holder: String, ) -> Option { token::balance(&token, &holder) } fn tenants(ctx: &ViewContext, account_id: String) -> Option> { ctx.model().accounts().get(account_id).map(|a| { [a.owner()] .into_iter() .chain(a.other_tenants().keys()) .collect() }) } } ``` **Key points:** * Uses `interface!` not `import!` for dynamic token contract address * `ctx.model()` for storage access * Map operations: `accounts().get(id)` returns `Option` * Nested struct updates: `account.update_balance(|b| b + n)` * Error helpers return `Error::Message(...to_string())` * `authorized()` function takes `&AccountModel` not `&AccountWrapper` * `ctx.generate_id()` creates unique account IDs * Calls `token::balance()` and `token::transfer()` with contract addresses ## Testing The test demonstrates creating accounts, depositing, withdrawing, and testing authorization: ```rust theme={null} #[cfg(test)] mod tests { use testlib::*; interface!(name = "token", path = "../../token/contract/wit"); interface!(name = "shared-account"); #[testlib::test(contracts_dir = "../../")] async fn test_shared_account_contract() -> Result<()> { let alice = runtime.identity().await?; let bob = runtime.identity().await?; let claire = runtime.identity().await?; let dara = runtime.identity().await?; let token = runtime.publish(&alice, "token").await?; let shared_account = runtime.publish(&alice, "shared-account").await?; token::mint(runtime, &token, &alice, 100.into()).await??; let account_id = shared_account::open( runtime, &shared_account, &alice, token.clone(), 50.into(), vec![&bob, &dara], ) .await??; let result = shared_account::balance(runtime, &shared_account, &account_id).await?; assert_eq!(result, Some(50.into())); shared_account::deposit(runtime, &shared_account, &alice, token.clone(), &account_id, 25.into()) .await??; let result = shared_account::balance(runtime, &shared_account, &account_id).await?; assert_eq!(result, Some(75.into())); shared_account::withdraw(runtime, &shared_account, &bob, token.clone(), &account_id, 25.into()) .await??; let result = shared_account::balance(runtime, &shared_account, &account_id).await?; assert_eq!(result, Some(50.into())); shared_account::withdraw(runtime, &shared_account, &alice, token.clone(), &account_id, 50.into()) .await??; let result = shared_account::balance(runtime, &shared_account, &account_id).await?; assert_eq!(result, Some(0.into())); let result = shared_account::withdraw(runtime, &shared_account, &bob, token.clone(), &account_id, 1.into()) .await?; assert_eq!( result, Err(Error::Message("insufficient balance".to_string())) ); let result = shared_account::withdraw(runtime, &shared_account, &claire, token.clone(), &account_id, 1.into()) .await?; assert_eq!(result, Err(Error::Message("unauthorized".to_string()))); let result = shared_account::token_balance(runtime, &shared_account, token.clone(), &alice).await?; assert_eq!(result, Some(75.into())); let result = token::balance(runtime, &token, &bob).await?; assert_eq!(result, Some(25.into())); let result = shared_account::tenants(runtime, &shared_account, &account_id) .await? .unwrap(); assert_eq!(result.iter().len(), 3); assert!(result.contains(&alice.to_string())); assert!(result.contains(&dara.to_string())); assert!(result.contains(&bob.to_string())); Ok(()) } } ``` # 2. Token Source: https://docs.kontor.network/docs/sigil/examples/token **Full working code**: The complete source code for this example is available in [`test-contracts/test-token/`](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts/test-token) This example demonstrates persistent storage, user balances, transactions, and error handling for a fungible token. ## WIT Interface Exports functions for a fungible token with minting, burning, transfers, and balance queries: ```wit theme={null} package root:component; world root { include kontor:built-in/built-in; use kontor:built-in/context.{core-context, view-context, proc-context, signer}; use kontor:built-in/error.{error}; use kontor:built-in/numbers.{integer}; record balance { key: string, value: integer, } export init: func(ctx: borrow); export mint: func(ctx: borrow, n: integer) -> result<_, error>; export burn: func(ctx: borrow, n: integer) -> result<_, error>; export transfer: func(ctx: borrow, to: string, n: integer) -> result<_, error>; export balance: func(ctx: borrow, acc: string) -> option; export balances: func(ctx: borrow) -> list; export total-supply: func(ctx: borrow) -> integer; } ``` ## Rust Implementation The token contract uses a Map to store account balances, with helper functions for validation and a total supply tracker: ```rust theme={null} use stdlib::*; contract!(name = "token"); const BURNER: &str = "burn"; #[derive(Clone, Default, StorageRoot)] struct TokenStorage { pub ledger: Map, pub total_supply: Integer, } fn assert_gt_zero(n: Integer) -> Result<(), Error> { if n <= 0.into() { return Err(Error::Message("Amount must be positive".to_string())); } Ok(()) } impl Guest for Token { fn init(ctx: &ProcContext) { TokenStorage::default().init(ctx); } fn mint(ctx: &ProcContext, n: Integer) -> Result<(), Error> { assert_gt_zero(n)?; let to = ctx.signer().to_string(); let ledger = ctx.model().ledger(); let balance = ledger.get(&to).unwrap_or_default(); ledger.set(to, balance.add(n)?); ctx.model().try_update_total_supply(|t| t.add(n))?; Ok(()) } fn burn(ctx: &ProcContext, n: Integer) -> Result<(), Error> { Self::transfer(ctx, BURNER.to_string(), n)?; ctx.model().try_update_total_supply(|t| t.sub(n))?; Ok(()) } fn transfer(ctx: &ProcContext, to: String, n: Integer) -> Result<(), Error> { assert_gt_zero(n)?; let from = ctx.signer().to_string(); let ledger = ctx.model().ledger(); let from_balance = ledger.get(&from).unwrap_or_default(); let to_balance = ledger.get(&to).unwrap_or_default(); if from_balance < n { return Err(Error::Message("insufficient funds".to_string())); } ledger.set(from, from_balance.sub(n)?); ledger.set(to, to_balance.add(n)?); Ok(()) } fn balance(ctx: &ViewContext, acc: String) -> Option { ctx.model().ledger().get(acc) } fn balances(ctx: &ViewContext) -> Vec { ctx.model() .ledger() .keys() .filter_map(|k| { if [BURNER.to_string()].contains(&k) { None } else { Some(Balance { value: ctx.model().ledger().get(&k).unwrap_or_default(), key: k, }) } }) .collect() } fn total_supply(ctx: &ViewContext) -> Integer { ctx.model().total_supply() } } ``` **Key points:** * Storage accessed via `ctx.model()`, not a separate `storage()` function * Map operations: `ledger.get(&key)` and `ledger.set(key, value)` - no extra ctx parameter * Errors use `Error::Message(...to_string())` variant * Uses checked arithmetic (`.add(n)?`, `.sub(n)?`) for overflow safety * Helper validation function (`assert_gt_zero`) * Total supply tracking with `try_update_*` closures ## Testing The test demonstrates minting, transferring, and error handling with proper use of the double `??` operator for functions returning `Result`: ```rust theme={null} #[cfg(test)] mod tests { use testlib::*; interface!(name = "token"); #[testlib::test] async fn test_contract() -> Result<()> { let minter = runtime.identity().await?; let holder = runtime.identity().await?; let token = runtime.publish(&minter, "token").await?; token::mint(runtime, &token, &minter, 900.into()).await??; token::mint(runtime, &token, &minter, 100.into()).await??; let result = token::balance(runtime, &token, &minter).await?; assert_eq!(result, Some(1000.into())); // Test insufficient funds error let result = token::transfer(runtime, &token, &holder, &minter, 123.into()).await?; assert_eq!( result, Err(Error::Message("insufficient funds".to_string())) ); token::transfer(runtime, &token, &minter, &holder, 40.into()).await??; token::transfer(runtime, &token, &minter, &holder, 2.into()).await??; let result = token::balance(runtime, &token, &holder).await?; assert_eq!(result, Some(42.into())); let result = token::balance(runtime, &token, &minter).await?; assert_eq!(result, Some(958.into())); // Test non-existent account let result = token::balance(runtime, &token, "foo").await?; assert_eq!(result, None); // Test balances list and total supply let balances = token::balances(runtime, &token).await?; assert_eq!(balances.len(), 2); let total = balances .iter() .fold(Integer::from(0), |acc, x| acc + x.value); assert_eq!(total, token::total_supply(runtime, &token).await?); Ok(()) } } ``` **Key points:** * Uses `interface!` to generate bindings, not `import!` * `#[testlib::test]` auto-injects `runtime` variable * `runtime.identity()` creates test users * `runtime.publish()` deploys the contract * Functions returning `Result` use `??` to unwrap both the runtime Result and contract Result * Integers created with `.into()` or `Integer::from()` # Introduction Source: https://docs.kontor.network/docs/sigil/introduction **Sigil** is a **next-generation WebAssembly-based smart contract framework** that enables developers to develop, deploy, and run rich smart contract applications on Bitcoin with the Kontor metaprotocol. Sigil is designed to offer strong compile-time type safety, powerful language built-ins, an expressive storage interface, rich native types, native cross-contract calls and excellent overall ergonomics. **Attention:** Sigil is currently in *early preview*. Planned features include: * Typescript SDK for interacting with contracts client-side * Support for list types in storage * Native `Address` type * Enhanced querying capabilities, including key prefix matching for `Map` types ## Why Sigil? The primary goal of the Sigil framework is to make writing safe, secure smart contracts easy and natural. Unlike other smart contract frameworks, writing contracts in Sigil is designed to feel like writing normal software, requiring developers to learn a minimum number of additional concepts and to jump through as few hoops as possible to write, deploy and run their contracts on Bitcoin with Kontor. Sigil notably leverages the WebAssembly Component Model to allow contracts to link natively and with full type-safety. This provides deep integration with conventional IDEs and popular Rust tooling. It shifts errors to build-time, keeps determinism and gas behavior predictable, and reduces the trusted computing base. Sigil thus dramatically improves developer ergonomics while fully supporting a flexible and extensible ecosystem built on open standards. ## WebAssembly Component Model WebAssembly is a natural choice for blockchain smart contracts because it offers deterministic execution, efficient bytecode, and broad language support. However, most WASM-based blockchain platforms treat contracts as isolated modules with custom ABIs for host interaction—requiring developers to manually serialize data, manage dependencies, and coordinate interfaces through ad-hoc conventions. Sigil takes a new approach by adopting the WebAssembly Component Model, a standardized system for composing WASM modules with typed interfaces. This means contracts can call each other with full type safety, interfaces are specified in a language-agnostic IDL (WIT), and integration errors are caught at compile time rather than discovered when transactions fail on-chain. ## Type Safety In Sigil, contracts export functions with explicit signatures defined in WIT. If Contract A calls Contract B, Sigil generates a typed stub for B's interface that A imports at compile time. If B's interface changes—say, adding a field to a struct or modifying a function parameter—A will fail to compile until the change is addressed. This eliminates an entire class of bugs common in other smart contract systems, where interface mismatches cause runtime failures after deployment. The same type checking applies to storage: Sigil's storage system uses a path-based hierarchy with Rust traits that ensure you can't accidentally store the wrong type at a given path or retrieve a value with an incorrect type annotation. This type safety extends to contract standards: an interface like ERC-20 can be defined once in WIT. When you use the `import!` macro to reference a specific contract at a known address, the compiler verifies that contract implements the interface exactly—providing full compile-time safety. When using `interface!` to accept dynamic contract addresses, the compiler ensures your contract makes valid calls to the interface, but runtime errors can still occur if users provide addresses that don't implement the expected interface. ## Contract Structure and Execution A Sigil contract defines an `init` function (called once when the contract is deployed), procedural functions that can read and write state, and view functions that are read-only. Functions receive a context object that provides access to the transaction signer, enables cross-contract calls, and exposes storage operations. Storage is lazy-loaded—reading a value queries the database only for that specific path rather than deserializing an entire contract state object. This makes gas costs predictable and proportional to actual usage. Cross-contract calls are synchronous and type-checked; the callee executes immediately within the caller's transaction, and re-entrancy is prohibited to prevent common exploit patterns. ## Developer Experience Sigil provides simple macros that generate the necessary boilerplate, allowing developers to focus on application logic rather than framework integration. The `contract!` macro sets up the module structure and WIT bindings. The `#[derive(Storage)]` macro generates typed accessors for contract state. Cross-contract calls use the `import!` macro to pull in another contract's interface and generate a local stub. The result is code that looks like ordinary Rust: strongly typed, well-integrated with language tooling, and testable using standard Rust test frameworks. Because the runtime primitives are abstracted behind traits, the same contract code runs in unit tests (with an in-memory backend) and on-chain (with the indexer's SQLite backend) without modification. # Getting Started Source: https://docs.kontor.network/docs/sigil/quickstart This guide walks you through setting up your development environment for Sigil smart contract development. ## Full Development Environment If you're setting up a full Kontor development environment (including running the indexer and tests), see the [Kontor README](https://github.com/KontorProtocol/Kontor/blob/main/README.md) which covers: * Installing all build dependencies (Bitcoin, ZMQ, etc.) * Compiling Bitcoin Core for local testing * Running the full test suite * Docker deployment ## Contract Development Only To develop and test Sigil contracts without running a local indexer: ### Install Dependencies **MacOS:** ```bash theme={null} brew install binaryen brotli ``` **Ubuntu:** ```bash theme={null} sudo apt install binaryen brotli ``` ### Set Up Rust 1. Install Rust from [rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) 2. Add the WebAssembly target: ```bash theme={null} rustup target add wasm32-unknown-unknown ``` 3. Install additional tools: ```bash theme={null} cargo install wasm-opt ``` ## Working with Example Contracts The [example contracts](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts) in the Kontor repository require the Kontor indexer `core` libraries. **The examples live in the same repository as the `core` libraries**, so the paths work out of the box: ```bash theme={null} git clone https://github.com/KontorProtocol/Kontor.git cd Kontor/test-contracts ``` **If you copy an example into your own workspace**, update the workspace dependencies in your root `Cargo.toml`: ```toml theme={null} [workspace.dependencies] testlib = { path = "/path/to/Kontor/core/testlib" } stdlib = { path = "/path/to/Kontor/core/stdlib" } ``` Alternatively, create your own contract workspace from scratch following the [Hello World example](/docs/sigil/examples/hello-world), which shows the complete setup. ## Next Steps After completing Hello World, explore more examples: * [**Token**](/docs/sigil/examples/token) - Learn about storage, maps, and state management * [**Shared Account**](/docs/sigil/examples/shared-account) - See cross-contract calls in action * [**AMM**](/docs/sigil/examples/amm) - Build a full automated market maker with complex logic * [**Pool**](/docs/sigil/examples/pool) - Understand liquidity pool patterns For deeper understanding, see the [Reference documentation](/docs/sigil/reference/contract-structure). # Contract Structure Source: https://docs.kontor.network/docs/sigil/reference/contract-structure A Sigil contract consists of five key components: imports, the contract declaration, interface imports (optional), storage definitions, and the Guest trait implementation containing your contract logic. ## Anatomy of a Contract ```rust theme={null} // 1. Import the standard library use stdlib::*; // 2. Declare the contract contract!(name = "token"); // 3. Import other contract interfaces (optional) interface!(name = "other_contract", path = "other/wit"); // 4. Define storage structure(s) #[derive(Clone, Default, StorageRoot)] struct TokenStorage { pub ledger: Map, } // 5. Implement the Guest trait (contract logic) impl Guest for Token { fn init(ctx: &ProcContext) { TokenStorage::default().init(ctx); } fn mint(ctx: &ProcContext, n: Integer) { let ledger = ctx.model().ledger(); let account = ctx.signer().to_string(); let balance = ledger.get(&account).unwrap_or_default(); ledger.set(account, balance + n); } fn balance(ctx: &ViewContext, acc: String) -> Option { ctx.model().ledger().get(&acc) } } ``` ## The contract! Macro The `contract!` macro generates the necessary boilerplate for your contract: ```rust theme={null} contract!(name = "my_contract"); ``` **What it does:** * Generates wit-bindgen glue code * Creates the `Guest` trait you implement * Sets up WASM component exports * The name must match your WIT package **Requirements:** * Must appear after `use stdlib::*;` * Name must match the contract name in your WIT file ## Context Types Every function in a Sigil contract receives a context as its first parameter. There are three context types: ### ViewContext - Read-Only Queries ```rust theme={null} fn balance(ctx: &ViewContext, acc: String) -> Option { ctx.model().ledger().get(acc) } ``` **Available methods:** * `ctx.storage()` - Access low-level storage * `ctx.model()` - Get generated storage model (read-only) **Use for:** Functions that don't modify state, callable via API ### ProcContext - State-Modifying Transactions ```rust theme={null} fn transfer(ctx: &ProcContext, to: String, n: Integer) -> Result<(), Error> { let from = ctx.signer().to_string(); // ... modify storage } ``` **Available methods:** * `ctx.signer()` - Transaction signer * `ctx.contract_signer()` - Contract's own address (for receiving tokens) * `ctx.storage()` - Access storage * `ctx.model()` - Get generated storage model (read-write) * `ctx.generate_id()` - Generate unique IDs * `ctx.view_context()` - Get read-only view **Use for:** Functions that modify state, called via blockchain transactions ### FallContext - Fallback Handler ```rust theme={null} fn fallback(ctx: &FallContext, expr: String) -> String { // Proxy to another contract if let Some(addr) = ctx.view_context().model().contract_address() { foreign::call(ctx.signer(), &addr, &expr) } else { "".to_string() } } ``` **Available methods:** * `ctx.signer()` - Returns `Option` (may be None for view calls) * `ctx.proc_context()` - Returns `Option` (Some if called with signer) * `ctx.view_context()` - Always available **Use for:** Generic delegation and proxy patterns ## WIT Files WIT (WebAssembly Interface Type) files define your contract's public interface. ### Relationship to Rust Code * WIT files are **hand-written** by you * They define the **public API** of your contract * Every exported function in WIT must be implemented in Rust * The `contract!` macro generates Rust types from your WIT ### Example WIT File ```wit theme={null} package root:component; world root { // Include built-in types and interfaces include kontor:built-in/built-in; // Import specific types you'll use use kontor:built-in/context.{view-context, proc-context}; use kontor:built-in/numbers.{integer}; use kontor:built-in/error.{error}; // Export functions (your contract's API) export init: func(ctx: borrow); export mint: func(ctx: borrow, n: integer); export transfer: func(ctx: borrow, to: string, n: integer) -> result<_, error>; export balance: func(ctx: borrow, acc: string) -> option; } ``` ### Built-in WIT Interfaces The `kontor:built-in` package provides: * **`context`** - Storage and execution context (ViewContext, ProcContext, FallContext) * **`numbers`** - Arbitrary precision Integer and Decimal types * **`error`** - Error type with variants (Message, Overflow, DivByZero, SyntaxError) * **`crypto`** - Hash functions * **`foreign`** - Cross-contract calls See `core/indexer/src/runtime/wit/deps/built-in.wit` for complete reference. ## Module Organization ### Workspace Structure Contracts are organized in a Cargo workspace: ```toml theme={null} [workspace] members = ["token", "amm", "pool"] resolver = "2" [workspace.dependencies] wit-bindgen = "=0.47.0" stdlib = { path = "../core/stdlib" } ``` ### One Contract Per Crate Each contract is a separate Rust crate with: * `Cargo.toml` - Dependencies and build config * `src/lib.rs` - Contract implementation * `wit/contract.wit` - Interface definition * `wit/deps/` - Symlink to built-in types ### Build Configuration **Cargo.toml:** ```toml theme={null} [lib] crate-type = ["cdylib"] # Required for WASM [dependencies] wit-bindgen = "=0.47.0" # Must be exact version stdlib = { path = "../core/stdlib" } ``` **.cargo/config.toml:** ```toml theme={null} [build] target = "wasm32-unknown-unknown" [target.wasm32-unknown-unknown] rustflags = ["-C", "target-feature=-simd128"] ``` ## Hooks Sigil defines specific function names that serve as hooks: ### init Hook ```rust theme={null} fn init(ctx: &ProcContext) { MyStorage::default().init(ctx); } ``` **Called when:** * Contract is first deployed (publish transaction) * Automatically by the runtime **Use for:** * Setting initial storage values * Contract initialization logic * Data migrations for upgrades ### fallback Hook ```rust theme={null} fn fallback(ctx: &FallContext, expr: String) -> String { // Handle calls to non-existent functions } ``` **Called when:** * A function is called that doesn't exist * Primarily for proxy contracts **Use for:** * Implementing proxy patterns * Version upgrades * Generic delegation ## Quick Reference ### Context Types **ProcContext** * Enables state-modifying operations, such as balance transfers * Provides write access to storage and signer access via `ctx.signer()` * Used in functions like `mint` or `transfer` to update blockchain state **ViewContext** * Supports read-only queries for inspecting contract state * Restricts access to read-only storage operations, no signer or mutations allowed * Used in functions like `balance` for retrieving data without modifying the blockchain **FallContext** * Manages unmatched calls via the `fallback` hook, enabling proxy patterns * Converts to `ViewContext` or `Option` for storage reads * Exclusive to the `fallback` function for dynamic routing ### Context Traits (for helper functions) **WriteContext** * For mutation logic (e.g., internal updates) * Implemented only by `ProcContext` **ReadContext** * For read-only logic (e.g., internal queries) * Implemented by both `ViewContext` and `ProcContext`, enabling shared read operations ### Storage **StorageRoot** * Marks the root struct or enum for contract storage **Map\** * Key-value store for collections (e.g., account balances) * Supports `get`, `set`, and `keys` methods **Storage Access** * `ctx.model()` - Returns the typed storage model * Field accessors (e.g., `ctx.model().ledger()`) - Provide structured access * Get/set methods - No ctx parameter needed (e.g., `ledger.get(&key)`, `ledger.set(key, value)`) ### Signer Access * `ctx.signer()` - Retrieves the transaction signer (ProcContext only) * `ctx.contract_signer()` - Returns the contract's own address (for receiving tokens) ### Cross-Contract Calls **Static imports:** ```rust theme={null} import!(name = "token", height = 12345, tx_index = 0, path = "path/to/wit"); // Direct calls: token::transfer(...) ``` **Dynamic interfaces:** ```rust theme={null} interface!(name = "token_dyn", path = "path/to/wit"); // Dynamic calls with address: token_dyn::transfer(&address, ...) ``` ### Utilities **crypto::generate\_id() -> String** * Generates unique IDs for entities (e.g., account IDs) **crypto::hash(String) -> (String, Vec\)** * Applies sha256 hash and returns hex encoded string and raw bytes **crypto::hash\_with\_salt(data: String, salt: String) -> (String, Vec\)** * Applies sha256 hash to string concatenated with salt ### Error Type ```rust theme={null} enum Error { Message(String), // Custom error messages Overflow(String), // Arithmetic overflow DivByZero(String), // Division by zero SyntaxError(String), // Syntax errors } ``` ### Macros **contract!(name = "name")** * Defines contract name and generates boilerplate code from WIT file **import!(name, height, tx\_index, path)** * Statically imports another contract's WIT for cross-contract calls **interface!(name, path)** * Defines dynamic interface for runtime contract calls with dynamic addresses # Cross-Contract Calls Source: https://docs.kontor.network/docs/sigil/reference/cross-contract-calls Sigil contracts can call other contracts with full type safety. The framework provides two macros for importing contract interfaces: `interface!` for dynamic addresses and `import!` for fixed addresses. ## The interface! Macro Import another contract's interface without specifying its address: ```rust theme={null} interface!(name = "token", path = "token/wit"); ``` **What it does:** * Reads the WIT file from the specified path * Generates type-safe Rust bindings * Creates functions you can call from your contract ## The import! Macro Import a specific contract instance at a known address: ```rust theme={null} import!( name = "token", mod_name = "my_token", height = 100, tx_index = 0, path = "token/wit" ); ``` **Parameters:** * `name` - Contract name * `mod_name` - Rust module name (avoid conflicts) * `height` - Block height where contract was deployed * `tx_index` - Transaction index in that block * `path` - Path to WIT file The macro generates functions similar to `interface!` but with the contract address fixed at compile time. ### When to Use Each **Use `interface!` for dynamic addresses** (passed as parameters): ```rust theme={null} fn deposit(ctx: &ProcContext, token: ContractAddress, amount: Integer) -> Result<(), Error> { token::transfer(&token, ctx.signer(), &ctx.contract_signer().to_string(), amount)?; // ... } ``` **Use `import!` for fixed addresses** (known at compile time): ```rust theme={null} // In tests or contracts with known dependencies import!( name = "arith", mod_name = "arith_v1", height = 1, tx_index = 0, path = "arith/wit" ); fn calculate(ctx: &ProcContext, x: u64) -> u64 { arith_v1::eval(ctx.signer(), x, arith_v1::Op::Id).value } ``` ## Standard Interfaces The `interface!` macro enables **standards** similar to Ethereum's ERC-20: define an interface once, and contracts can work with any implementation of that interface. ### Defining a Token Standard Extract common token functions into a standalone WIT file that any token can implement: ```wit theme={null} // erc20-standard.wit package erc20:standard; interface erc20 { use kontor:built-in/context.{view-context, proc-context}; use kontor:built-in/error.{error}; use kontor:built-in/numbers.{integer}; total-supply: func(ctx: borrow) -> integer; balance-of: func(ctx: borrow, account: string) -> integer; transfer: func(ctx: borrow, to: string, amount: integer) -> result<_, error>; approve: func(ctx: borrow, spender: string, amount: integer) -> result<_, error>; allowance: func(ctx: borrow, owner: string, spender: string) -> integer; transfer-from: func(ctx: borrow, from: string, to: string, amount: integer) -> result<_, error>; } ``` ### Using Standards with Dynamic Addresses Contracts accept any contract address that implements the standard interface: ```rust theme={null} contract!(name = "dex"); // Import the standard interface interface!(name = "erc20", path = "../erc20-standard.wit"); impl Guest for Dex { fn swap( ctx: &ProcContext, token: ContractAddress, // User can pass any token amount: Integer, ) -> Result { // Compiler ensures this call is valid for the interface // Runtime error if token doesn't actually implement erc20 erc20::transfer(&token, ctx.signer(), &ctx.contract_signer().to_string(), amount)?; Ok(amount) } } ``` The [AMM example](/docs/sigil/examples/amm) demonstrates this pattern, accepting any token address and calling its transfer functions. ### How This Differs from Ethereum **Ethereum's ERC-20:** A social convention where contracts implement functions named `transfer`, `balanceOf`, `approve`, etc. No compile-time or runtime verification that a contract actually implements the standard—you just call functions and hope they exist. **Sigil's `interface!`:** The WIT interface provides strong typing in your contract code: * The compiler ensures your contract makes valid calls to the interface * The compiler ensures implementations export all required functions with correct signatures * The runtime validates that the target contract matches the interface before executing calls * Interface mismatches surface as clear runtime errors, not undefined behavior ## Making Cross-Contract Calls ### Basic Example From the shared-account contract: ```rust theme={null} use stdlib::*; contract!(name = "shared-account"); // Import token interface dynamically interface!(name = "token", path = "token/wit"); impl Guest for SharedAccount { fn deposit( ctx: &ProcContext, token: ContractAddress, // Dynamic token address account_id: String, n: Integer, ) -> Result<(), Error> { // Call token contract's transfer function token::transfer(&token, ctx.signer(), &ctx.contract_signer().to_string(), n)?; // Update local state let account = ctx.model().accounts().get(account_id).ok_or(unknown_error())?; account.update_balance(|b| b + n); Ok(()) } fn token_balance( _ctx: &ViewContext, token: ContractAddress, holder: String, ) -> Option { // Call token contract's balance function (view) token::balance(&token, &holder) } } ``` ## ContractAddress Type ```rust theme={null} pub struct ContractAddress { pub name: String, pub height: s64, pub tx_index: s64, } ``` **Creating addresses:** ```rust theme={null} let addr = ContractAddress { name: "token".to_string(), height: 100, tx_index: 0, }; ``` ## Call Context and Signers ### The Signer Parameter When making cross-contract calls, you choose whose authority to use: **Execute as the current user:** ```rust theme={null} fn transfer_from_caller(ctx: &ProcContext, token: ContractAddress, to: String, n: Integer) -> Result<(), Error> { // Transfer from caller's account token::transfer(&token, ctx.signer(), &to, n) } ``` **Execute as the contract itself:** ```rust theme={null} fn transfer_from_contract(ctx: &ProcContext, token: ContractAddress, to: String, n: Integer) -> Result<(), Error> { // Transfer from contract's own account token::transfer(&token, ctx.contract_signer(), &to, n) } ``` **When to use contract\_signer():** * Contract holds tokens on behalf of users * Contract acts as custodian * Escrow patterns ## Error Propagation Errors from cross-contract calls propagate automatically with `?`: ```rust theme={null} fn fib_of_sub( ctx: &ProcContext, arith_address: ContractAddress, x: String, y: String, ) -> Result { // If arith::checked_sub returns an error, it propagates let n = arith::checked_sub(&arith_address, &x, &y)?; Ok(Self::fib(ctx, arith_address, n)) } ``` **Cross-contract errors roll back all storage changes** in the entire call chain. ## Re-entrancy Protection The runtime prevents re-entrancy automatically: ``` Contract A calls Contract B Contract B tries to call Contract A → Error: "reentrancy prevented" ``` **Example:** ```rust theme={null} // arith contract calls fib contract // fib contract calls back to arith -> OK (different direction) // But if arith calls fib, and fib calls back to arith: // -> Runtime error: "reentrancy prevented" ``` **Why this matters:** * Prevents common exploit patterns * Simplifies reasoning about contract behavior * No need for re-entrancy guards ## Multi-Contract Interaction Example AMM (Automated Market Maker) calling Token contract: ```rust theme={null} contract!(name = "amm"); interface!(name = "token_dyn", path = "token/wit"); fn create( ctx: &ProcContext, pair: TokenPair, amount_a: Integer, amount_b: Integer, fee_bps: Integer, ) -> Result { // Transfer tokens from user to AMM contract let custodian = ctx.contract_signer().to_string(); token_dyn::transfer(&pair.a, ctx.signer(), &custodian, amount_a)?; token_dyn::transfer(&pair.b, ctx.signer(), &custodian, amount_b)?; // Create pool let lp_shares = (amount_a * amount_b).sqrt()?; // Store pool data in AMM's storage // ... Ok(lp_shares) } ``` **Pattern:** 1. Call external contracts first (get tokens) 2. Validate received amounts 3. Update local state 4. Return results ## The foreign::call Low-Level API For advanced use cases, you can make dynamic calls: ```rust theme={null} use stdlib::*; fn fallback(ctx: &FallContext, expr: String) -> String { if let Some(addr) = ctx.view_context().model().contract_address() { // Dynamic call to any contract foreign::call(ctx.signer(), &addr, &expr) } else { "".to_string() } } ``` **Signature:** ```rust theme={null} pub fn call( signer: Option<&Signer>, contract_address: &ContractAddress, expr: &str ) -> String ``` **When to use:** * Proxy contracts * Generic delegation patterns * When you need to construct calls dynamically * Fallback handlers **Most contracts should use `interface!` or `import!` instead** for type safety. ## Best Practices **1. Use interface! for flexibility** ```rust theme={null} // Good: Accept any token fn deposit(ctx: &ProcContext, token: ContractAddress, n: Integer) -> Result<(), Error> { token::transfer(&token, ctx.signer(), &ctx.contract_signer().to_string(), n) } ``` **2. Validate before external calls (CEI pattern)** ```rust theme={null} fn withdraw(ctx: &ProcContext, token: ContractAddress, n: Integer) -> Result<(), Error> { // Check: Verify authorization and balance let account = ctx.model().account(); if account.balance() < n { return Err(insufficient_balance_error()); } // Effect: Update local state first account.set_balance(account.balance() - n); // Interaction: Make external call last token::transfer(&token, ctx.contract_signer(), &ctx.signer().to_string(), n)?; Ok(()) } ``` **3. Handle cross-contract errors** ```rust theme={null} // Propagate errors token::transfer(&addr, signer, to, amount)?; // Or handle explicitly match token::transfer(&addr, signer, to, amount) { Ok(()) => { /* success */ }, Err(e) => { // Log or handle error return Err(Error::Message(format!("Transfer failed: {}", e))); } } ``` **4. Document contract dependencies** ```rust theme={null} // At top of contract //! This contract requires a token contract implementing: //! - transfer(to: string, n: integer) -> result<_, error> //! - balance(acc: string) -> option ``` # Error Handling Source: https://docs.kontor.network/docs/sigil/reference/error-handling Sigil provides robust error handling through Rust's `Result` type combined with automatic storage rollback. Understanding when to use errors versus panics, and how rollback works across contract calls, is essential for writing correct contracts. ## The Error Type ```rust theme={null} pub enum Error { Message(String), Overflow(String), DivByZero(String), SyntaxError(String), } ``` Defined in `core/indexer/src/runtime/wit/deps/built-in.wit`. ## When to Use Result vs Panic ### Use Result for Expected Errors **Expected failure cases:** ```rust theme={null} fn transfer(ctx: &ProcContext, to: String, n: Integer) -> Result<(), Error> { let from = ctx.signer().to_string(); let ledger = ctx.model().ledger(); let from_balance = ledger.get(&from).unwrap_or_default(); // Business logic error - use Result if from_balance < n { return Err(Error::Message("insufficient funds".to_string())); } ledger.set(from, from_balance - n); let to_balance = ledger.get(&to).unwrap_or_default(); ledger.set(to, to_balance + n); Ok(()) } ``` **Use cases:** * Business logic errors (insufficient funds, unauthorized access) * Invalid input validation * Cross-contract call errors * Any error the user might trigger through normal operation ### Use panic! for: **Invariant violations:** ```rust theme={null} fn internal_operation(ctx: &ProcContext) { let value = ctx.model().critical_value(); // This should never be None if contract is correct if value.is_none() { panic!("Critical invariant violated: value must exist"); } } ``` **Use cases:** * Invariant violations (should never happen) * Internal bugs * Unrecoverable errors **Important:** Panics roll back ALL storage changes across the entire call chain. ## Creating Custom Error Messages ### Basic Error Messages ```rust theme={null} fn validate_pair(pair: &TokenPair) -> Result<(), Error> { if pair.a.name.is_empty() || pair.b.name.is_empty() { return Err(Error::Message( "Token addresses must not be empty".to_string() )); } if pair.a.to_string() >= pair.b.to_string() { return Err(Error::Message( "Token pair must be ordered A < B".to_string() )); } Ok(()) } ``` ### Reusable Error Constructors ```rust theme={null} fn insufficient_balance_error() -> Error { Error::Message("insufficient balance".to_string()) } fn unauthorized_error() -> Error { Error::Message("unauthorized".to_string()) } fn unknown_error() -> Error { Error::Message("unknown account".to_string()) } // Usage fn deposit(ctx: &ProcContext, account_id: String, n: Integer) -> Result<(), Error> { let account = ctx .model() .accounts() .get(account_id) .ok_or(unknown_error())?; if !authorized(ctx.signer(), &account) { return Err(unauthorized_error()); } // ... } ``` ## Overflow and Division Errors ### Checked Operations Integer and Decimal types provide checked operations: ```rust theme={null} impl Integer { fn add(self, other: Integer) -> Result; // Overflow checked fn sub(self, other: Integer) -> Result; fn mul(self, other: Integer) -> Result; fn div(self, other: Integer) -> Result; // Div by zero checked fn sqrt(self) -> Result; } ``` **Usage:** ```rust theme={null} fn mint_checked(ctx: &ProcContext, n: Integer) -> Result<(), Error> { let to = ctx.signer().to_string(); let ledger = ctx.model().ledger(); let balance = ledger.get(&to).unwrap_or_default(); // .add() returns Result ledger.set(to, balance.add(n)?); Ok(()) } ``` ### Unchecked Operations Standard operators panic on overflow: ```rust theme={null} let result = a + b; // Panics on overflow let result = a - b; // Panics on underflow let result = a * b; // Panics on overflow let result = a / b; // Panics on division by zero ``` **When to use unchecked:** * When you've already validated the operation won't overflow * When panic is acceptable (simple contracts, testing) * When performance is critical (avoid double checks) **When to use checked:** * User-provided inputs * Complex calculations * When you want to return a specific error message ## Rollback Semantics ### What Rolls Back When a function returns an `Err` or panics, **ALL storage modifications are rolled back**. **Example:** ```rust theme={null} fn outer(ctx: &ProcContext) -> Result<(), Error> { ctx.model().set_count(1); inner(ctx)?; // If this errors, set_count(1) is rolled back ctx.model().set_count(3); Ok(()) } fn inner(ctx: &ProcContext) -> Result<(), Error> { ctx.model().set_count(2); Err(Error::Message("oops".to_string())) // Both set_count(1) and set_count(2) are rolled back } ``` ### Cross-Contract Rollback **Both errors and panics roll back the entire call chain:** ```rust theme={null} fn outer(ctx: &ProcContext) -> Result<(), Error> { ctx.model().set_count(1); inner(ctx)?; // If this errors, set_count(1) is rolled back ctx.model().set_count(3); Ok(()) } fn inner(ctx: &ProcContext) -> Result<(), Error> { ctx.model().set_count(2); Err(Error::Message("oops".to_string())) // Both set_count(1) and set_count(2) are rolled back } ``` **This applies across cross-contract calls:** * If Contract A calls Contract B, and B returns an error * ALL storage changes in both A and B are rolled back * The entire transaction has no effect ### No Partial Commits A transaction either: * Completes fully with all storage changes persisted * Fails entirely with no storage changes This applies across all cross-contract calls in the transaction. ## Error Handling Best Practices ### 1. Validate Early ```rust theme={null} fn create_pool(ctx: &ProcContext, pair: TokenPair, amount_a: Integer, amount_b: Integer) -> Result<(), Error> { // Validate all inputs first validate_pair(&pair)?; validate_amount(amount_a)?; validate_amount(amount_b)?; // Then proceed with logic // ... } ``` ### 2. Use Descriptive Error Messages ```rust theme={null} // Bad Err(Error::Message("error".to_string())) // Good Err(Error::Message("Pool for this pair already exists".to_string())) ``` ### 3. Propagate Cross-Contract Errors ```rust theme={null} fn deposit(ctx: &ProcContext, token: ContractAddress, n: Integer) -> Result<(), Error> { // Propagate token transfer errors to caller token::transfer(&token, ctx.signer(), &ctx.contract_signer().to_string(), n)?; // Update local state let account = ctx.model().account(); account.update_balance(|b| b + n); Ok(()) } ``` ### 4. Handle Option Types Carefully ```rust theme={null} // Option 1: Use ok_or let account = ctx.model().accounts().get(id).ok_or(unknown_error())?; // Option 2: Pattern match match ctx.model().accounts().get(id) { Some(account) => { /* ... */ }, None => return Err(unknown_error()), } // Option 3: Use unwrap_or_default for safe defaults let balance = ledger.get(&account).unwrap_or_default(); ``` ### 5. Follow Checks-Effects-Interactions (CEI) Pattern ```rust theme={null} fn withdraw(ctx: &ProcContext, token: ContractAddress, amount: Integer) -> Result<(), Error> { // CHECK: Verify conditions let account = ctx.model().account(); if account.balance() < amount { return Err(insufficient_balance_error()); } // EFFECT: Update storage account.set_balance(account.balance() - amount); // INTERACTION: Make external calls token::transfer(&token, ctx.contract_signer(), &ctx.signer().to_string(), amount)?; Ok(()) } ``` **Why this matters:** * External calls can fail * State changes before the call are preserved (unless you propagate the error) * CEI pattern makes reasoning easier ## Testing Errors ### Testing Expected Errors ```rust theme={null} #[testlib::test] async fn test_insufficient_funds() -> Result<()> { let alice = runtime.identity().await?; let bob = runtime.identity().await?; let token = runtime.publish(&alice, "token").await?; // Alice has no tokens, transfer should fail let result = token::transfer(runtime, &token, &alice, &bob, 100.into()).await?; assert_eq!( result, Err(Error::Message("insufficient funds".to_string())) ); Ok(()) } ``` ### Testing Error Messages ```rust theme={null} let result = risky_operation().await; assert!(result.is_err_and(|e| e.root_cause().to_string().contains("expected substring") )); ``` # Numbers and Arithmetic Source: https://docs.kontor.network/docs/sigil/reference/numbers Sigil provides arbitrary precision number types—`Integer` and `Decimal`—for financial calculations that require exact arithmetic without overflow or rounding errors. ## Integer Type The `Integer` type provides 256-bit signed arbitrary precision integers. ### Structure ```rust theme={null} pub struct Integer { r0: u64, r1: u64, r2: u64, r3: u64, sign: Sign, // Plus or Minus } ``` **Maximum value:** 2^256 - 1 (roughly 115 quattuorvigintillion) ### Creating Integers ```rust theme={null} // From literals let a: Integer = 42.into(); let b: Integer = "1_000_000_000_000_000_000".into(); // From u64 let c = Integer::from(12345u64); // Default is zero let zero = Integer::default(); ``` ### Arithmetic Operations **Unchecked operations** (panic on overflow): ```rust theme={null} let sum = a + b; // Panics on overflow let diff = a - b; // Panics on underflow let product = a * b; // Panics on overflow let quotient = a / b; // Panics on division by zero let remainder = a % b; ``` **Checked operations** (return Result): ```rust theme={null} let sum = a.add(b)?; // Returns Result let diff = a.sub(b)?; let product = a.mul(b)?; let quotient = a.div(b)?; // Returns Error::DivByZero on zero let sqrt = a.sqrt()?; ``` ### Comparisons ```rust theme={null} if a > b { } if a == b { } if a <= b { } // Find maximum let max = if a > b { a } else { b }; ``` ### Conversions ```rust theme={null} // To string let s = a.to_string(); // From string let n: Integer = "12345".into(); // To Decimal let d = Decimal::from(a); ``` ### When to Use Integer **Use Integer for:** * Token balances * Large financial calculations * Vote counts * Pool liquidity amounts * Any value that might exceed u64 (18.4 quintillion) * Exact arithmetic requirements **Use u64 for:** * Small counters * Block heights * Array indices * When you know the value fits in 64 bits ## Decimal Type The `Decimal` type provides arbitrary precision decimals for calculations requiring fractional values. ### Structure ```rust theme={null} pub struct Decimal { r0: u64, r1: u64, r2: u64, r3: u64, sign: Sign, } ``` ### Creating Decimals ```rust theme={null} // From Integer let i = Integer::from(100); let d = Decimal::from(i); // From u64 let d = Decimal::from(42); ``` ### Operations ```rust theme={null} // Logarithm (base 10) let d = Decimal::from(100); let log = d.log10()?; // Result: "2.0" // Conversion to Integer (truncates) let i = d.to_integer(); ``` ### When to Use Decimal **Use Decimal for:** * Price calculations * Percentage calculations * Logarithmic operations * Scientific calculations **Most contracts use Integer** for exact arithmetic and avoid decimals entirely. ## Example: Token Contract ```rust theme={null} use stdlib::*; contract!(name = "token"); #[derive(Clone, Default, StorageRoot)] struct TokenStorage { pub ledger: Map, } impl Guest for Token { fn mint(ctx: &ProcContext, n: Integer) { let to = ctx.signer().to_string(); let ledger = ctx.model().ledger(); let balance = ledger.get(&to).unwrap_or_default(); // Unchecked: simple addition ledger.set(to, balance + n); } fn mint_checked(ctx: &ProcContext, n: Integer) -> Result<(), Error> { let to = ctx.signer().to_string(); let ledger = ctx.model().ledger(); let balance = ledger.get(&to).unwrap_or_default(); // Checked: returns overflow error ledger.set(to, balance.add(n)?); Ok(()) } fn transfer(ctx: &ProcContext, to: String, n: Integer) -> Result<(), Error> { let from = ctx.signer().to_string(); let ledger = ctx.model().ledger(); let from_balance = ledger.get(&from).unwrap_or_default(); let to_balance = ledger.get(&to).unwrap_or_default(); if from_balance < n { return Err(Error::Message("insufficient funds".to_string())); } // Unchecked: subtraction safe after validation ledger.set(from, from_balance - n); ledger.set(to, to_balance + n); Ok(()) } fn balance_log10(ctx: &ViewContext, acc: String) -> Result, Error> { ctx.model() .ledger() .get(acc) .map(|i| Decimal::from(i).log10()) .transpose() } } ``` ## Example: AMM Pool Math ```rust theme={null} fn create_pool( ctx: &ProcContext, amount_a: Integer, amount_b: Integer, ) -> Result { // LP shares = sqrt(amount_a * amount_b) let product = amount_a.mul(amount_b)?; // Checked multiplication let lp_shares = product.sqrt()?; // Checked sqrt // Store in pool ctx.model().set_lp_total_supply(lp_shares.clone()); Ok(lp_shares) } fn calculate_swap( reserve_in: Integer, reserve_out: Integer, amount_in: Integer, fee_bps: Integer, ) -> Result { // Constant product formula: x * y = k // With fee: out = (amount_in * (10000 - fee_bps) * reserve_out) / (reserve_in * 10000 + amount_in * (10000 - fee_bps)) let fee_multiplier = Integer::from(10000) - fee_bps; let amount_in_with_fee = amount_in.mul(fee_multiplier)?; let numerator = amount_in_with_fee.mul(reserve_out)?; let denominator = reserve_in.mul(Integer::from(10000))?.add(amount_in_with_fee)?; numerator.div(denominator) } ``` ## Choosing Between Checked and Unchecked **Use checked operations when:** * Working with user inputs * Complex calculations where overflow is possible * You want specific error messages for overflow * Financial calculations requiring exact results **Use unchecked operations when:** * You've already validated the operation is safe * Performance is critical * The overflow would indicate a bug (panic is appropriate) See the token contract example above for combining both approaches—validation with checked arithmetic, then unchecked operations after validation. ## Common Patterns ### Safe Division ```rust theme={null} fn calculate_ratio(numerator: Integer, denominator: Integer) -> Result { if denominator == Integer::from(0) { return Err(Error::DivByZero("Cannot divide by zero".to_string())); } numerator.div(denominator) } ``` ### Square Root for LP Tokens ```rust theme={null} fn mint_liquidity(amount_a: Integer, amount_b: Integer) -> Result { let product = amount_a.mul(amount_b)?; product.sqrt() // Common pattern for initial LP shares } ``` ### Percentage Calculations ```rust theme={null} fn apply_fee(amount: Integer, fee_bps: Integer) -> Result { // fee_bps is basis points (1% = 100 bps) let fee = amount.mul(fee_bps)?.div(Integer::from(10000))?; amount.sub(fee) } ``` ### Comparison Utilities ```rust theme={null} fn max(a: Integer, b: Integer) -> Integer { if a > b { a } else { b } } fn min(a: Integer, b: Integer) -> Integer { if a < b { a } else { b } } ``` ## Quick Reference ### Number Types and Ranges **Integer** * 256-bit signed arbitrary precision integers * Range: `±115_792_089_237_316_195_423_570_985_008_687_907_853_269_984_665_640_564_039_457` * Maximum value: 2^256 - 1 **Decimal** * Arbitrary precision decimals with up to 18 decimal places * Range: `±(2^256 - 1) / 10^18` * Full range: `±115_792_089_237_316_195_423_570_985_008_687_907_853_269_984_665_640_564_039_457.584_007_913_129_639_936` ### Arithmetic Operations Both types support basic arithmetic operations (`add`, `sub`, `mul`, `div`) and comparisons. **Unchecked (using operators):** ```rust theme={null} let result = a + b; // Panics on overflow let result = a - b; // Panics on underflow let result = a * b; // Panics on overflow let result = a / b; // Panics on division by zero ``` **Checked (using methods):** ```rust theme={null} let result = a.add(b)?; // Returns Result let result = a.sub(b)?; // Error::Overflow on overflow let result = a.mul(b)?; // Error::Overflow on overflow let result = a.div(b)?; // Error::DivByZero on zero ``` ### Advanced Operations **Integer:** * `.sqrt()` - Square root (returns `Result`) **Decimal:** * `.log10()` - Base-10 logarithm (returns `Result`) * Additional operations to be expanded ### Type Conversions ```rust theme={null} // Integer from literals let i: Integer = 42.into(); let i: Integer = "1_000_000".into(); // Decimal from Integer let d = Decimal::from(integer); // Integer from Decimal (truncates) let i = decimal.to_integer(); ``` # Storage Patterns Source: https://docs.kontor.network/docs/sigil/reference/storage Sigil uses a hierarchical path-based storage model, similar to a filesystem. The storage system provides type-safe accessors through Rust derives that generate methods for reading and writing state. ## Storage Derives ### StorageRoot - Top-Level Contract Storage Every contract has exactly one `StorageRoot`: ```rust theme={null} #[derive(Clone, Default, StorageRoot)] struct TokenStorage { pub ledger: Map, } ``` **What it generates:** * `.init(ctx)` method to initialize storage * Accessible via `ctx.model()` **Usage:** ```rust theme={null} fn init(ctx: &ProcContext) { TokenStorage::default().init(ctx); } fn mint(ctx: &ProcContext, n: Integer) { let ledger = ctx.model().ledger(); // Access via model() // ... } ``` ### Storage - Nested Structures For structures nested within your storage root: ```rust theme={null} #[derive(Clone, Default, Storage)] struct Account { pub balance: Integer, pub owner: String, pub permissions: Map, } ``` **What it generates:** * Same accessors as `StorageRoot` but without `.init()` * Used for nested data structures ### Store and Model **`#[derive(Store)]`** - Generates only persistence methods (`__set()`) **`#[derive(Model)]`** - Generates only read/write accessor types (`*Model`, `*WriteModel`) Most contracts only need `StorageRoot` and `Storage`. ## Type Constraints ### Supported Storage Types **Primitive types:** * `u64`, `s64`, `bool`, `String` **Built-in types:** * `Integer` - Arbitrary precision (256-bit) * `Decimal` - Arbitrary precision with decimals * `ContractAddress` - References to other contracts **Structured types:** * Enums and structs with `#[derive(Storage)]` or `#[derive(Wavey)]` * `Option` where T is a supported type * `Map` where K: `ToString` + `FromString`, V is a supported type ### Not Directly Supported **Vec** - Use `Map` as workaround: ```rust theme={null} // Instead of pub items: Vec, // Use pub items: Map, // Access fn add_item(ctx: &ProcContext, item: String) { let items = ctx.model().items(); let len = items.keys().count() as u64; items.set(len, item); } ``` **Unsupported primitives** - Use alternatives: * No `u32`, `u16`, `u8` → Use `u64` * No `f64`, `f32` → Use `Decimal` ## Working with Basic Fields ```rust theme={null} #[derive(Clone, Default, StorageRoot)] struct CounterStorage { pub count: u64, pub owner: String, pub active: bool, } impl Guest for Counter { fn init(ctx: &ProcContext) { CounterStorage { count: 0, owner: ctx.signer().to_string(), active: true, }.init(ctx); } fn increment(ctx: &ProcContext) { let current = ctx.model().count(); // Read ctx.model().set_count(current + 1); // Write } fn get_owner(ctx: &ViewContext) -> String { ctx.model().owner() // Read } } ``` ## Working with Options ```rust theme={null} #[derive(Clone, Default, StorageRoot)] struct ProxyStorage { contract_address: Option, } impl Guest for Proxy { fn get_address(ctx: &ViewContext) -> Option { ctx.model().contract_address() // Returns Option } fn set_address(ctx: &ProcContext, addr: ContractAddress) { ctx.model().set_contract_address(Some(addr)); } fn clear_address(ctx: &ProcContext) { ctx.model().set_contract_address(None); } } ``` ## Working with Maps Maps provide key-value storage: ```rust theme={null} #[derive(Clone, Default, StorageRoot)] struct TokenStorage { pub ledger: Map, } impl Guest for Token { fn mint(ctx: &ProcContext, n: Integer) { let ledger = ctx.model().ledger(); let account = ctx.signer().to_string(); // Get (returns Option) let balance = ledger.get(&account).unwrap_or_default(); // Set ledger.set(account, balance + n); } fn all_holders(ctx: &ViewContext) -> Vec { // Iterate keys ctx.model().ledger().keys().collect() } } ``` ### Map API **For read-only access (ViewContext):** ```rust theme={null} pub trait MapModel { fn get(&self, key: K) -> Option; fn keys(&self) -> impl Iterator; } ``` **For read-write access (ProcContext):** ```rust theme={null} pub trait MapWriteModel { fn get(&self, key: K) -> Option; fn set(&self, key: K, value: V); fn keys(&self) -> impl Iterator; } ``` ### Map Initialization with Data ```rust theme={null} fn init(ctx: &ProcContext) { FibStorage { cache: Map::new(&[(0, FibValue { value: 0 })]), }.init(ctx); } ``` ## Nested Structures Storage structures can be nested arbitrarily deep: ```rust theme={null} #[derive(Clone, Default, Storage)] struct Account { pub balance: Integer, pub owner: String, pub other_tenants: Map, } #[derive(Clone, Default, StorageRoot)] struct SharedAccountStorage { pub accounts: Map, } impl Guest for SharedAccount { fn deposit(ctx: &ProcContext, account_id: String, n: Integer) -> Result<(), Error> { let accounts = ctx.model().accounts(); let account = accounts.get(&account_id).ok_or(unknown_error())?; // Access nested fields let current_balance = account.balance(); // Update nested field account.set_balance(current_balance + n); Ok(()) } fn check_permission(ctx: &ViewContext, account_id: String, user: String) -> bool { ctx.model() .accounts() .get(account_id) .map(|account| { // Access nested Map account.other_tenants().get(user).unwrap_or(false) }) .unwrap_or(false) } } ``` ### Updating Nested Fields with Closures ```rust theme={null} fn deposit(ctx: &ProcContext, account_id: String, n: Integer) -> Result<(), Error> { let account = ctx.model().accounts().get(account_id).ok_or(unknown_error())?; // Update a field using a closure account.update_balance(|b| b + n); Ok(()) } ``` ## Storage Key Scoping Keys are scoped by path, preventing collisions: ```rust theme={null} struct Storage { pub map_a: Map, // path: "map_a.*" pub map_b: Map, // path: "map_b.*" } ``` Keys in `map_a` and `map_b` are independent—they can have the same key without collision. ## Gas Considerations Storage operations consume gas: **General guidance:** * Storage writes are more expensive than reads * Minimize storage writes * Cache reads when accessing multiple times * Use lazy evaluation (models load only what you access) * Map iterations over many keys can be expensive # Testing Contracts Source: https://docs.kontor.network/docs/sigil/reference/testing Sigil contracts are tested using the `testlib` crate, which provides a test runtime that simulates the blockchain environment. Tests use the same contract code that runs on-chain, with an in-memory backend for fast iteration. ## Test Harness Setup ### Test File Structure ```rust theme={null} use testlib::*; // Import contract interface (path relative to test directory) interface!(name = "token", path = "contract/wit"); // Test function with #[testlib::test] attribute #[testlib::test] async fn test_token_contract() -> Result<()> { // 'runtime' is injected automatically let signer = runtime.identity().await?; let token = runtime.publish(&signer, "token").await?; // Test calls (note the double ?? for Result>) token::mint(runtime, &token, &signer, 100.into()).await??; let balance = token::balance(runtime, &token, &signer).await?; assert_eq!(balance, Some(100.into())); Ok(()) } ``` ### The #\[testlib::test] Attribute **Syntax:** ```rust theme={null} #[testlib::test] async fn my_test() -> Result<()> { ... } #[testlib::test(mode = "regtest")] async fn my_integration_test() -> Result<()> { ... } ``` **Parameters:** * `mode` - Optional: `"regtest"` for integration tests against real blockchain **What it provides:** * `runtime: &mut Runtime` - Automatically injected variable * Test isolation (each test gets fresh state) * Automatic contract loading from compiled bytecode ## Runtime API ```rust theme={null} impl Runtime { // Create a new identity (test user) async fn identity(&mut self) -> Result; // Publish a contract async fn publish(&mut self, signer: &Signer, name: &str) -> Result; // Get contract's WIT interface async fn wit(&mut self, contract_address: &ContractAddress) -> Result; // Issue gas to a signer async fn issuance(&mut self, signer: &Signer) -> Result<()>; } ``` ## Creating Test Identities Each test can create multiple users: ```rust theme={null} #[testlib::test] async fn test_multi_user() -> Result<()> { let alice = runtime.identity().await?; let bob = runtime.identity().await?; // Both users are funded with gas automatically let token = runtime.publish(&alice, "token").await?; token::mint(runtime, &token, &alice, 1000.into()).await?; token::transfer(runtime, &token, &alice, &bob, 100.into()).await??; Ok(()) } ``` ## Publishing Contracts ```rust theme={null} #[testlib::test] async fn test_publish() -> Result<()> { let deployer = runtime.identity().await?; // Publishes from example-contracts/target/.../token.wasm.br let token = runtime.publish(&deployer, "token").await?; // Contract is initialized (init() is called automatically) // Returns ContractAddress { name: "token", height: 1, tx_index: 0 } Ok(()) } ``` **Contract addressing:** * Contracts are identified by `(name, height, tx_index)` * In tests, first published contract is at height 1, tx\_index 0 * Second contract is at height 2, tx\_index 0, etc. ## Calling Contract Functions ### Understanding the Double Question Mark Generated contract functions return `Result>`: ```rust theme={null} // Signature of generated function: pub async fn transfer( runtime: &mut Runtime, contract_address: &ContractAddress, signer: &Signer, to: &str, n: Integer ) -> Result> ``` **Why two Results?** * **Outer `Result`** - Runtime errors (network, execution failures) * **Inner `Result`** - Contract errors (business logic failures) **Usage:** ```rust theme={null} // Option 1: Propagate both errors token::transfer(runtime, &token, &alice, &bob, 100.into()).await??; // ^^ // First ? unwraps runtime error // Second ? unwraps contract error // Option 2: Handle separately let result = token::transfer(runtime, &token, &alice, &bob, 100.into()).await?; match result { Ok(()) => { /* transfer succeeded */ }, Err(e) => { /* handle contract error */ }, } ``` ### Testing View Functions ```rust theme={null} #[testlib::test] async fn test_balance_query() -> Result<()> { let alice = runtime.identity().await?; let token = runtime.publish(&alice, "token").await?; // View functions return Option or direct values let balance = token::balance(runtime, &token, &alice).await?; assert_eq!(balance, None); // No tokens yet token::mint(runtime, &token, &alice, 100.into()).await?; let balance = token::balance(runtime, &token, &alice).await?; assert_eq!(balance, Some(100.into())); Ok(()) } ``` ## Testing Errors ### Expected Business Logic Errors ```rust theme={null} #[testlib::test] async fn test_insufficient_funds() -> Result<()> { let alice = runtime.identity().await?; let bob = runtime.identity().await?; let token = runtime.publish(&alice, "token").await?; // Alice has no tokens, transfer should fail let result = token::transfer(runtime, &token, &alice, &bob, 100.into()).await?; assert_eq!( result, Err(Error::Message("insufficient funds".to_string())) ); Ok(()) } ``` ### Using is\_err\_and ```rust theme={null} let result = risky_operation().await; assert!(result.is_err_and(|e| e.root_cause().to_string().contains("expected error message") )); ``` ## Testing Cross-Contract Calls ```rust theme={null} #[testlib::test] async fn test_fib_with_arith() -> Result<()> { let signer = runtime.identity().await?; // Publish both contracts let arith = runtime.publish(&signer, "arith").await?; let fib = runtime.publish(&signer, "fib").await?; // Fib contract calls arith contract internally let result = fib::fib(runtime, &fib, &signer, arith.clone(), 8).await?; assert_eq!(result, 21); Ok(()) } ``` ## Testing Re-entrancy Protection The runtime prevents re-entrancy automatically: ```rust theme={null} #[testlib::test] async fn test_reentrancy() -> Result<()> { let signer = runtime.identity().await?; let fib = runtime.publish(&signer, "fib").await?; let arith = runtime.publish(&signer, "arith").await?; // arith tries to call fib which calls back to arith -> Error let result = arith::fib(runtime, &arith, &signer, fib.clone(), 9).await; assert!(result.is_err_and(|e| e.root_cause().to_string().contains("reentrancy prevented") )); Ok(()) } ``` ## Complete Test Example ```rust theme={null} use testlib::*; use tracing::info; interface!(name = "token", path = "../example-contracts/token/wit"); async fn run_test_token_contract(runtime: &mut Runtime) -> Result<()> { info!("test_token_contract"); let minter = runtime.identity().await?; let holder = runtime.identity().await?; let token = runtime.publish(&minter, "token").await?; // Mint tokens token::mint(runtime, &token, &minter, 900.into()).await?; token::mint(runtime, &token, &minter, 100.into()).await?; // Check balance let result = token::balance(runtime, &token, &minter).await?; assert_eq!(result, Some(1000.into())); // Test insufficient funds error let result = token::transfer(runtime, &token, &holder, &minter, 123.into()).await?; assert_eq!( result, Err(Error::Message("insufficient funds".to_string())) ); // Successful transfers token::transfer(runtime, &token, &minter, &holder, 40.into()).await??; token::transfer(runtime, &token, &minter, &holder, 2.into()).await??; let result = token::balance(runtime, &token, &holder).await?; assert_eq!(result, Some(42.into())); let result = token::balance(runtime, &token, &minter).await?; assert_eq!(result, Some(958.into())); // Test non-existent account let result = token::balance(runtime, &token, "foo").await?; assert_eq!(result, None); Ok(()) } #[testlib::test] async fn test_token_contract() -> Result<()> { run_test_token_contract(runtime).await } ``` ## Integration Tests (Regtest Mode) Tests can run against a real Bitcoin regtest blockchain: ```rust theme={null} #[testlib::test(mode = "regtest")] async fn test_token_on_regtest() -> Result<()> { // Enable logging logging::setup(); let alice = runtime.identity().await?; let token = runtime.publish(&alice, "token").await?; token::mint(runtime, &token, &alice, 1000.into()).await?; // State is persisted to real blockchain Ok(()) } ``` **Regtest mode:** * Runs against actual Bitcoin regtest * State persists across tests * Slower but provides full blockchain validation * Useful for final integration testing before testnet deployment ## Running Tests ```bash theme={null} # Run all tests in a contract cd example-contracts/token cargo test # Run specific test cargo test test_token_contract # Run with logging output cargo test test_token_contract -- --nocapture # Run integration tests cargo test test_token_contract_regtest --release ``` ## Test Organization **Test directory structure:** ``` test/ ├── Cargo.toml ├── build.rs # Compiles contract to WASM └── src/ └── lib.rs # Test module ``` **Test file (test/src/lib.rs):** ```rust theme={null} #[cfg(test)] mod tests { use testlib::*; interface!(name = "my_contract", path = "contract/wit"); #[testlib::test] async fn test_something() -> Result<()> { // Test code } } ``` ## Best Practices **1. Test both success and failure cases** ```rust theme={null} // Success path token::transfer(runtime, &token, &alice, &bob, 10.into()).await??; // Failure path let result = token::transfer(runtime, &token, &alice, &bob, 99999.into()).await?; assert!(result.is_err()); ``` **2. Use descriptive test names** ```rust theme={null} #[testlib::test] async fn test_transfer_fails_with_insufficient_funds() -> Result<()> { ... } ``` **3. Factor out common setup** ```rust theme={null} async fn setup_token_with_balance(runtime: &mut Runtime, amount: Integer) -> Result<(Signer, ContractAddress)> { let signer = runtime.identity().await?; let token = runtime.publish(&signer, "token").await?; token::mint(runtime, &token, &signer, amount).await?; Ok((signer, token)) } #[testlib::test] async fn test_with_setup() -> Result<()> { let (alice, token) = setup_token_with_balance(runtime, 1000.into()).await?; // ... test logic } ``` ## Quick Reference ### Runtime API The test runtime is automatically injected via the `#[testlib::test]` attribute: **runtime.identity() -> Result\** * Creates a new test identity (user) with automatic gas funding **runtime.publish(\&signer, "name") -> Result\** * Deploys a contract from compiled bytecode and returns its address **runtime.wit(\&contract\_address) -> Result\** * Retrieves the WIT interface of a deployed contract ### Test Attribute **Basic test:** ```rust theme={null} #[testlib::test] async fn my_test() -> Result<()> { // 'runtime' is auto-injected let alice = runtime.identity().await?; let contract = runtime.publish(&alice, "my-contract").await?; // ... test code Ok(()) } ``` **Integration test (Bitcoin regtest):** ```rust theme={null} #[testlib::test(mode = "regtest")] async fn my_integration_test() -> Result<()> { // ... test code } ``` ### Interface Macro Generates type-safe bindings for contract calls: ```rust theme={null} interface!(name = "token", path = "contract/wit"); ``` Generated functions are async and take `runtime`, `contract_address`, and `signer` parameters: ```rust theme={null} token::mint(runtime, &token_address, &signer, amount).await??; ``` **Note:** For tests, use `interface!` which generates bindings for any contract address. The `import!` macro (for fixed addresses) is rarely needed in tests. # Introduction Source: https://docs.kontor.network/docs/sigil/sdk The official Kontor TypeScript SDK documentation is available [here](http://sdk.kontor.network). ## Usage ```ts [with-indexer-client.ts] theme={null} import { signet, custom, http, parseWit, mnemonicToAccount, createKontorWalletClient, createKontorIndexerClient } from "@kontor/kontor-sdk"; const contract = [ "record balance { acc: string, amt: decimal }", "record transfer { src: string, dst: string, amt: decimal }", "record burn { src: string, amt: decimal }", "record mint { dst: string, amt: decimal }", "export mint: func(ctx: borrow, amt: decimal) -> result;", "export burn: func(ctx: borrow, amt: decimal) -> result;", "export transfer: func(ctx: borrow, dst: string, amt: decimal) -> result;", "export balance: func(ctx: borrow, acc: string) -> option;", "export balances: func(ctx: borrow) -> list;", "export total-supply: func(ctx: borrow) -> decimal;", "export attach: func(ctx: borrow, vout: u64, amt: decimal) -> result;", "export detach: func(ctx: borrow) -> result;", ] as const; const wit = parseWit(contract); const account = mnemonicToAccount("mnemomic phrase"); const indexerClient = createKontorIndexerClient({ chain: signet, transport: http(), }); const walletClient = createKontorWalletClient({ account, chain: signet, transport: custom({ request: async (args: any) => {}, }), }); const composeResponse = await indexerClient.procContract({ wit, account: ["taproot-address", "x-only-public-key"], contractAddress: 'token_0_0', functionName: "transfer", args: ["dst-taproot-address", [1n, 18]], satsPerVByte: 1, utxos: [ "txid:vout", ], gas: 10n, }) const signedCommitHex = await walletClient.signCommit({ psbt: composeResponse.result.commit_psbt_hex, }) const signedRevealHex = await walletClient.signReveal({ psbt: composeResponse.result.reveal_psbt_hex, parcipantScripts: composeResponse.result.per_participant, }) // ... broadcast ... const destinationBalance = await indexerClient.viewContract({ wit, contractAddress: 'token_0_0', functionName: "balance", args: ["dst-x-only-public-key"], }) ``` # Security Source: https://docs.kontor.network/docs/sigil/security Kontor contracts execute in a sandboxed WebAssembly runtime with several security properties designed to prevent common attack vectors and ensure deterministic execution. ## Runtime Security **Gas metering** prevents infinite loops and resource exhaustion—execution halts when gas is consumed, with side effects rolled back. Every operation consumes gas proportional to its computational cost. **Re-entrancy protection** is enforced at the level of the runtime. Cross-contract calls execute synchronously, and contracts cannot be re-entered during execution. **Storage isolation** ensures contracts cannot access each other's state except through explicit function calls with defined interfaces. Each contract has its own isolated storage namespace. ## Compile-Time Safety **Type safety** catches many common errors at compile time—interface mismatches, incorrect storage types, and ABI incompatibilities fail during development rather than on-chain. The WebAssembly Component Model enforces strict typing between contracts. The Sigil framework leverages Rust's ownership model to prevent memory safety issues. Combined with the WebAssembly sandbox, this provides defense-in-depth against exploitation. ## Determinism The runtime is configured for strict determinism: * Threading disabled * SIMD off * Floating-point behavior canonicalized This ensures that all indexers compute identical state from the same transaction history. Non-deterministic behavior would break consensus between indexers. # Challenge and Response Source: https://docs.kontor.network/docs/storage/challenge-response The challenge-response mechanism is what makes trustless storage verification possible. Without it, rational storage nodes would delete data immediately and pocket rewards until caught. The protocol solves this by randomly auditing files and requiring nodes to prove cryptographically that they possess specific data at the moment of challenge. ```mermaid theme={null} sequenceDiagram participant BC as Bitcoin Blockchain participant IX as Indexer participant SN as Storage Node BC->>IX: Mine Block (height h) IX->>IX: Generate Challenges SN-->>IX: Fetch Challenges SN->>SN: Generate Nova IVC Proof SN->>BC: Submit Proof Tx BC->>IX: Mine Block (height h+k) IX->>IX: Verify Proof IX->>IX: Process Expired (h+W_proof) ``` ## Challenge Generation Each Bitcoin block, Kontor indexers deterministically derive a set of challenges from the block hash. The block hash provides unpredictable, high-entropy randomness that cannot be biased by miners (grinding attacks are economically irrational—the cost of discarding a valid block vastly exceeds any benefit from influencing which nodes are challenged). Using Bitcoin's proof-of-work as the randomness source ensures challenges are fair, verifiable, and impossible to predict in advance. **File selection.** Each file in the active set is selected for challenge with constant probability $p_f = C_{\text{target}} / B$ per block, where $C_{\text{target}} = 12$ is the target annual challenges per file and $B \approx 52,560$ is expected Bitcoin blocks per year. This creates approximately 12 challenges per file annually regardless of network size. The constant challenge rate provides consistent security guarantees as the network scales—a file stored in a network with 1,000 files receives the same audit frequency as one stored in a network with 1,000,000 files. **Node selection.** For each selected file, the protocol randomly chooses one of its storing nodes with uniform probability $1/|N_f|$ where $N_f$ is the set of nodes storing that file. This means nodes storing many copies of the same file don't gain advantage—each node has equal chance of being challenged regardless of how many other nodes store the file. **Challenge parameters.** The challenge specifies which file, which node, and which randomly-selected sectors must be proven. The protocol samples $s_{\text{chal}} = 100$ sectors uniformly at random from the file's Merkle tree (or all sectors if the file has fewer than 100). These sector indices are derived deterministically using domain-separated hashing of the block hash combined with the file identifier, ensuring indexers compute identical challenges while maintaining unpredictability. ## Proof Submission The challenged node has approximately two weeks ($W_{\text{proof}} = 2016$ blocks) to generate and submit a proof. This window serves two purposes: it provides operational buffer for node maintenance and network issues, and it enables proof aggregation—nodes can batch multiple challenges from multiple files into a single Bitcoin transaction, dramatically reducing per-challenge proving costs. **Proof generation.** The node generates a Nova IVC (Incrementally Verifiable Computation) proof demonstrating possession of the challenged sectors. For each challenged sector, the proof verifies: 1. The sector index was correctly derived from the challenge seed 2. The node possesses the 31-byte symbol at that index 3. A valid Merkle path connects that symbol to the public Merkle root The proof is constructed iteratively—each sector verification becomes one step in the recursive proof. Nova's folding scheme allows these steps to accumulate into a single proof structure that grows logarithmically with the number of steps, then compresses to a compact form using Spartan compression. The final compressed proof is compact enough to include in a Bitcoin transaction with minimal overhead. **Multi-file aggregation.** Storage nodes can aggregate challenges across multiple files into a single proof transaction. The protocol constructs a File Ledger—a Merkle tree built over the root commitments of all files in the system. Each file has a canonical position in this ledger (determined by lexicographic file identifier ordering). When proving multiple files, the node includes Merkle paths from each file's root commitment to the ledger root, then proves all challenged sectors across all files in one aggregated IVC proof. This aggregation is what makes large-scale storage operations economical—a node storing thousands of files can batch weeks of challenges into a single \~10 kB proof, amortizing the Bitcoin transaction fee across the entire proof set. **Submission.** The node broadcasts the proof transaction to Bitcoin. The two-week expiration window provides substantial buffer against Bitcoin network congestion—even during periods of high fees and full blocks, nodes have ample time to ensure their proof transactions confirm. This long window is intentional: it allows nodes to optimize transaction timing, wait for favorable fee conditions, and batch multiple challenges without risking expiration. Indexers detect these transactions, read the proof and take the challenge identifiers from the transaction (the proof does not serialize them), and verify the proof deterministically. Because the proof system uses transparent setup (no trusted ceremony), anyone can verify proofs using only the public parameters, the challenge parameters, and the on-chain Merkle roots. ## Verification and Detection When indexers receive a proof transaction, they verify it against the challenge parameters and file metadata. The verification checks: 1. The proof was submitted before the challenge expired 2. The public inputs match the expected challenge (correct file root, seed, tree depth) 3. The Nova SNARK verification passes (proving the node executed the correct circuit for all challenged sectors) 4. The final state matches the initial state (the Merkle root remained consistent throughout all verifications) Verification takes approximately 50 milliseconds regardless of file size or number of challenged sectors—the constant-time verification is what makes on-chain verification practical. If verification succeeds, the challenge is marked as satisfied and removed from the active challenge queue. If the proof is invalid or missing when the challenge expires, the node is slashed. **Detection guarantees.** The challenge mechanism provides probabilistic guarantees calibrated to make selective storage economically irrational. For a node that has lost fraction $\mu$ of its data, the probability of detection per challenge is approximately $1 - (1-\mu)^{s_{\text{chal}}}$. With $s_{\text{chal}} = 100$: * 10% data loss → 99.997% detection probability per challenge * 50% data loss → >99.999999999999% detection probability per challenge * Complete data loss → detection is virtually certain on first challenge With 12 challenges per year, a node storing incomplete data faces near-certain detection within weeks. The expected time to detection for even modest data loss (10%) is measured in days to weeks, not months or years. This makes the risk-reward calculation decisively unfavorable—nodes save marginal storage costs (already small relative to capital costs) but face near-certain loss of staked capital plus all future earning potential from that file. **Erasure coding safety margin.** The 10% data loss threshold is significant because of the Reed-Solomon erasure coding applied during file preparation. Each codeword can reconstruct the original data from any 231 of its 255 symbols (90%). A node storing exactly 90% of each codeword can still answer all challenges (by reconstructing missing sectors on-demand) while maintaining a fully recoverable file. The detection probability is calibrated to catch nodes at or above this threshold—ensuring that files passing challenges remain fully reconstructible. There's no need to detect tiny amounts of missing data; the protocol only needs to ensure nodes don't cross the recovery threshold. ## Challenge Expiration and Failure If a node fails to submit a valid proof within the challenge window, the challenge expires and the node is slashed. The slashing penalty is $\lambda_{\text{slash}} \cdot k_f$ where $k_f$ is the base stake requirement for that file and $\lambda_{\text{slash}}$ is a protocol parameter. A fraction $\beta_{\text{slash}}$ of the penalty is burned; the remainder is distributed equally to the other nodes storing that file. The node is immediately removed from the file agreement and loses all future emission rights from that file. If the slashing causes the node's total stake to fall below its requirement for remaining files, an automatic stake-sufficiency restoration process triggers—the node is gracefully removed from additional files (with penalties for each involuntary exit) until stake is sufficient, or if that's impossible, its entire remaining stake is burned and it's removed from all agreements. This mechanism ensures that challenge failures carry severe consequences. The lost stake typically exceeds storage costs by factors of 10-100×, and the loss of future earnings compounds the penalty. The protocol doesn't need surveillance beyond the challenge mechanism itself—the economics make honest storage the dominant strategy for rational operators. ## Why This Works The challenge-response mechanism transforms the storage problem from "trust that nodes are storing data" to "make it extremely unprofitable for nodes not to store data." Three properties make this transformation effective: **Unpredictable challenges.** Nodes cannot predict which files will be challenged or which sectors will be sampled until the Bitcoin block is mined. This prevents nodes from storing only frequently-challenged sectors or reconstructing data on-demand from partial copies. By the time a node knows it's been challenged, it's too late to acquire the data—the proof must demonstrate possession of data the node already had when the challenge was generated. **High detection probability.** Even modest data loss has near-certain detection within short timeframes. The expected loss from detection (slashed stake plus lost future earnings) vastly exceeds the savings from not storing data. The protocol doesn't need to catch every byte of missing data—it only needs to make the expected value calculation decisively negative for selective storage strategies. **Capital cost dominance.** The protocol is designed so that capital costs (opportunity cost of staked KOR) dominate operational costs by orders of magnitude. Attacks that attempt to save storage costs while maintaining rewards must still bear full capital costs. Since capital costs dwarf storage costs, the savings from deletion are negligible compared to the capital that must still be committed to earn rewards. This asymmetry—combined with high detection probability—makes honest storage the most profitable strategy for rational operators. # Cryptographic System Source: https://docs.kontor.network/docs/storage/cryptographic-system The storage protocol's cryptographic foundation enables compact proofs that scale from single files to thousands of aggregated challenges. The system combines Merkle trees for cryptographic commitments, Reed-Solomon codes for fault tolerance and adversarial erasure protection, [Nova](https://eprint.iacr.org/2021/370) recursive SNARKs for efficient proof compression, and blockchain settlement for non-repudiation and tamper-proof consensus. These components work together to make verification economically practical at scale. This system is designed for proof-of-retrievability and does not provide data confidentiality. All committed data structures (Merkle roots, proofs) are publicly verifiable, and file contents are not encrypted by the protocol itself. Privacy, if needed, must be implemented at the application layer through client-side encryption before data is committed to storage. ## Reed-Solomon Erasure Coding The protocol applies [Reed-Solomon](https://doi.org/10.1137/0108018) codes for both fault tolerance and adversarial erasure protection, following [Compact Proofs of Retrievability](https://eprint.iacr.org/2008/073.pdf) work. Files are encoded over GF(2^8) with 231 data symbols and 24 parity symbols per 255-symbol codeword, allowing reconstruction from any 231 symbols. **Extractability and public retrievability.** One of the PoR security properties is *extractability*: if a prover passes challenges with high probability, an efficient extractor can recover the entire file. Reed-Solomon's Maximum Distance Separable (MDS) property ensures that possessing any $k$ of $n$ codeword symbols enables full reconstruction. The public encoding matrix enable *public retrievability*: any party with sufficient symbols can reconstruct files without secret keys. **Hardness amplification against rational adversaries.** Erasure coding provides hardness amplification in the sense that it transforms weak guarantees about random symbol possession into strong guarantees about complete file retrievability. Consider an adversary $\mathcal{A}$ who stores only a fraction $\rho < k/n$ of codeword symbols to minimize storage costs. Under random challenge sampling, $\mathcal{A}$ succeeds in answering individual challenges with probability $\rho$, but crucially, $\mathcal{A}$ cannot reconstruct the missing symbols to respond to challenges on deleted positions. The MDS property establishes a threshold: any $(k,n)$ code achieving the guarantees that possessing fewer than $k$ symbols renders reconstruction computationally infeasible, causing audit failure probability approaching $1 - \rho$ over repeated challenges. This amplifies partial deletion into total audit failure, an adversary must maintain $\rho \geq k/n$ or face inevitable slashing. ## Merkle Trees and Commitments Each file is committed through a binary Merkle tree built over its encoded symbols using Poseidon hashing. The tree's root serves as a compact cryptographic commitment—a single 32-byte value that binds to the entire file contents with collision resistance. **Symbol-to-leaf encoding.** Each 31-byte symbol encodes directly as a Pallas field element through little-endian byte representation with no intermediate hashing. This direct encoding is critical for proof-of-retrievability: to prove possession of a challenged symbol, a node must possess the actual 31 bytes, encode them as a field element, and provide the Merkle path. An attacker storing only the Merkle tree (field elements) without underlying bytes cannot answer challenges—the SNARK circuit verification requires demonstrating knowledge of the bytes that encode to each challenged leaf. **Poseidon hashing.** The protocol uses [Poseidon](https://eprint.iacr.org/2019/458) for all in-circuit hashing because it's optimized for arithmetic circuits over prime fields. Poseidon requires hundreds of constraints per invocation versus thousands for SHA-256 or Blake2, reducing IVC proving cost by orders of magnitude. The hash function operates over the Pallas and Vesta curve cycle (254-bit prime fields), providing approximately 128 bits of security under current best-known attacks. **Tree structure.** The tree is binary—two children per node. Higher-arity trees (quaternary, octal) would reduce depth but increase proof size (more siblings per level) and circuit complexity (multiple hash inputs per verification step). When a tree level has odd node count, the final node is duplicated (hashed with itself) to maintain uniform circuit structure. Domain separation tags prevent cross-layer collision attacks: leaf encoding uses one tag, internal node hashing uses another. **Merkle proofs.** A proof for leaf index $i$ consists of the sibling hashes along the path from leaf to root plus direction bits indicating whether the node is a left or right child at each level. For tree depth $d$, the proof size is $d$ field elements (around 32 bytes each). The verifier recomputes the path: starting from the leaf, hash with the sibling according to the direction bit, producing the parent; repeat until reaching the root. If the computed root matches the public commitment, the proof is valid. ## Bitcoin Layer The protocol leverages Bitcoin blockchain as a primitive providing critical security properties that enable trustless coordination. **Non-repudiation.** Once a storage node publishes a proof to the blockchain, it cannot later deny having submitted that proof. The blockchain's cryptographic chain of signatures and block headers creates an immutable record of all protocol actions. **Non-tampering (immutability).** After sufficient block confirmations, published data becomes computationally infeasible to alter or remove. No single party - not storage nodes, verifiers, or protocol operators - can retroactively modify historical protocol state. **Canonical ordering.** Bitcoin establishes a global, deterministic ordering of all protocol events through its block height and transaction ordering within blocks. This temporal ordering is critical for the storage protocol: challenges are issued at specific block heights, proof submission deadlines are defined relative to challenge blocks, and protocol state transitions follow a deterministic sequence. **Decentralization.** The blockchain operates without central authority or trusted intermediaries. Multiple independent miners or validators process transactions, and consensus rules are enforced through cryptographic and economic mechanisms rather than institutional trust. This decentralization ensures the storage protocol inherits censorship resistance (no single party can block valid proofs), permissionless participation (anyone can join as storage node or verifier), and sovereign verification (anyone can independently validate the complete protocol history). ## Nova Incremental Verification The protocol uses Nova, a recursive SNARK system that enables incrementally verifiable computation. Nova allows a prover to demonstrate correct execution of a computation over many steps while producing a proof that remains constant in size regardless of step count. This property is what makes multi-challenge aggregation practical. **Folding scheme.** Nova works through "folding"—a technique where each step's proof gets folded into the previous proof, accumulating evidence of correct execution without growing the proof structure. The prover maintains a running witness (the IVC accumulator) that tracks all previous steps. Each new step verifies the previous accumulator, executes the step circuit, and produces a new accumulator. The accumulator grows logarithmically with step count but compresses to a compact form through Spartan at the end. **Proof-of-retrievability circuit.** The step circuit $\phi_{\text{PoR}}$ verifies possession of one challenged symbol. The public inputs are the file root $\rho$, a state accumulator $s$, the challenge seed $\sigma$, and tree depth $d$. The private witness includes the challenged leaf, its Merkle path siblings, and direction bits. The circuit: 1. Derives the challenge index deterministically from the seed and current state 2. Verifies the Merkle path from the challenged leaf to the root 3. Updates the state accumulator by hashing it with the verified leaf 4. Outputs the updated state for the next iteration This circuit executes once per challenged symbol. For a challenge requiring 100 symbol proofs, the circuit executes 100 times, each time folding the proof into the accumulator. The deterministic index derivation ensures verifiers compute the same challenge indices; the Merkle verification proves possession; the state accumulation prevents replay attacks. **Spartan compression.** After all IVC folding steps complete, the accumulator is compressed using Spartan—a transparent SNARK that converts the logarithmic-size accumulator into a compact SNARK (\~10 kB) plus metadata. Spartan verification takes $O(\log^2 s)$ time where $s$ is step count, approximately 50 milliseconds for typical challenges. The compressed proof is what gets published to Bitcoin. **Transparent setup.** Unlike SNARKs such as Groth16 or PLONK, Nova and Spartan require no trusted setup ceremony. The public parameters can be generated deterministically by anyone from the circuit structure without relying on secret randomness that must be destroyed. This eliminates trusted setup risks and improves decentralization—any party can independently generate or verify the parameters for a given circuit shape. ## Multi-File Proof Aggregation Storage nodes frequently face challenges on multiple files within the proof window. Aggregating these challenges into a single proof dramatically reduces Bitcoin transaction costs. The File Ledger enables this aggregation while maintaining security. **File Ledger construction.** The File Ledger is a Merkle tree built over the root commitments of all active files. For each file $f$ with root $\rho_f$ and depth $d_f$, the protocol computes a root commitment: $\text{rc}_f = H_{\text{Poseidon}}(\text{TAG}_{\text{rc}}, \rho_f, d_f)$ This binds the file's root and depth together, preventing depth-spoofing attacks where an adversary might try to reuse proofs across files with different tree structures. Files are ordered deterministically (lexicographically by file identifier), and the ledger is constructed from these root commitments: $L = \text{Merkle-Tree}([\text{rc}_1, \text{rc}_2, ..., \text{rc}_{|F|}])$ Each file has a canonical ledger index corresponding to its position in sorted order. The ledger root $\rho_L$ commits to all files in the system at a specific block height. **Aggregated proof structure.** When a node is challenged on $k$ files, it generates a single proof demonstrating possession of challenged symbols across all files. The proof includes: * For each file: Merkle path from its root commitment to the ledger root (size $O(\log |F|)$) * For each challenged symbol in each file: Merkle path from symbol to file root (size $O(\log n_{\text{total}})$) * Nova IVC accumulator tracking all verifications across all files * Compressed Spartan SNARK proving correct execution The uncompressed witness size is $O(k \cdot \log |F| + k \cdot s_{\text{chal}} \cdot \log n_{\text{total}})$ where $k$ is challenged files, $s_{\text{chal}}$ is symbols per file, and $n_{\text{total}}$ is typical file size. After Spartan compression, this reduces to \~10 kB for the SNARK plus metadata. **Public inputs for multi-file proofs.** Single-file proofs use public inputs $[ρ, s_0, σ, d]$. Multi-file proofs extend this to: $z_0 = [\rho_L, s_0, I_{\text{ledger}}, D, \Sigma, Z_{\text{out}}]$ where $\rho_L$ is the ledger root, $I_{\text{ledger}}$ contains ledger indices for each challenged file, $D$ contains tree depths, $\Sigma$ contains challenge seeds, and $Z_{\text{out}}$ contains final states after processing all symbols. These ledger indices are not part of the serialized proof: the verifier derives each file's stable ledger index from its own ledger/registry and supplies it as a public input. The circuit verifies each file's root commitment is at that index, so a prover cannot claim a false position. The on-wire proof itself is constant-size, carrying only the compressed SNARK, the ledger root, and the aggregated tree depth. **Ledger state binding.** Proofs are cryptographically bound to a specific ledger state through the ledger root in public inputs. This root must correspond to the ledger at challenge block height, not the current state. The binding prevents ledger substitution (proving against a different file set), index manipulation (claiming files are at different positions), and state confusion (using historical proofs against current challenges). ## Verification Process When indexers receive a proof transaction, they read the constant-size proof and take the challenge identifiers from the transaction (the proof does not carry them), reconstruct public inputs from the challenge parameters and verifier-derived ledger indices, and verify the Spartan SNARK. Verification checks: 1. **Proof timeliness:** Submitted before challenge expiration 2. **Public input consistency:** Matches expected challenge parameters (correct roots, seeds, depths) 3. **SNARK verification:** Spartan verification passes (proving correct circuit execution) 4. **Challenge binding:** Proof covers the exact challenges claimed (ordering matches, no substitution) The constant-time verification (\~50 milliseconds) allows indexers to verify thousands of proofs efficiently while maintaining deterministic consensus. If verification succeeds, challenges are marked satisfied; if it fails or the proof is absent at expiration, the node is slashed. ## Security Properties The proof system provides several security guarantees critical to the storage protocol: **Soundness (unforgeability).** A computationally bounded adversary who doesn't possess challenged data cannot generate valid proofs except with negligible probability. Breaking soundness requires either defeating the Nova/Spartan SNARK (computationally infeasible under discrete log assumption) or finding Poseidon collisions (computationally infeasible for 128-bit security). The multi-layered cryptographic construction ensures breaking any single component is insufficient. **Completeness (honest prover success).** An honest prover possessing complete file data can always generate valid proofs that pass verification, provided cryptographic primitives function as specified. This guarantee holds deterministically—honest provers don't fail due to probabilistic factors. **Public verifiability.** Proofs can be verified by any party possessing only the public parameters, Merkle root commitments, and challenge parameters. Verifiers need not possess file data, trust the prover, or interact beyond receiving the proof. This enables the deterministic verification that makes metaprotocol consensus possible. **Transparent security.** The transparent setup eliminates trusted ceremonies and associated risks. Anyone can independently generate public parameters and verify they match the protocol specification. There's no "toxic waste" that could compromise security if not properly destroyed, and no multi-party computation ceremonies that could fail or be subverted. The combination of these properties—cryptographic unforgeability, efficient verification, and transparent security—is what makes trustless storage verification possible at the scale required for a perpetual storage network. # File Agreements Source: https://docs.kontor.network/docs/storage/file-agreements A file agreement is the protocol's commitment to store a specific file forever. Creating an agreement involves three phases: preparing the file for storage, distributing it to potential storage nodes, and broadcasting the agreement transaction to Bitcoin. Once enough nodes join and the agreement activates, the file enters the active set and begins earning emissions. ```mermaid theme={null} sequenceDiagram participant U as User participant SN as Storage Nodes participant BC as Bitcoin Blockchain participant IX as Indexer U->>U: Prepare File
(Erasure + Merkle) U->>SN: Distribute File Data U->>BC: Create Agreement Tx BC->>IX: Mine Block IX->>IX: Create Inactive Agreement rect rgb(240, 240, 240) SN->>BC: Join Agreement Tx BC->>IX: Mine Block IX->>IX: Add Node to Agreement IX->>IX: Activate ($$ N_f > n_{min}$$) end ``` ## File Preparation Before anything touches the blockchain, the user prepares the file locally. This preparation transforms raw data into a fault-tolerant, self-authenticating structure that storage nodes can efficiently prove they possess. **Partitioning and encoding.** The file is partitioned into fixed 31-byte symbols—the maximum size that fits within a Pallas scalar field element (255 bits). This allows symbols to encode directly as field elements for Merkle tree construction with no intermediate hashing. The symbols are then encoded using Reed-Solomon erasure coding over GF(2^8) in a multi-codeword structure. Each codeword encodes 231 data symbols and generates 24 parity symbols (10% overhead), totaling 255 symbols per codeword. Files larger than 7,161 bytes use multiple independent codewords. This erasure coding serves a critical purpose: it ensures files remain reconstructible even when storage nodes have lost data below the detection threshold. A node can lose up to 10% of each codeword's symbols and still reconstruct the complete file. This creates a safety margin—files passing random challenges remain fully recoverable. **Merkle commitment.** The encoded symbols (data plus parity) are arranged into a binary Merkle tree using Poseidon hashing, which is optimized for arithmetic circuits and requires hundreds of constraints per invocation versus thousands for SHA-256. The tree's root serves as a cryptographic commitment to the entire file. Each symbol becomes exactly one Merkle leaf, ensuring 1:1 correspondence between challenged data and tree leaves. When the tree has an odd number of nodes at any level, the final node is duplicated (hashed with itself) to maintain uniform circuit structure. **Verification and distribution.** The user computes a file identifier as the SHA-256 hash of the original data and distributes both the original file bytes and the public metadata (Merkle root, file size, erasure parameters) to potential storage nodes off-chain—typically through direct peer-to-peer transfer or public storage portals. Nodes independently prepare the file using the protocol's deterministic algorithm and verify that their computed Merkle root matches the user's commitment. This ensures nodes can verify data integrity without trusting the user's computation. ## Agreement Creation Once the file is prepared and distributed, the user broadcasts a file agreement creation transaction to Bitcoin. This transaction embeds the file metadata—Merkle root, file identifier, size, erasure configuration—in the witness data using Kontor's Taproot commit-reveal pattern. The metadata contains everything needed to identify and verify the file, but not the file contents themselves. **Economic parameters.** The protocol assigns the file a unique creation rank based on when it enters the network. This rank is sequential and immutable: the first file has rank 1, the second has rank 2, and so on. The rank determines the file's emission weight, which controls its perpetual share of network rewards: $ω_f = \ln(s_f^{\text{bytes}}) / \ln(1 + \text{rank}_f)$ Files created when the network is small receive permanently higher emission weights than identical files created later. This "grandfathering" effect creates bootstrap incentives—early participants take on greater risk with an unproven protocol and are compensated through permanently higher returns. The logarithmic scaling of file size ensures that a 1 GB file has only about twice the emission weight of a 1 MB file, making both small critical files and large archival data equally attractive to store. The user pays a one-time storage fee calculated deterministically from current network parameters: $υ_f = χ_{\text{fee}} \cdot k_f$ where $k_f$ is the per-node base stake requirement for this file and $χ_{\text{fee}}$ is a fixed protocol parameter. This fee is immediately burned—it does not fund the storage but serves as a spam deterrent and deflationary mechanism. The actual storage incentive comes from perpetual KOR emissions. **Activation threshold.** The agreement enters the protocol state in an inactive state. It remains inactive until storage nodes holding the file data broadcast transactions to join the agreement. When the agreement reaches the minimum required number of nodes, it activates. At this point, the file enters the active file set, its emission weight adds to the global network total, and the file becomes eligible for challenges. Activation is permanent—once active, file agreements never deactivate. ## File Size and Scaling The protocol currently supports files from 1 KB (minimum) to 1 GB (maximum). The minimum prevents spam and ensures reasonable proving costs relative to storage value. The maximum is determined by practical constraints—tree depth, memory requirements, proving time—rather than fundamental protocol limitations. A 1 GB file requires tree depth around 26, which remains practical for proof generation and verification. **Logarithmic economics.** The logarithmic scaling of emission weights, stake requirements, and user fees creates a deliberately regressive cost-per-byte structure. This design principle reflects a fundamental truth: the economic value of data is independent of its physical size. A 1 KB file containing a private key may be far more valuable than a 1 GB video file. Linear scaling would create structural bias where large files dominate emissions, making small but critical files economically unattractive to store. The logarithmic relationship compresses size differences. A file 1,000× larger in bytes has only about 2× higher emission weight, stake requirement, and user fee. This ensures: * **Fair node incentives:** Return on capital is roughly constant across file sizes, making both small and large files similarly attractive to store * **Equitable user costs:** Small files face no economic barriers while users consuming more storage resources still pay proportionally larger (but sublinear) total fees * **Uniform security:** All files receive the same level of commitment from storage nodes regardless of their physical size This is a deliberate choice to engineer an economy that prioritizes data security irrespective of data size, ensuring the protocol serves blockchain applications equally well whether storing NFT metadata or protocol snapshots. # Introduction Source: https://docs.kontor.network/docs/storage/introduction Bitcoin was designed as a state machine for decentralized agreement on payments, not for storing large quantities of data. While the protocol offers miners strong economic incentives for honest behavior, it provides no direct incentives for non-miners to run full nodes—and certainly none for storing provably prunable data like witness signatures or arbitrary payloads. Embedding file data directly in Bitcoin transactions externalizes costs to the entire network: a file stored on-chain must be downloaded and validated by every full node forever, creating a persistent burden that the uploader pays for once but the network bears indefinitely. This makes direct on-chain storage both economically prohibitive and contentious within the Bitcoin community. Yet blockchain applications need data with strong availability guarantees. NFTs need to reference images. DeFi protocols need state snapshots and oracle data. Assets need metadata. Kontor squares the circle: it provides Bitcoin users with scalable, permanent, off-chain file storage where the data storage *system* itself is entirely on-chain. The file contents live off-chain with storage nodes who are economically incentivized to maintain them forever. The blockchain records only the cryptographic commitments and periodic proofs of possession—the stateful components that require consensus. This architecture makes large-scale data storage economically practical while providing Byzantine fault-tolerant availability guarantees that are as strong as those of on-chain storage.
Storage Agreement Lifecycle Storage Agreement Lifecycle
## Pay Once, Store Forever Traditional cloud storage operates on subscription models: stop paying, lose your data. This is fundamentally incompatible with blockchain use cases where assets might sit dormant for years. A one-time payment cannot fund perpetual service—even a large upfront fee will eventually be depleted. Storage nodes incur continuous costs for equipment, bandwidth, power, and facilities that must be met indefinitely. Kontor solves this through perpetual emissions. Storage nodes earn KOR each block proportional to the files they store, creating an ongoing revenue stream that lasts as long as the protocol exists. The user's one-time fee is burned rather than escrowed—it serves to prevent spam and create deflationary pressure, but it does not directly fund the storage. The actual funding comes from inflation, calibrated so that the net present value of future emissions exceeds storage costs for rational operators, making it profitable to maintain files indefinitely. This emission-based model enables responsive incentives that fixed payments cannot provide. If replication falls below safe levels, emissions increase automatically, making storage more profitable and attracting additional capacity. The system self-regulates without requiring governance decisions or manual parameter updates. Files created earlier in the network's history receive higher emission weights permanently, ensuring that valuable historical data remains attractive to store as the network grows, and the protocol handles files from kilobytes to gigabytes with logarithmic cost scaling—a 1 GB file costs only about twice as much to store as a 1 MB file, ensuring that storage nodes find both small but critical files and large archival data equally attractive to maintain. ## The Verification Problem The fundamental challenge is verifying that storage nodes actually possess the data they claim to store. Without verification, rational nodes would delete data immediately and pocket rewards until caught. The protocol solves this through a challenge-response mechanism built on compact proofs of retrievability. Each Bitcoin block, the protocol uses the block hash as a randomness source to deterministically select files for auditing and nodes to challenge. The challenged node must prove cryptographically that it possesses specific randomly-chosen sectors of the file. The proof is compact (around 10 kB regardless of file size), verifiable by anyone, and impossible to forge. If the node produces a valid proof within approximately two weeks, it continues earning rewards. If it fails or cannot respond, it is slashed and removed from the agreement. The detection probability is calibrated to make deletion economically irrational. A node that has lost even 10% of a file's data has a 99.997% chance of being caught per challenge. With 12 challenges per year, expected time to detection is measured in weeks, not years. The capital at risk—the node's stake, which typically exceeds storage costs by orders of magnitude—makes the gamble unattractive. The protocol doesn't need to challenge every byte constantly; it only needs to make the expected cost of deletion far exceed any possible savings. ## Comparison with Alternatives Several projects have attempted to solve blockchain data storage, but each has fundamental limitations that Kontor addresses. **Filecoin, Sia, and Chia** are designed to compete with centralized cloud storage services like Amazon S3 rather than provide blockchain-native solutions. They optimize for bulk data at competitive pricing but offer no long-term availability guarantees. Storage deals must be continuously renewed—the uploading user must keep paying or the data disappears. This makes them unsuitable for blockchain applications where the data must outlive the original uploader. **Arweave** has goals similar to Kontor's but its design is incomplete. Most critically, Arweave's economic model does not actually incentivize perpetual storage—it relies on an "endowment pool" that developers hope will be sufficient, and the Yellow Paper explicitly does not claim the network will continue producing blocks in perpetuity. Storage nodes face no penalties for failing to store committed data. And because Arweave is a separate blockchain, Bitcoin users must purchase additional cryptocurrency off-chain with incompatible tooling. **IPFS** is commonly used for file distribution and retrieval but provides no availability guarantees whatsoever. Without an integrated cryptocurrency, there's no mechanism for incentivizing nodes to host data. Files disappear when nodes decide to prune them, making IPFS unreliable for critical applications without additional infrastructure. Kontor provides what these systems cannot: pay once and store forever, strong Byzantine fault-tolerant availability guarantees, direct integration with Bitcoin requiring no separate tokens or bridges, and economic penalties that make rational nodes prefer honest storage over any alternative strategy. ## Technical Design The protocol uses Nova recursive SNARKs to create efficient, compact proofs. This cryptographic construction enables storage nodes to aggregate multiple challenges across multiple files into a single proof. Without this aggregation, proving costs would scale linearly with stored files, making large-scale operations uneconomical. With aggregation, a node storing thousands of files can batch weeks of challenges into a single Bitcoin transaction, amortizing the transaction fee across the entire proof set. File preparation applies Reed-Solomon erasure coding (10% redundancy) and builds a Merkle tree commitment over 31-byte symbols. The erasure coding ensures files remain reconstructible even when nodes have lost data below the detection threshold. The Merkle tree enables efficient challenges: nodes prove possession of randomly-selected sectors without revealing the entire file. The proof system uses Poseidon hashing optimized for arithmetic circuits, providing orders of magnitude better performance than SHA-256 for in-circuit operations. The economic layer—emissions, slashing, dynamic stake requirements—ensures that rational operators naturally behave honestly. Capital costs (opportunity cost of staked KOR) dominate operational costs by design, making attacks that save storage costs while maintaining rewards fundamentally unprofitable. Security emerges from aligned incentives rather than imposed restrictions. For the complete technical specification including cryptographic primitives, protocol parameters, and formal security analysis, see the [Storage Protocol Specification](/docs/resources/file-storage). # Storage Node Operations Source: https://docs.kontor.network/docs/storage/node-operations Storage nodes are rational economic actors seeking to maximize profit. The protocol structures incentives so that profit-maximizing behavior naturally aligns with protocol goals: storing files reliably and responding to challenges honestly. A node's decisions—whether to join agreements, which files to store, when to exit—are determined by comparing expected revenues against costs over a time horizon. ## Joining File Agreements Storage nodes can join file agreements in two ways: directly (if they can obtain the file data through off-chain means) or through sponsorship (paying commission to an existing storer who provides the data). Both paths require sufficient staked KOR to cover the new commitment. **Direct joining.** If a node can acquire file data from the original user, public portals, or other sources, it broadcasts a join transaction to Bitcoin. The transaction must be signed by the node and references the target file agreement. The protocol verifies the node has sufficient stake for its projected file set (existing files plus this new file), then adds the node to the agreement. Once added, the node begins earning emissions from that file immediately and becomes eligible for challenges. **Sponsored joining.** When a node cannot obtain file data through public channels, it can enter a sponsorship arrangement with an existing storer. The process is trustless through a bond-escrow mechanism: 1. The sponsor (an existing storer) posts a public sponsorship offer on-chain via transaction, specifying the target entrant, commission rate, duration, and required bond amount 2. The entrant receives file data from the sponsor off-chain and verifies it matches the on-chain Merkle root 3. The entrant accepts the offer by broadcasting a join transaction, which atomically locks the bond in escrow, creates the sponsorship agreement, and adds the entrant to the file agreement 4. When the entrant is first challenged for this file, the bond is released: returned to the entrant if they prove successfully, transferred to the sponsor if they fail During the sponsorship period (measured in blocks), the entrant pays the sponsor a fraction of its emission rewards from that specific file. The commission applies only to the sponsored file—other files stored by the entrant earn full rewards. This market-driven mechanism ensures permissionless entry: any existing storer can offer sponsorship, creating competition that drives commission rates toward equilibrium. **Stake requirements.** The protocol uses a pooled stake model—each node maintains a single stake balance that must cover all its file commitments. The required stake is: $k_{\text{req}}(n) = \left(\sum_{f \in F_n} k_f\right) \cdot \lambda_{\text{stake}}(n)$ where $F_n$ is the set of files stored by node $n$, $k_f$ is the per-node base stake for each file, and $\lambda_{\text{stake}}(n) = 1 + \lambda_{\text{slash}} / \ln(2 + |F_n|)$ is the dynamic stake factor. This factor imposes a capital penalty on nodes storing few files—a node storing a single file must stake 20-30× more capital per file than a node storing thousands. This makes risk compartmentalization through Sybil identities prohibitively expensive, incentivizing operators to consolidate rather than fragment. ## Revenue Streams Storage nodes earn revenue from three sources: **Storage emissions.** The primary revenue is KOR emissions distributed each block. A node storing file $f$ receives $\varepsilon_f / |N_f|$ where $\varepsilon_f$ is the file's emission allocation and $N_f$ is the set of nodes storing that file. This base reward is modified by sponsorship agreements—if the node is a sponsored entrant, it pays commission to its sponsor; if it sponsors others, it earns commissions from them. The commission applies only to the specific sponsored file, not the node's entire portfolio. Total emissions scale with KOR supply and adjust dynamically based on network health. When emission-weighted replication falls toward minimum thresholds, the emission multiplier increases, raising per-node profitability and attracting new capacity. This counter-cyclical mechanism stabilizes the network during stress. **Slashing redistribution.** When a node fails a challenge, a portion of its slashed stake $(1 - \beta_{\text{slash}}) \cdot \lambda_{\text{slash}} \cdot k_f$ is distributed equally among other nodes storing that file. This creates additional incentive for honest behavior—maintaining data and responding to challenges not only avoids being slashed but also generates revenue when others fail. **Sponsorship commissions.** Nodes can earn commissions by sponsoring entrants. The sponsor transfers file data off-chain in exchange for time-limited commission payments on the entrant's emissions from that file. The equilibrium commission rate balances the sponsor's bandwidth cost against the net present value of commission payments, typically resulting in low rates that make sponsorship attractive to entrants while profitable for sponsors. ## Cost Structure Storage nodes face three categories of costs, though capital costs dominate by design: **Capital costs.** The opportunity cost of staked KOR is typically the largest expense. Staked capital cannot be used for other purposes, creating an opportunity cost $k_{\text{req}} \cdot \rho$ where $\rho$ is the operator's discount rate. With stake requirements calibrated to file emissions and the dynamic stake factor, capital costs typically exceed storage costs by factors of 10-100×. This asymmetry is what enables economic security—attacks that save storage costs while maintaining rewards must still bear full capital costs, making them unprofitable. **Proving costs.** Nodes must periodically submit storage proofs on-chain, paying Bitcoin transaction fees. The two-week proof window allows nodes to aggregate multiple challenges into a single transaction, amortizing the fee across many proofs. A node storing thousands of files can batch weeks of challenges into a single \~10 kB proof transaction, making per-challenge proving costs negligible for diversified operators. **Physical storage costs.** The cost of disk space, bandwidth for data repair and replication, and facilities are typically small compared to capital costs. These scale roughly linearly with file size but are dwarfed by the opportunity cost of capital for any realistic parameter values. ## Leaving Agreements Nodes can voluntarily exit file agreements when profitability declines, but departure is constrained to protect file availability. The protocol combines preventative disincentives with programmatic restrictions. **Leave fee.** When voluntarily exiting, nodes must pay a leave fee from their spendable balance (not stake): $\varphi_{\text{leave}} = k_f \cdot \left(\frac{n_{\text{min}}}{|N_f|}\right)^2$ The fee scales quadratically with file vulnerability—when replication is far above minimum, the fee is negligible; as replication approaches the threshold, the fee becomes prohibitively expensive. This creates hysteresis: nodes require substantial sustained losses before exit becomes rational, providing cushion against cascading exits during temporary market stress. **Minimum replication.** Voluntary departure is programmatically forbidden when $|N_f| \leq n_{\text{min}}$. Nodes at minimum replication cannot leave regardless of profitability—they're economically locked in. This ensures files never drop below the minimum threshold through voluntary action. Exit becomes possible only when new nodes join and raise replication above the minimum. **Involuntary removal.** If a node is slashed or its stake falls below requirements for other reasons, automatic stake-sufficiency restoration triggers. The node is gracefully removed from non-critical files (where $|N_f| > n_{\text{min}}$) with penalties until stake is sufficient. If that's impossible without violating minimum replication, the node's entire remaining stake is burned and it's removed from all agreements. This prevents nodes from gaming insufficiency by ensuring maximum penalty extraction. ## Economic Decision Framework Nodes evaluate decisions by computing net present value over a planning horizon. To join a file agreement, a node calculates the marginal profit from adding that file—the additional emissions minus the increased capital cost, proving cost increment, and storage cost. Joining is profitable when this marginal NPV is positive. To decide whether to leave, a node compares continued participation against exit. Leaving requires paying the leave fee and forfeiting all future emissions. Staying requires continued capital lock-up and proving costs but generates ongoing emissions. The leave fee creates a substantial barrier that grows as files become more vulnerable, making premature exit expensive. The break-even condition for staying versus leaving depends on expected future emissions, capital costs, and the leave fee. For a well-replicated file with low leave fees, a small profitability decline may trigger exit. For a file near minimum replication with high leave fees, nodes require large sustained losses before exit becomes rational. This asymmetry is intentional—it prevents cascading exits when files are most vulnerable while allowing efficient exit when files are over-replicated. **Crowding and equilibrium.** As more nodes join an agreement, per-node rewards decrease (emissions are divided among more participants). This creates a natural equilibrium: files with few nodes have high per-node rewards, attracting new joiners; files with many nodes have low per-node rewards, discouraging additional joiners. The system self-balances without coordination. **Repair incentives.** If a file becomes under-replicated (through slashing or other events), per-node rewards increase hyperbolically—the same total emissions divided among fewer nodes. This creates powerful incentive for new nodes to join and "repair" the file's replication. Combined with the high leave fees preventing further exits, this mechanism stabilizes files during stress. ## Operational Considerations Running a storage node requires operating a Kontor indexer—the software that processes Bitcoin blocks, maintains protocol state, tracks challenges, and verifies proofs. Without an indexer, nodes cannot know when they've been challenged or what the current protocol state is. The indexer provides the authoritative view of all active challenges, file metadata, and expiration deadlines. Nodes have complete autonomy in proof batching strategy. Within the two-week expiration window, nodes can batch any subset of pending challenges, time submissions strategically to optimize transaction fees, aggregate challenges from multiple files and multiple block heights, and respond to high-value challenges immediately while batching others. This flexibility enables nodes to optimize operational costs—a node might batch many challenges into one transaction during low-fee periods, or submit urgent proofs immediately when profitability justifies higher fees. The protocol provides no direct guarantees about retrieval service—nodes are incentivized to store data (through emissions and slashing) but not required to serve it. However, competitive markets naturally emerge: multiple nodes storing the same file can offer retrieval services at competitive prices, and users can verify data integrity through Merkle paths against the on-chain root. File retrieval typically occurs through off-chain payment channels using atomic symbol-for-payment exchanges, with erasure coding solving the final-symbol problem by allowing users to request extra symbols beyond the reconstruction threshold. # Sponsorship Markets Source: https://docs.kontor.network/docs/storage/sponsorship-markets The sponsorship mechanism solves a fundamental bootstrapping problem: how do new nodes obtain file data when they don't already have it? Nodes cannot join file agreements without possessing the data (they'd fail challenges immediately), but existing storers could gatekeep by refusing to share. The sponsorship market breaks this deadlock by making it profitable for existing storers to provide data to newcomers. ## The Data Distribution Problem When a file agreement is created, the user distributes file data off-chain to potential storage nodes. Wide initial distribution benefits the user—faster activation, competitive retrieval markets, lower future sponsorship costs. But after activation, the protocol faces a coordination problem: what incentivizes existing storers to share data with nodes that want to join later? Without sponsorship, existing storers could form cartels and refuse to share data, creating a permanent monopoly. Any node wanting to join would be blocked. The file would remain at minimum replication forever, concentrated among the original participants who could extract monopoly rents for retrieval services. This violates the protocol's openness—new nodes should be able to enter freely based on economic incentives, not permission from incumbents. The sponsorship mechanism solves this by creating a market for data provision. Existing storers can offer to provide file data in exchange for time-limited commission on the entrant's future rewards. This transforms data sharing from altruism to profit opportunity—sponsors earn revenue for helping new nodes join. The market structure ensures any existing storer can offer sponsorship, creating competition that prevents gatekeeping cartels. ## Trustless Bond-Escrow Mechanism The sponsorship process must be trustless—neither party should be able to exploit the other. The protocol achieves this through a two-step on-chain commitment with bond escrow: **Step 1: Sponsor posts offer.** The sponsor (an existing storer) broadcasts a `Create-Sponsorship-Offer` transaction to Bitcoin specifying: * Target entrant (specific node identifier) * Commission rate ($\gamma_{\text{rate}}$) and duration ($\gamma_{\text{duration}}$ in blocks) * Required bond amount ($\beta_{\text{bond}}$ in KOR) * Expiration deadline ($W_{\text{offer}}$ blocks, typically \~1 day) This on-chain commitment fixes the terms before any off-chain data transfer. The sponsor cannot modify terms after posting—the entrant sees exactly what they're accepting. The Bitcoin transaction fee paid to post the offer creates a credible commitment. **Step 2: Entrant accepts.** After receiving the offer on-chain, the sponsor transfers file data to the entrant off-chain (through direct peer-to-peer transfer or other means). The entrant independently verifies the data matches the on-chain Merkle root by running the file preparation algorithm. If verification succeeds, the entrant broadcasts a `Join-Agreement` transaction accepting the offer. This transaction atomically: * Locks the bond ($\beta_{\text{bond}}$) in escrow from entrant's spendable balance * Creates the sponsorship agreement with the offer's terms * Adds the entrant to the file agreement * Removes the accepted offer from state The bond remains in escrow until the entrant's first challenge for this file is resolved. **Bond resolution.** When the entrant is first challenged for the sponsored file: * **Success:** If the entrant submits a valid proof, the bond is returned to the entrant's balance and the sponsorship continues normally for its full duration * **Failure:** If the entrant fails the challenge, the bond is transferred to the sponsor (compensating fees and bandwidth costs), the sponsorship is voided retroactively (no commission ever paid), and the entrant is slashed and removed normally This mechanism makes sponsorship fully trustless: * The sponsor cannot extort—terms are fixed on-chain before data transfer * The entrant cannot grief—the bond at risk equals the sponsor's costs * Both parties have symmetric incentives to perform honestly ## Market Dynamics and Equilibrium The sponsorship market operates through competition. Multiple sponsors can post offers for the same entrant (or different entrants) with varying commission rates and durations. Entrants select the most favorable terms, and sponsors compete by offering lower rates. **Equilibrium commission rate.** The equilibrium rate $\gamma_{\text{eq}}$ balances the sponsor's costs against the net present value of commission payments. The sponsor incurs: * Bitcoin transaction fees for posting the offer ($c_{\text{BTC}}^{\text{offer}}$) * Bandwidth costs for transferring the file ($c_{\text{transfer}}^{\text{USD}}$) The sponsor receives commission payments $\gamma_{\text{rate}} \cdot \varepsilon_f / |N_f|$ each block for $\gamma_{\text{duration}}$ blocks, where $\varepsilon_f$ is the file's emissions and $|N_f|$ is the node count after the entrant joins. The NPV of this stream depends on the sponsor's discount rate $\rho$. For sponsorship to be profitable, the NPV of commissions must exceed costs. The equilibrium rate emerges where marginal sponsors break even: $\gamma_{\text{eq}} \approx \frac{(c_{\text{BTC}}^{\text{offer}} \cdot \xi_{\text{BTC/USD}} + c_{\text{transfer}}^{\text{USD}}) \cdot \rho \cdot (|N_f|+1)}{\varepsilon_f \cdot \xi_{\text{KOR/USD}} \cdot (1 - (1 + \rho)^{-\gamma_{\text{duration}}})}$ where $\xi$ represents exchange rates. With typical parameters (moderate file emissions, reasonable duration, standard bandwidth costs), equilibrium rates are low—typically single-digit percentages. This makes sponsorship attractive to entrants while profitable for sponsors. **Competitive pressure.** Even if a cartel of storers attempts to monopolize a file by coordinating high commission rates, any single member has incentive to defect and undercut the cartel price, capturing the entire commission for themselves. The protocol doesn't mandate sponsorship—nodes can join directly if they obtain data through other means—but it provides a permissionless fallback that ensures no file can be permanently locked to a closed group. **Duration selection.** Longer durations provide sponsors more total commission but increase the entrant's cost. Shorter durations reduce entrant cost but may not justify sponsor's upfront investment. Market competition pushes durations toward values that balance these tradeoffs—typically measured in weeks to months, long enough to recover sponsor costs but short enough to remain attractive to entrants. ## Attack Resistance The bond-escrow mechanism prevents several potential attacks: **Sponsor extortion.** A sponsor posts an offer, the entrant expresses interest off-chain, and the sponsor demands a higher commission before transferring data. This fails because commission rate is committed on-chain before any off-chain interaction. The entrant sees the final terms in protocol state and can accept, reject, or seek competing offers. The sponsor cannot renegotiate. **Entrant griefing.** A malicious actor creates a Sybil node, accepts a sponsorship offer, receives file data, then intentionally fails the first challenge to impose costs on the sponsor. Without the bond, this would cost the sponsor transaction fees and bandwidth (\~$0.60) while the attacker loses slashed stake (~$200). The attack isn't profitable but enables disproportionate cost imposition. The bond requirement ($\beta_{\text{bond}} \approx 3$ KOR ≈ $0.60 typical) eliminates this: if the entrant fails, the bond transfers to the sponsor, fully compensating their costs. The attacker loses both the bond and the slashed stake (~$200.60 total) while the sponsor loses nothing. The griefing attack becomes strictly unprofitable. **Front-running.** An adversary observes an acceptance transaction in the mempool and attempts to accept the same offer first by submitting a competing transaction with higher fees. This fails because each offer specifies a target entrant identifier. The `Join-Agreement` procedure validates `signer == offer.entrant`, rejecting acceptance attempts from any node other than the designated target. The targeted offer design makes front-running impossible. **Sponsor ghosting.** A sponsor posts an offer but never transfers the data off-chain. The entrant waits but receives nothing. The sponsor loses Bitcoin transaction fees (wasted expenditure) and gains nothing. The entrant loses opportunity cost of waiting (bounded by $W_{\text{offer}}$ ≈ 1 day). This attack is economically irrational—the sponsor incurs costs for no benefit. Entrants can mitigate by soliciting multiple parallel offers from different sponsors. ## Market Benefits The sponsorship market provides several systemic benefits beyond just solving the data distribution problem: **Permissionless entry.** New nodes can always join file agreements based purely on economic incentives, never requiring permission from existing participants. This maintains the protocol's openness as files age and original participants turn over. **Competitive pricing.** Multiple potential sponsors create competitive markets for data provision. Commission rates reflect actual costs rather than monopoly prices, benefiting entrants and ensuring efficient node entry. **Dynamic repair.** When files become under-replicated (through slashing or other events), per-node rewards increase, making sponsorship more valuable. Sponsors have stronger incentive to recruit new nodes when files need repair most, creating automatic rebalancing. The sponsorship mechanism exemplifies the protocol's design philosophy: rather than mandating behavior or requiring coordination, create economic incentives that make desirable behavior individually profitable. The result is a self-organizing market that maintains protocol openness without central enforcement. # Storage Security Source: https://docs.kontor.network/docs/storage/storage-security The storage protocol's security operates on two layers: cryptographic security ensures only nodes with actual data can produce valid proofs, while economic security ensures rational nodes prefer honest storage over any alternative strategy. Understanding how these layers interact is essential to understanding why the protocol provides strong perpetual availability guarantees. ## Cryptographic Foundation The proof system's security relies on standard cryptographic hardness assumptions. Breaking the system requires defeating multiple independent cryptographic primitives simultaneously. **Proof unforgeability.** A computationally bounded adversary who doesn't possess challenged data cannot generate valid proofs except with negligible probability. Forging a proof requires breaking Nova/Spartan soundness (computationally infeasible under discrete logarithm assumption on Pallas/Vesta curves, \~128-bit security), finding Poseidon hash collisions (computationally infeasible for 128-bit security target), or breaking Merkle tree binding (follows from collision resistance). The multi-layered construction ensures breaking any single component is insufficient. **Challenge unpredictability.** Challenges derive from Bitcoin block hashes, which provide high-entropy unpredictable randomness. Miners could theoretically grind block hashes to bias challenge selection, but this is economically irrational—the cost of discarding a valid block vastly exceeds any benefit from influencing which storage nodes are challenged. The protocol uses the block hash directly because the economic disincentive against grinding is overwhelming. **Replay prevention.** Each challenge has a unique deterministic identifier computed via domain-separated hashing. Challenge identifiers prevent proof reuse across different challenges, files, or nodes. The state accumulation in the PoR circuit prevents partial replay—each proof is cryptographically bound to a specific sequence of challenged sectors in a specific order. **Transparent setup.** The proof system requires no trusted ceremony. Anyone can independently generate public parameters from circuit structure and verify they match the protocol specification, eliminating the trust assumptions and coordination risks of trusted setup ceremonies. ## Detection Economics The challenge mechanism provides probabilistic guarantees calibrated to make selective storage economically irrational. The key insight is that the protocol doesn't need perfect detection—it only needs to make the expected cost of cheating far exceed any possible savings. **Detection probability.** For a node that has lost fraction $\mu$ of its data, detection probability per challenge is approximately $1 - (1-\mu)^{100}$. With 12 challenges per year, even 10% data loss yields 99.997% per-challenge detection with expected time to detection around 3 days. The capital at risk (staked KOR) typically exceeds storage costs by factors of 10-100×, making the risk-reward calculation decisively unfavorable. **Erasure coding safety margin.** The 10% detection threshold aligns with Reed-Solomon fault tolerance. Each codeword can reconstruct from any 231 of 255 symbols (90%). The detection probability is calibrated to catch nodes at or above this threshold—ensuring files passing challenges remain reconstructible. The protocol needs only to prevent nodes from crossing the recovery threshold. **Economic security emerges from detection.** High detection probability is what enables economic security. Without reliable detection, nodes could safely store nothing and gamble on not being checked. With near-certain detection within days, storing less than the full file becomes a losing bet. This detection probability, combined with capital cost dominance (stake opportunity costs dwarf storage costs), is what makes honest storage the dominant strategy for rational operators. For comprehensive analysis of how detection economics prevent specific attacks, see [Economic Security](/docs/economics/economic-security). ## Protocol Mechanisms Several protocol mechanisms enforce correct operation and prevent manipulation: **Dynamic staking.** The dynamic stake factor $\lambda_{\text{stake}}(n) = 1 + \lambda_{\text{slash}} / \ln(2 + |F_n|)$ creates capital efficiency for large consolidated operations while making Sybil fragmentation prohibitively expensive. For detailed analysis of Sybil resistance, see [Economic Security: Sybil Resistance via Dynamic Staking](/docs/economics/economic-security#sybil-resistance-via-dynamic-staking). **Leave mechanism.** The quadratic leave fee $\varphi_{\text{leave}} = k_f \cdot (n_{\text{min}} / |N_f|)^2$ prevents cascading exits when files approach minimum replication, creating hysteresis that stabilizes the network during stress. Combined with programmatic prohibition of voluntary departure at minimum replication, this ensures files never drop below thresholds through coordinated action. **Stake insufficiency handling.** Automatic restoration when stake becomes insufficient prevents strategic gaming—the protocol ensures maximum penalty extraction through graceful removal from non-critical agreements or, if necessary, total forfeiture. **Sponsorship bond mechanism.** The bond-escrow design prevents both sponsor extortion (terms fixed on-chain first) and entrant griefing (bond at risk equals sponsor costs), maintaining trustless data distribution markets. ## Integration with Bitcoin The protocol's integration with Bitcoin provides security properties while creating dependencies: **Security inheritance.** Every storage protocol transaction is a Bitcoin transaction, secured by Bitcoin's full hashpower. The protocol inherits Bitcoin's immutability, censorship resistance, and ordering guarantees. Attacking the storage protocol requires attacking Bitcoin itself. **Reorg handling.** Bitcoin reorganizations are handled deterministically—the indexer's append-only database structure allows straightforward reprocessing from reorg points. Deep reorgs (beyond \~6 blocks) are extremely rare and would indicate systemic Bitcoin issues. **Censorship resistance.** The two-week proof window means sustained censorship requires excluding proof transactions for 2,016 consecutive blocks. The economic cost of this sustained attack is prohibitive. For detailed analysis of censorship and front-running attacks, see [Economic Security](/docs/economics/economic-security). **Deterministic execution.** The security model assumes all honest indexers processing the same Bitcoin history arrive at the same state. Non-deterministic behavior is eliminated through Wasmtime configuration and careful runtime design. ## Attack Resistance Summary The storage protocol resists various economic attacks through mechanisms detailed in the [Economic Security](/docs/economics/economic-security) documentation: * **Selective storage** (storing partial data): High detection probability and capital cost dominance make this unprofitable. See [Capital Cost Dominance](/docs/economics/economic-security#capital-cost-dominance). * **Sybil attacks** (risk compartmentalization): Dynamic staking makes fragmentation prohibitively expensive. See [Sybil Resistance](/docs/economics/economic-security#sybil-resistance-via-dynamic-staking). * **Wash-trading** (self-storage for rewards): Upfront burned fee exceeds NPV of future rewards. See [Wash-Trading Prevention](/docs/economics/economic-security#wash-trading-prevention). * **Collusion** (coordinated slashing): Burn fraction ensures groups always lose money net. See [Collusion and Redistribution](/docs/economics/economic-security#collusion-and-redistribution). Each attack is prevented by ensuring expected costs exceed expected benefits—the protocol doesn't rely on detecting malicious intent but on making dishonest behavior economically irrational.