Skip to content

feat(core): let the first assertion of an unclaimed cid take the claim until content auth turns strict - #577

Merged
rickyrombo merged 4 commits into
mainfrom
mjp-content-auth-first-assertion
Sep 16, 2026
Merged

rickyrombo merged 4 commits into
mainfrom
mjp-content-auth-first-assertion

Conversation

@rickyrombo

@rickyrombo rickyrombo commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Rebased onto main now that #574 has merged. Must be in the bootstrap build alongside #572.

Why

validateTrackContentAuth had three outcomes: claimed by you, pass; claimed by someone else, reject; claimed by nobody, reject. The third one breaks the migration. The API's new-chain flusher resubmits every post-snapshot write through ForwardTransaction, which validates like a live write, and it does not advance past a refused row. Every track created on the old chain after the snapshot names cids nobody attested — old-chain mediorum never does, and the replay only seeds the snapshot's tracks — so the queue stalls at the first one and step 12 of the runbook never becomes reachable. Around 429 track creates a day fall in that window.

What

A first-assertion window, bounded by a new upgrade height. The scheduler has no end height, so ContentAuthStrictHeight is a second gate over the same rule (Rules.ContentAuthStrict):

  • Enforced, not yet strict: a cid nobody holds passes, and the projection records the writer as claimant (projectAssertedTrackCids), the same thing the migration replay does for its rows. From then on the cid is that user's alone; a second user naming it is refused with was not uploaded for user.
  • Strict: a cid nobody holds is refused with is not attested to any uploader, and live writes claim nothing. This is the pre-PR behavior and the steady state.

The resolved config.Rules is passed into applyAuthProjection and validateTrackContentAuth alongside the transaction (it is a property of the height, not of the transaction, so it is not a field on authTx), and both read ContentAuthStrict directly. Validation only runs under ContentAuthEnforced; the projection runs for every live write, so it checks enforced explicitly too.

Why the window must close. The upload row is public on GET /uploads while it transcodes, so orig_file_cid is readable before any attestation exists, and the attestation transaction is visible in the mempool before it lands. A ContentAttestation does not project into the proposal overlay, so a create naming the cid passes validation whichever side of the attestation it sits in the block, and the writer ends up with an indexed track naming someone else's audio — the decoy content auth exists to prevent. That is acceptable only while the flusher needs it.

What does not change.

  • A cid someone holds is still refused at validation, and projectAssertedTrackCids never reassigns a held cid at finalize, so a block from a proposer without the gate cannot transfer one by naming it.
  • Claims from live writes are recorded only inside the window. Before the enforcement gate a chain accumulates nothing from live traffic, preserving feat(core): authorize track cids against validator upload attestations (unsigned attribution) #477's principle that pre-enforcement state is never trusted.
  • Track updates project only this claim, and only after the same signer-authority check a create gets.

