producer

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: Apache-2.0 Imports: 14 Imported by: 3

Documentation

Overview

Package producer is the orchestration a producer of committed records runs around the builders in mint and carrier: where the fee for a mined transaction comes from and where its change goes, the funding tree a carrier spends and when to mint the next one, the one copy of each transaction the producer keeps while it is unproven, and the collection of proofs for what was published before it mined.

A producer publishes on two legs. The settlement leg takes mined transactions (a funding tree, a state token, an anchor) for mining; the object leg takes each one, and every carrier, as a BEEF to the overlay hosts. With proofs collected later rather than waited for, a transaction reaches the hosts before it mines, so its BEEF carries its unproven ancestry, and the producer keeps that BEEF until the proof arrives. When it does, the proven BEEF is published again, and every host upgrades the copy it holds.

The pieces:

  • Payer takes a fee input from a coin pool (bwallet.Pool), gives it back if the transaction is not sent, takes change back into the pool, and settles: hands a transaction to the settlement leg and waits for its proof or, with Async, returns once the leg has accepted it.
  • Kept is the cache that makes every use of one kept transaction the same object, so a BEEF built from it never merges two copies.
  • Trees is the funding-tree lifecycle: spend from the current tree, or mint, settle, record and publish the next one when the current tree cannot fund what is asked.
  • Proofs asks whether a transaction mined; Collector asks it for everything still waiting, records each proof, republishes the proven transaction, and releases change the pool held back.

The application keeps its own state (a state file in its own format) and the words for its own commands. The library reaches that state through TreeState, Kept.Load and the Pending callbacks, and reports progress through a Note function in plain lines that name no application. What a user should do about a refusal is the application's to say, so the refusals an application may want to word itself come back as typed errors (NoCoinError, NoKeyError) or through Pending's Refused and Unbuilt hooks.

Nothing here is safe for concurrent use: one producer run owns its Payer, Kept and Trees.

Index

Examples

Constants

View Source
const (
	DefaultTimeout = 10 * time.Minute
	DefaultPoll    = 5 * time.Second
)

DefaultTimeout is how long a Payer waits for a proof when Timeout is zero, and DefaultPoll how often it asks when Poll is zero.

Variables

View Source
var ErrRefused = errors.New("refused by the network")

ErrRefused is a published transaction the network will not mine. It is not an ordinary "not yet": hosts hold a state built on it that will never be real, and the operator has to know.

Functions

func Index

func Index(all []funding.Tree, cur *funding.Tree) []funding.Tree

Index keeps the history of trees in step with the current one, so a sweep sees the same Next the spends advanced: the entry with cur's txid becomes a copy of cur, or cur is appended when no entry has it. It returns the history, which the caller stores back. A nil cur changes nothing.

Types

type Collector

type Collector struct {
	// Proofs answers whether each transaction mined.
	Proofs Proofs
	// Kept, when set, gives the copy of a proven transaction already handed
	// out in this run its proof too.
	Kept *Kept
	// Pool, when set, has the change it holds back until a proof arrives
	// released as each proof is collected.
	Pool *bwallet.Pool
	// Journal, when set, has its entries still waiting on a proof stamped
	// mined, and the entries of the Pending items that ask for it.
	Journal *publish.Journal
	// Facade and Topic are where a proven transaction is published again.
	// With no Facade nothing is republished.
	Facade *publish.Facade
	Topic  string
	// Retry, when set, ends the note about a proof that could not be
	// published with how the application sends it again.
	Retry string
	// Save saves the application's state after a proof is recorded; a
	// failure is a note.
	Save func() error
	// Note receives each line Collect reports; nil discards them.
	Note func(format string, args ...any)
}

Collector collects the proofs of what a producer published before it mined, and publishes each proven transaction again.

The republish is the point. A host admitted the unproven copy and will go on serving it until it sees the proof; the proven BEEF is different bytes, so the plane delivers it to every host like any other object, and each host verifies the path against its own headers and upgrades what it holds. One submission reaches all of them, and no host asks anyone.

Collect never fails the run it is part of: a proof that has not arrived is the ordinary case, and a check that could not be made is a note. The one loud outcome is a refusal.

func (*Collector) Collect

func (c *Collector) Collect(ctx context.Context, items []Pending, skip ...string)

Collect asks for the proof of each pending item in order, then of each journal entry that was published, has not mined, and is not one of skip, then of each parent whose change the pool holds back.

A journal entry for a transaction superseded before its own proof was collected is no longer anything the application tracks, yet it mined all the same, and left alone it would read as pending for ever; skip names the transactions the application does track, whose entries the items handle.

Example

Collect asks for the proof of each transaction still waiting for one. With no proof source configured nothing has mined, which is the ordinary answer between a publish and its block, so the item is reported pending and nothing is recorded.

package main

import (
	"context"
	"fmt"
	"strings"

	"github.com/bsv-blockchain/go-sdk/transaction"

	"github.com/lightwebinc/bcommon/producer"
)

func main() {
	var lines []string
	c := &producer.Collector{
		Proofs: producer.Proofs{}, // an arcade installation and a node in an application
		Note:   func(format string, args ...any) { lines = append(lines, fmt.Sprintf(format, args...)) },
	}
	txid := strings.Repeat("ab", 32)
	c.Collect(context.Background(), []producer.Pending{{
		What: "anchor", Txid: txid,
		Proven:  func(mp *transaction.MerklePath, height uint32) { fmt.Println("recorded at", height) },
		Refused: func(err error) { fmt.Println("tell the operator:", err) },
	}})
	for _, l := range lines {
		fmt.Println(strings.ReplaceAll(l, txid, "<anchor>"))
	}
}
Output:
anchor <anchor>: accepted, proof pending

type Kept

type Kept struct {
	// Load rebuilds a transaction the application keeps from its own state,
	// usually with funding.Rebuild. Its error is returned as it is, so it is
	// also how the application words "not one of mine".
	Load func(txid string) (*transaction.Transaction, error)
	// contains filtered or unexported fields
}

Kept is the one in-memory copy of each transaction a producer published and still keeps, by txid.

A transaction the producer spends before it mines (a state token that is the next transition's previous input, a funding tree whose change pays the next fee) can be needed in more than one place in one run. Two copies of it, one that carries its ancestry and one that does not, would leave a BEEF built from both depending on which copy it merged first. Every place asks Kept, so every place gets the same object, and a proof collected during the run reaches that object too.

The zero value is usable once Load is set.

func (*Kept) Prove

func (k *Kept) Prove(txid string, mp *transaction.MerklePath)

Prove gives the copy of txid already handed out, if there is one, its proof, so a transaction being built that holds it stops its BEEF at the proof rather than carrying the ancestry. A nil Kept holds nothing.

func (*Kept) Tx

func (k *Kept) Tx(txid string) (*transaction.Transaction, error)

Tx returns the kept transaction txid: the copy already handed out, or one Load rebuilds, which is then the copy every later call gets. A failed Load is not remembered.

type NoCoinError

type NoCoinError struct {
	Err  error
	Held int
}

NoCoinError is Take finding no coin it may spend. Err is the pool's answer, usually bwallet.ErrNoSpendable. Held counts the transactions whose change the pool holds back until their proofs arrive: coin that becomes spendable once those are collected, so an application can tell "fund the pool" from "wait for a block".

func (*NoCoinError) Error

func (e *NoCoinError) Error() string

func (*NoCoinError) Unwrap

func (e *NoCoinError) Unwrap() error

type NoKeyError

type NoKeyError struct {
	Outpoint string
}

NoKeyError is a pool coin locked to a fund key that none of the Payer's keys derives. Outpoint names it, txid.vout.

func (*NoKeyError) Error

func (e *NoKeyError) Error() string

type Payer

type Payer struct {
	// Pool is the coin every fee comes from.
	Pool *bwallet.Pool
	// Tip is the chain height coinbase maturity is judged at.
	Tip uint32
	// Keys is every key the producer signs with, by identity key hex: a coin
	// locked to one of their fund keys is spent under that key, and change
	// paid to any of them is taken back into Pool. A producer that has
	// rotated its identity holds its predecessors here too.
	Keys map[string]*bwallet.Signer
	// KeyFor returns the key of the identity that owns a received payment,
	// by identity key hex, which re-derives that payment's key. Nil looks
	// the identity up in Keys.
	KeyFor func(identityHex string) (*bwallet.Signer, error)
	// Kept is the producer's kept transactions. A coin that is change from
	// one of them is spent against that same object.
	Kept *Kept
	// Allow returns the kept transactions whose unproven change Take may
	// spend when no proven coin is left: those the transaction being built
	// already carries in full (see bwallet.Pool.TakeAllowing). Nil allows
	// none.
	Allow func() []string
	// Settler is the settlement leg.
	Settler publish.Settler
	// Asset is the node proofs are waited for from and, with Async, where a
	// coin's parent comes from when the pool does not hold it.
	Asset *nodeapi.Asset
	// Async settles without waiting for a block: Settle returns once the
	// leg has accepted a transaction, and the proof is collected later
	// (Collector). A transaction spending a coin before it mines then
	// carries the coin's parent, with its proof, fetched from Asset.
	Async bool
	// Fees is the fee policy of what the Payer mints itself (a funding tree
	// through Trees). mint.DefaultFees is the usual one; the zero value
	// pays no fee, which a network refuses.
	Fees mint.Fees
	// Timeout bounds a wait for a proof; zero is DefaultTimeout. Poll is how
	// often the wait asks; zero is DefaultPoll.
	Timeout time.Duration
	Poll    time.Duration
	// Note receives each line the Payer reports: what it settled and how,
	// and a key whose change it could not recognise. Nil discards them.
	Note func(format string, args ...any)
	// contains filtered or unexported fields
}

Payer pays for a producer's mined transactions from a coin pool and gets them mined.

Take reserves a coin and returns it as a fee input, signed by the key its output is locked to; GiveBack returns every coin taken when the transaction is not sent after all. Change takes a transaction's outputs that pay one of the Payer's keys back into the pool. Settle, SettleAndWait and Await put a transaction on the settlement leg and collect its proof.

func (*Payer) Await

Await waits, up to Timeout, for tx to mine, asking Asset every Poll, and gives tx its proof. It is the waiting half of SettleAndWait, for a transaction that reached the network some other way, such as a wallet's own broadcast.

func (*Payer) Change

func (p *Payer) Change(tx *transaction.Transaction, height uint32, mp *transaction.MerklePath)

Change adds tx's outputs that pay one of the Payer's keys to the pool. The parent is kept whole, with its proof when it has one, so the coin can be a fee input for a transaction published before it mines. Change from a transaction that has not mined (mp nil) is held back from spending until its proof is collected; see bwallet.Output.Unproven.

A key with no usable wallet profile has no fund script to recognise its change by, and change left out of the pool reads exactly like spent coin, so that key is named in a note rather than passed over in silence.

func (*Payer) GiveBack

func (p *Payer) GiveBack()

GiveBack returns every coin Take reserved to the pool.

func (*Payer) Parent

Parent rebuilds enough of a pool coin's parent transaction to sign against it, and for a spender published before it mines, enough to carry in its BEEF.

Unproven change is only ever taken when its parent is kept (Allow), and then the parent is the kept object itself, ancestry and all. A coin whose parent the pool holds with its proof is rebuilt from both. With Async, a parent the pool holds without a proof, or does not hold at all, is fetched from the node with its proof, because the spender is published before it mines and its BEEF must carry a parent that verifies on its own. Otherwise a parent the pool does not hold (a coinbase) is a stub carrying just the coin's output, which is enough to sign against when the spender is mined before it is published, since a mined transaction's BEEF carries no ancestry.

func (*Payer) Settle

Settle hands tx to the settlement leg. It waits for the proof, as SettleAndWait does, unless Async is set; then it returns once the leg has accepted tx, with no proof, and the proof is collected later. what names tx in the notes and errors ("funding tree").

func (*Payer) SettleAndWait

func (p *Payer) SettleAndWait(ctx context.Context, what string, tx *transaction.Transaction) (*transaction.MerklePath, uint32, error)

SettleAndWait hands tx to the settlement leg and waits for its proof, whatever Async says: for a transaction that must be mined before anything else happens, such as a kill switch's sweep.

func (*Payer) Take

func (p *Payer) Take(ctx context.Context) (mint.Input, error)

Take reserves a pool coin as a fee input. Everything taken is returned to the pool by GiveBack if the transaction is not sent.

A received payment carries its derivation, and its owner's key re-derives the spending key. Any other coin is spent by whichever key's fund script its locking script is; after a rotation the pool holds more than one.

A failure to find a coin is a *NoCoinError and a coin no key holds is a *NoKeyError, each returned as it is, so the application can put its own words to them. Every other error is returned with the coin's outpoint.

Example

A fee input comes from the pool, signed by the key its coin is locked to. Change from a transaction published before it mined is held back, so the next fee finds no coin, and says why; allowing that parent, which the next transaction carries anyway, spends the change against the one kept copy of it.

package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"

	"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/wallet"

	"github.com/lightwebinc/bcommon/bwallet"
	"github.com/lightwebinc/bcommon/carrier"
	"github.com/lightwebinc/bcommon/goldentest"
	"github.com/lightwebinc/bcommon/mint"
	"github.com/lightwebinc/bcommon/producer"
	"github.com/lightwebinc/bcommon/pushdrop"
)

// exampleProfile and exampleParams are the example application's wallet
// profile and carrier parameters, under the protocol and tags reserved for
// tests. A real application registers its own.
var exampleProfile = bwallet.Profile{
	FundProtocol:   wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryApp, Protocol: "vector sample"},
	FundKeyID:      "coin",
	FundBasket:     "vector sample coin",
	Version:        "vector-sample-1",
	LegacyPoolFile: "coins.json",
}

// exampleProducer is a producer on a lab chain that exists only in this
// process: the fixed test key's signer, and a pool in dir holding one
// 50,000-satoshi coin paying its fund key, with a stand-in proof at height
// 90. Its lines go to lines.
func exampleProducer(dir string, lines *[]string) (*bwallet.Signer, *producer.Payer, error) {
	w, err := wallet.NewCompletedProtoWallet(goldentest.FixedKey())
	if err != nil {
		return nil, nil, err
	}
	signer := &bwallet.Signer{Interface: w, Identity: goldentest.FixedKey().PubKey(), Originator: "example.com", Profile: exampleProfile}
	pool, err := bwallet.LoadPool(filepath.Join(dir, "wallet.json"))
	if err != nil {
		return nil, nil, err
	}
	lock, err := signer.FundScript()
	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: lock})
	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},
	}})
	if _, err := pool.Add(bwallet.Output{TxID: coin.TxID().String(), Vout: 0, Satoshis: 50000,
		LockingScript: lock.String(), Height: 90, Raw: coin.Hex(), Bump: coin.MerklePath.Hex()}); err != nil {
		return nil, nil, err
	}
	payer := &producer.Payer{
		Pool: pool, Tip: 100, Keys: map[string]*bwallet.Signer{signer.IdentityHex(): signer},
		Kept: &producer.Kept{}, Fees: mint.DefaultFees,
		Note: func(format string, args ...any) { *lines = append(*lines, fmt.Sprintf(format, args...)) },
	}
	return signer, payer, nil
}

func main() {
	ctx := context.Background()
	dir, err := os.MkdirTemp("", "producer-example-")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer os.RemoveAll(dir)
	var lines []string
	signer, payer, err := exampleProducer(dir, &lines)
	if err != nil {
		fmt.Println(err)
		return
	}

	fee, err := payer.Take(ctx)
	if err != nil {
		fmt.Println(err)
		return
	}
	change, err := signer.FundScript()
	if err != nil {
		fmt.Println(err)
		return
	}
	tx, err := mint.Payment(ctx, &script.Script{script.OpTRUE}, 1000, fee, change, mint.DefaultFees)
	if err != nil {
		fmt.Println(err)
		return
	}
	payer.Change(tx, 0, nil) // published, not yet mined
	fmt.Println("held back:", payer.Pool.UnprovenTxids()[0] == tx.TxID().String())

	_, err = payer.Take(ctx)
	var nc *producer.NoCoinError
	fmt.Println(errors.As(err, &nc), nc.Held)
	fmt.Println(err)

	payer.Kept.Load = func(txid string) (*transaction.Transaction, error) { return tx, nil }
	payer.Allow = func() []string { return []string{tx.TxID().String()} }
	next, err := payer.Take(ctx)
	fmt.Println("spent against the kept copy:", next.Tx == tx, err)
}
Output:
held back: true
true 1
fee input: bwallet: no spendable output in the wallet: the other coins are change from 1 transaction(s) whose proofs have not arrived; they become spendable once mined and collected
spent against the kept copy: true <nil>

type Pending

type Pending struct {
	// What names the transaction in the lines Collect reports ("funding
	// tree").
	What string
	// Txid, RawHex and BeefHex are the kept copy: its id, its raw bytes, and
	// the BEEF kept while it is unmined (funding.KeepBEEF), which the proven
	// transaction is rebuilt from to be published again.
	Txid    string
	RawHex  string
	BeefHex string
	// Proven records the proof in the application's state: the proof's
	// hex (funding.BumpHex), the height, and the kept BEEF cleared, which a
	// proven transaction no longer needs. Collect saves the state after it.
	Proven func(mp *transaction.MerklePath, height uint32)
	// Stamp marks the journal entry (Seq, Txid) mined once the proof is
	// saved.
	Stamp bool
	Seq   uint64
	// Refused reports that the network refused the transaction, which
	// means hosts hold something built on it that will never mine. The
	// error wraps ErrRefused. Nil reports it in a generic warning line; an
	// application usually says what it means for its users.
	Refused func(err error)
	// Unbuilt reports a transaction that mined but whose kept copy does not
	// rebuild; its proof is then not recorded. Nil reports it in a generic
	// note.
	Unbuilt func(err error)
}

Pending is one transaction the producer published before it mined and keeps until its proof is collected: a funding tree, a state token, an anchor.

type Proofs

type Proofs struct {
	// Arcade, when set, is asked first: an arcade installation tracks what
	// it broadcast and reports a refusal, which a node cannot. It does not
	// know a transaction that reached the network some other way, and then
	// Asset answers.
	Arcade *publish.Arcade
	// Asset is the node's asset API. With neither set, nothing has mined.
	Asset *nodeapi.Asset
}

Proofs asks whoever can answer whether a transaction has mined.

func (Proofs) Of

Of returns txid's proof and block height if it has mined. It returns nodeapi.ErrNotMined when it has not yet, and an error wrapping ErrRefused when arcade reports that the network refused it.

Arcade's proof is held to the checks a node's is: it must parse through the BUMP guard, name the transaction asked about, and agree with the block height arcade reported. A proof of some other transaction verifies perfectly and proves nothing.

type TreeState

type TreeState interface {
	// Current is the tree carriers spend from now, or nil before the first.
	// Trees reads it and never writes through it; the application advances
	// its Next as it spends outputs, then calls Index.
	Current() *funding.Tree
	// Adopt makes t the current tree, adds it to the trees kept for a
	// sweep, and saves the state. Trees calls it once the tree is minted
	// and settled, before the tree is published.
	Adopt(t funding.Tree) error
}

TreeState is where an application keeps its funding trees: the current tree, which carriers spend from, and every tree it minted, which a kill switch sweeps. The application's own state file holds both, in its own format.

type Trees

type Trees struct {
	// Payer pays for a tree minted from the pool, settles it and takes its
	// change. Its Kept holds the current tree.
	Payer *Payer
	// State is the application's record of its trees.
	State TreeState
	// Identity is the identity key hex whose derived key locks the tree's
	// outputs. A current tree locked to another identity is replaced,
	// because a successor cannot spend a predecessor's tree.
	Identity string
	// Count is how many outputs a new tree has, at least; Spend mints more
	// when one spend needs more. Sats is the value of each.
	Count int
	Sats  uint64
	// Funder is recorded in the new tree's funding.Tree.Funder: how the
	// tree was paid for, in the application's words.
	Funder string
	// Lock returns the funding lock, normally carrier.FundingLock under the
	// application's carrier.Params. It is asked only when the pool pays for
	// a tree, since deriving it asks the wallet.
	Lock func(ctx context.Context) (*script.Script, error)
	// Change returns the script the fee input's change is paid to, normally
	// the signer's bwallet.Signer.FundScript. It is asked only when the pool
	// pays for a tree.
	Change func() (*script.Script, error)
	// Fund, when set, mints and settles a tree some other way, such as a
	// BRC-100 wallet that funds and broadcasts it itself, and returns it
	// with its proof and height, or with neither when it is collected later.
	// The pool, Lock and Change are then not used, and DryRun does not
	// apply.
	Fund func(ctx context.Context, count int) (*transaction.Transaction, *transaction.MerklePath, uint32, error)
	// DryRun builds a tree the pool pays for and returns it without
	// settling, recording or publishing it. The fee input stays reserved
	// until the Payer's GiveBack.
	DryRun bool
	// Facade and Topic are the object leg a new tree is published on: hosts
	// admit its outputs so that a later sweep of them, the kill switch, is a
	// spend they see.
	Facade *publish.Facade
	Topic  string
}

Trees is the funding-tree lifecycle. Spend answers the tree the next carriers spend from: the current one while it has the outputs, or a new one, minted, settled, recorded and published.

func (*Trees) Spend

func (t *Trees) Spend(ctx context.Context, need uint32) (*transaction.Transaction, uint32, error)

Spend returns the tree the next need carriers spend from, and the index of the first output they spend.

The current tree is used while it has need outputs left and is locked to Identity. Otherwise a new tree is minted, never smaller than need, since need is known before anything is minted: a producer that spent a tree halfway through one publish and then stopped would leave carriers on the plane that nothing commits to. The outputs a replaced tree has left are stranded, and a sweep takes them with the rest.

A new tree is paid for by Fund when it is set, and otherwise from the pool through the Payer, and then settled (Payer.Settle) and its change taken. Its funding.Tree, with the BEEF it is kept as while it is unmined, is adopted into the state before the tree is published, so a crash after the publish never leaves a tree on the plane the state does not know.

Example

Spend answers the tree the next carriers spend from, minting one when the current tree cannot fund them. Six carriers ask more than the four-output tree the producer mints by default, so the tree is sized to them. A dry run builds it and records, settles and publishes nothing.

package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"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/wallet"

	"github.com/lightwebinc/bcommon/bwallet"
	"github.com/lightwebinc/bcommon/carrier"
	"github.com/lightwebinc/bcommon/funding"
	"github.com/lightwebinc/bcommon/goldentest"
	"github.com/lightwebinc/bcommon/mint"
	"github.com/lightwebinc/bcommon/producer"
	"github.com/lightwebinc/bcommon/pushdrop"
)

// exampleProfile and exampleParams are the example application's wallet
// profile and carrier parameters, under the protocol and tags reserved for
// tests. A real application registers its own.
var (
	exampleProfile = bwallet.Profile{
		FundProtocol:   wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryApp, Protocol: "vector sample"},
		FundKeyID:      "coin",
		FundBasket:     "vector sample coin",
		Version:        "vector-sample-1",
		LegacyPoolFile: "coins.json",
	}
	exampleParams = carrier.Params{
		Derivation: pushdrop.Derivation{
			Protocol: wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryApp, Protocol: "vector sample"},
			KeyID:    "object",
		},
		FundingTag:      []byte{'v', 'x', 0x02},
		ValidatePayload: func([]byte) error { return nil },
	}
)

// exampleProducer is a producer on a lab chain that exists only in this
// process: the fixed test key's signer, and a pool in dir holding one
// 50,000-satoshi coin paying its fund key, with a stand-in proof at height
// 90. Its lines go to lines.
func exampleProducer(dir string, lines *[]string) (*bwallet.Signer, *producer.Payer, error) {
	w, err := wallet.NewCompletedProtoWallet(goldentest.FixedKey())
	if err != nil {
		return nil, nil, err
	}
	signer := &bwallet.Signer{Interface: w, Identity: goldentest.FixedKey().PubKey(), Originator: "example.com", Profile: exampleProfile}
	pool, err := bwallet.LoadPool(filepath.Join(dir, "wallet.json"))
	if err != nil {
		return nil, nil, err
	}
	lock, err := signer.FundScript()
	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: lock})
	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},
	}})
	if _, err := pool.Add(bwallet.Output{TxID: coin.TxID().String(), Vout: 0, Satoshis: 50000,
		LockingScript: lock.String(), Height: 90, Raw: coin.Hex(), Bump: coin.MerklePath.Hex()}); err != nil {
		return nil, nil, err
	}
	payer := &producer.Payer{
		Pool: pool, Tip: 100, Keys: map[string]*bwallet.Signer{signer.IdentityHex(): signer},
		Kept: &producer.Kept{}, Fees: mint.DefaultFees,
		Note: func(format string, args ...any) { *lines = append(*lines, fmt.Sprintf(format, args...)) },
	}
	return signer, payer, nil
}

// exampleState keeps the example's trees in memory. An application keeps
// them in its own state file.
type exampleState struct {
	cur *funding.Tree
	all []funding.Tree
}

func (s *exampleState) Current() *funding.Tree { return s.cur }

func (s *exampleState) Adopt(t funding.Tree) error {
	s.cur = &t
	s.all = append(s.all, t)
	return nil
}

func main() {
	ctx := context.Background()
	dir, err := os.MkdirTemp("", "producer-example-")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer os.RemoveAll(dir)
	var lines []string
	signer, payer, err := exampleProducer(dir, &lines)
	if err != nil {
		fmt.Println(err)
		return
	}
	state := &exampleState{}
	trees := &producer.Trees{
		Payer: payer, State: state, Identity: signer.IdentityHex(), Count: 4, Sats: 1000, Funder: "pool",
		Lock: func(ctx context.Context) (*script.Script, error) {
			return carrier.FundingLock(ctx, signer, signer.Originator, exampleParams)
		},
		Change: signer.FundScript,
		DryRun: true,
	}

	tree, first, err := trees.Spend(ctx, 6)
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, l := range lines {
		fmt.Println(strings.ReplaceAll(l, tree.TxID().String(), "<tree>"))
	}
	funded := 0
	for _, out := range tree.Outputs {
		if _, ok := carrier.DecodeFunding(out.LockingScript, exampleParams.FundingTag); ok {
			funded++
		}
	}
	fmt.Println("funding outputs:", funded, "first:", first, "recorded:", state.Current() != nil)
	payer.GiveBack()
	fmt.Println("coins in the pool after GiveBack:", payer.Pool.Count())
}
Output:
this transition spends 6 outputs, so the tree is minted with 6 rather than 4
funding tree <tree>: 6 output(s) of 1000 sat
funding outputs: 6 first: 0 recorded: false
coins in the pool after GiveBack: 1

Jump to

Keyboard shortcuts

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