feat(mediorum): authenticate audio uploads and attest their cids on chain - #460
Open
rickyrombo wants to merge 1 commit into
Open
feat(mediorum): authenticate audio uploads and attest their cids on chain#460rickyrombo wants to merge 1 commit into
rickyrombo wants to merge 1 commit into
Conversation
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 7, 2026 06:25
419c9ab to
93878ea
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 7, 2026 06:37
93878ea to
cd4cfd9
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 7, 2026 06:48
cd4cfd9 to
58d7dfa
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 7, 2026 07:46
58d7dfa to
0bb71fa
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
2 times, most recently
from
August 7, 2026 08:31
1073aab to
b9127ac
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 7, 2026 08:45
b9127ac to
237fc08
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 7, 2026 09:00
237fc08 to
54855c3
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
2 times, most recently
from
August 7, 2026 09:29
e8fcacf to
1da4b79
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 7, 2026 09:32
1da4b79 to
43f2651
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 7, 2026 09:47
43f2651 to
3eebfd5
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 7, 2026 09:56
3eebfd5 to
24ce10c
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 7, 2026 10:05
24ce10c to
759266a
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 7, 2026 10:33
759266a to
0058e99
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 7, 2026 23:37
0058e99 to
696647f
Compare
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 8, 2026 07:16
696647f to
77859b4
Compare
…hain Consensus can now gate a track's cid fields on a validator attesting that the claiming wallet uploaded those bytes. This produces that attestation. Verify an EIP-712 signature at tus upload creation for audio, and set Upload.UserWallet from the recovered address. It was previously read straight out of client-supplied tus metadata under a key the SDK never sent, so it was NULL on every SDK upload and forgeable on the rest — harmless while nothing read it, not harmless now that it decides who may claim the resulting cids. Typed data rather than a hashed JSON payload. It removes JSON canonicalization from the protocol entirely — the type definition is the encoding, so there is no key ordering or number formatting for client and server to agree on and later drift apart over — and it gives domain separation, which the previous scheme lacked: a signature produced for any other purpose over the same two fields would have been replayable as an upload authorization. It also matches how the SDK already signs entity-manager writes, so both sides use standard calls rather than a hand-rolled hashing scheme. The payload covers the user id and a timestamp, not the cid: at tus-create time the bytes have not been sent and nobody knows the cid yet. Both fields also travel unsigned in tus metadata because the verifier needs them to rebuild the typed data, but they are reproduced inside it, so tampering only breaks recovery. Stale and future-dated signatures are rejected — a captured one would otherwise be a standing credential to upload as someone else. Only audio is covered. Images are served unauthenticated, so there is no gating story to protect, and requiring signatures would break signup, which uploads a profile picture before the account has a user id. After transcoding, submit a ContentAttestation over all three cids — the original, the 320 transcode that becomes track_cid, and the preview. The original matters most: it is the download path, the lossless master. Both sides derive the signed bytes from one shared constructor so they cannot drift on field order or address casing. Content authorization runs off its own flag rather than ProgrammableDistributionEnabled. That flag governs DDEX; tying them together meant this bypass could only be closed on networks that also enable an unrelated subsystem with no production consumers, which is backwards for a fix to the ordinary track-upload path. Attestation requires a verified signature, not merely a wallet. The legacy POST /uploads and gRPC paths still populate UserWallet from an unverified X-User-Wallet-Addr header, so keying off the wallet alone would let a forged header mint a claim over someone else's content. Those paths are otherwise left alone; the header fallback is a pre-existing hole worth closing separately. Attestation failures are logged, not fatal. A dropped attestation costs the uploader the ability to claim those cids, which re-uploading fixes; failing the transcode would throw away the transcode too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rickyrombo
force-pushed
the
feat/content-auth-mediorum
branch
from
August 8, 2026 08:04
77859b4 to
4d33128
Compare
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.
Stacked on #459. Review that first — this branch contains only the commit on top of it.
#459 lets consensus gate a track's cid fields on a validator attesting that the claiming wallet uploaded those bytes. This produces that attestation.
Knowing who uploaded
Upload.UserWalletwas read straight out of client-supplied tus metadata under a key the SDK never sent, so it was NULL on every SDK upload and forgeable on the rest. Harmless while nothing read it; not harmless now that it decides who may claim the resulting cids.Audio uploads now verify an EIP-712 signature at tus creation and set
UserWalletfrom the recovered address. Unsigned uploads are still accepted where verification is not required, but are never credited to a wallet — recording an unverified one would put a forgeable value in the field attestations are built from. A signature that is offered and fails to verify is always an error, so a bad one cannot pass itself off as absent.Why EIP-712
The previous revision of this PR hashed canonical JSON and signed it with
personal_sign. Typed data is better on two counts:It also matches how the SDK already signs entity-manager writes, so both sides use standard calls (
signTypedData, go-ethereum'sapitypes) rather than a hand-rolled scheme.The payload covers the user id and a timestamp, not the cid: at tus-create time the bytes have not been sent. Both fields also travel unsigned in tus metadata so the verifier can rebuild the typed data, but they are reproduced inside it, so tampering only breaks recovery. Stale and future-dated signatures are rejected — a captured one would otherwise be a standing credential to upload as someone else.
Only audio is covered. Images are served unauthenticated, so there is no gating story to protect, and requiring signatures would break signup, which uploads a profile picture before the account has a user id.
Decoupled from the DDEX flag
Content authorization runs off
ContentAuthEnabledrather thanProgrammableDistributionEnabled. The latter governs DDEX; tying them together meant this bypass could only be closed on networks that also enable an unrelated subsystem with no production consumers — backwards for a fix to the ordinary track-upload path.An upload is not done until its cids are attested
A client creates its track the moment an upload reads
done, and consensus rejects a track naming cids it holds no claim for. Publishing the status first would race the attestation and reject perfectly legitimate uploads, so attestation happens before the status flips and a failure fails the transcode instead.SendTransactionblocks until the transaction is committed, so once the status isdonethe claims are genuinely on chain.Cases where no claim was ever possible — content auth off, or an upload with no verified signer — return cleanly rather than erroring, since there is nothing to wait for and no reason to hold the upload back. That decision is split into
contentAttestationForso the rules are testable without a chain.Cids are attested as they come into being
The initial upload sends one attestation covering the original, the 320 transcode and the preview, since all three exist by the time transcoding finishes.
Previews are the reason #459's attestation carries a plain cid list rather than named orig/transcode/preview fields.
POST /uploads/:idlets a client changepreviewStartSecondsafter the fact, which regenerates the preview and yields a new cid every time. Under a single fixed-shape attestation sent at transcode completion, that cid would never be attested and saving the track would be rejected. The edit path now sends its own attestation for the new cid, before responding, so a caller never receives a preview cid it cannot yet use.Attestation requires a verified signature
Not merely a wallet. The legacy
POST /uploadsand gRPC paths still populateUserWalletfrom an unverifiedX-User-Wallet-Addrheader, so keying off the wallet alone would let a forged header mint a claim over someone else's content. Those paths are otherwise untouched; the header fallback is a pre-existing hole worth closing separately.Testing
Unit coverage for signature recovery, staleness, future-dating, field tampering, the per-template rules, flag independence, which uploads warrant an attestation, and that the skip cases never block an upload from completing.
One is a cross-implementation fixture: a real EIP-712 signature produced by the SDK using viem, pinned so the two independently-written signing paths cannot drift apart silently while both suites still pass on their own. I confirmed it recovers to the expected address in Go before wiring it up.
There is no integration coverage of this path, and I could not add any in this PR.
TestUploadStream— the only devnet test that uploads audio — has been skipped since fe3d95c ("flaky on resource-constrained CI runners"), and even un-skipped it would not exercise this change: it uploads through the legacyPOST /uploadsmultipart endpoint with the old per-cid query-string signature, while upload authentication hooks the tus path (tusd.go). An earlier revision of this stack was reported here as devnet-verified; that was overstated, and I have removed the claim.What that leaves untested end to end: signature verification against a real tus client, the attestation reaching consensus, and the done-blocks-on-attestation behaviour. The unit tests cover the decision logic and the signing format on both sides, but not the wiring. Worth deciding whether this stack should carry a tus-based integration test before enforcement is scheduled.
Two pre-existing test issues found along the way, neither caused by this change:
TestGRPCpanics on a nil response (fails identically on the base commit), andTestUploadFile/TestRepair/TestPollDelistStatusesare not idempotent — they only pass against a fresh database.🤖 Generated with Claude Code