chaintoken

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 2, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package chaintoken reads and writes the BEEF a host admits a mined chain token in, and the token's own output.

A chain token is a mined PushDrop output that the next token spends, so that a chain of them is an ordered state a fork of which is a double spend. A host that admits one reads its predecessor from the parent the submission's BEEF carries, never from what the topic holds, so a verdict depends on the submission's bytes and the host's headers alone. That only works when a BEEF holds exactly what the object needs: nobody can attach anything to it, and two hosts given the same bytes decide the same.

The package holds the parts of that rule no application's record enters:

  • ReadWire reads a BEEF exactly as declared on the wire, before any parser merges two proofs of one block or collapses a transaction listed twice, so a rule counts what was sent;
  • MinimalPath and MergedPath hold a Merkle path to exactly the leaves a proof needs;
  • Wire.Token, Wire.Carrier and Wire.Alone are the three shapes: a token with its parent, a carrier with its funding tree, and a mined transaction alone;
  • TokenBEEF assembles the first shape from a stored token and its parent, which is how a replayer brings a chain to another host;
  • ReadOutput, Output.LockedTo and Output.SignedBy read a token output and hold it to the canonical script and a strict field signature, and Spends lists what a token spends of its parent.

The application supplies its tags, its record codec, its derivation and its transition rules. This package decides no admission.

Index

Examples

Constants

View Source
const DefaultMaxBEEF = 256 << 10

DefaultMaxBEEF is a BEEF bound for a host that names none, and the floor for a host's own: 256 KiB.

Variables

View Source
var ErrBEEF = errors.New("chaintoken: BEEF is not exactly what the object needs")

ErrBEEF is a BEEF that is over its bound, does not parse as exactly one BEEF with nothing after it, lacks its subject, or holds anything but what the object needs.

View Source
var ErrSignature = errors.New("chaintoken: signature is not strict low-S DER")

ErrSignature is a signature that is not the one strict, low-S DER encoding of its (r, s).

Functions

func CheckDER

func CheckDER(sig []byte) error

CheckDER refuses, as ErrSignature, a signature that is not the one strict, low-S DER encoding of its (r, s): 8 to 72 bytes, and re-serializing the parsed value gives back the same bytes. A signature with another encoding verifies as well as the strict one, so a host that took it would take two byte strings for one signature.

func MergedPath

func MergedPath(mp *transaction.MerklePath, a, b chainhash.Hash) bool

MergedPath reports whether mp is the union of the minimal paths of a and b (a == b for one transaction): at the lowest level a and b, each flagged as a txid and no other leaf flagged, and their siblings; above that only the siblings of their ancestors. A sibling that is itself an ancestor of the other transaction can be computed and MAY be left out, as BEEF writers do when they merge two paths of one block; every other sibling MUST be there, and nothing else may be.

func Mined

Mined reports whether tx carries a proof that verifies against the headers. No transaction, no proof or no headers means nothing is mined.

func MinimalPath

func MinimalPath(mp *transaction.MerklePath, txid chainhash.Hash) bool

MinimalPath reports whether mp proves txid with only the leaves that needs: at the lowest level the txid, flagged as one, and its sibling (a hash or a duplicate), and above that exactly the one sibling each level needs. A proof carrying anything else carries bytes a host would store and serve for nothing.

It judges the leaves of the levels the path has, not how many levels there are or what root they give: whether the proof is true is the header check's (Mined).

func Stored

func Stored(b []byte, bound int) (*transaction.Transaction, error)

Stored reads a token as a host stores it: the token's transaction alone, with its proof, as an overlay engine keeps a proven transaction (an Atomic BEEF, or any BEEF whose subject is the token). It returns the subject transaction, without a parent. An index and a reader read a stored token this way and never admit it again. bound is the BEEF bound, DefaultMaxBEEF when zero.

func TokenBEEF

func TokenBEEF(token, parent *transaction.Transaction) ([]byte, error)

TokenBEEF assembles the BEEF a mined token is admitted in (Wire.Token): a BEEF V1 of exactly the parent then the token, each with its own proof, or one merged proof, flagging both txids, when both are in one block. A replayer builds it from the token as a host stores it, alone with its proof, and the parent: the token before it, or for the first token of a chain the transaction whose output it spends.

