Repository navigation
feat(resolver): fix #29 #32 #31 #30 versioned commitments, expiry, weighted attestations and disputes - #157
Closed
AderonkeAfolabi wants to merge 1 commit into
Closed
AderonkeAfolabi wants to merge 1 commit into
AderonkeAfolabi wants to merge 1 commit into
Conversation
…ighted attestations and disputes - #29: salted, domain-separated ResolutionSpecV2 with register_spec_v2, golden vectors, spec-hash --v2/gen-salt/verify, seed-market reveals via V2, ADR 0004 - #32: permissionless expire_market after close_time + 30 days, ResolutionState::Expired, MarketExpired event - #31: WeightedKeys spec with off-chain Ed25519 submit_signatures, rounds, budget-capped batches, agents/core/attestation.ts - #30: SignersDisputable spec with bonded dispute, guardian/committee escalation, timeout and pull-based bond claims Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@AderonkeAfolabi Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
👋 Thanks for the contribution! This repository only accepts contributor pull requests into the Please re-open your PR with base: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #29
Closes #32
Closes #31
Closes #30
This PR hardens and extends the resolver contract in four connected ways:
transaction.
All four live in
contracts/resolver. They share one reveal pipeline and onecommittee abstraction, so each new resolution mode works with the V2
commitment envelope and the expiry fallback without extra code.
#29 — Versioned, salted, domain-separated commitments
Problem.
resolution_hash = sha256(XDR(ResolutionSpec))had three gaps:silently strand every unrevealed market.
committee.
commitments could be replayed.
Fix and new flow.
Golden vectors pin six legacy hashes: both
OracleAssetvariants, allfour
PriceDirections, and single and multi-signer specs. They are assertedin the resolver and again in
spec-hash, plus a test proving structs encodeby sorted field name. The vectors were also re-derived from hand-built XDR
outside Soroban.
ResolutionSpecV2 { version, network_id, market_core, salt, rule }. Thecommitment is
sha256("oracledesk:resolution-spec" || XDR(envelope)).register_spec_v2rejects:InvalidConfig);DomainMismatch = 21).It then runs the same reveal pipeline as the legacy entrypoints.
Legacy entrypoints are unchanged in behaviour and error precedence. The
existing tests pass unmodified.
Disjoint hash spaces. Every legacy preimage starts with the
ScVal::Vectag
00 00 00 10, and every V2 preimage starts with ASCIIorac, so thetwo cannot collide short of a SHA-256 collision. A property test checks this.
scripts/spec-hashis split into a testedlib.rsand a thinmain.rs.It adds
--v2 --salt --network-passphrase --market-core,gen-salt(OSCSPRNG) and
verify(exit 1 on mismatch). It hashes through the resolver'sown
legacy_spec_hash/spec_v2_hash, so it cannot drift from thecontract.
scripts/seed-market.shgenerates a salt and writes it todeployments/specs/<resolution_hash>.json(gitignored). It re-derives thecommitment from the stored file, and only then creates the market and
reveals with
register_spec_v2. If the salt cannot be persisted, no marketis created.
New ADR 0004 documents the threat model, salt custody, what is
deliberately not bound (
market_id, the resolver address) and themigration policy. ADR 0001 links to it.
Why it matters. Encoding drift is now caught by CI. The rule stays hidden
until reveal. A commitment cannot be replayed on another network or
deployment. Existing testnet commitments keep resolving.
#32 — Resolution deadline and permissionless expiry
Problem. A market whose spec is never revealed, whose feed is permanently
stale, or whose signers go silent stayed
Openforever. Collateral was lockeduntil the single guardian key chose to act.
Fix and new flow.
RESOLUTION_GRACE_SECS(30 days) is a compile-time constant, so noadmin can shorten it.
resolution_deadline(market_id)is a view. It derives the deadline frommarket-core's
close_timealone, so it applies to unrevealed andpre-existing markets with no migration.
expire_market(market_id)needs no auth and is liveness-first. After thedeadline:
ResolutionAvailable = 20so
finalize_signerscan settle it;MarketExpiredis emitted with
ExpiryMode::{Unrevealed, Price, Signers}.Before the deadline the call returns
DeadlineNotReached = 19.Broken feeds. A feed contract that traps, or an address with no
contract, is treated as "price unavailable", so a broken oracle address
cannot block expiry forever.
ResolutionState::Expiredis terminal. All former== Finalizedchecksuse
is_terminal(), and registration now refuses terminal markets.Refactor.
resolve_pricenow uses a sharedtry_price_outcomeandsettle_price.Docs. The griefing analysis is in ADR 0001 ("Liveness fallback"), and
docs/architecture.mdanddocs/STATUS.mdare updated.Why it matters. Holders can always reach a settlement (resolution, or the
void payout), with no dependency on one admin key, and expiry can never void
a market that can still resolve correctly.
#31 — Aggregated off-chain Ed25519 attestations with weighted thresholds
Problem. Attested resolution needed every signer to send their own funded
transaction, and every signer had equal weight.
Fix and new flow.
ResolutionSpec::WeightedKeys(WeightedKeyConfig)with rawEd25519 keys and weights, revealed with
register_weighted_key_spec.Validation requires 1–32 unique keys, positive weights, an overflow-checked
total and
0 < threshold ≤ total.network id, resolver address, market id, round, outcome and the market's
resolution_hash; the format is in ADR 0001.submit_signatures(market_id, outcome, round, sigs)is permissionless.It verifies each signature with
ed25519_verify(strict, host-side). Itskips non-members and keys already counted this round for either outcome,
and accumulates weight per
(market, round, outcome). A proposal is writtenonly when the threshold is first reached, so
proposed_atcan never bepushed out.
cancel_proposalmoves a weighted market to a new round, sopreviously collected signatures cannot be replayed.
current_roundexposesthe round to signers.
MAX_SIGS_PER_CALL = 32. 32 signatures cost 23.1M instructions /2.9 MB with the compiled wasm, under 6% and 7% of the 400M / 40 MiB
transaction limits. Larger committees can also accumulate weight across
calls.
agents/core/attestation.tsaddsbuildAttestationMessage,signAttestationandverifyAttestation. Golden vectors are shared byte forbyte with the Rust test.
spec-hash weighted-keysis added.Why it matters. One transaction, paid by anyone, can resolve a market for a
committee of keys that need no funded Stellar accounts. Weighted committees
are expressible, and replay across networks, deployments, markets and
cancelled rounds is impossible.
#30 — Bonded optimistic dispute mechanism
Problem. The "dispute window" let no one but the guardian dispute, and
Error::DisputeActivewas defined but unused.Fix and new flow.
ResolutionSpec::SignersDisputable(DisputableSignerConfig)adds a bond, an escalation authority and an escalation timeout. The
authority is
Escalation::Guardian, orEscalation::Committee(members, k)disjoint from the signers. Revealed with
register_disputable_spec.dispute(market_id, disputer, alternative). During the window, anyoneescrows the bond in the resolver, with the token snapshotted at dispute
time. This sets
Disputedand emitsDisputed. While disputed,finalize_signers,attest,cancel_proposalandexpire_marketreturnDisputeActive.settle_dispute, or bykcommittee votes through
escalation_attest:market-core's treasury;
Invalid) andrefunds the bond.
expire_disputeis permissionless afterescalation_timeout.It voids the market and refunds the bond.
guardian_void_marketduring adispute also refunds.
claim_dispute_bond, so a payee that cannot receive never blockssettlement. The tests assert after every step that the resolver's balance
equals the bonds it still holds.
more, as the architecture doc states.
Why it matters. Anyone holding evidence can freeze a wrong proposal and
force a ruling. The outcome no longer depends on the guardian noticing in
time, and nothing can stay stuck, because every dispute has a timeout.
Compatibility
(19/20 Add a committed resolution deadline with permissionless liveness-fallback voiding #32, 21 Version the ResolutionSpec commitment format with salted, domain-separated hashes #29, 22/23 Support aggregated off-chain Ed25519 attestations with weighted signer thresholds #31, 24/25 Add a bonded optimistic dispute mechanism to Attested resolution #30).
market_core.wasmare untouched.atteston a weighted-key market now returnsWrongModeinstead ofMarketNotConfigured.Changed
contracts/resolver/src/lib.rs:ResolutionSpecV2,register_spec_v2, the shared reveal pipeline and golden vectors.RESOLUTION_GRACE_SECS,resolution_deadline,expire_market,ResolutionState::Expired,MarketExpired.WeightedKeysspec,submit_signatures,current_round, the round bump on cancel, theCommitteeview.SignersDisputablespec,dispute,settle_dispute,escalation_attest,expire_dispute,claim_dispute_bond,ResolutionState::Disputed.contracts/resolver/Cargo.toml,Cargo.lock(Support aggregated off-chain Ed25519 attestations with weighted signer thresholds #31):ed25519-dalekdev-dependency for signing in tests.scripts/spec-hash/:lib.rsandmain.rs; adds--v2,gen-saltandverify.weighted-keys.signers-disputable.getrandomadded to its ownCargo.tomlandCargo.lock(Version the ResolutionSpec commitment format with salted, domain-separated hashes #29).scripts/seed-market.sh(Version the ResolutionSpec commitment format with salted, domain-separated hashes #29): salted V2 commitment, persist-then-verify salt beforeagent_create_market, reveal viaregister_spec_v2..gitignore(Version the ResolutionSpec commitment format with salted, domain-separated hashes #29): ignoredeployments/specs/, where the unrevealed salts live.Makefile(Version the ResolutionSpec commitment format with salted, domain-separated hashes #29):make testalso runs thespec-hashtests.agents/core/attestation.ts,agents/core/attestation.test.ts(Support aggregated off-chain Ed25519 attestations with weighted signer thresholds #31): off-chain attestation signing and verification, with golden vectors.packages/bindings/resolver/src/index.ts(Version the ResolutionSpec commitment format with salted, domain-separated hashes #29, Add a committed resolution deadline with permissionless liveness-fallback voiding #32, Support aggregated off-chain Ed25519 attestations with weighted signer thresholds #31, Add a bonded optimistic dispute mechanism to Attested resolution #30): regenerated resolver bindings.docs/adr/0004-spec-versioning.md(Version the ResolutionSpec commitment format with salted, domain-separated hashes #29): new ADR.docs/adr/0001-resolution-commitment.md:docs/architecture.md:docs/STATUS.md(Add a committed resolution deadline with permissionless liveness-fallback voiding #32): known limitation for creator-controlled oracles.Testing
All commands were run from
oracledesk-stellar/, on this branch and on afresh clone with the same patches applied:
Results:
cargo fmt --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspacespec-hashclippy + testsbash -n scripts/seed-market.shstellar contract build --package resolvernpm run build(all 4 packages)agents:npm test/npx tsc --noEmitAdditional verification:
scripts/seed-market.shran end to end against a stubstellarCLI usingdeployments/testnet.json. The spec file existed beforeagent_create_market. With the spec directory unwritable, the script exited 1 before anystellarcall.stellar network settings(testnet and mainnet: 400M instructions, 40 MiB).market_core.wasmfiles remain valid.Not run: the manual testnet flow from #29's test plan (create →
register_spec_v2→ attest → finalize). It needs the new resolver deployed with funded identities.Scope Notes
contracts/resolverchanged.market-core,treasuryandreasoning-registrysource, and both vendoredmarket_core.wasmfiles, are untouched.x402/is untouched. Inagents/only a library helper (core/attestation.ts) was added; no CLI or strategy wiring.packages/bindings/resolverwas regenerated; the other contracts' interfaces did not change.deployments/testnet.json. Existing testnet commitments keep resolving through the legacy entrypoints..github/workflows/ci.yml) is unchanged.make testruns thespec-hashtests, but CI only runscargo test --workspace; aspec-hashCI step is left to maintainers.void_market. It was deliberately not made: doc comments are embedded in the wasm, so the edit would change the vendoredmarket_core.wasmthat the issue requires to stay identical.develop, notmain. The repo'senforce-develop-baseworkflow closes contributor PRs intomain.developdoes not exist on the remote yet, so a maintainer may need to create it first.implementation.mdand thisPR_NOTES.md. There are noCODEBASE_INDEX.md,.cursor/or test snapshot files in the change;test_snapshots/is gitignored.Push Command