Skip to content

v0.4.0

Latest

Choose a tag to compare

@github-actions github-actions released this 28 May 23:39
v0.4.0
13ffbfd

Added

  • Bounty + contract time-gated lifecycle (chain_hash-bumping) (#541). Wires the SDK chain_state module into both bounty and contract so handlers read the current rollup / DA height, completing every time-based transition that was previously stubbed. Bounty: PostBounty rejects expiry-in-the-past; ClaimBounty rejects claims at/after expiry and records each claim's height; DisputeAttestation enforces the per-claim dispute_window_blocks window (a dispute now requires a settled claim); CancelBounty gains the expired-with-claims refund path (BountyStatus::Expired); new permissionless FinaliseBounty sweeps dust + marks Finalised, guarded on no-open-disputes + all-windows-closed. New state (claim_height, latest_claim_height, open_dispute_count) + BountyFinalised event. Contract: real delivered_at_block (was pinned 0); DeliverContract rejects deliveries past expiry; new permissionless FinalizeDelivery auto-accepts a delivered contract once the acceptance window elapses (the work-for-hire guarantee: the worker is paid even if the buyer never responds); CancelContract gains the expired refund path (ContractStatus::Expired). REST: GET /modules/bounty/bounties/{id}/disputes/{attestationId}, GET /modules/contract/contracts/{id}/delivery + /disputes. Bumps chain_hash b7e07380…60ec4d0b… (new state maps + two new call variants), so v0.4.0 is NOT state-compatible with ligate-devnet-2 and requires a ligate-devnet-3 re-genesis. Comprehensive lifecycle tests (bounty 19 + 10, contract 6 + 6) via TestRunner (#543).
  • contract module handler implementation. Replaces the seven no-op handlers from the skeleton (#537) with full state-transition logic. (1) PostContract validates pool / expiry / arbiter_fee_bps (capped at 5000 = 50%), derives the unique ContractId, escrows pool AVOW into the module address, writes the ContractState under ContractStatus::Open, emits ContractPosted. (2) CommitToContract allows multiple workers to commit in parallel; locks bond per (contract_id, worker), writes the CommitRecord, transitions Open → Committed on the first commit. (3) DeliverContract verifies active commit + cross-module attestation existence, writes the DeliveryRecord, transitions to Delivered. (4) AcceptDelivery (poster-gated) pays pool → worker + refunds bond + transitions to Accepted. (5) RejectDelivery (poster-gated) writes a DisputeRecord, transitions to Disputed. (6) ResolveContractDispute (arbiter-gated) pays arbiter their arbiter_fee_bps slice first, routes pool_after_cut + bond to the winner (AcceptDelivery → worker, RejectDelivery → poster), transitions to Accepted/Rejected. (7) CancelContract (poster-gated, Open-only at v0) refunds escrow and transitions to Cancelled. ~17 typed ContractError variants cover input validation, auth, state-transition guards, cross-module read failures. Handler-only change does NOT bump chain_hash (same as the bounty handlers PR #532). The 6 existing unit tests still pass; TestRunner-based integration tests covering each handler's happy + rejection paths ship in the follow-up PR.
  • contract module skeleton (chain_hash-bumping) at crates/modules/contract/. New work-for-hire deliverable contracts module composed into the runtime alongside bounty and attestation. Implements the design from the contract primitive RFC (chain#536) at docs/protocol/contract-primitive.md. Five state maps (contracts, commits, deliveries, disputes, escrow) plus the next_poster_nonce counter. Seven CallMessage variants: PostContract, CommitToContract, DeliverContract, AcceptDelivery, RejectDelivery, ResolveContractDispute, CancelContract. Eight Event variants matching the lifecycle. ContractId is an lct-prefixed Bech32m hash32 type following the family pattern (lsc/las/lph/lpk/lat/lbt); CommitKey is a composite (ContractId, worker_bytes) carrying a 28-byte address field so the struct stays Copy. Handlers are no-op stubs at v0; real state-transition logic lands in follow-up PRs against chain#536. Discriminant CONTRACTS_DISCRIMINANT = 23 in constants.toml. Prometheus counter ligate_contract_calls_total wired through metrics.rs. REST route GET /modules/contract/contracts/{contractId} via query.rs. Six unit tests in tests/integration.rs cover ContractId::derive determinism + non-collision under distinct nonces / criteria docs, plus CommitKey string round-trip + malformed-input rejection. The contract module exists side-by-side with bounty because the two primitives serve different economic needs: bounty = "open call for fungible evidence under a schema", contract = "specific deliverable from one party, named arbiter, escrow + commit-reveal". Same chain_hash bump rationale as the bounty skeleton (#531): adding a module shifts the borsh schema fingerprint; doing it in isolation keeps subsequent handler PRs zero-impact on chain identity.
  • bounty module handler implementation (chain_hash-bumping). Five no-op stubs from the skeleton (#519) replaced with real state-transition logic: (1) PostBounty validates pool / per_attestation / expiry, verifies the board schema exists on the attestation module, derives the unique [BountyId] from (poster, board_schema_id, per-poster nonce), escrows pool AVOW from the poster into the bounty module's own address (self.id.to_payable() per the sov-sequencer-registry pattern), writes the BountyState under BountyStatus::Open, updates the open_by_schema reverse index, emits BountyPosted. (2) ClaimBounty pre-validates each claim against the bounty's AcceptancePredicate (Any/AttestorSet/PayloadHashes/PeerCount variants live; AttestorSet + PeerCount tighten in a v1 follow-up via indexer enrichment), checks total batch payout fits remaining escrow, then loops through paying out per_attestation AVOW per claim from escrow to each attestation's submitter, emitting one BountyClaimed event per success. Auto-transitions to BountyStatus::Exhausted when escrow drops below per_attestation. (3) DisputeAttestation locks a bond equal to per_attestation from the disputer into module escrow, writes the DisputeState, emits BountyDisputed. (4) ResolveDispute is gated to the board operator (the attestation::Schema::owner of the bounty's board_schema_id); bond flows to the winner per DisputeDecision (Accept → disputer, Reject → bounty poster), dispute record is removed. (5) CancelBounty auths the poster, allows cancellation only when no claims have been paid out (escrow == pool), refunds remaining escrow, sets BountyStatus::Cancelled, removes the bounty from the open index. ~15 typed BountyError variants cover input validation, auth, state-transition guards, and cross-module read failures. Cross-module composition added: #[module] bank: sov_bank::Bank<S> + #[module] attestation: AttestationModule<S>. New state field next_poster_nonce: StateMap<S::Address, u64> for deterministic-id derivation. AVOW token id read at tx time from the attestation module's authoritative state value, keeping BountyConfig empty and the bounty module's wallet schema clean. Cancel-with-pending-claims-but-not-yet-expired remains rejected at v0; the expired-with-claims case lands once handlers can read the current DA height. Test integration via sov-test-utils::TestRunner ships in the next PR (the v0 unit tests on BountyId::derive + DisputeKey round-trip stay).
  • bounty module skeleton (chain_hash-bumping) at crates/modules/bounty/. New module composed into the runtime alongside attestation, ships the v0 wire shape from the bounty marketplace RFC: four state maps (bounties, open_by_schema, escrow, disputes), five CallMessage variants (PostBounty, ClaimBounty, DisputeAttestation, ResolveDispute, CancelBounty), five Event variants, the four-variant AcceptancePredicate (Any, AttestorSet, PayloadHashes, PeerCount). BountyId is an lbt-prefixed Bech32m hash32 type following the lsc/las/lph/lpk/lat family from the attestation module; DisputeKey is a composite (BountyId, AttestationId) with manual FromStr/Display for the colon-joined REST path. Handlers are no-op stubs at v0; real state-transition logic lands in follow-up PRs against #519 (escrow transfer + sum-invariant on PostBounty, payout composition with fee-routing on ClaimBounty, bond lock + dispute write on DisputeAttestation, etc). Discriminant BOUNTY_DISCRIMINANT = 22 in constants.toml. Prometheus counters ligate_bounty_posted_total, ligate_bounty_claimed_total, ligate_bounty_avow_paid_out_total wired through metrics.rs. REST route GET /modules/bounty/bounties/{bountyId} via query.rs. Five unit tests in tests/integration.rs cover BountyId::derive determinism + non-collision under distinct nonces / boards, plus DisputeKey string round-trip + malformed-input rejection. Skeleton intentionally ships separate from handler logic because adding a module to the runtime composition bumps chain_hash (new module shifts the borsh schema fingerprint); doing that change in isolation keeps subsequent handler PRs zero-impact on chain identity. Closes #519.
  • RFC: bounty marketplace design at docs/protocol/bounty-marketplace.md. Specs the on-chain bounty primitive that closes the gap between "schema owner posts demand for evidence" and "Mneme user clicks Attest to earn." Bounty as schema (per chain#383 item 9), lifecycle (post / claim / dispute / finalize), escrow model (module-internal StateMap with bank-invariant audit), off-chain matching (indexer-side, the same boundary rest-api.md already draws), hybrid dispute mechanism (stake-bond v0, curator escalation v1, full slashing v2). Resolves all four open decisions on #385 with recommendations. Identifies 5 sub-issues to file after the RFC merges (module skeleton, anchor-buyer LOI tracking, Mneme UI, indexer matching service, board-operator dashboard). Not implementation; this is the design that the implementation work composes against. Code lands in follow-up PRs per the sub-issues.

Changed

  • Bump peter-evans/create-pull-request@v6@v8 in .github/workflows/bump-versions.yml. v6 runs on Node.js 20, which GitHub forces to Node.js 24 on 2026-06-02 and removes from runners entirely on 2026-09-16. v8 is the latest line and ships Node 24 compatibility. The auto-PR semantics are unchanged across the bump (same inputs, same Authorization-header collision fix from #501). No other Actions in chain's workflow tree show the Node-20 deprecation warning today.

Added

  • REST error envelope: audit, document, regression-pin (closes #201). Three pieces. (1) Audit finding: the canonical SDK error helpers (sov_modules_api::rest::utils::errors::not_found_404 / bad_request_400 / internal_server_error_500) produce a uniform {status, message, details} envelope across every /v1/ledger, /v1/sequencer, /v1/rollup, /v1/modules endpoint that uses them. The two intentionally-different shapes are /ready (k8s readiness probe convention; uses {status, synced_da_height, target_da_height}) and POST /v1/sequencer/txs 503-Syncing (uses the standard envelope with a details.Syncing discriminant). (2) Documentation: docs/protocol/rest-api.md's "Error envelope" section was describing a JSON:API-flavoured shape ({errors:[{title,details}]}) that hadn't matched the SDK reality for many releases. Replaced with the actual shape including the X-Request-Id correlation header convention. (3) Regression test: crates/rollup/tests/binary_spawn_smoke.rs::rest_error_envelope_pin spawns the binary, fires a known-404 (well-formed bech32m SchemaId::from([0xAA; 32]) rendered via the canonical lsc1 Display path, no schema registered at that id) and a known-400 (malformed bech32m), asserts status / message / details / per-helper details keys, asserts the X-Request-Id header is present + non-empty + ulid-unique per request. #[serial] against the existing binary_spawn_round_trips_register_and_submit so the two binary-spawn tests run sequentially (the parallel-spawn iteration of this test in the first attempt timed out both spawns at wait_for_ready on free-tier runners; same fix that restart_recovery.rs already used file-wide).
  • Auto-upgrade VMs on chain_hash-stable releases. Closes the manual-upgrade gap surfaced when v0.3.0 → v0.3.1 shipped with unchanged chain_hash but the live VM stayed on v0.3.0 until a human ran upgrade-chain.sh. Two pieces. (1) release.yml emits a chain-hash-unchanged.flag release asset when the matching CHANGELOG section asserts that chain_hash is unchanged from the prior release (regex matches the canonical prose we already write by hand). (2) ops/systemd/ligate-auto-upgrade.{service,timer} polls the GitHub Releases API hourly: if a new tag is published AND the flag is present AND the bump isn't major-version AND the operator hasn't set /etc/ligate/auto-upgrade.disabled as a kill-switch sentinel, the timer invokes upgrade-chain.sh <tag> to do the binary swap. Anything that fails any gate is a no-op log line in journald, nothing destructive happens implicitly. Cloud-init installs the timer + service + script automatically on fresh VMs; existing VMs pick it up via install -m 0755 scripts/auto-upgrade.sh /opt/ligate/scripts/auto-upgrade.sh + install -m 0644 ops/systemd/ligate-auto-upgrade.* /etc/systemd/system/ + systemctl enable --now ligate-auto-upgrade.timer. Closes #503.

Fixed

  • Cross-binary serialisation for the binary-spawn tests (#540). binary_spawn_smoke.rs + restart_recovery.rs compile to separate test binaries that cargo test runs in parallel; plain #[serial] only serialises within one binary, so two ligate-node spawns competed for CPU on 2-vCPU CI runners and intermittently missed the readiness budget. Switched to serial_test's file_serial(node_spawn) (fslock across binaries) and raised the ready timeout to 180s.