Repository navigation
feat(core): let the first assertion of an unclaimed cid take the claim until content auth turns strict - #577
Merged
Merged
Conversation
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
force-pushed
the
mjp-content-auth-integration-test
branch
from
September 15, 2026 21:33
e390d9e to
7e35981
Compare
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>
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
force-pushed
the
mjp-content-auth-first-assertion
branch
from
September 16, 2026 00:41
7deee59 to
58234e9
Compare
…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>
Merged
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>
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.
Rebased onto main now that #574 has merged. Must be in the bootstrap build alongside #572.
Why
validateTrackContentAuthhad 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 throughForwardTransaction, 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
ContentAuthStrictHeightis a second gate over the same rule (Rules.ContentAuthStrict):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 withwas not uploaded for user.is not attested to any uploader, and live writes claim nothing. This is the pre-PR behavior and the steady state.The resolved
config.Rulesis passed intoapplyAuthProjectionandvalidateTrackContentAuthalongside the transaction (it is a property of the height, not of the transaction, so it is not a field onauthTx), and both readContentAuthStrictdirectly. Validation only runs underContentAuthEnforced; 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 /uploadswhile it transcodes, soorig_file_cidis readable before any attestation exists, and the attestation transaction is visible in the mempool before it lands. AContentAttestationdoes 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.
projectAssertedTrackCidsnever reassigns a held cid at finalize, so a block from a proposer without the gate cannot transfer one by naming it.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) needsContentAuthStrictHeightset 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-unitgreen. New:TestContentAuthStrictCloseWindow(config),TestStrictContentAuthRejectsUnattestedCid, plus the existingTestContentAuthUnattestedCidGoesToFirstAsserter,TestAssertedClaimNeverOverridesAnExistingOne,TestPreGateLiveTrackClaimsNothing,TestUpdateClaimsUnattestedCidOnlyForAuthorizedSigner.make test-integrationon a devnet built from this branch: all eightTestContentAuthcases green, withUnattestedCidIsRejectedrestored since the devnet is strict from height 1.🤖 Generated with Claude Code