Neither transaction is changed: the paths are copied before they are merged.

func VerifyField

func VerifyField(key *ec.PublicKey, signed, der []byte) bool

VerifyField checks a PushDrop field signature: strict low-S DER (CheckDER), over SHA-256 of signed, the fields concatenated, under key.

Types

type Entry

type Entry struct {
	// Tx is the transaction, nil for a txid-only entry. Its MerklePath is
	// the BUMP the entry names, and each input's SourceTransaction is the
	// earlier entry that holds it, when one does.
	Tx *transaction.Transaction
	// Txid is the transaction's txid, hash byte order.
	Txid chainhash.Hash
	// Bump is the index of the entry's BUMP, or -1 when it has none.
	Bump int
	// TxidOnly marks a BRC-96 txid-only entry.
	TxidOnly bool
}

Entry is one transaction as the BEEF declares it.

type Output

type Output struct {
	// Fields are the pushes before the signature: by convention the tag,
	// then the record.
	Fields [][]byte
	// Signature is the last push.
	Signature []byte
	// Vout is the output's index.
	Vout uint32
}

Output is a token output read leniently: the fields its lock pushes before the signature, and the signature.

func ReadOutput

func ReadOutput(o *transaction.TransactionOutput, vout uint32, nfields int) (*Output, bool)

ReadOutput reads o as a token output of nfields fields and a signature holding exactly 1 satoshi, and reports false for any other shape. It reads the script leniently (pushdrop.Fields) and decides nothing about its encoding or its key: LockedTo and SignedBy do, once the caller has decoded the record and derived the key it names.

func (*Output) LockedTo

func (t *Output) LockedTo(script []byte, key *ec.PublicKey) bool

LockedTo reports whether script is, byte for byte, the canonical lock-before PushDrop of the output's fields and signature under key (pushdrop.Script).

func (*Output) Signed

func (t *Output) Signed() []byte

Signed returns the bytes the field signature covers: the fields concatenated.

func (*Output) SignedBy

func (t *Output) SignedBy(key *ec.PublicKey) bool

SignedBy reports whether the field signature is strict and verifies under key (VerifyField).

type Spend

type Spend struct {
	// Input is the index of the token's input.
	Input int
	// Vout is the index of the parent's output it spends.
	Vout uint32
	// Output is that output.
	Output *transaction.TransactionOutput
}

Spend is one input of a token that spends an output of its parent.

func Spends

func Spends(tx *transaction.Transaction, parent *Entry) []Spend

Spends lists the inputs of tx that spend an output of parent, in input order. An input naming an output the parent does not have, or one with no locking script, is left out, as is every input that spends another transaction: those pay the token's fee and are not read.

A caller classifies each: the predecessor token it must spend exactly once, or for the first token of a chain the output it is created from.

type Wire

type Wire struct {
	// Atomic reports an Atomic BEEF (BRC-95).
	Atomic bool
	// Subject is the Atomic BEEF's subject, else the last entry's txid.
	Subject chainhash.Hash
	// Bumps are the BUMPs in the order declared.
	Bumps []*transaction.MerklePath
	// Entries are the transactions in the order declared.
	Entries []Entry
	// contains filtered or unexported fields
}

Wire is a BEEF exactly as declared on the wire: its form, its BUMPs and its transactions in order.

func ReadWire

func ReadWire(b []byte, bound int) (*Wire, error)

ReadWire reads a BEEF V1, V2 or Atomic BEEF of at most bound bytes (DefaultMaxBEEF when bound is zero) as declared on the wire. The guard walks it first, so every count and length it declares is held to the bytes present before anything is allocated for it. It then links each transaction's inputs to the earlier entries that hold their source transactions, and each proven transaction to its BUMP.

Every refusal wraps ErrBEEF: over the bound, not one well-formed BEEF, bytes after it, no transaction, or no subject transaction carried whole.

func (*Wire) Alone

func (w *Wire) Alone(tx *transaction.Transaction) error

Alone is the shape a mined transaction with no parent to read is admitted in, a sweep for one: exactly one transaction, tx, proven by the one BUMP, its own minimal path.

func (*Wire) AnyTxidOnly

func (w *Wire) AnyTxidOnly() bool

AnyTxidOnly reports a BRC-96 txid-only entry. Every shape here carries each transaction whole.

