Skip to content

refactor(bounty): drop bodyHash, sign body fields directly#868

Merged
biwasxyz merged 2 commits into
mainfrom
chore/bounty-drop-body-hash
May 16, 2026
Merged

refactor(bounty): drop bodyHash, sign body fields directly#868
biwasxyz merged 2 commits into
mainfrom
chore/bounty-drop-body-hash

Conversation

@biwasxyz
Copy link
Copy Markdown
Contributor

Summary

Aligns bounty Create + Submit signing with the rest of the codebase. Every other signed-action endpoint (/api/outbox, /api/heartbeat, /api/vouch, /api/inbox mark-read, /api/challenge) signs a plain-text message with the full content inlined. Bounty was the only place using a sha256(canonicalJSON(...)) bodyHash, which forced every client (and any future MCP tool) to do an extra hashing step that nothing else in the platform needs.

New signed messages

AIBTC Bounty Create | {posterBtc} | {title} | {description} | {rewardSats} | {expiresAt} | {tagsCommaJoined} | {signedAt}
AIBTC Bounty Submit | {bountyId} | {submitterBtc} | {message} | {contentUrl} | {signedAt}
  • tagsCommaJoined = tags.join(",") or "" when no tags
  • contentUrl = "" when omitted

Accept / Paid / Cancel messages are unchanged (they were already plain — Accept/Paid/Cancel did not use bodyHash even before this PR).

Trust model preserved

All body fields are part of the signed message, so any tampering with title / description / rewardSats / expiresAt / tags / submission message / contentUrl breaks the signature. Same precedent as /api/outbox signing the full reply body up to 1000 chars; bounty descriptions are up to 4000 chars but agents sign programmatically via MCP btc_sign_message, not a human-readable wallet popup, so message length isn't a UX concern.

Why now

The bounty system merged a few hours ago in #843. The bounties table on prod is empty (no posted bounties), so changing the signing contract is safe with zero migration burden. After this lands, the MCP server can ship a one-call bounty_create_native tool: build the message string → btc_sign_messagePOST /api/bounties. No hash. Same pattern as every other signed action.

Files touched

File Change
lib/bounty/signatures.ts Drop canonicalJSON + bodyHash. Update buildCreateMessage + buildSubmitMessage signatures.
lib/bounty/constants.ts Update SIGNATURE_MESSAGE_FORMATS.CREATE + SUBMIT templates.
lib/bounty/index.ts Stop exporting canonicalJSON + bodyHash.
lib/bounty/validation.ts Update signature-error hint strings (no behavior change — bodyHash was never a request field, only a hint).
app/api/bounties/route.ts POST handler rebuilds message from body fields directly. Drop bodyHash import. Update self-doc envelope.
app/api/bounties/[id]/submit/route.ts Same.
app/api/openapi.json/route.ts Update Create + Submit descriptions and signature field doc.
app/docs/[topic]/route.ts Update bounties topic doc.
app/bounty/new/page.tsx Collapse 3-step instructions into 2 steps (sign + POST).
lib/bounty/__tests__/signatures.test.ts Drop canonicalJSON + bodyHash suites. Add 4 new assertions for Create + Submit formats, including empty-tags and empty-contentUrl segments.

Verification

$ npm run test -- lib/bounty
 lib/bounty/__tests__/signatures.test.ts  (10 tests)
 lib/bounty/__tests__/types.test.ts       (15 tests)
 lib/bounty/__tests__/txid-verify.test.ts (16 tests)
Test Files  3 passed (3)
     Tests  41 passed (41)

$ npx tsc --noEmit
clean for bounty + bodyHash

Plus repo-wide grep -rn "bodyHash\|canonicalJSON" returns zero hits.

Test plan

  • CI green
  • After merge: post a real bounty via the simplified flow — btc_sign_messagecurl POST /api/bounties — to verify prod accepts the new format
  • File follow-up issue on aibtc-mcp-server to add bounty_create_native / bounty_submit_native tools pointing at aibtc.com (replacing the drx4-targeted ones)

biwasxyz added 2 commits May 16, 2026 18:08
Resolves filename collision with 013_identity_cache.sql (merged in #852)
that landed on main shortly before #843. Wrangler tracks applied
migrations by full filename so both would have run, but the duplicate
slot number is confusing.

Already applied to remote D1 as 014_bounties.sql.
Aligns Create + Submit with the rest of the codebase. Every other
signed-action endpoint (/api/outbox, /api/heartbeat, /api/vouch,
/api/inbox mark-read, /api/challenge) signs a plain-text message with
the full content inlined. Bounty was the only place using a
sha256-of-canonical-JSON bodyHash, which forced every client (and any
future MCP tool) to do an extra hashing step that nothing else needs.

New signed messages:

  AIBTC Bounty Create | {posterBtc} | {title} | {description} | {rewardSats} | {expiresAt} | {tagsCommaJoined} | {signedAt}
  AIBTC Bounty Submit | {bountyId} | {submitterBtc} | {message} | {contentUrl} | {signedAt}

tagsCommaJoined = tags.join(",") or "" when no tags.
contentUrl = "" when omitted.

Accept / Paid / Cancel signed messages unchanged (already plain).

Trust model is preserved: all body fields are part of the signed
message, so any tampering with title / description / reward / expiry
/ tags / submission text breaks the signature. Same precedent as
/api/outbox signing the full reply body.

Drops:
- canonicalJSON, bodyHash from lib/bounty/signatures.ts
- bodyHash import in /api/bounties POST + /api/bounties/[id]/submit POST
- bodyHash references from openapi.json, docs/bounties.txt, /bounty/new

Tests:
- signatures.test.ts: drop canonicalJSON + bodyHash suites; add
  coverage for new Create + Submit formats (including empty tags and
  empty contentUrl)
- bounty suite: 41 tests pass, no other changes needed
@biwasxyz biwasxyz merged commit c71b4c3 into main May 16, 2026
7 of 8 checks passed
@biwasxyz biwasxyz deleted the chore/bounty-drop-body-hash branch May 16, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant