You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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_hashb7e07380… → 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 poolAVOW 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 poolAVOW 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_attestationAVOW 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.