Threat model
This section is written for security researchers. It sets out what the Lineage Tokens assume, what we allow an attacker to do, and the attacks we have tested the tokens against. Every claim here points at a test in libtokensv 0.2.0, so that you can start your investigation from code that runs, and not from our description of it.
If you haven’t read Concepts yet, do so first. The defences below are all phrased in terms of its parts: the header, the code, the record, and steps A and B.
What we assume
Section titled “What we assume”The tokens rest on a short list of assumptions. Anything outside this list is something we expect the Script to defend.
- Consensus runs the Script. A Transaction that spends a token Output is mined only if the token’s Script accepts it, under the Bitcoin SV rules for a version 1 Transaction.
- The hash functions hold. SHA-256 (and so the txid) is collision and second-preimage resistant. Steps A and B compare txids, so a second preimage of a txid would defeat them.
- Signatures hold. ECDSA over secp256k1, for the owner’s Signatures, and the Rabin signature scheme (1024-bit moduli in the tests), for the Issuer’s Attestation and the attesters’ resets.
- The Issuer is who the anchor says it is. The Issuer’s identity anchor is mined, and the payee checks its merkle proof against its own block headers.
- The network settles double-spends. Two valid spends of one token Output can both exist until one of them is mined.
- For the resettable fungible token only: at most one of its three attesters is dishonest.
What the attacker may do
Section titled “What the attacker may do”We give the attacker a great deal of room:
- Write any Transaction they like, with any bytes, and pay any fee. In particular, they can copy a token Output byte for byte, write a header naming a real Token ID, and build a record of their own choosing.
- See every Transaction ever made with the token, since every transfer carries its full history.
- Hold a token honestly, and then try to do more with it than its owner should: inflate an amount, skip a hop limit, pay less than a price.
- Relay someone else’s transfer, and change it in flight.
- Run several Issuer keys, attester keys and identity anchors of their own.
What the attacker can’t do is break the assumptions above, or get hold of a key they don’t own.
What the Script guarantees
Section titled “What the Script guarantees”Under those assumptions, the Script itself guarantees the following, on every node, whatever any wallet does. The IDs match libtokensv’s own doc/spiffy/SECURITY.md and our Host security rules.
| ID | Guarantee | How |
|---|---|---|
| G1 | Only the owner can transfer or end a token. | Each spend needs a Signature by the key whose hash is the header’s owner, on both paths. |
| G2 | A token can’t be copied. | Step A rebuilds the token’s own Transaction and compares its txid with the outpoint being spent. A copy has a different outpoint. |
| G3 | A token traces back to an issuance the Issuer signed. | Step B proves each hop’s parent; at the issuance’s first spend, the Script checks the Issuer’s Attestation. |
| G4 | A token can’t be relabelled as another token. | The Token ID is in every header, and the rebuild compares it. |
| G5 | One NFT Output gives at most one NFT Output. | The Script fixes the Outputs of a transfer (the forward covenant). |
| G6 | A token has at most one successor. | Only one Transaction can spend the end’s Output 0. |
| G7 | Nobody can change a transfer after it is signed. | Every Input signs with SIGHASH_ALL, and the Transaction must be version 1. |
The Attacks page goes through each attack these guarantees answer, and the Trust and open questions page covers what they don’t.
How the defences are tested
Section titled “How the defences are tested”The tests live in libtokensv’s test/ directory, one file per kind of token (lt_nft_test.dart, lt_ft_test.dart, and so on). Four things about the way they are written make them a useful starting point.
Every spend runs through a Script interpreter. The tests run each Input through dartsv’s interpreter, as a node would, with the flags FORKID, LOW_S, NULLFAIL, MINIMALDATA, MINIMALIF, SIGPUSHONLY and UTXO_AFTER_GENESIS. Before it does, the test checks that dartsv reads and writes the Transaction back to exactly the same bytes, since a single changed byte changes the txid that the next hop rebuilds.
A refusal is asserted on the values the Script compared. It is not enough for an attack to fail. It must fail at the check we claim stops it. Most refusals are asserted with a matcher, refusedComparing(a, b), which requires the Script to stop at an OP_EQUALVERIFY whose two operands were exactly a and b. For example, the copied Output fails because the Script compared the original’s txid with the copy’s, and for no other reason.
Each removable check is removed once. The Script generator takes switches, e.g. LtScriptGen.nft.code(stepB: false), which build the token’s Script without one particular check. The tests use them to show that, without that check, the attack succeeds. A fabricated parent is refused by the real Script, and accepted by the Script without step B. This tells you which check carries the weight in each case. The switches exist only for these tests.
The NFT and every mode have been mined on a regtest node. The *_localnet_test.dart files broadcast and mine the Transactions on a local node. The fungible token has been tested through the interpreter only.
To run them yourself:
git clone https://github.com/twostack/libtokensv.gitcd libtokensvgit checkout v0.2.0dart pub getdart test test/lt_nft_test.dart test/lt_ft_test.dartThe regtest tests are skipped unless a local node is running. libtokensv’s
doc/lt/LT.mddescribes the specification the tests are written against.

