carrier

package
v0.14.2 Latest Latest
Warning

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

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

Documentation

Overview

Package carrier is an object's transport: one transaction that is never mined, whose record output holds the object's payload as a signed PushDrop [payload] under the producer's derivation, and whose txid is the commitment a mined token can carry.

A carrier is kept off the chain by BRC-60's device turned the other way round: a far-future nLockTime with a non-final input makes it unmineable in practice, while SPV still verifies it through its funding parent. Nothing in the SDK's verification path reads finality (verified at v1.5.2 and v1.7.1), so a host admits it like any other object; the topic manager is what refuses one that could reach the chain.

The carrier is funded from a funding tree whose outputs are PushDrop [tag] with no signature under the same derivation, so the derivation's one unlocker spends a funding output whether a carrier or the kill switch (Sweep) is the spender.

The application supplies what is its own through Params: the derivation, the funding tag, the payload's rules and the sentinel an identity-parse failure wraps. Decode takes the application's Classify, which decides which PushDrop outputs hold its payload.

Index

Examples

Constants

View Source
const LockTime uint32 = 4102444800

LockTime is 2100-01-01T00:00:00Z. With an input whose sequence is below the maximum, a transaction with this nLockTime is not mineable before then. Frozen at the first mint: a reader refuses anything lower.

View Source
const Sequence uint32 = 0

Sequence is the carrier's input sequence. Any value below 0xFFFFFFFF keeps the locktime in force; zero is the conventional non-final value.

View Source
const SigHashType byte = 0x41

SigHashType is the one sighash type a carrier's input is signed with, SIGHASH_ALL|FORKID, as Mint signs it through the derivation's unlocker.

Variables

View Source
var (
	ErrNotCarrier = errors.New("carrier: not a carrier")
	ErrShape      = errors.New("carrier: record output has the wrong shape")
	ErrMineable   = errors.New("carrier: mineable; the record could reach the chain")
	ErrLock       = errors.New("carrier: locking key is not the identity's record key")
	ErrSignature  = errors.New("carrier: field signature does not verify")
	// ErrUnlocking is a carrier whose input is not spent by exactly the one
	// canonical signature push CheckUnlocking describes. Anyone who sees a
	// carrier can rewrite a looser unlocking script without the key, and the
	// rewrite is a different txid spending the same funding output.
	ErrUnlocking = errors.New("carrier: unlocking script is not one canonical signature push")
	// ErrIdentity is what an identity key that does not parse, or is not
	// the one canonical compressed encoding (guard.ParsePubKey), wraps when the
	// application names no sentinel of its own in Params.ErrIdentity.
	ErrIdentity = errors.New("carrier: identity key does not parse")
)

The refusals. Their texts are what readers print, so they do not change.

Functions

func CheckUnlocking added in v0.3.4

func CheckUnlocking(tx *transaction.Transaction) error

CheckUnlocking refuses, as ErrUnlocking, a carrier that does not have exactly one input, or whose input's unlocking script is not exactly one minimally encoded data push holding a strict DER ECDSA signature (BIP 66) with R in [1, n-1] and S in [1, n/2], followed by the one sighash byte SigHashType.

The carrier's txid is its commitment, and a host reads a funding output spent by another txid as the record retracted. The script interpreter accepts a high-S signature, a non-minimal push and an extra push or no-op, so without this check anyone who sees a carrier could spend its funding output under a new txid carrying the same record and retract it. With it, one signature from the key is the only spend a reader takes. The check is structural: whether the signature satisfies the funding output is the SPV check's (verify.Check).

func Commitment

func Commitment(tx *transaction.Transaction) [32]byte

Commitment is the carrier's txid in hash byte order: SHA-256d over the transaction bytes, exactly as a token carries it and as an overlay keys the object. It is NOT the display hex reversed.

func DecodeFunding

func DecodeFunding(s *script.Script, tag []byte) (*ec.PublicKey, bool)

DecodeFunding reports whether s is a funding output under tag and returns its locking key. Only the one encoding FundingLock writes is a funding output (pushdrop.CheckCanonical).

func FundingLock

func FundingLock(ctx context.Context, w wallet.Interface, originator string, p Params) (*script.Script, error)

FundingLock is the script a funding-tree output is locked with: the wallet's key under the derivation with the funding tag as its one field and no signature, `<key> OP_CHECKSIG <tag> OP_DROP`. The derivation's unlocker spends it with a single signature exactly as it would a bare pay-to-public-key; the tag is for the host, which admits funding outputs by it so that a later spend of one (the kill switch) is something it sees.

func Mint

func Mint(ctx context.Context, w wallet.Interface, originator string, p Params, payload []byte,
	funding *transaction.Transaction, vout uint32) (*transaction.Transaction, error)

Mint builds and signs a carrier for payload, spending output vout of funding through the wallet. Output 0 carries the whole input value back under the derivation, so the fee is zero and SPV's outputs<=inputs holds.

Mint does not run p.ValidatePayload. The caller built the payload and applies its own rules before encoding it; the validator is the reader's check on bytes someone else produced.

Only wallet.Interface touches key material: the field signature comes from Lock and the input signature from the derivation's unlocker, so a hardware or remote wallet mints exactly as an embedded one does.

Example

A producer mints a carrier for a payload from one output of its funding tree; the carrier decodes, validates against the producer's identity key, and proves through the tree to the test chain's header, all offline.

package main

import (
	"bytes"
	"context"
	"errors"
	"fmt"

	"github.com/bsv-blockchain/go-sdk/chainhash"
	ec "github.com/bsv-blockchain/go-sdk/primitives/ec"
	"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/carrier"
	"github.com/lightwebinc/bcommon/goldentest"
	"github.com/lightwebinc/bcommon/mint"
	"github.com/lightwebinc/bcommon/pushdrop"
	"github.com/lightwebinc/bcommon/verify"
)

// examplePrefix starts every payload of the example application: three
// bytes and a version byte of 1. A real application defines its own payload
// format; this one belongs to no application.
var examplePrefix = []byte{'v', 'x', 'r', 0x01}

// exampleParams are the example application's carrier parameters: the test
// vectors' derivation and funding tag, which are reserved for tests, and the
// payload's own rules.
func exampleParams() carrier.Params {
	return carrier.Params{
		Derivation: pushdrop.Derivation{
			Protocol: wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryApp, Protocol: "vector sample"},
			KeyID:    "object",
		},
		FundingTag: []byte{'v', 'x', 0x02},
		ValidatePayload: func(p []byte) error {
			if !bytes.HasPrefix(p, examplePrefix) {
				return errors.New("example: not a version 1 payload")
			}
			return nil
		},
	}
}

// exampleClassify takes the outputs whose payload starts with the example
// prefix's first three bytes.
func exampleClassify(p []byte) (bool, error) {
	return bytes.HasPrefix(p, examplePrefix[:3]), nil
}

// testChainTree builds a test chain that exists only in this process: a
// stand-in mined coin at height 90, which the returned tracker holds the root
// of, and an unmined funding tree of four 1,000-satoshi outputs spending it.
func testChainTree(ctx context.Context, w wallet.Interface, p carrier.Params) (*transaction.Transaction, *goldentest.Tracker, error) {
	addr, err := script.NewAddressFromPublicKey(goldentest.FixedKey().PubKey(), false)
	if err != nil {
		return nil, nil, err
	}
	payTo, err := p2pkh.Lock(addr)
	if err != nil {
		return nil, nil, err
	}
	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: payTo})
	sibling := chainhash.Hash(goldentest.Fill(0x33))
	isTxid := true
	coin.MerklePath = transaction.NewMerklePath(90, [][]*transaction.PathElement{{
		{Offset: 0, Hash: &sibling},
		{Offset: 1, Hash: coin.TxID(), Txid: &isTxid},
	}})
	root, err := coin.MerklePath.ComputeRoot(coin.TxID())
	if err != nil {
		return nil, nil, err
	}
	tracker := &goldentest.Tracker{Roots: map[uint32]string{90: root.String()}, Tip: 100}

	payer, err := p2pkh.Unlock(goldentest.FixedKey(), nil)
	if err != nil {
		return nil, nil, err
	}
	lock, err := carrier.FundingLock(ctx, w, "example.com", p)
	if err != nil {
		return nil, nil, err
	}
	tree, err := mint.FundingTree(lock, 4, 1000, mint.Input{Tx: coin, Vout: 0, Unlocker: payer}, payTo, mint.DefaultFees)
	if err != nil {
		return nil, nil, err
	}
	return tree, tracker, nil
}

// A producer mints a carrier for a payload from one output of its funding
// tree; the carrier decodes, validates against the producer's identity key,
// and proves through the tree to the test chain's header, all offline.
func main() {
	ctx := context.Background()
	w, err := wallet.NewCompletedProtoWallet(goldentest.FixedKey())
	if err != nil {
		fmt.Println(err)
		return
	}
	p := exampleParams()
	tree, tracker, err := testChainTree(ctx, w, p)
	if err != nil {
		fmt.Println(err)
		return
	}

	payload := append(append([]byte{}, examplePrefix...), "an object"...)
	tx, err := carrier.Mint(ctx, w, "example.com", p, payload, tree, 0)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("locktime:", tx.LockTime, "sequence:", tx.Inputs[0].SequenceNumber)
	fmt.Println("output 0:", tx.Outputs[0].Satoshis, "sats, fee:", tree.Outputs[0].Satoshis-tx.TotalOutputSatoshis())

	c, err := carrier.Decode(tx, exampleClassify)
	if err != nil {
		fmt.Println(err)
		return
	}
	identity := goldentest.FixedKey().PubKey().Compressed()
	fmt.Printf("payload %q validates: %v\n", c.Payload, c.Validate(p, identity))

	// The commitment is the txid in hash byte order, which is the reverse
	// of the display hex.
	commitment := carrier.Commitment(tx)
	fmt.Println("commitment is the txid:", chainhash.Hash(commitment).String() == tx.TxID().String())

	verdict, err := verify.Check(ctx, tx, tracker)
	fmt.Println("proves through its funding tree:", verdict == verify.Passed, err)

	// The same carrier checked against somebody else's identity key.
	_, other := ec.PrivateKeyFromBytes(bytes.Repeat([]byte{0x07}, 32))
	err = c.Validate(p, other.Compressed())
	fmt.Println("another identity:", errors.Is(err, carrier.ErrLock))
}
Output:
locktime: 4102444800 sequence: 0
output 0: 1000 sats, fee: 0
payload "vxr\x01an object" validates: <nil>
commitment is the txid: true
proves through its funding tree: true <nil>
another identity: true

func Sweep

func Sweep(ctx context.Context, w wallet.Interface, originator string, p Params, tree *transaction.Transaction, vouts []uint32,
	fee *transaction.Transaction, feeVout uint32, feeUnlocker transaction.UnlockingScriptTemplate, change *script.Script, feeRate, floor uint64) (*transaction.Transaction, error)

Sweep builds and signs the kill switch: one transaction spending the given outputs of a funding tree, whether or not a carrier already spent them. Once mined, every carrier that spent one of those outputs is a double spend of a consumed output and no host or index will stand behind it again.

Output 0 is a TOMBSTONE: a funding-shaped output (FundingLock) carrying the swept tree value. It is not there for the satoshis. A host records a spend of an admitted output by the spender's ADMITTED outputs, so a sweep that admits nothing is seen live but not on restart; a sweep that admits its tombstone leaves the kill in storage. With a fee input, the fee input's remainder goes to change as output 1.

Sweep runs its own copy of the fee loop because the fee comes out of the tombstone when no fee input is given, which no builder in package mint does; the rule the loop settles by is the same. Both of its modes, with and without a fee input, are pinned by testdata/vectors/transactions-v1.json.

func SweepAt added in v0.14.1

func SweepAt(ctx context.Context, w wallet.Interface, originator string, p Params, tree *transaction.Transaction, vouts []uint32,
	fee *transaction.Transaction, feeVout uint32, feeUnlocker transaction.UnlockingScriptTemplate, change *script.Script, fees mint.Fees) (*transaction.Transaction, error)

SweepAt is Sweep at a fee policy: the rate in fees (a fraction of a satoshi per byte included) rounded up, at least fees.Floor, refused above fees.Max, and change from the fee input kept when it is at least the dust threshold (fees.Dust, or fees.Floor when Dust is zero). Sweep is SweepAt at a whole satoshi rate with the floor as the dust threshold.

Types

type Carrier

type Carrier struct {
	Tx          *transaction.Transaction
	OutputIndex uint32
	// Payload is the exact bytes pushed, which the field signature covers.
	Payload    []byte
	LockingKey *ec.PublicKey
	Signature  []byte
}

Carrier is a decoded carrier transaction.

func Decode

func Decode(tx *transaction.Transaction, classify Classify) (*Carrier, error)

Decode finds the one record output: a signed PushDrop of one field that classify takes. A transaction with no record output is not a carrier; one with two is refused as well, because the commitment would then name two records at once. A record output the classifier takes that is not the one encoding the template writes (pushdrop.CheckCanonical) is refused as ErrShape.

func (*Carrier) Validate

func (c *Carrier) Validate(p Params, identityKey []byte) error

Validate applies what a carrier must satisfy on its own: the payload's own rules, unmineability, the canonical unlocking script (CheckUnlocking), the lock derivation from identityKey, and the field signature under that lock. The chain rules that need the previous state belong to the verifier and the lookup service, not here.

The order is part of the contract, because a carrier that breaks two rules is refused for the first: the payload's rules, then the finality checks, then the unlocking script, and only then the identity key. So an invalid payload is refused as such even when the carrier could be mined, and a mineable carrier as mineable even when its identity key does not parse.

type Classify

type Classify func(payload []byte) (ours bool, err error)

Classify reports whether a PushDrop's first field is the application's payload. (false, _) skips the output, (true, nil) takes it, and (true, err) refuses the whole transaction: the output claims to be a payload and is not a well-formed one.

type Params

type Params struct {
	// Derivation locks the record output and the funding outputs.
	Derivation pushdrop.Derivation
	// FundingTag is the one field of a funding output.
	FundingTag []byte
	// ValidatePayload runs first inside Validate, before the finality checks,
	// and its error is returned as is, so a payload that breaks its own rules
	// is refused for them even when the carrier could also be mined. Validate
	// refuses a Params without one rather than accept every payload.
	ValidatePayload func(payload []byte) error
	// ErrIdentity is the sentinel an identity-parse failure wraps; nil means
	// ErrIdentity.
	ErrIdentity error
}

Params are what an application supplies to make the carrier its own. Derivation and FundingTag are frozen at the application's first mint: the derivation is hashed into every locking key and the funding tag is what hosts admit funding outputs by. ValidatePayload and ErrIdentity decide what a reader accepts and how it reports a refusal; nothing minted holds them.

Jump to

Keyboard shortcuts

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