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), 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 ¶
- Constants
- Variables
- func Commitment(tx *transaction.Transaction) [32]byte
- func DecodeFunding(s *script.Script, tag []byte) (*ec.PublicKey, bool)
- func FundingLock(ctx context.Context, w wallet.Interface, originator string, p Params) (*script.Script, error)
- func Mint(ctx context.Context, w wallet.Interface, originator string, p Params, ...) (*transaction.Transaction, error)
- func Sweep(ctx context.Context, w wallet.Interface, originator string, p Params, ...) (*transaction.Transaction, error)
- type Carrier
- type Classify
- type Params
Examples ¶
Constants ¶
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.
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.
Variables ¶
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") // ErrIdentity is what an identity key that does not parse 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 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 ¶
DecodeFunding reports whether s is a funding output under tag and returns its locking key.
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 lab 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
}
// labTree builds a lab 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 labTree(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 lab 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 := labTree(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.
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.
func (*Carrier) Validate ¶
Validate applies what a carrier must satisfy on its own: the payload's own rules, unmineability, 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, 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 ¶
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.