Schedules. Devnet and sandbox are strict from height 1, so local chains and integration tests exercise the steady state; the window is covered by unit tests. audius-mainnet-beta (added by #572) needs ContentAuthStrictHeight set in a follow-up release as soon as the flusher has drained, and never before, or the first refused row stalls it.

Residual flusher stall, not addressed here. A post-snapshot track whose cid is already held by a different user (duplicate bytes, or a re-upload of a pre-snapshot track) is still refused. The flusher should skip and log a refused row rather than halt.

Verification

  • make test-unit green. New: TestContentAuthStrictCloseWindow (config), TestStrictContentAuthRejectsUnattestedCid, plus the existing TestContentAuthUnattestedCidGoesToFirstAsserter, TestAssertedClaimNeverOverridesAnExistingOne, TestPreGateLiveTrackClaimsNothing, TestUpdateClaimsUnattestedCidOnlyForAuthorizedSigner.
  • make test-integration on a devnet built from this branch: all eight TestContentAuth cases green, with UnattestedCidIsRejected restored since the devnet is strict from height 1.

🤖 Generated with Claude Code

rickyrombo added a commit that referenced this pull request Sep 9, 2026
…the bootstrap build

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rickyrombo
rickyrombo force-pushed the mjp-content-auth-integration-test branch from e390d9e to 7e35981 Compare September 15, 2026 21:33
rickyrombo added a commit that referenced this pull request Sep 15, 2026
…runbook (#573)

* docs(genesis-writer): add the post-cutover follow-ons to the rollout runbook

Two pieces of work were designed around the migration but the runbook did not
carry them: the upgrade schedule for audius-mainnet-beta (#572), which must
merge before the bootstrap build or the new chain runs with no enforcement and
no cid attestation, and the legacy reward wire-compat removal (#232), which
must not merge until every node has left the old chain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(genesis-writer): put the schedule prerequisite in step 3, verify enforcement at step 10

Review fixes. The placeholder substitution had mangled
OPENAUDIO_DELEGATE_PRIVATE_KEY in Appendix J; restored. Step 17 had bundled a
pre-bootstrap prerequisite (#572) with a post-cutover cleanup (#232) under an
"after 15" label; #572 now lives in step 3 with a rebase instruction, and step
17 is #232 alone. The enforcement checks move to the first migrated node in
step 10 and name the real submission path and the real skew symptom (a commit
timeout, not the pre-gate error text). Appendix C gains the claim-coverage
check. Step 11 records, as unresolved, that the flusher stalls on the first
post-snapshot track create under content auth.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(genesis-writer): #577 resolves the flusher stall; require it in the bootstrap build

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Base automatically changed from mjp-content-auth-integration-test to main September 16, 2026 00:16
rickyrombo and others added 2 commits September 15, 2026 17:33
Content auth rejected a track naming a cid nobody holds. Every cid that can
belong to someone else is already claimed — migrated tracks are seeded by the
replay, uploads to an enforcing node are attested before the upload reads done
— so what an unclaimed cid can be is audio that reached storage without an
attestation: uploads made before the node ran the gate, and multipart uploads
that named no user. Refusing those strands every such track, and stalls the
API's new-chain flusher on the first post-snapshot track create, since it
never advances past a refused row.

Now the write passes and the projection records the writer as claimant; from
then on the cid is theirs alone. A cid someone already holds is still refused
at validation and never reassigned at finalize, so a block from a proposer
without the gate cannot transfer one. Claims from live writes are recorded
only under ContentAuthEnforced, so a chain accumulates nothing from live
traffic before its gate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…th height

First-assertion-wins exists for the genesis migration: the API flusher
replays old-chain track writes naming cids nobody attested and stalls on
the first refused row. It is not safe as a steady state. The upload row is
public on GET /uploads while it transcodes, so orig_file_cid is readable
before any attestation exists, and the attestation itself is visible in the
mempool before it lands. A ContentAttestation does not project into the
proposal overlay, so a create naming the cid passes validation whichever
side of the attestation it sits in the block, and the writer ends up with an
indexed track naming someone else's audio — the decoy content auth exists
to prevent.

The scheduler has no end height, so add ContentAuthStrictHeight as a second
gate over the same rule. Enforced but not strict: an unclaimed cid passes
and the writer takes the claim. Strict: an unclaimed cid is refused with
"is not attested to any uploader" and live writes claim nothing. Both
proposal-time and finalize-time callers derive ClaimUnattested from the
pair, so the two sides cannot disagree.

Devnet and sandbox are strict from height 1, so the integration case goes
back to UnattestedCidIsRejected; the window is covered by unit tests. The
prod schedule must set the strict height once the flusher has drained, and
never before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rickyrombo
rickyrombo force-pushed the mjp-content-auth-first-assertion branch from 7deee59 to 58234e9 Compare September 16, 2026 00:41
@rickyrombo rickyrombo changed the title feat(core): let the first assertion of an unclaimed cid take the claim feat(core): let the first assertion of an unclaimed cid take the claim until content auth turns strict Sep 16, 2026
rickyrombo and others added 2 commits September 15, 2026 17:53
…tagging the tx

Whether a live track write may take the claim on an unclaimed cid is a
property of the height's rules, not of the transaction, so it does not
belong on authTx. applyAuthProjection and validateTrackContentAuth now take
the resolved config.Rules alongside the transaction, and firstAssertionOpen
derives the answer in one place. Migration rows never branch on the rules
and pass an empty ruleset. The behavior is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Drop the firstAssertionOpen helper. Validation only runs under
ContentAuthEnforced, so strict alone decides there. The projection runs for
every live write, so it checks enforced explicitly too: before the gate a
chain must accumulate no claims from live traffic. Behavior is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rickyrombo
rickyrombo merged commit a8a108a into main Sep 16, 2026
9 of 10 checks passed
@rickyrombo
rickyrombo deleted the mjp-content-auth-first-assertion branch September 16, 2026 20:28
rickyrombo added a commit that referenced this pull request Sep 16, 2026
Resolves the TestScheduleForChainID conflict with #577: devnet and sandbox
stay strict from height 1, while audius-mainnet-beta activates both
enforcements at height 1 and deliberately leaves ContentAuthStrictHeight
unset so the migration flusher's first-assertion window stays open.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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