Host security rules
The token Scripts enforce some properties on every node, whatever any wallet might do. Other properties are your app’s job. This page lists both, and then the rules (S01 to S12) that a host app must follow. The IDs match those in libtokensv’s own doc/spiffy/SECURITY.md.
What the Script guarantees
Section titled “What the Script guarantees”| ID | Guarantee |
|---|---|
| G1 | Only the owner can transfer or end a token: each spend needs a signature by the key the header names. |
| G2 | A token cannot be copied: each spend rebuilds the token’s own transaction and its parent and compares txids, and a copied output has another outpoint. |
| G3 | A token traces back to an issuance its issuer signed: the first spend checks the issuer’s attestation, and each later spend proves the one before. |
| G4 | A token cannot be relabelled: the token id is in every header and the rebuild compares it. |
| G5 | One NFT output gives at most one token output. |
| G6 | A token has at most one successor: only one transaction can end it, and only one can spend that end’s output 0. A wallet checks a successor with the links. |
| G7 | Nobody can change a transfer after it is signed: every input signs SIGHASH_ALL, and the transaction is version 1. |
What it does not guarantee
Section titled “What it does not guarantee”| ID | Not guaranteed | What to do |
|---|---|---|
| N1 | That a transfer will be mined. The payer can spend the token twice before a block. | Count a token as received only when the network has the transfer (S08). |
| N2 | That the issuer is honest. An issuer can issue any number of tokens. | Trust the issuer as a person or company; the anchor names it. |
| N3 | That a reissue keeps the token’s identity: the successor has a new id. | Follow the links; show the holding’s originId. |
| N4 | That a token without links is the newest form of its lineage. A holder can leave the links out. | A token without links shows its own id as its origin. That is correct, but tells less. |
| N5 | Privacy. Every transfer contains the token’s history since issuance, and the change goes to the funding coin’s key. | Do not use lineage tokens where the history must stay private. |
Rules for the host
Section titled “Rules for the host”S01: Register before use. Register each plugin before the wallet loads or receives a Transaction. libspiffy only finds the owner of a token Output through its plugin. A token received while its plugin is not registered is not recorded, and recovering it later (by importing the Transaction again) is not tested.
S02: Use the correct network. A plugin names owner Addresses on its own network, which is NetworkType.TEST by default. With the wrong network, the wallet does not find its tokens.
S03: Protect the Rabin Key. Anyone with the Issuer’s Rabin Private Key can issue tokens that verify as the Issuer’s. Keep it in secure storage, and never in plugin params, logs, messages, or fixtures that leave the machine. The plugins only take Attestations.
S04: Protect the wallet keys. The plugins never get a Private Key. They ask the wallet for each Signature by key hash. Do not hand one a key in some other way.
S05: Do not spend a token as a coin. A token Output also accepts a spend with only the owner’s Signature, and such a spend ends it. libspiffy keeps token Outputs out of ordinary payments only while their plugin is registered. Do not build your own Transactions that spend token Outputs.
S06: Issue to the Issuer. The plugins always issue to the issuing wallet’s own key, and we distribute with a transfer. The first spend checks the Attestation, so a bad one fails with the Issuer, never with a customer.
S07: Approve irreversible operations. end needs irreversible: true, and reissue needs the token’s Issuer key hash. Treat these params as statements of intent, and pass them only after a person or a policy has approved the operation.
S08: Count a token only when the network has it. A valid BEEF shows that the funding history is in blocks, and the receive check shows that the bytes are a valid token for you. Neither one shows that the network accepted this transfer. Show “received” only after ARC reports SEEN_ON_NETWORK or MINED.
S09: Do not change the bytes. The next spend rebuilds a token Transaction byte for byte. Keep and pass on the raw bytes you received. Do not re-serialise them with a library that may change the encoding, and never make a Transaction of a version other than 1.
S10: Keep the links. Links are not on chain in a form that a wallet can find. If you lose them, later holders cannot find a reissued token’s origin (although the token stays valid).
S11: Check the anchor. The receive check verifies the anchor’s Signature, its txid, its Ed25519 key and the Rabin Key it names. Get the anchor with its merkle proof, and check the proof against your headers. A valid Signature on an unmined anchor does not prove that it is the Issuer’s published identity.
S12: One operation at a time per token. libspiffy refuses a second payment that spends a token Output already held, but it checks against its read model, so two payments started together can both pass. Take a lock per token in your app.
Rules for particular tokens
Section titled “Rules for particular tokens”- Fuelled family. The fuel is part of the token, not spendable balance, so show it with the token. A payee receives whatever fuel the earlier transfers left, so show it before acceptance. Use your fee policy’s rate, since whatever a transfer does not keep in the token goes to the miners. A top-up stays in every later Transaction (143 bytes), so prefer a reissue to refuel when the Issuer is available.
- Voucher NFT. The fuel is the Issuer’s. The Issuer cannot take a voucher before its expiry, but can at any time after it. Do not accept a voucher whose expiry has passed or is near, and check for foreign spends before showing an expired one. Send a forced redemption only after the expiry.
- Note NFT. Show what the holder gets on each ending before acceptance. After
callAtthe Issuer can end the note at any time. Send a mature exit or a call only after its height. - Listed NFT. A listing is an offer to everyone, and cannot be reliably withdrawn. Always pass
maxPricetobuy. Check that the listed Output is unspent before buying. Never list and swap the same Output. - The swap. The price and payee are fixed by the Signatures, so always pass the agreed
pricetoswapAcceptandswapTake. A buyer checks the token before signing (the plugin requiresanchorTx). A signed half is an open option until its Input is spent.
Known gaps
Section titled “Known gaps”| ID | Gap | Effect |
|---|---|---|
| K1 | The BEEF of a plugin payment has no ancestors and no proofs. | A payee’s libspiffy is expected to refuse it (not tested). Send the payee a BEEF that carries the transfer’s ancestry. |
| K2 | libspiffy’s hold check uses its read model, not the wallet aggregate. | Two concurrent payments of one token can both be built (S12). |
| K3 | A token received while its plugin is not registered is not recorded. | The wallet does not show it. Recovery is not tested. |
| K4 | An identity anchor cannot rotate its Rabin key. | A new key needs a new anchor and new issuances. |
| K5 | The transfer message envelope is not decided. | Each app defines how it carries the BEEF, anchor, links and payload. |

