Skip to content

Software Architecture For Dynamic Contract Composability#48

Open
JosephDenman wants to merge 1 commit intomainfrom
JosephDenman/dynamic-contract-calls-proposal
Open

Software Architecture For Dynamic Contract Composability#48
JosephDenman wants to merge 1 commit intomainfrom
JosephDenman/dynamic-contract-calls-proposal

Conversation

@JosephDenman
Copy link
Copy Markdown
Contributor

@JosephDenman JosephDenman commented Jan 28, 2026

Adds CoIP 1 to replace #36 and facilitate discussion.

@JosephDenman JosephDenman requested review from a team as code owners January 28, 2026 21:50
@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 28, 2026

Plugin Test Results

 1 files   3 suites   1s ⏱️
21 tests 21 ✅ 0 💤 0 ❌
23 runs  23 ✅ 0 💤 0 ❌

Results for commit 2ebb622.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 28, 2026

Compactc E2E Tests Results

2 707 tests   - 12 293   2 707 ✅  - 12 292   5m 13s ⏱️ - 22m 24s
    1 suites ±     0       0 💤 ±     0 
    1 files   ±     0       0 ❌  -      1 

Results for commit 2ebb622. ± Comparison against base commit ade0471.

This pull request removes 15000 and adds 2707 tests. Note that renamed tests count towards both.
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_0.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_1.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_10.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_100.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_101.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_102.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_103.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_104.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_105.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_106.compact'
…
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.jrRh61/temp-test-CHYxG8/contract_0.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.jrRh61/temp-test-CHYxG8/contract_1.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.jrRh61/temp-test-CHYxG8/contract_10.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.jrRh61/temp-test-CHYxG8/contract_100.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.jrRh61/temp-test-CHYxG8/contract_1000.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.jrRh61/temp-test-CHYxG8/contract_1001.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.jrRh61/temp-test-CHYxG8/contract_1002.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.jrRh61/temp-test-CHYxG8/contract_1003.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.jrRh61/temp-test-CHYxG8/contract_1004.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.jrRh61/temp-test-CHYxG8/contract_1005.compact'
…

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown

E2E test results

build: Run #180

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
452 446 0 6 0 0 0 56.1s

🎉 All tests passed!

Github Test Reporter

@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 28, 2026

Flaky Rate

Overall Flakiness 0.00%    ±0

No current tests have flaky rates to display ✨

Github Test Reporter by CTRF 💚


Flaky Rate

Overall Flakiness 0.00%    ±0

No current tests have flaky rates to display ✨

Github Test Reporter by CTRF 💚

@kmillikin
Copy link
Copy Markdown
Contributor

Please remove CoIP-1 from this PR, it logically belongs to the CoIP-1 PR and not some other CoIP. If you are proposing changes to CoIP-1, please do it in the CoIP-1 PR, otherwise it's impossible to tell what's going on.

The TSC will approve CoIP-1 before we begin the CoIP process.

The proposed CoIP should be file name coip-xxxx.md because the CoIP draft author can't practically pick a number without clashes. They don't know what order the TSC will consider CoIPs and if there are two that even temporarily have the same number that creates confusion.

@kmillikin
Copy link
Copy Markdown
Contributor

