The issuer
Every Lineage Token names its Issuer, and every payee checks that the Issuer really did sign it. The setup only needs to be done once per Issuer, and goes as follows:
- Generate a Rabin Key pair, and keep it secret.
- Build an Identity Anchor Transaction that publishes the Rabin Key’s hash under an Ed25519 Signature, and broadcast it.
- For each token you issue, sign its ID with the Rabin Key (this is the Attestation), and put it on the issuance before you distribute it.
Payees need your anchor Transaction, with its merkle proof, to check your tokens. You can either publish it, or send it along with each transfer.
The Rabin Key
Section titled “The Rabin Key”Why Rabin? Rabin Signatures are what the token Script verifies, and checking one in Script takes only a few arithmetic opcodes, where ECDSA would take thousands. We generate a 1024-bit key pair once:
// We do this once per Issuer. Keep p and q secret, as you would a Private Key,// since whoever holds them can issue tokens that verify as yours.final RabinKeyPair rabin = Rabin.generateKeyPair(1024);The examples use a key that was generated once and fixed in test/support/fixtures.dart, so that every run builds the same Transactions:
/// The Issuer's Rabin Key pair: two 512-bit primes, both 3 mod 4, made once/// with `Rabin.generateKeyPair(1024)` and fixed here. A real Issuer/// generates its own and keeps p and q secret.final issuerRabin = () { final p = BigInt.parse( 'dd6d4d5d351694548f624ffd855350a717792a8e448c93613a05566da57b79b4834749bbce229f5f1bcb9e92dd27bf33bffc1c651181184f8d1ae1e6646854cb', radix: 16); final q = BigInt.parse( 'd988da744baea09093cc648f7f30d815fa5b7d470b5850f5b8ac5593cfe391e9f16dd8396d04b424b3c4f222847a51a03f030b4b58c4eb7691eff3f3a499cd73', radix: 16); return RabinKeyPair(p * q, p, q);}();The Identity Anchor
Section titled “The Identity Anchor”The anchor is a Transaction with three Outputs:
- the change,
- a MAP Output with the Issuer’s metadata (including
rabinPKH, the hex HASH160 of the Rabin Key), - an AIP Output with an Ed25519 Signature over that metadata.
LtIdentity.anchorMetadata adds the Rabin Key’s hash to whatever else you publish, and IdentityAnchorBuilder builds and signs the Transaction from a funding coin:
/// The Issuer's Identity Anchor: a Transaction that publishes the Rabin/// Key's hash (MAP metadata) under an Ed25519 Signature (AIP). It is funded/// here from a made-up coin of [anchorKey]. A real Issuer pays for it from/// its wallet and broadcasts it once.Future<Transaction> buildAnchor(RabinKeyPair rabin, {String name = 'Example Issuer'}) async { final anchorKey = keyOf(7000); final address = anchorKey.toAddress(networkType: NetworkType.TEST); final p2pkh = P2PKHLockBuilder.fromAddress(address); final funding = (TransactionBuilder() ..spendFromOutpoint(TransactionOutpoint('ab' * 32, 0, BigInt.from(200000), p2pkh.getScriptPubkey()), TransactionInput.MAX_SEQ_NUMBER, P2PKHUnlockBuilder(anchorKey.publicKey)) ..spendToLockBuilder(p2pkh, BigInt.from(546)) ..spendToLockBuilder(p2pkh, BigInt.from(199000)) ..withOption(TransactionOption.DISABLE_DUST_OUTPUTS)) .build(false);
// The Issuer's Ed25519 identity key, from a fixed seed. final ed = Ed25519(); final wand = await ed.newSignatureWandFromKeyPair(await ed.newKeyPairFromSeed(List<int>.filled(32, 7)));
final metadata = LtIdentity.anchorMetadata(rabin, {'name': name}); return IdentityAnchorBuilder(metadata).buildTransaction( funding, DefaultTransactionSigner(sighashAllForkId, anchorKey), anchorKey.publicKey, address, wand);}The Ed25519 key is the Issuer’s identity, so keep its seed as safe as the Rabin Key. In an app, the funding Transaction is one of the wallet’s own. Broadcast the anchor once, and keep its txid.
Anyone can read the anchor back and check its Signature:
final anchor = await buildAnchor(issuerRabin, name: 'Example Issuer');
// Let's read back what anyone can see in the anchor, and check itfinal metadata = IdentityAnchorBuilder.extractMetadata(anchor);expect(metadata['name'], 'Example Issuer');expect(metadata['rabinPKH'], LtIdentity.anchorMetadata(issuerRabin)['rabinPKH']); // HASH160 of the keyexpect(await IdentityVerification.verifyIdentityAnchor(anchor), isTrue); // the AIP SignatureThe Issuer in the builders
Section titled “The Issuer in the builders”LtIdentity.issuer combines the Rabin Key with the anchor into the LtIssuer that the builders and the Attestation use. It throws if the anchor publishes a different Rabin Key:
final anchor = await buildAnchor(issuerRabin);// The Issuer as the builders use it: the Rabin Key, the anchor's txid and// its Ed25519 key. This throws if the anchor names another Rabin Key.final LtIssuer issuer = LtIdentity.issuer(issuerRabin, anchor);The Attestation
Section titled “The Attestation”We put issuer.pkh (the Rabin Key’s hash) in the issuance’s header, and then sign the issuance’s txid. The Attestation goes in the issuance’s x, where the first transfer picks it up. LtIdentity.attestationError runs the same check that the Script will run, and LtIdentity.verify runs the payee’s check against the anchor:
final issuance = b.issue(first: coinOf(k), fund: coinOf(k), issuerPkh: issuer.pkh, owner: pkhOf(k));issuance.x = issuer.attest(issuance.txid);// The Script's own check, without the anchorexpect(LtIdentity.attestationError(issuance), isNull);// The payee's check, against the anchorexpect(await LtIdentity.verify(issuance, anchor), isNull);Check the Attestation before you build on it. A transfer whose Attestation is wrong is refused by every node:
// Check an Attestation before building on it. The Script refuses a// transfer whose Attestation does not verify, so every node would.iss.x = issuer.lt.attest(sha256([1, 2, 3])); // signed over the wrong idexpect(LtIdentity.attestationError(iss), 'the issuer\'s Rabin signature does not verify');final t1 = b.transfer(from: iss, k: 1, ownerKey: issuerKey.lt, fund: coinOf(issuerKey), outs: [LtOut(pkhOf(alice))]);expect(() => spends(t1.raw, 0, iss.lock(1, b.code), 1), refusedByScript);The payee’s check likewise fails for a token whose anchor is not the Issuer’s:
// Another issuer's anchor does not vouch for this token.final other = await buildAnchor(attesterRabins[0], name: 'Someone Else');expect(await LtIdentity.verify(issuance, other), isNotNull);In a wallet
Section titled “In a wallet”The plugins take the Attestation as a hex param on the first transfer of an issuance. The Issuer’s app computes it from the issuance’s txid.
Please note the byte order here. The plugin params use display order, while the Attestation signs the internal order.
/// The Issuer's Attestation over [issuance]'s txid, as the plugin takes it:/// hex, over the txid in internal byte order (the reverse of `issuance.id`).String attestationFor(LtIssuer issuer, Transaction issuance) => hex.encode(issuer.attest(hex.decode(issuance.id).reversed.toList()));The plugin checks the Attestation (attestationError) before it builds the transfer. See Wallet plugins.

