XAS-007d: On-Ledger NFT Metadata via Remarks (xahau-nft-remark/v1)
#791
Replies: 1 comment
|
Adding some production data from the reference-implementation side (Odin's Eyes Ravens) - we've been running sections 1-4 live on mainnet since early July, so a few of the answers this proposal asks for we can now give from real usage rather than theory. Scale so far: every license NFT minted since #7 carries the full Fragment ordering (numeric vs lexical): numeric is the right call and should stay normative. Our images routinely split into 25+ The 32-remark ceiling in practice: tighter than it looks, but workable. Our budget function solves Mutable overlay (section 5): strong +1 from live experience. We shipped with the Two implementer notes for anyone emitting the package from a hook (each took real debugging to learn):
Happy to contribute the encode/decode reference (xahau-nft-remark-composer) and our budget/verification code to whatever test-vector suite this standard ends up with. Verifying the sha256 of the reassembled image against the schema manifest has caught every partial-load we've ever had - a normative test-vector set around exactly that would be my main ask. |
Uh oh!
There was an error while loading. Please reload this page.
Status of this draft
xahau-nft-remark/v1is already deployed on Xahau mainnet. This documentwrites down what is in use and proposes a small extension mechanism on top.
The reference token throughout is an Odin's Eyes Raven, issued by
rodinGM9xiKq7rDDnLPyieREkqGJ6zKyb, URITokenEEF55FAE…227C1351.Everything in §1–§4 was read off that object and verified, including its image
checksum.
schemaremark; §2 the.fragment separator and numeric ordering; §3 themetafamily; §4 theimgfamily withmime/parts/sha256; theskey by which a payload names its own type; theremarks:<family>reference scheme.meta2overlay and its merge rules; §5 the#separator for sets; §6 declaring families governed by other standards; §9 a companion standard using both. None of it is required to read any token minted so far.Where this document and deployed practice disagree, practice is correct and this
document should be corrected.
Abstract
Xahau's
Remarksamendment lets a URIToken carry key/value annotations on theledger object itself. This proposal defines a convention for using them to hold
an NFT's complete metadata — including its image — on-ledger, with no
off-ledger fetch required to render the token.
It specifies how a value larger than one remark is split and reassembled, how
binary content is carried and integrity-checked, how a reader identifies what it
is looking at from the remark name alone, and how a token may declare annotations
governed by other standards.
It also proposes a split between immutable identity and mutable state (§3.1), so
that a token's attributes can evolve without giving up the guarantee that the
parts an issuer promised at mint cannot be quietly rewritten afterwards.
§9 describes a companion standard,
xahau-nft-signature/v1, which recordsthird-party endorsement of a token in a way that does not rest on the issuer's
word — the first concrete use of the extension mechanism proposed here.
Motivation
A URIToken's
URIfield is a pointer, and conventionally everything a walletwants to display sits behind it. That makes every rendering of the token a
request to somebody's web server, and tokens outlive web servers. IPFS moves the
problem rather than solving it: content addressing guarantees that what you fetch
is what was pinned, not that anyone is still pinning it.
Remarks allow the alternative: the metadata and the image live in the ledger
object. A Raven's thumbnail is 6,506 bytes of WebP stored across 26 remarks, and
rendering it requires nothing but a ledger read.
Three problems follow immediately, and this standard exists to answer them:
reader must know that
img.0…img.25are fragments of one file rather than26 separate things.
is real and shared between metadata, image and anything else.
explorer can only render unknown remarks as opaque blobs — or worse, mangle a
split value by rendering its fragments as separate documents.
Rationale
Why a
schemaremark. One place declares what the object as a whole follows,so a reader knows what it is holding before parsing anything, and knows how many
fragments to expect before reassembling.
Why raw bytes rather than base64 for binary families. A remark value is a
blob; it does not need text encoding. Base64 would cost 33% more, and at 256
bytes per remark against a 32-remark ceiling, a third of the budget is a third of
the image.
Why
sha256in the descriptor. Reassembly is the one place this conventioncan go silently wrong — a missing fragment, a mis-ordered one, an off-by-one on
the last. A checksum turns "the image renders as garbage" into a check that
either passes or does not. It is verified on the Raven and matches exactly.
Why numeric rather than lexical fragment ordering. Indices are not zero
padded in production (
img.2andimg.10both exist). Sorting those as stringsputs
img.10beforeimg.2and corrupts the file. This is a conformance trapworth stating loudly, because a lexical sort produces a plausible-looking result
that is wrong only in the middle.
Why the separator grammar belongs to this standard rather than to each
extension. If every extension chose its own separator, a generic reader would
need to know every extension to parse any name. Reserving the grammar centrally
lets a reader that knows only this document split any name into
(family, kind, index), group fragments, count members, and correctly presentwhat it cannot interpret.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as
described in RFC 2119.
1. The
schemaremarkA token following this standard MUST carry a remark named
schemawhose value isa UTF-8 JSON object. From the reference token, verbatim:
{ "std": "xahau-nft-remark/v1", "meta": { "parts": 5 }, "img": { "mime": "image/webp", "parts": 26, "sha256": "516BDE7EB6B9FFA4B0331EED75C341F0C1D5110B01665EE24F9AD5510F8CB348" } }stdxahau-nft-remark/v1.metametafamily (§3).meta2meta2family (§3.1).imgimgfamily (§4).Every other top-level key is a family descriptor (§6). A reader MUST ignore
top-level keys it does not recognise, so a later revision can add core families
without breaking existing readers.
2. Remark names and fragment ordering
A remark name is one of:
.and#are reserved as separators. A family name MUST NOT containeither. A reader determines the family by splitting the name at its first
separator; a name with no separator is a singleton.
For split families:
<N>is a decimal index starting at0, ascending, with no gaps.fragments numerically, not lexically. Lexical ordering places
img.10beforeimg.2and silently corrupts the reassembled value.NOT attempt to parse one alone. Fragments are filled to the 256-byte remark
limit, so a boundary falls wherever 256 bytes lands — mid-character, mid-token,
anywhere.
or padding between them.
partsMUST equal the number of fragments present. A readerfinding a mismatch MUST treat the value as incomplete rather than use what it
has.
3. The
metafamilyA split family whose reassembled value is a single UTF-8 JSON object.
The object SHOULD name its own type with an
skey, so it is interpretablewithout reference to the
schemaremark. Both deployed collections do this(
odinseyes.raven.license/v2,xahaucards.card/v1).Beyond
s, the content of the metadata document is out of scope here — thisstandard defines the envelope, not what an application puts in it. Fields
observed in production, offered as convention rather than requirement:
namedescimageipfs://,https://, orremarks:<family>.thumbnailremarks:img.attributes{"trait_type": …, "value": …}.The
remarks:<family>reference scheme. A metadata field MAY reference afamily carried on the same ledger object by the pseudo-URI
remarks:<family>—for example
"thumbnail": "remarks:img". A reader resolves it by reassemblingthat family from the same object. This is what lets a token point at its own
on-ledger image without naming a host.
3.1
meta2— a second metadata family (proposed)A token MAY carry a second metadata family,
meta2, with the same split rules asmeta. A reader that finds both MUST merge them (§3.2) to obtain the token'sfull metadata.
The point of the split is not size — a single family can already be split
across as many fragments as the budget allows. It is mutability: two families
can be flagged differently, and that is the only thing one family cannot do.
metaandmeta2are otherwise identical. Neither is inherently the fixed one.Which is which is declared per family and enforced by the remark flags, not by
the name — an issuer may make
metamutable andmeta2immutable if that suitshow the token is written, and readers MUST NOT assume otherwise.
When a second family is needed
metameta2metawith more fragments does the same. Justified only to stage writes across separate transactions.meta.meta2alonemeta. Readers MUST still handle it.The rule the table encodes: a second family earns its place only when the two
halves differ in mutability. In every other case it is a more complicated way
to write one family, and complication in an immutable record is not free.
An issuer choosing which half a field goes in is choosing whether to guarantee
it. There is no way to have both, and pretending otherwise is the failure mode
§3.2 exists to prevent.
Declaring it
{ "std": "xahau-nft-remark/v1", "meta": { "parts": 2 }, "meta2": { "parts": 1, "mutable": true } }mutable: trueSHOULD be present on whichever descriptor describes a rewritablefamily —
meta,meta2, or in the redundant cases both. It tells a reader thatthe merged result contains issuer-rewritable content before it has looked at a
single remark flag. Its absence means the family is fixed.
partsfor a mutable family is a reserved maximum, not a count. This is aconsequence of
schemabeing immutable: a descriptor written at mint cannot beupdated later, so a
meta2that grows from one fragment to two would leavepartspermanently wrong. Therefore, for a family declaredmutable:partsis the largest number of fragments the issuer has reserved.treat a count below
partsas incomplete.0. A gap is incomplete, as in §2.An issuer SHOULD reserve more
partsthan it currently needs. Once minted, thereservation cannot be raised, and remarks not yet written still count against the
32-remark ceiling only when they are written — so reserving costs nothing until
used.
3.2 Merge rules (proposed)
Under-specified merging is worse than no merging: two explorers would show
different traits for the same token and both would believe they were right. The
rules are therefore narrow and total.
Let
Mbe the parsedmetaobject andOthe parsedmeta2object. The rulesare symmetric: nothing below depends on which of the two is mutable.
Mandevery key from
O.attributes.An issuer wanting a field to be changeable puts it in
Oand not inM.attributesconcatenates,Mfirst, thenO.trait_typeMUST NOT appear in bothM.attributesandO.attributes.sis taken frommeta, positionally — not from whichever half happensto be immutable. If only
meta2is present, frommeta2.There is deliberately no precedence rule, because rules 2 and 4 mean no conflict
can arise. That is the whole design: a mutable family may only add, never
override. Any precedence rule would mean a field in the fixed half could be
silently replaced by the rewritable one, which would make the fixed half's
immutability worthless — an issuer could promise "Legendary" immutably at mint
and overwrite it the next day.
A reader encountering a violation of rule 2 or 4 MUST treat the token as
malformed and SHOULD present the immutable half alone rather than guess. Displaying a
merged result from a token that broke the rules is displaying something the
standard cannot vouch for.
Distinguishing halves after merge
A reader that merges MUST retain which half each field came from, and SHOULD make
that visible wherever the distinction could matter to someone making a decision —
a marketplace listing above all. A trait the issuer can rewrite tomorrow and a
trait fixed at mint look identical after a naive merge, and a buyer paying for
the second while receiving the first has been misled by the reader, not by the
issuer.
4. The
imgfamilyA split family whose reassembled value is raw binary — the image file
itself, not base64 and not hex.
mimeimage/webp.partssha256A reader that finds
sha256present MUST verify it after reassembly and MUST NOTpresent the image if it does not match. A mismatch means a fragment is missing,
mis-ordered, or altered — all of which otherwise produce a corrupt image rather
than an error.
Verified against the reference token: 26 fragments (25 × 256 bytes + 106),
6,506 bytes total, beginning
RIFF….WEBP, SHA-256516BDE7E…F8CB348— matching the descriptor exactly.This family is a template for any binary payload. A future
audioormodelfamily SHOULD follow the same descriptor shape.
5. Sets: the
#separator (proposed)Reserved so that repeated, independent annotations have a
form distinct from fragments.
Each member is complete on its own; the number identifies which member, not
which piece. Losing
img.7destroys an image; losingCardSignature#07losesone annotation and nothing else. A reader can tell these two situations apart
from the name alone, which is the entire point of a distinct separator.
Members are unordered. Zero padding to a fixed width is RECOMMENDED so names sort
correctly and so implementations parsing at fixed offsets — a hook, for instance
— need no variable-length handling.
#is chosen because it cannot appear in a family name, is never escaped inJSON, and is never trimmed or collapsed in transit. Implementations placing these
names in URL paths should note it requires
%23there.6. Families governed by other standards (proposed)
A family defined by this standard (
meta,img) needs nostdin itsdescriptor — this document defines it. A family governed by another standard
declares that standard:
{ "std": "xahau-nft-remark/v1", "meta": { "parts": 5 }, "img": { "mime": "image/webp", "parts": 26, "sha256": "…" }, "CardSignature": { "std": "xahau-nft-signature/v1" } }The presence of
stdin a descriptor is what distinguishes an external familyfrom a core one. A reader MUST ignore descriptors naming standards it does not
implement, and MUST ignore keys inside a descriptor it does not recognise.
A descriptor MAY carry
form(one,split,set) to declare shape withoutrequiring the reader to know the governing standard. It is redundant with the
separator in the names themselves and is OPTIONAL.
Declaration is advisory, never authoritative. The
schemaremark is writtenat mint and is immutable, so every declaration is a forward statement about data
that may not exist yet — and a standard invented after a token was minted can
never be declared on it. Therefore:
self-describing via their own
skey, so an undeclared family remains readable.value is authoritative. It was written later, by the party doing the work.
A descriptor MUST NOT reference a schema off-ledger. A URL would make an
immutable record depend on a host that can vanish, defeating the purpose.
7. Immutability
Remarks defined by this standard SHOULD be written with
tfImmutable, with onedeliberate exception: a family declared
mutable: true(§3.1) exists preciselyto be rewritable and MUST NOT be flagged immutable.
The mistake is unrecoverable in one direction only, which is worth knowing before
minting. A family flagged immutable can never be made mutable — the flag cannot
be cleared — so an issuer that intended an evolving half and flagged it by
accident has a token that can never evolve. The reverse is recoverable: a family
left mutable by accident carries no guarantee, but nothing stops the issuer
writing what it meant to write.
The declaration and the flags MUST agree. A descriptor claiming
mutable: trueon remarks that are flagged immutable is malformed, and a reader SHOULD trust the
flags over the declaration — the flags are what the ledger enforces.
Practice currently differs and implementers should know it: XahauCards writes all
remarks immutable; the reference Raven writes none of them so
(
Flags: 0on all 32). An issuer retains the ability to rewrite any remark itdid not flag, on a token someone else owns, for as long as that token exists.
Note the limit of the guarantee: a flagged remark cannot be changed or removed,
but the set of remarks on an object is not closed. An issuer can always add
new ones. That is what makes post-mint extension possible (§6), and it is a
standing capability implementations SHOULD disclose rather than leave implicit.
8. Limits and budget
tecTOO_MANY_REMARKSOwnerCountunchangedThe 32-remark budget is the binding constraint of this standard, and it is
shared. The reference Raven spends 1 on
schema, 5 onmetaand 26 onimg—all 32. It has no room for any further annotation, ever, because remarks cannot
be removed once immutable and the object cannot hold more.
Issuers SHOULD budget deliberately: an image large enough to fill the object
forecloses every future extension of that token. A smaller thumbnail on-ledger
with full-resolution art addressed elsewhere — which is what the Raven's
image/thumbnailsplit does — preserves room.Implementations writing a remark to an object whose contents they do not fully
control SHOULD check the current count first. An emitted transaction that exceeds
the cap fails in a later ledger, after the originating transaction has already
reported success.
9. Companion standard:
xahau-nft-signature/v1The problem it solves
Everything in a remark is written by the token's issuer. That is fine for
metadata — the issuer is the natural authority on what its own token is called —
but it breaks the moment a remark makes a claim about somebody else. A remark
reading
"endorsed by Alice"is the issuer asserting something about Alice, andthe issuer could write it whether Alice ever agreed or not.
Remarks alone therefore cannot record third-party endorsement. Anything built
naively on them records the issuer's word and dresses it as somebody else's.
The mechanism
Alice sends her own transaction referencing the token. The issuer then writes
a remark recording who endorsed it and, critically, the hash of that
transaction. Any reader can fetch it and read its
Accountfield directly.The issuer can still write a remark naming Alice without her involvement — but it
cannot produce a validated transaction signed by her, so the claim fails the
moment anyone checks.
Shape
A set family (§5), because a token may carry several independent
endorsements, each complete on its own. The family name is the implementation's
choice, declared per §6:
Each member's value:
{"s":"xahau-nft-signature/v1","by":"rEndorserAddress","tx":"<64 hex>"}sxahau-nft-signature/v1.bytxRoughly 113 bytes — one remark, no splitting. Members SHOULD be written with
tfImmutable: an endorsement the issuer can rewrite is not an endorsement.Verification
A reader MUST NOT treat the remark as proof by itself:
tx.Accountequalsby.uses.
Steps 1–3 make the record trustless. Step 4 stops an unrelated transaction by the
same account being presented as an endorsement of this token. A reader skipping
these and rendering
byas an endorsement is displaying the issuer's claim asthough it were the endorser's — the exact failure the standard exists to prevent.
What it does not establish
That
byis any particular person. Binding an account to a human identity isoutside this standard and outside any on-chain mechanism: it rests on the account
being publicly known, or on an
xrp-ledger.tomlat the account'sDomain.Implementations SHOULD NOT imply more certainty than they have.
Why it belongs in this discussion
It exercises both proposed additions and shows what they are for. §5 gives it a
name form that says "each of these is whole" rather than "these are fragments",
so a reader that has never heard of endorsements still handles the remarks
correctly. §6 lets a token advertise the family without the standard having to
exist when the token was minted — which matters, because tokens outlive the
standards written for them.
Backwards Compatibility
No amendment, no gating, no transactor change. Nodes require no change.
Tokens not following this convention are unaffected.
For readers:
schemaremark is not following this standard. A reader SHOULDrender what it can and MUST NOT fail.
declarations. They remain fully readable under §1–§4.
#as a separator conflicts with nothing deployed: production tokensuse
.and no other separator.Security Considerations
Remarks are the issuer's writing. Everything in them is an assertion by the
issuer, including assertions that appear to concern third parties. A remark
reading "endorsed by Alice" is the issuer's claim, not Alice's. Standards
recording third-party involvement MUST provide verification that does not rest on
the issuer —
xahau-nft-signature/v1does this by recording the hash of theendorser's own transaction, which any reader can fetch and check.
Metadata is untrusted input.
name,descand trait values are arbitraryissuer-supplied strings. Readers MUST escape them before rendering and MUST NOT
interpolate them into markup, shell commands, or SQL.
Binary families are untrusted content.
imgbytes are attacker-influenced towhatever extent the issuer is untrusted. Readers SHOULD decode them in a
hardened path, SHOULD NOT trust the declared
mimeover content sniffing forsecurity decisions, and SHOULD enforce a size bound before decoding. Verifying
sha256proves integrity — that the bytes are the ones the issuer committed to.It proves nothing about safety.
Reassembly is a parsing surface. Enforce the declared
partsand a totalsize bound before concatenating or parsing.
Off-ledger references degrade the guarantee.
imagefields pointing atipfs://orhttps://reintroduce the liveness dependency this standard existsto remove. That is a legitimate trade for full-resolution art, but a reader
SHOULD NOT present off-ledger content as though it carried the same permanence as
on-ledger content.
Immutability is a claim to verify, not assume. Check the flags rather than
trusting that the issuer set them — deployed practice varies (§7).
A merged view hides who can change what. After merging
metaandmeta2(§3.2) every field looks alike, but half of them are rewritable by the issuer at
will. A reader that presents the merged result without distinguishing the halves
is showing a buyer guarantees the token does not carry. This matters most exactly
where money changes hands: a marketplace displaying a rewritable trait as though
it were fixed at mint has misled the buyer, and the issuer never had to lie to
achieve it. Readers MUST retain provenance through the merge (§3.2).
Neither half may override the other. Rules 2 and 4 of
§3.2 forbid a key or
trait_typeappearing in both halves, and that prohibitionis load-bearing rather than tidiness: a precedence rule allowing
meta2to winwould let an issuer promise anything immutably at mint and replace it the next
day, making
meta's immutability worthless. A reader finding a collision MUSTtreat the token as malformed rather than resolve it.
Test Plan
Names and ordering
name alone.
img.2,img.10) reassemble in numericorder. A lexical sort must fail this test.
partsdisagreeing with the fragments present is treated as incomplete.Metadata
metareassembles and parses as JSON.meta(parts: 1) is handled.remarks:<family>in a metadata field resolves against the same object.Mutable overlay
metaandmeta2merge per §3.2;attributesconcatenate in order.trait_typepresent in both halves is rejected as malformed.meta2with fewer fragments present thanpartsreserved is treated ascomplete, not truncated.
meta2rewritten between two reads yields the new value on the second.metamutable andmeta2immutable merges identically to thereverse — a reader keying off the family name rather than the flags fails this.
mutable: trueover immutable remarks is reported asmalformed, with the flags believed over the declaration.
Binary
imgreassembles to bytes whose SHA-256 matches the descriptor.not presented.
Extensions
s.rather than fatal.
Limits and flags
§1–§4 can be tested against any Raven on mainnet. §5, §6 and §9 have no production
deployment yet; an implementer adding them should expect to write the live cases
themselves, asserting on what the ledger holds after the fact rather than only on
transaction results — a remark that never landed and a remark that landed wrong
both leave a successful transaction behind.
Appendix A: the reference token, walked
URIToken
EEF55FAE…227C1351, issuerrodinGM9xiKq7rDDnLPyieREkqGJ6zKyb,URI=odinseyes:raven:v2:0007011f. 32 remarks:schema,meta.0–meta.4,img.0–img.25.schema. Declaresxahau-nft-remark/v1, metadata in 5 fragments, aWebP image in 26.
meta. Sortmeta.0…meta.4numerically, concatenate →1,158 bytes of UTF-8 JSON. It declares
"s":"odinseyes.raven.license/v2"andcarries
name,desc,attributes, and arenderblock describing how thefull-resolution art is deterministically reproduced from the traits.
img. Sortimg.0…img.25numerically — lexical orderingcorrupts this file — and concatenate → 6,506 raw bytes beginning
RIFF….WEBP. SHA-256 matches the descriptor."thumbnail":"remarks:img"points at the family justreassembled.
"image"points at IPFS for the full-resolution render.The token renders completely from a single ledger read. The IPFS reference is an
enhancement, not a dependency.
Appendix B: a token using §5, §6 and §9
XahauCards, Xahau testnet — the same envelope carrying an external family:
A reader knowing only §1–§4 correctly identifies
CardSignature#07as onecomplete member of a set, presents it as an unrecognised annotation, and does not
mistake it for a fragment of something larger. A reader implementing
xahau-nft-signature/v1additionally fetches the transaction named intxandconfirms its
Accountis thebyaddress — the step that makes the annotationworth anything, because the remark itself is only the issuer's word.
Appendix C: an evolving token
A token whose rank changes with use.
metais flagged immutable;meta2is not.Merged, per §3.2:
{ "s": "example.card/v1", "name": "Ember Drake", "image": "remarks:img", "attributes": [ { "trait_type": "Species", "value": "Drake" }, // from meta, fixed { "trait_type": "Rarity", "value": "Legendary" }, // from meta, fixed { "trait_type": "Rank", "value": 3 }, // from meta2, changeable { "trait_type": "Battles", "value": 47 } // from meta2, changeable ] }Three things to notice:
Rarityis inmetaand nowhere else. The issuer promised it at mint andcannot take it back. Had it also appeared in
meta2, the token would bemalformed under rule 4 — precisely so that no issuer can promise
Legendaryimmutably and quietly demote it later.
Rankis inmeta2and nowhere else. It is expected to change, so it is notpromised.
meta2.1is reserved but unwritten:partsis 2, one fragment ispresent, and §3.1 says that is complete rather than truncated. Rewriting
Rankto 4 means rewriting
meta2.0, which is not immutable and so may be rewritten.A reader must not flatten the distinction. The comments above are the
provenance §3.2 requires be retained. A marketplace showing all four traits
identically tells a buyer that
Rank 3is as fixed asLegendary. It is not.Nothing here depends on
metabeing the immutable one. The same token writtenwith
metamutable andmeta2immutable behaves identically: the flags decide,the descriptors declare, and a reader that keyed off the family name rather
than the flags would get it exactly backwards.
Reference implementations
schema,meta,img)rodinGM9xiKq7rDDnLPyieREkqGJ6zKyb. Every claim in those sections was read off that collection and verified.xahau-nft-signature/v1)Anyone can verify §1–§4 independently: read any Raven's remarks, reassemble them
by the rules above, and check the image SHA-256 against the descriptor.
All reactions