A few high-level comments:

  • I think the language feature we're proposing has a simple name like "Contract Calls". "Dynamic" seems like a noise word and it probably confuses people (especially if these are the only kind of contract calls). I don't like "composability" because it's generic and can mean anything. I don't think "cross-contract" is necessary (a contract can actually call itself using this mechanism!).
  • I would restructure it to first describe the language feature, and only then describe the concrete realization in the Midnight Network. Eventually Compact might work on other chains (that's our aspiration!) and the Midnight specific stuff should not be front and center.
  • We probably want a notion of contract interfaces as Compact types. I can take a stab at writing that once the structure is in place.
  • Remove all description of concrete implementation. The TSC will not in general approve feature designs that mandate any specific implementation. We can have one in mind when we write it, but it doesn't belong as part of the feature design.

Signed-off-by: JosephDenman <joseph.denman@iohk.io>
@JosephDenman JosephDenman force-pushed the JosephDenman/dynamic-contract-calls-proposal branch from 33c62b2 to 2ebb622 Compare January 29, 2026 15:19
@github-actions
Copy link
Copy Markdown

E2E test results

build: Run #182

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
452 446 0 6 0 0 0 56.8s

🎉 All tests passed!

Github Test Reporter

@dybvig
Copy link
Copy Markdown
Contributor

dybvig commented Feb 2, 2026

"Cross-contract call" might more clearly distinguish these kinds of calls from ordinary calls within a contract, especially in distinguishing a call to another instance of the same contract from a call within the same contract.

Don't we already have contract interfaces? Isn't that what the existing contract types are?

The ZKIR format changes from v3.0 to v3.1 (minor version bump):

- `IrSource` adds `arg_alignment` and `return_val_alignment` fields (already present in current code, now required)
- `Impact` instruction changes from `inputs: Vec<Fr>` to `ops: Vec<SymbolicOp>` plus `field_count: u64`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this change require a major version bump?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding specific fields to specific structs is out of scope for a CoIP feature design proposal. If we want to we can provide a reference implementation as a separate artifact that goes along with the proposal.

As a practical matter, a proposal accepted by the Minokawa TSC can't actually mandate a specific implementation in another component anyway.

So this should be removed from the proposal, I think.

- `Impact` instruction changes from `inputs: Vec<Fr>` to `ops: Vec<SymbolicOp>` plus `field_count: u64`
- New `ContractCall` instruction added

Existing v3.0 ZKIR files will fail to parse with the v3.1 interpreter. This is acceptable because ZKIR is not stored on-chain today—it lives in the compiled JavaScript bundle. When users upgrade the compiler, they get new ZKIR. No migration needed.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a concern, because it renders contracts implemented with the ZKIR v3.0 entirely incompatible with the new model with no path for enablement. Providing a backwards compatibility in the ZKIR would still enable adding missing information in the future by means of contract maintenance actions.


* A malicious contract could implement an interface correctly but behave unexpectedly. Callers should only interact with trusted or audited contracts. Interfaces provide type safety, not behavioral safety.
* Reentrancy shouldn't be any more of a concern than it was when we planned on implementing only static contract calls.
* How are costs attributed across caller and callee? Who pays?
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pays the user or a some other fee-paying entity. Fee payment almost always happens after all contract calls are wrapped in a transaction.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The good news is that transaction fees are really outside the language's control so we don't need to have a solution to this, just raise the question for the people who can solve it.

* Reentrancy shouldn't be any more of a concern than it was when we planned on implementing only static contract calls.
* How are costs attributed across caller and callee? Who pays?
* What happens when the ZKIR format changes? How do old contracts interoperate with new ones?
* How do we handle maintenance updates for contracts? These can modify circuits and therefore ZKIR.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They need to enable changing whole triplet of verifier key, prover key hash and ZKIR at once


The ZKIR interpreter currently has two modes: `preprocess` (verify witness consistency) and `circuit` (generate ZK constraints). These will be updated to handle `ContractCall` and symbolic Impact, but their core logic stays the same.

The main addition is a new **interpret mode** (rehearsal). This mode actually executes the circuit to compute witnesses, `Effects`, and proof pre-images:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it makes perfect sense from a purely functional perspective - do we know if the interpretation mode would be fast enough to not become an obstacle from UX/performance standpoint?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't know, but I don't believe it will. It's a separate linear pass over the ZKIR representation, in addition to the one needed to make the proof (we might actually be able to combine the two passes), and I suspect that the proving time dominates the interpretation time (barring time spent executing witnesses, which might not even terminate :) but we have to do that anyway).

@kmillikin
Copy link
Copy Markdown
Contributor

@dybvig:

"Cross-contract call" might more clearly distinguish these kinds of calls from ordinary calls within a contract, especially in distinguishing a call to another instance of the same contract from a call within the same contract.

We could use that term, as well.

Don't we already have contract interfaces? Isn't that what the existing contract types are?

Yes, I think so. They're probably missing features and have some open questions around subtyping (structural or nominal) and explicit vs. implicit "implementation" of a contract type by a contract. I'll try to write this up using contract types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants