What the Chain Knows About a Song
Putting music onchain means more than selling shares of it. The chain should know what the audio is, and be able to trust it.
· 8 min read
Most attempts to bring media onchain start and end with ownership. Wrap the asset in a token, split the token into shares, and sell the shares. That’s useful. But a token that stands for a song usually knows nothing about the song. To the chain, it could be a song, a film, or a parking space. It only knows who holds the shares.
That limits what anyone can build on top. If the chain can’t tell what an asset is or how it works, the only thing a contract can do with it is move it between owners. Every business model has to fit into “buy shares, hold shares, sell shares.”
We think bringing media onchain should also mean bringing its properties onchain. The more the chain understands about an asset, the more kinds of business can be built on it.
A Song Is a Sound File
For music, the place to start is the sound file. A lossless audio file has a handful of technical properties that say exactly what it is:
- Channels: one for mono, two for stereo.
- Bit depth: how precisely each sample records the waveform, usually 16 or 24 bits.
- Sample rate: how many samples are taken per second, usually 44,100 or 48,000.
- Samples per channel: how many samples the recording holds in total.
These aren’t descriptions that someone typed in. They’re measurements of the file. And they combine. Divide the samples per channel by the sample rate and you get the recording’s duration. A track with 10,584,000 samples at 44,100 per second is exactly 240 seconds long.
Duration matters because time is what listening is made of. A stream is time spent with a recording. A royalty rate, a pay-per-minute player, a payout that depends on how much of a track someone actually heard: they all need to know how long the track is. Once the chain knows a recording’s duration, contracts can price, split and reward listening time directly. That’s the basis of what people call the attention economy, and it only works if the clock it runs on is real.
Data You Can Build On Has to Be True
Suppose we put a struct onchain with fields for channels, bit depth, sample rate and sample count, and let anyone fill it in. Nothing would stop someone from saying their 30-second clip is an hour long, or that a low-quality rip is a 24-bit master. Every contract that read those fields would inherit the lie, and paying by duration would mean paying whoever lied best.
The chain can’t open an audio file and look. Decoding minutes of audio is far too much work to do inside a transaction, and the file isn’t onchain anyway. So something offchain has to measure the file, and the chain needs a reason to believe it.
That reason can’t be a company’s word, including ours. We built the measuring service so that the chain can check who did the measuring and what code they ran.
Measuring Audio in an Enclave
Nautilus is Sui’s framework for verifiable offchain computation. It runs a program inside an AWS Nitro enclave, an isolated machine that not even the server’s operator can see into or change. When the enclave boots, AWS signs an attestation document recording measurements of the exact program image it’s running. Sui can check that document onchain, so a contract can know that a given signing key belongs to an enclave running a specific, published build, and to nothing else.
We wrote audio-verifier, a Nautilus service that does one job: it takes a FLAC file, measures it, and signs what it found.
Measuring here means more than reading the file’s header. FLAC files begin with a block that declares the audio’s channels, bit depth, sample rate and total samples, plus an MD5 checksum of the decoded audio. Anyone can write whatever they like into that block. So the enclave decodes the whole file with libFLAC, the reference decoder, and checks:
- that every frame of audio agrees with the header about channels, bit depth and sample rate,
- that each frame starts exactly where the previous one ended,
- that the number of samples it decoded matches the number the header claims,
- and that the MD5 of the decoded audio matches the checksum in the header.
If anything disagrees, the file is rejected and nothing is signed.
While it decodes, the enclave computes two fingerprints. The first is a BLAKE3 digest of the decoded audio itself, not the file, so the same recording gives the same digest whether it was saved as FLAC, WAV or AIFF. The second is the file’s Walrus blob ID, the content address the exact file will have when it’s stored on Walrus. Then the enclave signs the measurements and both fingerprints together with a key that never leaves it.
Before it accepts any uploads, the service also runs a self-test, decoding known files and computing known blob IDs and checking the answers, so a build that computes wrongly on its hardware refuses to start.
The Audio Package
The onchain half comes in two Move packages. The first, audio, defines what verified audio is. It doesn’t know about enclaves at all.
public struct Audio<phantom Verifier> has copy, drop, store {
channels: u8,
bit_depth: u8,
sample_rate_hz: u32,
samples_per_channel: u64,
pcm_digest: vector<u8>,
blob: WalrusBlob,
}
The important part is the type parameter. Audio<Verifier> is audio facts vouched for by Verifier, and the only way to make one is to call audio::new with a value of that Verifier type. In Move, only the package that defines a type can create a value of it. So Audio<SomeVerifier> can only ever come from SomeVerifier’s package, and the type itself names who measured the audio. There’s no way to make audio that vouches for itself.
That gives anyone building on top a simple rule: accept Audio from the verifiers you trust, by name, and ignore the rest. Anyone can publish a package that vouches for anything, so a contract that accepts any Audio<V> has trusted nobody in particular. A contract that accepts Audio<AudioVerifier> has trusted one enclave.
The package keeps its own job small. new checks that the facts make sense: at least one channel, a bit depth from 1 to 32, a nonzero sample rate and sample count, a 32-byte digest, a duration that fits in a 64-bit number, and audio stored unencrypted on Walrus. It doesn’t check that the facts are true. That’s the verifier’s job. Duration comes out as a view function, duration_ms, computed from the samples and the sample rate rather than stored, so it can never disagree with them.
Audio can be copied, dropped and stored inside other objects, but it isn’t an object of its own. Copying it copies facts, not ownership. It’s meant to be attached to whatever needs it: a track, a release, a stream. And because the verifier is part of the type, an object can hold audio from several verifiers side by side, so a better one can be added later without replacing the old.
The Verifier Package
The second package, which ships with audio-verifier, connects the enclave to that type. Its main function takes the enclave’s measurements and signature and, if the signature checks out, turns them into audio:
public fun verify(
enclave: &Enclave<AUDIO_VERIFIER>,
format: String,
channels: u8,
bit_depth: u8,
sample_rate_hz: u32,
samples_per_channel: u64,
pcm_digest: vector<u8>,
blob_id: u256,
timestamp_ms: u64,
signature: vector<u8>,
): Audio<AudioVerifier>
verify rebuilds the exact message the enclave signed and checks the signature against the enclave’s registered key. If it passes, the package calls audio::new with its own AudioVerifier value and hands back Audio<AudioVerifier>. Since only this package can create AudioVerifier, and it only does so after a valid signature, every Audio<AudioVerifier> anywhere on Sui was measured by the enclave.
The enclave takes only Enclave<AUDIO_VERIFIER>, an enclave registered under this package’s own policy. That policy lists the measurements of the enclave image we’ve published, so an enclave running different code, or registered for a different app, can’t produce a signature that passes. When we ship a new image, we update the measurements and replace the registered enclaves in the same transaction. The events record which enclave and which policy version signed each piece of audio, so anyone who wants to treat an older image differently can.
There’s no freshness check, on purpose. The facts describe a fixed file at a fixed content address, and they don’t go stale. Replaying a signature just rebuilds the same value.
What It Proves, and What It Doesn’t
Audio<AudioVerifier> proves what a file contains. It doesn’t prove who made the recording, who owns the rights, or that the file is still available on Walrus. Anyone can verify a file and attach the result to their own object. Authorship and duplicates are questions for curation, and we keep them out of the measurement on purpose. A measurement that also tried to judge ownership would be easier to argue with.
What it does give the chain is a recording it can reason about: how long it is, how it was made, and a fingerprint that will match any other copy of the same audio. That’s enough to start building business models that work with the music itself, not just with shares of it.
The code is open. The audio package and audio-verifier are on GitHub under the Functional Source License, and each version becomes Apache 2.0 two years after release.