Interfaces

An interface is a contract-level declaration that a contract conforms to a known, versioned standard. Declaring one buys you three things: the compiler and the node both enforce the standard's shape, wallets and infrastructure can integrate your contract without reading its code, and conforming contracts unlock protocol features (partitioned state, compact operations) that ordinary contracts cannot use.

Interface IDs are versioned by suffix (_v1). A changed requirement is always a new ID, never a mutation of an existing one, so conformance can never silently drift under you.

Syntax

Interfaces and function classifications are declared with annotations:

@interface(utxo_token_v1)
contract Token {
    @partition_scoped
    fn transfer(amount: Int, dest_vout: Int) {
        // move `amount` to the outpoint created by output `dest_vout`
        // of the anchoring transaction
        ...
    }

    fn mint(amount: Int, dest_vout: Int) {
        ...
    }

    @global
    fn burn(amount: Int) {
        ...
    }
}

The annotation set:

  • @interface(<id>): declares conformance, placed on the contract. The ID must be a known interface; an unknown ID, a duplicate declaration, or a bare @interface with no argument fails the build with a named reason.
  • @partition_scoped: marks a function as touching only a bounded partition of state, derivable from its anchoring Bitcoin transaction. See Global and Partitioned State.
  • @global: the explicit spelling of the default. An undeclared function is global.
  • @compact_only: contract-level; declares that the contract is called exclusively through compact operations (below).
  • @writeable_by(fn_a, fn_b): field-level; restricts which functions may write a field. An empty writer list is rejected at compile time rather than silently locking the field.

utxo_token_v1

The first standard interface is the UTXO-bound token. Token ownership is bound to Bitcoin outpoints rather than address-keyed balance maps, so tokens inherit Bitcoin's double-spend safety and compose naturally with Lightning channels and HTLCs, which are outpoint-level constructs.

Required functions:

Function Signature Classification
transfer (amount: Int, dest_vout: Int) @partition_scoped required
mint (amount: Int, dest_vout: Int) global allowed (issuance may touch total supply)
burn (amount: Int) global allowed

Note the addressing model: recipients are vout indexes, not addresses. The anchoring transaction's own outputs do the addressing, so a transfer names "output 1 of this transaction" and the tokens bind to whatever outpoint that output becomes. Ownership of the outpoint is ownership of the tokens.

transfer must be @partition_scoped; the compiler rejects a utxo_token_v1 contract whose transfer is global, because the partitioned-stall guarantees of the interface depend on it.

How declarations are treated

Enforcement happens twice, deliberately:

  1. At compile time, sclc validates the declarations against the interface spec: every required function present, with exactly the required ABI signature and the required classification. Violations fail the build with a named reason. This is ergonomics: you find out immediately.
  2. At registration, the node re-runs the same checks against the emitted ABI and deploy metadata. This is consensus: a hand-crafted artifact cannot claim conformance it does not have.

The compiler emits the declarations as deploy metadata alongside the bytecode and ABI. Only non-default entries are recorded; an empty object means full defaults:

{
  "interfaces": ["utxo_token_v1"],
  "functions": { "transfer": "partition_scoped" }
}

This metadata travels in the metadata field of POST /deploy_contract.

Compact operations

For interface-defined operations, conforming contracts can skip the call envelope entirely. A compact operation carries the selector and arguments (43 bytes or less) directly inside the anchoring transaction's OP_RETURN, in place of the payload hash. A utxo_token_v1 transfer is about 10 bytes: selector, amount, and vout index, with the transaction's outputs doing the addressing.

The consequences are significant:

  • No envelope, nothing to withhold. The data rides where the hash would have been, so there is zero stall surface: nobody can freeze state by withholding a payload that is already on-chain.
  • Authorization is the Bitcoin spend itself. The spend of the token-bound input authorizes the operation; replay is structurally impossible.
  • Final at Bitcoin depth. Compact operations are unvoidable; there is no void window to wait out.

The trade-off is visibility: arguments (amounts) become chain-visible. Compact operations are therefore an opt-in per contract, not a replacement for hash-only envelope calls. A contract deployed @compact_only accepts nothing else, and as a result can never stall.

Why declare an interface

  • Enforced shape, twice. Conformance is checked by the compiler and re-checked by the node. Integrators rely on the standard, not on your discipline.
  • Legibility. Wallets, indexers, explorers, and Lightning infrastructure can enumerate balances and drive any conforming token through the same entry points, with no contract-specific knowledge.
  • Partitioned stalls. Only interface contracts with UTXO-bound state qualify for partition-scoped classification, which is what keeps one user's withheld data from freezing anyone else. See Global and Partitioned State.
  • Compact operations. The cheapest, stall-proof, replay-proof call path is exclusive to interface-defined operations.

The interface track is rolling out with the current protocol milestones. utxo_token_v1 is the first standard ID; further interfaces follow the same versioned pattern.