Skip to content
Open
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
24 changes: 23 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,25 @@

## Unreleased

### Features

- **Batch execution.** A poll now settles up to `MAX_BATCH_SIZE` requests of one application in a single enclave round-trip and a single transaction. New on-chain entry point `ProcessorEndpoint.batchStateUpdate(uint64 applicationId, Structs.BatchEntry[] entries, bytes batchSignature)` applies each entry exactly as `stateUpdate` applies its single one — same validation, events, refunds, withdrawals, trigger invocation and dequeuing — in queue order, all-or-nothing, chaining the state root through the entries and writing storage once. One signature covers the whole batch: `ITeeAuthenticator.checkBatchSignature(bytes32[] entryHashes, bytes signature)` verifies the EIP-191 digest of the concatenated per-entry hashes built by the new `UpdateEntryHash` library, and for a one-entry batch that digest is byte-identical to the one `checkSignature` accepts. A new `BatchProcessed(uint64 applicationId, bytes32[] entryHashes)` event carries the hashes the signature covered — the per-entry `StateRootUpdate`, `UserEvent`, `AppEvent` and `RequestCompleted` events are emitted unchanged. Off-chain: new `BatchProcessRequest`/`BatchProcessResponse` messages, `MAX_BATCH_SIZE` (new, default 5) on the manager, and one decrypt/re-encrypt of the application state per batch instead of per request. Inside a batch a soft failure appends an error payload and continues from the unchanged state, while a hard failure stops the batch and leaves the remaining requests pending for the next poll; the first request is always started, so a poll can never settle nothing. Batching is refused where it would skip a trusted on-chain callback: a TRUSTPROCESS or DEPLOYAPP queue head, and any application with a registered trigger contract, are limited to one entry (`BatchNotAllowed`), and an empty batch reverts `EmptyBatch`. See `docs/design/BATCH_EXECUTION.md`.
- **Upgradeable contracts.** `ProcessorEndpoint`, `TeeAuthenticator` and `AuthorityRegistry` are now deployed behind UUPS proxies (`ERC1967Proxy`), so an implementation can be replaced without moving the address or migrating state. Upgrade authority is the `ADMIN` role for `ProcessorEndpoint` and `owner()` for the other two. New scripts in `contracts/scripts/upgrade/` deploy a new implementation and repoint the proxy; `scripts/upgrade/processorEndpoint.ts` additionally refuses an upgrade whose `ProcessorEndpointExtension` bytecode does not match the local build (`ALLOW_STALE_EXTENSION=true` downgrades that to a warning), because reusing stale extension code does not revert. `NoAttestationTeeAuthenticator` (dev/test only) is deliberately not upgradeable. See `docs/design/UPGRADABLE_CONTRACTS_DESIGN.md` and `contracts/README.md`.
- **Per-application request queues with round-robin selection.** Requests are queued per application, plus two global queues for triggers (TRUSTPROCESS) and deploys (DEPLOYAPP); the contract — not the manager — picks what to serve next through the new `getPendingRequestsWithStateRoot(uint256 maxCount)` view, which returns the selected `applicationId`, its requests in queue order and its state root. A pending trigger is served first and alone, then a pending deploy alone, otherwise up to `maxCount` requests of the application whose turn it is. See `docs/design/BATCH_EXECUTION.md` section 4.

### Breaking changes

- **`ProcessorEndpoint.getFacilitatorNonce(address)` removed.** It duplicated the auto-generated getter of the `public facilitatorNonces` mapping; clients must read `facilitatorNonces(user)` instead. See `docs/design/FACILITATOR.md`.
- **`ProcessorEndpoint` constructor takes a new last argument**, the address of a deployed `ProcessorEndpointExtension`. Deploy the extension first.
- **`ProcessorEndpoint`'s constructor now takes only the address of a deployed `ProcessorEndpointExtension`**; every other setup argument moved to `initialize` (see the proxy entry below). Deploy the extension first.
- **A malformed facilitator signature now reverts `InvalidSignature`.** `submitRequestFor` recovers with `ECDSA.tryRecover`, so `ECDSAInvalidSignature`, `ECDSAInvalidSignatureLength` and `ECDSAInvalidSignatureS` are gone from the `ProcessorEndpoint` ABI. Clients that decode those three errors must handle `InvalidSignature` instead. Valid signatures behave exactly as before.
- **The contracts now target the `cancun` EVM version** (previously `paris`), for the ~720 bytes it saves on `ProcessorEndpoint`. Any chain they are deployed to must be at Cancun or later; Base, the deployment target, has been since March 2024.
- **`stateUpdate` now enforces the round-robin application turn.** A state update for a request taken from a per-application queue reverts `ApplicationNotSelected(uint64 selectedApplicationId)` unless the cursor scan reaches the submitted application before any queue head older than `selectionGrace` (default 60s, admin-settable via the new `updateSelectionGrace(uint256)`); younger heads are skipped, since they may have arrived after the manager read the selection view. Previously cross-application selection was advisory, so a manager could starve an application indefinitely while serving others; FIFO *within* an application was, and remains, enforced. The global queues' precedence is enforced under the same grace rule: a trigger-queue (`TRUSTPROCESS`) head older than `selectionGrace` makes deploy and per-application updates revert `PriorityQueueNotServed(TRUSTPROCESS)`, and an aged deploy-queue (`DEPLOYAPP`) head makes per-application updates revert `PriorityQueueNotServed(DEPLOYAPP)`; TRUSTPROCESS updates are never constrained, and neither global queue takes part in the cursor rotation. A manager that submits out of turn sees the transaction revert and retries on the next poll; no manager change is required, as both submit paths already roll local state back on any revert.
- **The upgradeable contracts are deployed and reached differently.** `ProcessorEndpoint`, `TeeAuthenticator` and `AuthorityRegistry` no longer take their setup arguments in the constructor: each has an `initialize(...)` called once through the proxy, and their constructors only disable initializers (`ProcessorEndpoint`'s also fixes the extension address). Every off-chain consumer must use the **proxy** address a deploy script prints, not the implementation's. `TeeAuthenticator.nitroProver` and `maxVerificationAge` were `immutable` and are now regular storage set in `initialize`. Each upgradeable contract reserves a trailing `uint256[50] __gap`, which must shrink as state is added. The OpenZeppelin upgrades manifest under `contracts/.openzeppelin/` is now a required repo artifact — an upgrade aborts without the manifest for the target network, and it is not recoverable from chain. `@openzeppelin/contracts` is pinned to 5.6.1 and `@openzeppelin/contracts-upgradeable` 5.6.1 is a new dependency.
- **`ProcessorEndpoint.getNextPendingRequest()` removed.** Selection is now the contract's decision rather than the caller's; use `getPendingRequestsWithStateRoot(uint256 maxCount)`, which returns the selected `applicationId`, that application's requests and its state root. The Go `blockchain.Client` interface follows: `GetNextPendingRequest` is replaced by `GetPendingRequestsWithStateRoot(ctx, maxCount)`, and `SubmitBatchStateUpdate(ctx, updates, batchSignature)` is added.
- **`getPendingRequests()` and `getPendingRequestsPage()` no longer report a single global FIFO.** With per-application queues they return the deploy queue followed by each application's queue — FIFO *within* a queue, but submission order across applications is not preserved, and trigger-queue requests are reported separately by `getTriggerRequests()`/`getTriggerQueueSize()`. `isCurrentPendingRequest(bytes32)` now means "head of the queue that holds it". New `requestById(bytes32)` returns the stored request from whichever queue holds it (request ids are unique across all queues), and returns a zeroed struct for an unknown id.
- **`Structs.ErrorCode.APP_NOT_DEPLOYED` removed, shifting every later enum value.** The enum ordinal is the value signed and stored on-chain, so anything that decodes a persisted or indexed `errorCode` against the old enum will now misread it. The Go counterparts `apperrors.CodeAppStateNotFound` and `apperrors.CategoryAppNotDeployedMeta` are gone with it; the condition it described is enforced on-chain before a request reaches the executor.
- **Guests may only declare an allowlisted set of host imports.** A module whose import section names anything outside the eight WASI functions TinyGo's `wasip1` target emits for the apps in this repo is refused at load, before instantiation, with the signed `CodeFailedLoadingOrGettingModule`. Notably absent is `poll_oneoff`, which TinyGo lowers `time.Sleep` to: a host call runs outside the execution bound (epoch interruption only fires in guest code), so a guest blocking inside one would outlast the guest bound, the caller's budget and shutdown alike while holding the global execution lock. Guests that reach a blocking import must be rebuilt without it. See `docs/design/WASM_HOST_ABI.md`.
- **The WASM feature set is pinned, and wasmtime is upgraded from v1.0.0 to v47.** Bulk memory, multi-value, reference types (funcref only) and fixed-width SIMD are on; `memory64`, `threads`, `multi-memory`, GC, function references, tail calls, exceptions, wide arithmetic, relaxed SIMD and the component model are off, so a module using any of them is rejected at load rather than behaving differently inside the enclave. Cranelift NaN canonicalization is on, since NaN payloads would otherwise be a source of non-determinism in a signed state transition. `github.com/bytecodealliance/wasmtime-go v1.0.0` is replaced by `github.com/bytecodealliance/wasmtime-go/v47 v47.0.0`.

### Changes

Expand All @@ -19,6 +31,16 @@
- **Requests carry a caller execution budget.** The manager now tells the executor how long it will still wait (`executionBudgetMs` on process, deploy and batch messages, derived from `MANAGER_COMMUNICATION_PARAMS_REQUEST_TIMEOUT_SEC`), and the executor shortens guest execution to fit. The field is optional and an absent value means "not supplied", so mixed-version peers interoperate. It can only ever *shorten* execution: the enclave keeps its own configured bound as the ceiling, and a budget-driven interruption is treated as host-side abandonment — the request stays pending and is retried rather than being signed — so no value crossing the TEE boundary can cause a user to be charged a fee.
- **The executor refuses a manager whose request timeout cannot cover the guest bound.** The manager now reports `MANAGER_COMMUNICATION_PARAMS_REQUEST_TIMEOUT_SEC` in the connection handshake (`requestTimeoutMs` on `GetKeysetRecoveryResponse`), and the executor rejects the connection — before restoring or generating any keyset — unless `EXECUTOR_GUEST_EXECUTION_TIMEOUT_MS` plus a 1 s safety margin is strictly below that timeout minus the 2 s reply margin (the safety margin covers epoch-interruption overshoot and the decode/decrypt work that happens before the deadline is armed). Both processes then fail at start-up with an error naming the two settings. Without this, a guest that outlives the manager's patience is interrupted and signed by the executor after the manager has already stopped listening, so the signed failure is never posted, no fee is charged, and the request is retried forever: one such request occupies the executor indefinitely for free. An earlier start-up check compared against the executor's own `EXECUTOR_COMMUNICATION_PARAMS_REQUEST_TIMEOUT_SEC` as a stand-in; that setting governs calls in the other direction and is no longer consulted for this. An older manager that does not report its timeout is served with a warning.
- **Executor shutdown no longer waits out a running guest.** `Close` interrupts in-flight guest calls instead of blocking, keeping a bounded lock acquisition only as a backstop for the paths epochs cannot interrupt (module compilation and time spent inside a WASI host call, both still unbounded).
- **New config `MAX_BATCH_SIZE`** (manager, default 5, must be > 0) caps how many requests one poll fetches and processes for the selected application. Wired through `dockerfiles/docker-compose.yml` and `.env.template`.
- **Per-guest memory is capped, and table storage is bounded separately.** `EXECUTOR_MAX_GUEST_MEMORY_BYTES` (new, `0` selects the default) limits the linear memory a single guest can grow to; the default is also the maximum, 2 GiB, which is a host-ABI constraint rather than a tuning choice — guest pointers cross the boundary as signed 32-bit offsets. Worst-case enclave RAM is roughly `EXECUTOR_MAX_CACHED_MODULES * EXECUTOR_MAX_GUEST_MEMORY_BYTES`, so size the two together. The store limiter also pins one instance and one memory per store (a guest declaring several memories would otherwise multiply the cap) and bounds WASM tables to 4 per store and 1,000,000 elements — table elements live outside linear memory, so an unbounded element count escaped the memory cap entirely: a 50M-entry funcref table committed ~390 MB resident under a 64 KiB memory cap. A guest exceeding either bound fails to instantiate with an error naming the limit.
- **A module is evicted from the cache when its guest faults.** A trap or an allocation the guest could not satisfy leaves its heap in an unknown state, so the module is dropped and recompiled for the next request instead of serving it from a corrupt instance; host-side failures (a missing export, a nil store) keep the module cached, since nothing about the guest is known to be wrong. Guest execution is also serialized against module teardown, so an LRU eviction can no longer close a store mid-call. `docs/design/WASM_HOST_ABI.md` summarises the three failure classes and their effect on the cache.
- **A duplicate deploy is rejected before the module is compiled and its start section runs**, rather than after.
- **New error codes for an interrupted guest.** `GUEST_EXECUTION_TIMEOUT` (category `WASM_INTERNAL`) marks a guest that outran the configured execution bound and is signed and posted on-chain — a wall-clock bound is non-deterministic, but the on-chain queue only advances when an update is submitted, so treating a runaway guest as transient would let one non-terminating app pin the queue head forever. `GUEST_EXECUTION_CANCELLED` marks a guest interrupted because the *host* gave up (context cancellation, shutdown, an expired caller budget) and is deliberately transient: `apperrors.IsTransient` routes it to a plain Go error so the request is retried, nothing is signed and no fee is charged. That is what keeps a host-supplied budget from ever producing a fee-charging failure.
- **`ADMIN` and `UPDATE_STATUS_ROLE` can now be rotated and revoked.** `ProcessorEndpoint` sets `ADMIN` as its own role admin and as the admin of `UPDATE_STATUS_ROLE` and `DEPLOYER_ROLE`; previously both defaulted to `DEFAULT_ADMIN_ROLE`, which is granted to nobody, so a compromised `ADMIN` key stayed valid forever and a lost one would have frozen every configuration setter and `_authorizeUpgrade` permanently — including the upgrade that would fix it. `TokenAllowlist` does the same for its own `ADMIN`. `RESET_OPERATOR` is deliberately left under `DEFAULT_ADMIN_ROLE`: that is what makes passing `address(0)` for it disable the reset functions permanently, as production requires.
- **TCP ports and vsock CIDs are range-checked at start-up.** `EXECUTOR_PORT`, `LOG_SERVER_PORT` and `MANAGER_ADMIN_PORT` are rejected above 65535, and `EXECUTOR_PORT`/`MANAGER_ADMIN_PORT` are rejected at `0` — a `0` port makes the listener bind an OS-assigned port while the peer dials the configured one, which previously failed later at connect time with nothing wrong at start-up (`0` still disables the TCP log listener, where that is the documented meaning). New helper `common.GetConfigVarUint32` parses these instead of narrowing an `int64` with a cast, which truncated an out-of-range value to `0` rather than falling back to the default. `common.GetConfigVarInt64` now parses the full 64-bit range so an out-of-range value reaches config validation instead of silently becoming the default.
- **`DEPLOY_ARTIFACTS_MAX_SIZE_MB` is bounded at start-up.** Above the new limit the megabytes-to-bytes conversion overflowed `int64`, and a negative byte limit disabled the upload body limit entirely instead of enlarging it.
- **Contract build and CI.** `solc` is pinned to 0.8.30 in CI (installed from the release binary, version asserted) because the committed bindings embed the compiler version and its metadata hash. `npm run check:layout` — new — asserts that `ProcessorEndpoint` and `ProcessorEndpointExtension` share one storage layout and that every error the extension can raise is declared on the endpoint, and it runs in CI. The Hardhat test network now enforces EIP-170 by default so the suite fails when a contract outgrows the limit, the way it would on Base; `npm run test:ignoresize` opts out for measuring a work-in-progress contract.
- **New and updated documentation**: `docs/design/WASM_HOST_ABI.md` (the host/guest contract: required exports, result encoding, allocation ownership, the 2 GiB offset ceiling, the pinned feature set and how failures are classified), `docs/design/PROCESSOR_ENDPOINT_SPLIT.md`, `docs/design/THROUGHPUT_BENCHMARKS.md`, `docs/design/DECOMPOSITION.md`, a rewritten `docs/design/BATCH_EXECUTION.md`, and upgrade/deploy instructions in `contracts/README.md`. A throughput benchmark suite lands in `tests/benchmark/`.

## 0.2.0

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.24.0

toolchain go1.24.3

require github.com/HorizenOfficial/vela-common-go v0.2.0
require github.com/HorizenOfficial/vela-common-go v0.3.0

require (
github.com/aws/aws-sdk-go-v2 v1.41.1
Expand Down
Loading