Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions agent/INVARIANTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,14 +259,16 @@ design citation alone does not establish current runtime behavior.
- **E3 program allowlist:** production initialization registers one deployed E3 program and assigns
Interfold ownership to the configured protocol owner. Later registration and retirement are
owner-only. Retirement closes only new request admission; existing E3s keep their snapshotted
program. Every registered address must contain runtime code. `MockE3Program` is the stateless
bootstrap option. It has no administrative controls and applies no application rules. Its
deterministic test receipt is not production data availability, so requests remain paused until a
production program is registered and wired. The request-time BFV ciphertext verifier and
decryption verifier remain mandatory. Its mutable failure controls live only in
`MockE3ProgramHarness`. A protocol upgrade that makes the program interface incompatible must
retire every incompatible bootstrap program before requests resume. — `Interfold.sol`;
`MockE3Program.sol`; `flow-trace/03`
program. Every registered address must contain runtime code and must advertise both `IE3Program`
and `IE3ProgramDataAvailability` through ERC-165. Interfold calls `verifyDataAvailability` on
every output publication, so a program that omits the selector could otherwise brick its own
rounds after the requester paid. `MockE3Program` is the stateless bootstrap option. It has no
administrative controls and applies no application rules. Its deterministic test receipt is not
production data availability, so requests remain paused until a production program is registered
and wired. The request-time BFV ciphertext verifier and decryption verifier remain mandatory. Its
mutable failure controls live only in `MockE3ProgramHarness`. A protocol upgrade that makes the
program interface incompatible must retire every incompatible bootstrap program before requests
resume. — `Interfold.sol`; `MockE3Program.sol`; `flow-trace/03`
- **Data availability binds per program and per round:** Interfold holds no protocol-level
data-availability verifier; it delegates to `IE3ProgramDataAvailability(e3Program)`, and a program
holds its verifier as an immutable. The Avail adapter re-checks `bridge.vectorx() == vectorx` on
Expand Down
44 changes: 24 additions & 20 deletions agent/flow-trace/00_INDEX.md

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions agent/flow-trace/03_E3_REQUEST_AND_COMMITTEE.md
Original file line number Diff line number Diff line change
Expand Up @@ -723,3 +723,22 @@ The EVM reader has typed coverage for `CommitteeFormationFailed`, `CommitteeActi
`CommitteeViabilityUpdated` in addition to ticket submission, finalization, publication, and
expulsion. These facts are stored in the E3's chain aggregate and projected into the dashboard's
committee stage, including submitted/required thresholds and post-expulsion viability.

## Zenith 2026-09 additions (post-fix semantics)

### ZEN2-13 — operator tree capacity

`CiphernodeRegistryOwnable.MAX_CIPHERNODE_LEAVES` is `2**TREE_DEPTH - 1` (1,048,575 at depth 20),
not `2**TREE_DEPTH`. The pinned `@zk-kit/lazy-imt.sol` sets `maxIndex = (1 << depth) - 1` and
inserts only while `index < maxIndex`. A cap of `2**TREE_DEPTH` let the last append pass the
registry check in `addCiphernode` and then revert inside the dependency. The comparison operator and
the free-index reuse list are unchanged. `CIPHERNODE_TREE_WARNING_THRESHOLD` stays at 80 percent of
the corrected cap.

### ZEN2-03 — committee key publication and the input window

`Interfold.onCommitteePublished` now reverts with `InputWindowClosedBeforeKeyPublication` when
`block.timestamp > e3.inputWindow[1]`. A relayer with a valid DKG proof could otherwise publish
before `dkgDeadline` but after the input window, giving a round that reaches `KeyPublished` and
never accepts an input. That round failed as a requester-paid `ComputeTimeout` instead of a
committee-paid `DKGTimeout`. See `04_DKG_AND_COMPUTATION.md` for the full publication trace.
14 changes: 14 additions & 0 deletions agent/flow-trace/04_DKG_AND_COMPUTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,8 @@ phase.
│ │ │ │ onCommitteePublished(e3Id, pk) { │ │
│ │ │ │ require(stage==CommitteeFinalized) │ │
│ │ │ │ require(now <= dkgDeadline) │ │
│ │ │ │ require(block.timestamp <= │ │
│ │ │ │ inputWindow[1]) │ │
│ │ │ │ e3.committeePublicKey = pk │ │
│ │ │ │ stage = KeyPublished │ │
│ │ │ │ computeDeadline = max(now, │ │
Expand Down Expand Up @@ -932,6 +934,18 @@ not consume the compute provider's allotted window, and publication still waits
window closes. The request-time timeout snapshot prevents later governance changes from changing an
active E3's deadlines.

`onCommitteePublished` also refuses a key that arrives after `inputWindow[1]`, with
`InputWindowClosedBeforeKeyPublication` (ZEN2-03). Such a round reaches `KeyPublished` but can never
receive an input, so it fails as a requester-paid `ComputeTimeout` instead of a committee-paid
`DKGTimeout`. The DKG deadline alone does not stop this, because `dkgDeadline` can fall after
`inputWindow[1]`. The refusal keeps failure attribution on the committee.

`publishCiphertextOutput` calls `IE3ProgramDataAvailability.verifyDataAvailability` on the
request-time program without a fallback. A program that omits that selector cannot publish an
output. `Interfold.registerE3Program` therefore probes the candidate program with ERC-165 for
`IE3Program` and `IE3ProgramDataAvailability` and reverts with `E3ProgramInterfaceMissing`
(ZEN2-01). The probe is bounded to 30000 gas and treats a failed, short, or false answer as missing.

---

## Phase 4: Decryption Share Generation (Each Committee Member, with C6 Proof)
Expand Down
29 changes: 27 additions & 2 deletions examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { IHonkVerifier } from "./interfaces/IHonkVerifier.sol";
import { IVotesToken } from "./interfaces/IVotesToken.sol";
import { IERC6372Clock } from "./interfaces/IERC6372Clock.sol";
import { IDataAvailabilityVerifier } from "@interfold/contracts/contracts/interfaces/IDataAvailabilityVerifier.sol";
import { IDataAvailabilityVerifier, IE3ProgramDataAvailability } from "@interfold/contracts/contracts/interfaces/IDataAvailabilityVerifier.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

interface IInterfoldProgramRegistry {
function e3Programs(IE3Program e3Program) external view returns (bool);
Expand All @@ -29,7 +30,7 @@ interface IInterfoldRegistryView {
function ciphernodeRegistry() external view returns (ICiphernodeRegistry);
}

contract CRISPProgram is IE3Program, Ownable, EIP712 {
contract CRISPProgram is IE3Program, IE3ProgramDataAvailability, IERC165, Ownable, EIP712 {
using InternalLazyIMT for LazyIMTData;

/// @notice Enum to represent credit modes
Expand Down Expand Up @@ -247,6 +248,7 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
/// round cannot conclude while its input stays pending.
error ComputeWindowTooShort(uint256 e3Id, uint256 computeWindow, uint256 required);
error KeyNotPublished(uint256 e3Id);
error E3NotAssignedToProgram(uint256 e3Id);
error E3NotAcceptingInputs(uint256 e3Id);
error InvalidComputeContext();
error InvalidDataAvailabilityVerifier();
Expand Down Expand Up @@ -452,6 +454,10 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
) external returns (bytes32) {
if (msg.sender != address(interfold) && msg.sender != owner()) revert CallerNotAuthorized();
if (e3Data[e3Id].paramsHash != bytes32(0)) revert E3AlreadyInitialized();
// Interfold stores the provisional E3 and its selected program before it calls `validate`.
// Read that record and refuse an E3 that Interfold assigned to a different program. Without
// this check the owner can create parallel CRISP round state for another program's E3.
_requireAssignedE3(e3Id);

// Delegated to its own frame rather than scoped inline: `validate` is close enough to the
// stack limit that holding the six decoded values alongside the parameters exceeds it.
Expand All @@ -466,6 +472,23 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
return ENCRYPTION_SCHEME_ID;
}

/// @inheritdoc IERC165
/// @dev Interfold probes these interfaces before it registers a program.
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
return
interfaceId == type(IE3Program).interfaceId ||
interfaceId == type(IE3ProgramDataAvailability).interfaceId ||
interfaceId == type(IERC165).interfaceId;
}

/// @notice Refuse an E3 that Interfold did not assign to this program.
/// @dev Interfold records the provisional E3 and its selected program before it calls
/// `validate`, so the assignment is readable at initialization time.
/// @param e3Id The E3 to check.
function _requireAssignedE3(uint256 e3Id) internal view {
if (address(interfold.getE3(e3Id).e3Program) != address(this)) revert E3NotAssignedToProgram(e3Id);
}

/// @notice Refuse a round that can close before a worst-case committee leaves one hour to vote,
/// or that leaves no budget for a late availability receipt.
/// @dev Interfold stores the E3 and its timeout snapshot before calling {validate}. Read those
Expand Down Expand Up @@ -714,6 +737,8 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {

function _keyPublishedE3(uint256 e3Id) internal view returns (E3 memory e3) {
e3 = interfold.getE3(e3Id);
// Defense in depth. `validate` already refuses an E3 that belongs to a different program.
if (address(e3.e3Program) != address(this)) revert E3NotAssignedToProgram(e3Id);
if (interfold.getE3Stage(e3Id) != IInterfold.E3Stage.KeyPublished) {
revert KeyNotPublished(e3Id);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,31 @@ contract MockInterfold {
mapping(uint256 => E3) public e3s;
mapping(IE3Program => bool) public e3Programs;

/// @notice The program that `getE3` reports as the assignee of every E3.
/// @dev Interfold assigns one program per E3. CRISP refuses an E3 that another program owns,
/// so this mock must report an assignee. Registration sets it, and `setE3Program` overrides it
/// for tests of the refusal path.
IE3Program public assignedE3Program;

/// @notice Per-E3 assignee, which takes precedence over {assignedE3Program}.
/// @dev A single global assignee cannot distinguish a program that reads the requested E3 from
/// one that reads another record, so a binding test would pass either way. Set this to bind one
/// E3 ID and leave the others reporting the global default.
mapping(uint256 => IE3Program) public e3ProgramOf;

function registerE3Program(IE3Program program) external {
e3Programs[program] = true;
assignedE3Program = program;
}

/// @notice Set the program that `getE3` reports as the assignee.
function setE3Program(IE3Program program) external {
assignedE3Program = program;
}

/// @notice Set the assignee of one E3, so a test can provision the exact ID it exercises.
function setE3ProgramFor(uint256 e3Id, IE3Program program) external {
e3ProgramOf[e3Id] = program;
}

function request(address program) external {
Expand All @@ -55,7 +78,7 @@ contract MockInterfold {
requestBlock: mockRequestBlock,
inputWindow: [uint256(0), uint256(0)],
encryptionSchemeId: ENCRYPTION_SCHEME_ID,
e3Program: IE3Program(address(0)),
e3Program: assignedE3Program,
paramSet: 0, // Insecure512
customParams: params,
decryptionVerifier: IDecryptionVerifier(address(0)),
Expand All @@ -81,7 +104,7 @@ contract MockInterfold {
requestBlock: mockRequestBlock,
inputWindow: [uint256(0), uint256(0)],
encryptionSchemeId: ENCRYPTION_SCHEME_ID,
e3Program: IE3Program(address(0)),
e3Program: assignedE3Program,
paramSet: 0, // Insecure512
customParams: abi.encode(address(0), nextE3Id, numOptions, 0, 0, 0, 0),
decryptionVerifier: IDecryptionVerifier(address(0)),
Expand Down Expand Up @@ -145,16 +168,20 @@ contract MockInterfold {
return mockSortitionSubmissionWindow;
}

function getE3(uint256) external view returns (E3 memory) {
function getE3(uint256 e3Id) external view returns (E3 memory) {
uint256[2] memory inputWindow = mockInputWindow[1] == 0 ? [uint256(0), block.timestamp + 100] : mockInputWindow;
// Report the per-E3 assignee when a test provisioned one. A caller that reads a different
// E3 record than the one it was asked about then fails, which a single global assignee
// could not detect.
IE3Program assignee = address(e3ProgramOf[e3Id]) == address(0) ? assignedE3Program : e3ProgramOf[e3Id];
return
E3({
seed: 0,
committeeSize: IInterfold.CommitteeSize.Minimum,
requestBlock: mockRequestBlock,
inputWindow: inputWindow,
encryptionSchemeId: ENCRYPTION_SCHEME_ID,
e3Program: IE3Program(address(0)),
e3Program: assignee,
paramSet: 0, // Insecure512
customParams: abi.encode(address(0), 0, 2, 0, 0, 0, 0),
decryptionVerifier: IDecryptionVerifier(address(0)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,36 @@ describe('CRISP Interfold binding', function () {

await expect(program.bindInterfold(interfoldAddress)).to.be.revertedWithCustomError(program, 'InterfoldAlreadyBound')
})

it('refuses to initialize round state for an E3 that Interfold assigned elsewhere', async function () {
// ZEN2-12. `validate` accepts the owner as a caller. Without an assignment check the owner
// could create parallel CRISP round state — input tree and params hash — for an E3 that
// Interfold gave to a different program.
const [, otherProgram] = await ethers.getSigners()
const mockInterfold = await deployMockInterfold()
const program = await deployCRISPProgram({ mockInterfold })
const params = ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'uint256', 'uint256', 'uint256', 'uint256', 'uint256', 'uint256'],
[ethers.ZeroAddress, 0n, 2, 0, 1, 0, 0],
)

// Interfold reports E3 1 as assigned to a different program. Bind the exact ID under test,
// so an implementation that reads another E3 record cannot pass.
await (await mockInterfold.setE3ProgramFor(1, otherProgram.address)).wait()
await expect(program.validate(1, 0, '0x', '0x', params))
.to.be.revertedWithCustomError(program, 'E3NotAssignedToProgram')
.withArgs(1)

// A different E3 is assigned to this program. E3 1 is still not, so reading the wrong
// record would wrongly succeed here.
await (await mockInterfold.setE3ProgramFor(2, await program.getAddress())).wait()
await expect(program.validate(1, 0, '0x', '0x', params))
.to.be.revertedWithCustomError(program, 'E3NotAssignedToProgram')
.withArgs(1)

// The same E3, once Interfold assigns it to this program, initializes normally.
await (await mockInterfold.setE3ProgramFor(1, await program.getAddress())).wait()
await (await program.validate(1, 0, '0x', '0x', params)).wait()
expect((await program.getRoundData(1)).numOptions).to.equal(2)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2427,5 +2427,5 @@
"deployedLinkReferences": {},
"immutableReferences": {},
"inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol",
"buildInfoId": "solc-0_8_28-b95d25dbcdea5354ca4c3fb8a90c5c3f029dfa00"
"buildInfoId": "solc-0_8_28-4b8d0dc29ace33e0d668b55bed6902aeeeebdfd3"
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,6 @@
"name": "CiphernodeNotEnabled",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "e3Id",
"type": "uint256"
},
{
"internalType": "uint64",
"name": "submissionDeadline",
"type": "uint64"
}
],
"name": "CommitteeAccusationWindowOpen",
"type": "error"
},
{
"inputs": [],
"name": "CommitteeAlreadyFinalized",
Expand Down Expand Up @@ -1978,5 +1962,5 @@
"deployedLinkReferences": {},
"immutableReferences": {},
"inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol",
"buildInfoId": "solc-0_8_28-3b542ec020382976a51d065eb59e269a2158aba9"
"buildInfoId": "solc-0_8_28-4b8d0dc29ace33e0d668b55bed6902aeeeebdfd3"
}
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,17 @@
"name": "E3NotFailed",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "e3Program",
"type": "address"
}
],
"name": "E3ProgramInterfaceMissing",
"type": "error"
},
{
"inputs": [
{
Expand Down Expand Up @@ -368,6 +379,22 @@
"name": "InputDeadlineNotReached",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "e3Id",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "inputDeadline",
"type": "uint256"
}
],
"name": "InputWindowClosedBeforeKeyPublication",
"type": "error"
},
{
"inputs": [
{
Expand Down Expand Up @@ -881,31 +908,6 @@
"name": "E3FailureProcessed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "e3Id",
"type": "uint256"
},
{
"indexed": false,
"internalType": "enum IInterfold.FailureReason",
"name": "previousReason",
"type": "uint8"
},
{
"indexed": false,
"internalType": "enum IInterfold.FailureReason",
"name": "reason",
"type": "uint8"
}
],
"name": "E3FailureReclassified",
"type": "event"
},
{
"anonymous": false,
"inputs": [
Expand Down Expand Up @@ -2937,5 +2939,5 @@
"deployedLinkReferences": {},
"immutableReferences": {},
"inputSourceName": "project/contracts/interfaces/IInterfold.sol",
"buildInfoId": "solc-0_8_28-4df03d23932e81a78580ce87af0ebe6fff484214"
"buildInfoId": "solc-0_8_28-4b8d0dc29ace33e0d668b55bed6902aeeeebdfd3"
}
Loading
Loading