func (*Wire) Carrier

func (w *Wire) Carrier(tx *transaction.Transaction) (*Entry, error)

Carrier is the shape an unmined carrier is admitted in: exactly two transactions carried whole and one BUMP, tx unproven and the transaction its first input spends proven by that BUMP, minimally. It returns that transaction's entry, the carrier's funding tree.

func (*Wire) Entry

func (w *Wire) Entry(txid chainhash.Hash) *Entry

Entry is the entry for txid, or nil. When a BEEF lists one txid twice it is the later one.

func (*Wire) SubjectTx

func (w *Wire) SubjectTx() *transaction.Transaction

SubjectTx is the subject transaction, which ReadWire has held to be present and carried whole.

func (*Wire) Token

func (w *Wire) Token(tx *transaction.Transaction) (*Entry, error)

Token is the shape a mined token is admitted in: a BEEF V1 or V2, never an Atomic BEEF, of exactly two transactions carried whole, tx and one other that an input of tx spends, each proven: two minimal paths of two blocks, or one merged path when both are in one block. Two separate paths of one block are refused, since BEEF writers merge them. It returns the other transaction's entry, the token's parent.

BRC-95 allows in an Atomic BEEF only the subject and the ancestors needed to validate its inputs, and a mined token needs none, so its parent would be an unrelated transaction there.

Example

A chain of two tokens on a test chain, and what a host does with the second: it reads the BEEF as declared, holds it to the token's shape, finds the parent in it, and reads the predecessor from the parent, never from what it holds. The tag, the record and the derivation are the application's; here they are the ones reserved for tests.

package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/bsv-blockchain/go-sdk/chainhash"
	"github.com/bsv-blockchain/go-sdk/script"
	"github.com/bsv-blockchain/go-sdk/transaction"
	"github.com/bsv-blockchain/go-sdk/transaction/template/p2pkh"
	"github.com/bsv-blockchain/go-sdk/wallet"

	"github.com/lightwebinc/bcommon/chaintoken"
	"github.com/lightwebinc/bcommon/goldentest"
	"github.com/lightwebinc/bcommon/mint"
	"github.com/lightwebinc/bcommon/pushdrop"
)

// mineAt gives tx a stand-in proof at offset 1 of a two-transaction block
// at height, and tells the tracker that block's root.
func mineAt(tx *transaction.Transaction, height uint32, tracker *goldentest.Tracker) error {
	sibling := chainhash.Hash(goldentest.Fill(byte(height)))
	isTxid := true
	tx.MerklePath = transaction.NewMerklePath(height, [][]*transaction.PathElement{{
		{Offset: 0, Hash: &sibling},
		{Offset: 1, Hash: tx.TxID(), Txid: &isTxid},
	}})
	root, err := tx.MerklePath.ComputeRoot(tx.TxID())
	if err != nil {
		return err
	}
	tracker.Roots[height] = root.String()
	return nil
}

