Open Standard · Working Draft
mekaniskt·kontrakt

Specification ·  Section 7 of 13

Contracts calling contracts

Covers inter-contract topology through ports, trust via pinned identity and verified receipts, compensational (saga) execution instead of distributed ACID, the rejection of governing/middleware contracts, transport-agnostic messaging, call-outs to services, and an import manifest for external functions.

7.1 may call is the port model, one step up

A port’s from: today names a party. Generalized, it names a contract:

ports:
  in  order.receive   from contract mk-se-2027-000042 port order.place   # only THAT contract,
                      pinned sha256:9f2c44...                            # at that version

The chain CUSTOMER A -may call-> CONTRACT A -may call-> CONTRACT B is then readable off two port manifests, exactly as single-contract capabilities are readable off one (§1). And procurement already LIVES in this topology - we declare it, we do not invent it:

  • ramavtal → avrop: the call-off contract is a child drawing on the parent’s pools. The takvolym is a shared place, and §3.1’s Petri nets are the sixty-year-old mathematics of exactly this - many child contracts drawing one parent pool without double-counting.
  • huvudentreprenör → underentreprenör: back-to-back contracts. Flow-down clauses are lawyers hand-copying terms between contract layers; a mechanical back-to-back references the upstream terms it mirrors, by hash.
  • factoring: assignment of a receivable = re-pointing an out-port’s to: - a declared, signed, receipted event instead of a denuntiation letter.

7.2 Trust: pinned identity + verified receipts, never claims

A contract’s identity is its id + version hash + genesis seals - which is exactly an artifact’s identity, so §5’s trust machinery transfers verbatim: a referencing contract pins the counterpart’s hash (its amendments reach the reference only via your amendment) or tracks under declared bounds, and believes data-level seals over transport (§5.5). Messages between contracts travel as enveloped documents (§2) through ports - there is no RPC, no shared runtime, no synchronous call into another machine’s internals. Contract B believes a message from Contract A for one reason only: it carries A’s judgment receipt, verified offline against A’s pinned identity, chain and seal intact, before any of B’s rules run.

Honesty about the boundary: B verifies that A’s machine judged X under A’s signed rules - it cannot verify that A’s rules are wise, any more than a subcontractor can audit the head contract. That is what back-to-back drafting has always meant; the mechanical version makes the dependency explicit and the evidence checkable, nothing more.

(And the Solidity graveyard stays closed: cross-contract calls are asynchronous documents between separately-sealed logs - no reentrancy, no mid-call state to observe, nothing to flash-loan. The starvation diet of §9 extends across the contract boundary by construction.)

7.3 Transactions: ACID inside, sagas between (the smiley is correct)

  • Inside one contract: full ACID, for free. The single-writer log of §3.3: one event = one atomic, serialized, durable, hash-chained transition. Every contract is its own serialization domain.
  • Across contracts: no distributed ACID - by design, not by weakness. Two-phase commit needs a coordinator both organizations obey and locks held across autonomous parties’ books. That is a shared-infrastructure bet we decline. It does exist: Canton/Daml achieves atomic cross-contract composition - at the price of every party joining one synchronization domain (the same trade §12 cites it for on privacy). Autonomous parties keep autonomous logs.
  • So: compensational execution - and the domain already standardized the compensations. Cross-contract flows are sagas (Garcia-Molina & Salem, 1987): sequences of locally-atomic steps with declared compensating actions. Procurement’s gift: the compensating documents ALREADY EXIST as standards - the kreditfaktura (EN 16931 credit note, type code 381) undoes an invoice, pacs.004 returns a payment, the Peppol order response cancels an order. Forty years of e-documents encoded “undo” as documents because commerce never had rollback - the law itself is compensational (remedies, damages, kreditfaktura; no court has ever un-executed anything). A contract system promising distributed ACID would be lying about the world it models.
  • Where atomic-like coupling is genuinely needed: reservations, not locks. §3.4’s reserve-with-expiry IS try-confirm-cancel (TCC): B reserves (try), the draw confirms when A’s receipt exists (confirm), and the expiry auto-releases if it never does (cancel - a deterministic timeout by event math, no coordinator). The avrop shape exactly: the child’s order stands only when the parent-pool reservation receipt exists; the reservation dies quietly otherwise. Booking systems have run on reservations-instead-of-locks for decades.
  • Failure handling is already in this document. A failed or timed-out saga leg is a thrown condition; §4.2’s escalation catches it; compensation legs are declared per port pair - signed contract terms, not an ops runbook - and finally survival obligations run regardless. (§4.2 already flagged the saga pattern as finally’s distributed-systems cousin; this section is that flag, cashed.)

Cross-contract consistency is therefore eventual and provable: a receipt embeds the hash of the foreign receipt it acted on, so any third party holding both chains can replay and verify that the saga either completed or compensated. A dispute over a broken flow is two receipt chains laid side by side, not two legal departments’ inbox archaeology. The slogan, honestly earned: ACID inside the contract, sagas between contracts, receipts everywhere.

Prior art: sagas (1987; revived wholesale by microservices), the TCC/reservation pattern (bookings, payment holds), 2PC/XA (the rejected road: blocking, coordinator trust), Canton synchronization domains (the accepted-cost counterexample), BPMN 2.0 compensation events (§4.2), and on the legal side: back-to-back/flow-down drafting, kreditfaktura practice, and ISDA close-out netting as industrial-strength compensational settlement.

7.3b Why we don’t support governing/middleware/proxy contracts - EVER

The tempting idea, recorded here so nobody re-invents it: a contract registers itself as MIDDLEWARE - binding to certain operations (financial pool ops, all call-outs), running in the same transaction as a recorder or decorator - a GOVERNING contract over many sub-contracts, where the subs don’t know about it: REQUEST => GOVERNOR => CONTRACT. The need is real, the mechanism is poison.

Four structural kills - each one fatal alone:

  1. It breaks replay, which breaks everything. Verdict = pure function of (contract, its own log) is THE covenant. A governor acting “in the same transaction” makes contract X’s outcome depend on state outside X’s signed document - and since a governor spans many subs, replaying X suddenly requires every sibling’s interleaved log. That is the global shared ledger rebuilt through a side door: the exact Canton trade §7.3 declines. One log per contract, or determinism dies. There is no halfway.
  2. An invisible governor IS the fraud we hunt. The thesis is absent port = prohibition; everything readable off the manifest; nothing moves silently. A layer the sub-contract “doesn’t know about” is a silent term - structurally identical to the negative-option renewal and the quietly-updated rate table, wearing an architecture diagram. And it murders pre-validation: the supplier runs the file, gets ACCEPTED, submits - a hidden layer vetoes. Pre-validation is the adoption story; middleware makes it a liar.
  3. “Same transaction” across two state domains = 2PC. Already litigated (§7.3). If the governor shares the child’s log it is not middleware, it is the same contract; if it doesn’t, “same transaction” needs the distributed coordinator we refused. The middle is marketing.
  4. The prior art is a graveyard. Solidity’s proxy/upgradeable pattern - an interceptable governing layer - is that ecosystem’s single richest source of catastrophic exploits and rug-pulls. Spring AOP taught a generation that invisible cross-cutting behavior is undebuggable. The ESB became the enterprise thing nobody could reason about. “A decorator you can’t see in the manifest” has been tried, repeatedly, expensively.

The salvage - every legitimate governor capability already has a declared, replay-safe mechanism:

Middleware wants The mechanical equivalent (already specified)
Observe/record every operation Fold the receipts: envelope grammar (§2) + a borrow disclosure scope - the governor reads every child’s chain, computes cross-contract state, alerts. Zero interception; this is literally the referee posture.
Veto/gate operations at runtime An approval leg naming the governor (§4.1): require approval of governing-contract, declared IN the child, verdict PENDING, governor answers through a port. Same power - visible, signed, replayable.
Shared limits across many subs Parent pool draws (§7.1, ramavtal → avrop): the cross-contract ceiling is a declared draw on the governor’s pool; §3.1’s Petri math already handles parallel draws.
Inject policy into every contract Profiles + tracked artifacts (§5.3, a rule stack): attestordning, LOU gates, org policy as artifacts the contracts track - updated once, under governance, receipted everywhere.
React to violations Escalation (§4.2) + amendment offers, downstream of receipts.

The principle, one line: governance flows DOWN through declared bindings and UP through receipts - never SIDEWAYS through interception. The governor sits before execution (genesis bindings) and after it (folding receipts), never inside it.

And the reason this is no compromise: it is how law itself works. Lex superior does not intercept. LOU has never sat inside an invoice transaction - it constrains what may validly be signed (ex ante) and supplies consequences from the record (ex post). The middleware instinct reaches for a governing layer; the law’s own architecture - constrain at formation, adjudicate from the record - is the mechanical answer, and both halves are already built.

7.4 Transport is not the contract’s business

The design already earns this: everything a transport would normally be trusted for lives ABOVE the pipe. Authenticity and integrity are in the envelope seal (§5.5’s rule - data-level signatures, never transport trust); duplicates are absorbed by idempotency keys; ordering belongs to the contract’s sequencer, not the wire; and all interaction is asynchronous documents, so latency is a quality, not a correctness, concern. The transport can be slow, lossy, duplicating, or actively hostile - the grammar doesn’t care. A port declares payload type + authorized sender, never a pipe.

“JSON in SMTP” is not even a joke - it is the industry’s actual history: AS1 is EDI over SMTP (RFC 3335), AS2 over HTTP (RFC 4130), AS3 over FTP, AS4 over web services - the same business documents, four standardized transports across three decades, and EDIFACT rode X.400 mail and VANs before that. Transport-agnostic payloads are how e-documents have always survived their pipes dying.

One EDIFACT lesson taken with it, so agnosticism doesn’t rot into fragmentation: the core is transport-free, but every PROFILE names a default rail (mk-se-public → Peppol eDelivery/AS4, because Swedish public sector is already on it; an mk-se-b2b pair may agree on a message queue or plain mail). “Any transport is possible” without a profile default is how the VAN-interconnect mess happened; a bound default with freedom to deviate bilaterally is how Peppol fixed it. Same CIUS discipline as everything else (§6).

7.5 Call-outs: contracts calling services (suspend and resume, blind to the clock)

Both points hold: tax computation does not belong in the contract (the industry already runs it as a service - Avalara, Vertex, Stripe Tax - and VAT-by-category is a moving regulatory target no signed document should embed), and the call-out needs no new suspend/resume machinery, because the design already contains it. A call-out decomposes into four things this document has:

emit      tax.request (request-id, basket lines)          an out-port emission
expect    tax.response within 5 days                      §3.6 expectation ladder
park      the triggering submission's receipt says        PENDING is already a
          verdict: PENDING (awaiting tax.response)        first-class verdict
resume    tax.response arrives through its in-port,       a rule guarding on the
          correlated by request-id -> rule completes,     response event - resume
          final receipt issued                            = the fold reads one more event
  • §4.1’s approval was the special case all along. An approval is a call-out to a human or agent; PENDING-APPROVAL generalizes to PENDING (awaiting: X). One waiting mechanism, three kinds of callee: humans (approvals), other contracts (§7.1-7.3), services (this). The envelope’s verdict enum absorbs it without growing.
  • Why the contract cannot tell two days from 20 ms - by construction, not by discipline. “Suspended” is not a paused thread and the continuation is not stored program state: it is a state in the fold, and the resumption logic is just a rule that guards on the response event. HTTP-instant and two-day-batch produce identical logs modulo timestamps; the machine is structurally incapable of observing the difference. (If a deadline matters, it is declared on the expectation ladder and judged as an event-time predicate like every other timeout - §3.6.)
  • The response is an input, not a computation. Like a fixing (§2), the tax answer enters the log as a signed fact from an executor-party (§3.6), under §5.5 trust anchors: store the response and its signature, never the URL. Replay reproduces the verdict bit-for-bit without re-calling the service - the two-day tax engine of 2027 need not exist in 2035 for the receipt chain to verify.
  • Trusted for judgment, never obeyed blindly. The contract still guards what comes back: if tax.response.rate not in declared-vat-rates -> reject; bounds, schema, cadence, all the usual gates. A compromised tax service can delay a contract; it cannot make one misprice.

Prior art: durable execution (Temporal, Azure Durable Functions, Restate - persist state, resume on external completion; our version is degenerate-simple because state IS the log), AWS Step Functions task tokens (§4.1 already borrowed them for human tasks - the same token-wait pattern for services), BPMN intermediate message events (the waiting node, standardized), async/await (the right intuition, minus the thread), and oracles (blockchain’s word for signed external facts - ours are accountable parties with keys and obligations, not consensus games).

7.6 External functions: the import manifest (the wasm model)

The wasm model is exact for this, so adopt it wholesale: a contract declares an import manifest - typed function slots whose signatures and bindings are contract terms while their implementations stay external. This is how every industry’s domain complexity (tax, freight rating, tariffs, payroll formulas, energy settlement) enters without ever polluting the starved core language:

imports:
  some_tax_function:
    mode:    inline                           # runs INSIDE the evaluation step: pure,
    has_side_effects: false                   #   replay-safe, no state saved (inline
    input:   shape order-lines                #   REQUIRES false - sandbox enforces it)
    output:  shape taxed-lines                # §8 shapes - checked like any port payload
    binding: art:se-vat-engine  pin v3  sha256:9e77...   # WHO computes is a contract term
    guards:  output.vat-rates within declared-vat-rates,  # the result is GUARDED,
             sum(input.lines) + sum(output.tax) == output.total   # never blindly trusted

  freight_rating:
    mode:    deferred                         # result arrives as a LATER EVENT: the
    has_side_effects: false                   #   submission parks (PENDING) and resumes
    input:   shape shipment                   #   on response - §7.5; §3.6 expectation
    output:  shape priced-shipment            #   ladder guards the wait. no side effects
    binding: oracle party:dhl port freight.response       #   = a re-askable QUOTE

  book_freight:
    mode:    deferred                         # changes the WORLD -> idempotency key is
    has_side_effects: true                    #   derived + attached AUTOMATICALLY:
    input:   shape shipment                   #   sha256(contract-id, submission-id,
    output:  shape booking-confirmation      #   import-name, input-hash) - retries and
    binding: oracle party:dhl port booking.response       #   crash re-emissions are safe
    compensate: booking.cancel within 24h     # the saga leg (§7.3), declared here

Two declarations, each naming exactly one property:

  • mode: inline | deferred - whether the machine must save state on the call. inline executes within the evaluation step: nothing parks, nothing resumes, the fold never notices the function left the building. deferred means the result arrives as a later event - §7.5’s call-out: verdict PENDING, resume = the fold reading the response. Deliberately NOT called sync/async: the contract cannot care about latency (§7.5) and must not care about transport (§7.4) - it cares only about statefulness. (Kotlin’s suspend functions are this same discrimination; we avoid the word because §4.2 uses “suspend” for escalation.)
  • has_side_effects: false | true - whether calling it changes the world outside the contract. This is where idempotency becomes a first-class property of imports, not just of in-ports: a side-effecting call gets an idempotency key derived deterministically from the log - sha256(contract-id, submission-id, import-name, input-hash) - attached automatically, so retries, crash re-emissions, and replays hit the external system with the SAME key and the world changes at most once (the §1 submission-id mechanism, pointed outward; Stripe’s pattern, closed into a loop). Side-effecting imports must also declare their compensation (the §7.3 saga leg) - “what undoes this” is a contract term, not an ops runbook. The grid completes itself: inline REQUIRES has_side_effects: false (the sandbox enforces what the declaration promises - a wasm blob has no ambient authority to break it); deferred + false is a re-askable quote; deferred + true is a world-changing act with a key and a declared undo.

Two binding modes behind the flags:

  • Pure import (the wasm case). The implementation is a content-addressed pure function artifact - literally a deterministic wasm blob (no clock, no I/O, no ambient authority; the sandbox enforces what §1 promises), stored and governed exactly like a §5 artifact: versioned, hash-pinned or tracked, its updates under declared cadence/approval. Because the function is identified by hash, the evaluator may execute it synchronously, locally, anywhere and determinism survives replay by construction: same hash -> same function -> same result, in 2035 as in 2027. Complex tax “by description” is exactly this: the contract knows the signature, the binding, and the guards - never the tax law.
  • Oracle import (the async case). The implementation is a party (Avalara-style tax service, a freight rater, Skatteverket one day) - which is just §7.5’s call-out and §3.6’s executor-as-party wearing a function signature: request out, PENDING, signed response in, guarded.

Strict linking - the machine REFUSES on any signature mismatch. Wasm’s instantiation rule, adopted verbatim: at genesis (and at every version change of contract OR binding), every import must resolve 100% exactly - the binding exists (artifact hash resolvable, or party+port declared), the input/output shapes match the declared signature precisely (no structural coercion, no best-effort mapping, no “close enough”), the guards are well-typed against those shapes. Any mismatch -> the contract cannot activate, with a link-error receipt naming the unresolved import. And it stays enforced at runtime: a track-bound function whose new version changes signature is an ILLEGAL update - rejected under §5.2’s rule, never becomes the bound version. A contract that cannot resolve its imports is not a contract that runs degraded; it is a contract that does not run.

The rest of the discipline: imports are declared like ports (absent import = the contract computes nothing externally - readable off the manifest), results are guarded like payloads (§8.2 - bounds, invariants, cross-sums; a compromised tax engine can stall a contract, never mis-tax it silently), and bindings are governed like artifacts (updating the tax function is a receipted governance event, not a deploy). Prior art is thick: wasm imports + the component-model WIT (typed interface declarations - our manifest verbatim), Ethereum precompiles (deterministic external functions at known addresses), deterministic-wasm chains (CosmWasm, Substrate chose wasm for exactly this replay property), SQL UDFs and XSLT extension functions (host languages importing typed externals for decades). The core stays if/which; the industries bring their math as sealed, pinned, guarded imports.