// A chain of two tokens on a test chain, and what a host does with the
// second: it reads the BEEF as declared, holds it to the token's shape,
// finds the parent in it, and reads the predecessor from the parent, never
// from what it holds. The tag, the record and the derivation are the
// application's; here they are the ones reserved for tests.
func main() {
	ctx := context.Background()
	key := goldentest.FixedKey()
	w, err := wallet.NewCompletedProtoWallet(key)
	if err != nil {
		fmt.Println(err)
		return
	}
	state := pushdrop.Derivation{
		Protocol: wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryApp, Protocol: "vector sample"},
		KeyID:    "state",
	}
	tag := []byte{'v', 'x', 0x01}
	tracker := &goldentest.Tracker{Roots: map[uint32]string{}, Tip: 200}

	// A mined coin to pay the fees from.
	addr, err := script.NewAddressFromPublicKey(key.PubKey(), false)
	if err != nil {
		fmt.Println(err)
		return
	}
	pay, err := p2pkh.Lock(addr)
	if err != nil {
		fmt.Println(err)
		return
	}
	payer, err := p2pkh.Unlock(key, nil)
	if err != nil {
		fmt.Println(err)
		return
	}
	coin := transaction.NewTransaction()
	nowhere := chainhash.Hash(goldentest.Fill(0x11))
	coin.AddInput(&transaction.TransactionInput{SourceTXID: &nowhere, UnlockingScript: &script.Script{},
		SequenceNumber: transaction.MaxTxInSequenceNum})
	coin.AddOutput(&transaction.TransactionOutput{Satoshis: 50000, LockingScript: pay})
	if err := mineAt(coin, 90, tracker); err != nil {
		fmt.Println(err)
		return
	}

	// The first token, then the second, which spends it and pays its fee
	// from the first one's change. Each is mined in a block of its own.
	lock := func(record string) *script.Script {
		s, err := state.Lock(ctx, w, "example.com", [][]byte{tag, []byte(record)}, true)
		if err != nil {
			panic(err)
		}
		return s
	}
	first, err := mint.Transition(lock("state 0"), 1, nil, mint.Input{Tx: coin, Vout: 0, Unlocker: payer}, pay, mint.DefaultFees)
	if err != nil {
		fmt.Println(err)
		return
	}
	if err := mineAt(first, 110, tracker); err != nil {
		fmt.Println(err)
		return
	}
	second, err := mint.Transition(lock("state 1"), 1,
		&mint.Input{Tx: first, Vout: 0, Unlocker: state.Unlocker(ctx, w, "example.com")},
		mint.Input{Tx: first, Vout: 1, Unlocker: payer}, pay, mint.DefaultFees)
	if err != nil {
		fmt.Println(err)
		return
	}
	if err := mineAt(second, 120, tracker); err != nil {
		fmt.Println(err)
		return
	}

	// The publisher, or a replayer holding both as a host stores them,
	// assembles the one BEEF a host admits the second token in.
	beef, err := chaintoken.TokenBEEF(second, first)
	if err != nil {
		fmt.Println(err)
		return
	}

	// The host.
	wire, err := chaintoken.ReadWire(beef, chaintoken.DefaultMaxBEEF)
	if err != nil {
		fmt.Println(err)
		return
	}
	tx := wire.SubjectTx()
	parent, err := wire.Token(tx)
	fmt.Println("a token and its parent:", err == nil, "parent is the first token:", parent.Txid == *first.TxID())
	fmt.Println("both mined:", chaintoken.Mined(ctx, tx, tracker) && chaintoken.Mined(ctx, parent.Tx, tracker))

	lockingKey, err := state.ExpectedLockingKey(key.PubKey())
	if err != nil {
		fmt.Println(err)
		return
	}
	out, ok := chaintoken.ReadOutput(tx.Outputs[0], 0, 2)
	fmt.Println("two fields and a signature of 1 satoshi:", ok)
	fmt.Printf("record %q, canonical lock %t, signed %t\n", out.Fields[1],
		out.LockedTo(*tx.Outputs[0].LockingScript, lockingKey), out.SignedBy(lockingKey))

	// What it spends of the parent: the predecessor, and the change that
	// pays its fee. Only an output that reads as a token is one.
	for _, s := range chaintoken.Spends(tx, parent) {
		pred, isToken := chaintoken.ReadOutput(s.Output, s.Vout, 2)
		if isToken {
			fmt.Printf("input %d spends output %d: the predecessor, record %q\n", s.Input, s.Vout, pred.Fields[1])
		} else {
			fmt.Printf("input %d spends output %d: not a token\n", s.Input, s.Vout)
		}
	}

	// The second token alone, as a host stores it, is not admissible: the
	// rule needs the parent in the same BEEF. A reader reads it with
	// Stored.
	alone, err := second.AtomicBEEF(false)
	if err != nil {
		fmt.Println(err)
		return
	}
	wire, err = chaintoken.ReadWire(alone, 0)
	if err != nil {
		fmt.Println(err)
		return
	}
	_, err = wire.Token(wire.SubjectTx())
	fmt.Println("alone:", errors.Is(err, chaintoken.ErrBEEF))
	stored, err := chaintoken.Stored(alone, 0)
	fmt.Println("stored:", stored.TxID().String() == second.TxID().String(), err)
}
Output:
a token and its parent: true parent is the first token: true
both mined: true
two fields and a signature of 1 satoshi: true
record "state 1", canonical lock true, signed true
input 0 spends output 0: the predecessor, record "state 0"
input 1 spends output 1: not a token
alone: true
stored: true <nil>

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL