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; with Ahead, the next tree is minted and settled in the background before the current one runs out, and a fee the pool cannot pay meanwhile waits for that tree's change; with Prepare, the application records each tree and its fee coin before the tree reaches the settlement leg, and Recover settles the record of a tree a stopped run never adopted.
- 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. The one goroutine the package starts itself is a Trees minting ahead (Trees.Ahead), and it touches none of them: the tree's fee coin is reserved and signed on the caller's goroutine, and the background only settles it, through the Payer's Settler and Asset, or calls Fund. Those, and the Settler's own Note, must tolerate being used from that goroutine while the application uses them from its own; the ones in publish and nodeapi do. The background's notes are held and reported through the Payer's Note by the Spend or Wait that collects its result.
One run owns every Payer and every Trees over one pool, not only the Payer a Trees was given. A take the pool cannot cover while a tree minted ahead holds one of its coins waits for that mint and collects it (see Payer.Take), on the goroutine that called Take, whichever Payer over that pool it was called on. Collecting calls the Note of the Trees' Payer and saves the pool, and calls nothing else of the application's: not TreeState, not Prepare, not a leg.
Index ¶
- Constants
- Variables
- func Index(all []funding.Tree, cur *funding.Tree) []funding.Tree
- type Collector
- type Kept
- type NoCoinError
- type NoKeyError
- type Payer
- func (p *Payer) Await(ctx context.Context, what string, tx *transaction.Transaction) (*transaction.MerklePath, uint32, error)
- func (p *Payer) Change(tx *transaction.Transaction, height uint32, mp *transaction.MerklePath)
- func (p *Payer) GiveBack()
- func (p *Payer) Parent(ctx context.Context, o bwallet.Output) (*transaction.Transaction, error)
- func (p *Payer) Settle(ctx context.Context, what string, tx *transaction.Transaction) (*transaction.MerklePath, uint32, error)
- func (p *Payer) SettleAndWait(ctx context.Context, what string, tx *transaction.Transaction) (*transaction.MerklePath, uint32, error)
- func (p *Payer) Take(ctx context.Context) (mint.Input, error)
- func (p *Payer) TakeAtLeast(ctx context.Context, sats uint64) (mint.Input, error)
- type Pending
- type Proofs
- type PublishError
- type Recovery
- type RecoveryOutcome
- type TreeState
- type Trees
- func (t *Trees) Held() []funding.Tree
- func (t *Trees) Prepared() *funding.Tree
- func (t *Trees) Publish(ctx context.Context, tree funding.Tree) error
- func (t *Trees) Recover(ctx context.Context, tree funding.Tree, coin bwallet.Output) (Recovery, error)
- func (t *Trees) Spend(ctx context.Context, need uint32) (*transaction.Transaction, uint32, error)
- func (t *Trees) Wait(ctx context.Context) error
Examples ¶
Constants ¶
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 ¶
var ErrPublish = errors.New("producer: funding tree adopted but not published")
ErrPublish is what errors.Is matches a *PublishError by: a funding tree that is adopted into the state and was not published.
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. A refusal found in the node's view of the transaction's inputs, whatever arcade said, also wraps a *nodeapi.SpentError, so errors.Is finds nodeapi.ErrDoubleSpent too.
Functions ¶
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 ¶
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 ¶
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". Minting counts the funding trees minted ahead (Trees.Ahead) that took a coin of this pool and had not settled when Take stopped waiting for them: coin that returns as change once one settles, so the same take can succeed later in the run. It is zero unless the wait ended first, on the Payer's Timeout or the context; see Take.
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. Only coinbase
// (only on a regtest chain you run, development and tests) has a
// maturity rule; a payment imported on mainnet or testnet is spendable
// on arrival.
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
// Chain, when set, is asked everything Asset is asked (a coin's parent,
// proofs, spends) in its place: a chain view that needs no node, such
// as nodeapi.ParseChain builds over WhatsOnChain, with proofs checked
// against the producer's headers.
Chain nodeapi.Chain
// 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 ¶
func (p *Payer) Await(ctx context.Context, what string, tx *transaction.Transaction) (*transaction.MerklePath, uint32, error)
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.
Each poll also asks Asset whether one of tx's inputs is spent by another transaction (nodeapi.WaitSettled). Such a transaction never mines, so Await returns at once with an error wrapping ErrRefused and a *nodeapi.SpentError rather than waiting out Timeout.
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, except one that paid for a funding tree Trees put on the settlement leg, which is spent, and one that a refusal at Settle, SettleAndWait or Await showed spent by another transaction, which is taken out of the pool instead (see Settle).
func (*Payer) Parent ¶
func (p *Payer) Parent(ctx context.Context, o bwallet.Output) (*transaction.Transaction, error)
Parent rebuilds enough of a pool coin's parent transaction to sign against it, and for a spender kept or 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 is fetched from Asset with its proof, because the spender is published before it mines and its BEEF must carry a parent that verifies on its own; without Async the pool's copy is enough, since the spender is mined before it is published.
A parent the pool does not hold at all (a coinbase, which the pool holds only on a regtest chain you run, for development and tests) is fetched from Asset with its proof, Async or not: an application may keep the spender as BEEF before it mines (funding.KeepBEEF), and that BEEF must carry the real parent. Only with no Asset is it a placeholder carrying just the coin's output under the coin's txid. A placeholder is enough to sign against but is not a transaction, and funding.BEEF refuses to write one (funding.ErrPlaceholder), so its spender must mine before its BEEF is built.
func (*Payer) Settle ¶
func (p *Payer) Settle(ctx context.Context, what string, tx *transaction.Transaction) (*transaction.MerklePath, uint32, error)
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").
With Asset set, a transaction one of whose inputs the node shows spent by another transaction is refused, whatever the leg answered: the error wraps ErrRefused and a *nodeapi.SpentError (errors.Is nodeapi.ErrDoubleSpent).
A transaction that is not settled leaves its fee coin reserved, for the caller's GiveBack to return. A coin another transaction spent must not go back, since the pool would hand it out again to fail the next build. So when the leg refuses tx at Submit, or the node's view refuses it afterwards, each coin this Payer's Take reserved that tx spends is asked about: one the refusal names another spender for (a *nodeapi.SpentError for its outpoint), or that Asset, when set, shows spent by a transaction other than tx, is forgotten and taken out of the pool (bwallet.Pool.Remove), with a note, and the GiveBack that follows returns only the rest. A coin the node shows unspent, or spent by tx itself, or cannot answer for, stays reserved and is returned as before. SettleAndWait and Await do the same, Await when its wait ends on such a refusal. Trees does it for a funding tree's coin: see Trees.Spend.
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 ¶
Take reserves a pool coin as a fee input. Everything taken is returned to the pool by GiveBack if the transaction is not sent. Take is TakeAtLeast(ctx, p.Fees.Floor): a coin below the fee floor cannot pay for any transaction, so it is left in the pool.
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.
A pool that has a coin for the take answers at once. When it has none, and a funding tree minted ahead (Trees.Ahead) took a coin of this same Pool and has not been collected, the coin the take needs may be that tree's change, which reaches the pool only when the mint is collected. A wallet with one coin is in that state from the moment a tree is minted ahead. Take then waits for the mint and collects it, exactly as Trees.Wait does (its notes go to the Note of that Trees' Payer, its change into the pool, or its unspent coin back), and takes again. The wait ends with the mint, with ctx, or after this Payer's Timeout (DefaultTimeout when zero), whichever is first; with several such mints, by several Trees over one pool, they are collected one at a time under that one bound, and the take is tried again after each. The mint itself runs under the context of the Spend that started it and, without Async, ends within the Timeout of that Trees' Payer.
What the collected mint leaves decides the second take. A tree that settled with its proof leaves proven change, which is taken. With Async on the Trees' Payer the change is unproven until its block, so the take answers a *NoCoinError whose Held counts it, unless Allow lets it through: the same "wait for a block" an application already handles. A mint that failed before the settlement leg returns its coin, which is taken; one that failed after it leaves nothing, and the take answers a *NoCoinError. When the wait ends first, the mint is still in flight and the *NoCoinError counts it in Minting.
Nothing else waits: not a take the pool can cover, not a mint ahead looking for its own coin, which is skipped with a note when there is none, and not a tree paid through Trees.Fund, which takes no pool coin. Take collects the mint on the calling goroutine, so it belongs to the goroutine that owns the Trees, as every use of a Payer and a Trees over one pool does (see the package documentation).
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 test 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>
func (*Payer) TakeAtLeast ¶ added in v0.5.0
TakeAtLeast is Take for a caller that knows what the coin must pay, the outputs it funds and the fee: only a coin of at least sats satoshis is taken (bwallet.Pool.TakeAtLeast), and a pool whose spendable coins are all smaller answers a *NoCoinError. A coin that is taken but cannot be signed for is back in the pool before the error is returned. It waits for a funding tree minted ahead as Take does.
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
// Source and Spends, when set, are asked in Asset's place: Source for
// a proof arcade cannot give (a transaction it was not sent), Spends
// for the inputs of one that has not mined. A chain view that needs no
// node (nodeapi.ParseChain over WhatsOnChain) is both; wrap a third
// party's proofs in nodeapi.Checked.
Source nodeapi.ProofSource
Spends nodeapi.SpendSource
// Tx, when set, returns the transaction Of is asked about (Kept.Tx is
// one), so that Of can hold a transaction that has not mined to the
// node's view of its inputs, as OfTx does. An error from it only skips
// that check.
Tx func(txid string) (*transaction.Transaction, error)
}
Proofs asks whoever can answer whether a transaction has mined.
func (Proofs) Of ¶
func (p Proofs) Of(ctx context.Context, txid string) (*transaction.MerklePath, uint32, error)
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, or, with Tx and Asset set, when the node shows one of its inputs spent by another transaction (see OfTx).
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.
func (Proofs) OfTx ¶ added in v0.5.4
func (p Proofs) OfTx(ctx context.Context, tx *transaction.Transaction) (*transaction.MerklePath, uint32, error)
OfTx is Of for a transaction the caller holds. While tx has not mined, and with Asset set, an input the node shows spent by another transaction is a refusal: arcade has answered ACCEPTED_BY_NETWORK for a transaction whose input was already spent and mined, which never mines. The error then wraps ErrRefused and a *nodeapi.SpentError.
type PublishError ¶ added in v0.6.2
type PublishError struct {
// Tree is the record the tree was adopted as.
Tree funding.Tree
// Err is why the publish failed.
Err error
}
PublishError is the error of Spend and Recover when TreeState.Adopt took a tree and the publish that follows it failed. The tree is recorded, it is the current tree, and its Prepare record is dropped: nothing is left to mint, settle or recover, and only the publish has to be repeated, which Trees.Publish does with Tree. Until it is, the hosts have not admitted the tree's outputs. errors.Is(err, ErrPublish) matches it, errors.As gives the tree, and Unwrap gives the cause.
func (*PublishError) Error ¶ added in v0.6.2
func (e *PublishError) Error() string
func (*PublishError) Is ¶ added in v0.6.2
func (e *PublishError) Is(target error) bool
Is matches ErrPublish.
func (*PublishError) Unwrap ¶ added in v0.6.2
func (e *PublishError) Unwrap() error
type Recovery ¶ added in v0.6.1
type Recovery struct {
Outcome RecoveryOutcome
// Tree is the tree's record with its proof and height, or the BEEF it
// is kept as while it is unmined: what it was adopted as, or will be.
// Set for TreeAdopted and TreeHeld. It is what Publish takes when
// Recover returns a *PublishError.
Tree funding.Tree
// By is the transaction that spent the fee coin. Set for CoinSpent.
By string
}
Recovery is the result of Trees.Recover.
type RecoveryOutcome ¶ added in v0.6.1
type RecoveryOutcome int
RecoveryOutcome is what Trees.Recover found for one record Prepare was given.
const ( // CoinReturned: the node knows no such tree and shows the fee coin // unspent, so the tree had not reached the chain when Recover asked. // The coin is back in the pool. The application keeps the record and // recovers it again on its next start, because a tree handed to the // settlement leg just before the run stopped can still land: see // Recover. CoinReturned RecoveryOutcome = iota + 1 // CoinSpent: the node shows the fee coin spent by another transaction, // named in Recovery.By, and has no proof for the tree: the tree never // reached the chain, or reached the node and lost a double spend, which // a node may go on serving. The coin is out of the pool, nothing is // adopted, nothing is left to recover, and the application drops the // record. CoinSpent // TreeAdopted: the tree is on the chain. Its fee coin is out of the // pool, its unspent change is in the pool, and it was adopted into the // state and published, as Spend adopts a new tree. Adopt has dropped // the record. When Recover returns it together with a *PublishError, // the tree is adopted and only the publish is to be repeated. TreeAdopted // TreeHeld: the tree is on the chain, its fee coin is out of the pool // and its unspent change is in it, but the current tree still has // outputs, so the tree is held, behind any tree already held (Held // answers them in order, Prepared the first), and adopted when a spend // switches to it. The application keeps the record until Adopt names // its txid. TreeHeld )
func (RecoveryOutcome) String ¶ added in v0.6.1
func (o RecoveryOutcome) String() string
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, up to mint.MaxFundingOutputs. A tree of
// more is refused with an error wrapping mint.ErrTreeTooLarge before a
// coin is taken or Fund is called. 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
// Ahead, when above zero, mints the next tree before the current one
// runs out: once a spend leaves Ahead outputs or fewer on the current
// tree, the next tree is minted and settled in the background, and Spend
// switches to it when the current tree cannot cover a spend, with no
// wait for a block. Zero mints a tree only when one is needed. See
// Spend for the rules, and Wait and Prepared for the tree minted ahead.
// A pool-paid mint holds its coin until it is collected, and a take the
// pool cannot cover meanwhile waits for it: see Payer.Take.
Ahead uint32
// Prepare, when set, is called with each tree the pool pays for, once it
// is signed and before it reaches the settlement leg: on the goroutine
// that called Spend, and for a tree minted ahead before its background
// half starts. tree is the record the tree is later adopted as, without
// a proof, a height or a kept BEEF, which it does not have yet, and coin
// is the fee coin it spends, already out of the pool. An application
// saves both, so a run that stops before Adopt leaves a record of a
// tree that may be on the chain and of the coin it took; Recover
// settles that record on the next start. An error aborts the mint and
// puts the coin back in the pool. Prepare is not called for a DryRun,
// nor for a tree Fund pays for: see Spend.
Prepare func(tree funding.Tree, coin bwallet.Output) error
// contains filtered or unexported fields
}
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) Held ¶ added in v0.6.2
Held returns the record of every tree that is settled and waits for a switch, in the order they were held: the tree minted ahead, and each tree Recover answered TreeHeld for. It is empty when there is none. When the current tree cannot cover a spend, Spend switches to the first of them that is locked to Identity and large enough, and adopts and publishes it then; one too small for that spend keeps its place. No tree is minted ahead while one is held.
func (*Trees) Prepared ¶ added in v0.4.0
Prepared returns the record the tree minted ahead will be adopted as, or nil when there is none: none was started, it is still in flight (see Wait), it failed, or Spend has switched to it. The tree is in no state until the switch; an application that wants a sweep to take its outputs after a crash keeps this record in its own history. When more than one tree is held, which Recover can bring about, it is the first of them, and Held answers them all.
func (*Trees) Publish ¶ added in v0.6.2
Publish publishes a tree the state already holds, on Facade and Topic, as Spend publishes a new one. It is how an application repeats the publish after a *PublishError, with the error's Tree, and it may be called for any adopted tree: the hosts answer a tree they already hold as a duplicate, which is not an error. The tree is rebuilt from its record (funding.Rebuild), so an unmined tree needs the BEEF it is kept as. A failure is again a *PublishError.
func (*Trees) Recover ¶ added in v0.6.1
func (t *Trees) Recover(ctx context.Context, tree funding.Tree, coin bwallet.Output) (Recovery, error)
Recover settles one record Prepare was given and Adopt never dropped: a tree that was signed for the coin and that a run stopped short of adopting. An application runs it on its next start, once for each such record, before the first Spend, with tree and coin as Prepare received them. It asks the Payer's Asset, which must be set, what became of the tree: whether the node serves it, then, when it does, for its proof, and then, for a tree with no proof, which transaction spent the coin. A proof settles it, since a mined tree is on the chain whatever else the node shows. Without one, that the node serves the tree's transaction is not enough, because a node may keep a transaction that lost a double spend, and the coin's spender decides:
- The tree has a proof, or the node shows the coin spent by it: the tree is on the chain. So is a tree the node serves while it shows the coin unspent, which is a node that has the tree and has not yet recorded what it spends. Its fee coin is spent, so it is taken out of the pool if an earlier CoinReturned put it there. Its proof is taken as Settle would take it (waited for, or with Async not waited for: the tree is adopted on the node's word that it spent the coin, and its proof is collected later), the change it pays the Payer's keys goes into the pool unless the node shows that change already spent, and the tree is adopted and published (TreeAdopted). When the current tree is locked to Identity and still has outputs, adopting would strand them, so the tree is held instead (TreeHeld) and adopted at a switch. Any number of trees are held, in the order Recover was asked about them, and Spend switches to the first that is large enough for the spend (see Held), so a second record found on the chain strands nothing. A held tree locked to another identity than Identity is never switched to (see Spend), so its record is one the application moves to its own history.
- The node does not know the tree and shows the coin unspent: the tree has not reached the chain. The coin goes back to the pool (CoinReturned).
- The tree has no proof and the node shows the coin spent by another transaction (CoinSpent, with that transaction in Recovery.By), whether or not the node serves the tree: a tree that never reached the chain, or one that lost a double spend and never mines. The coin is taken out of the pool if it is there, and nothing is adopted, held or published. The answer is the same when the coin is spent while Recover waits for the tree's proof, and on every later Recover.
The application's half is its record. Its Adopt drops the record of the tree it is given, in the same save, so TreeAdopted needs nothing more and a TreeHeld record lasts until the switch. After CoinSpent it drops the record itself. After CoinReturned it keeps the record and recovers it again on its next start, until a later Recover answers CoinSpent, TreeAdopted or TreeHeld, or until a transaction of its own that spends the coin has mined; the reason is the node's view, below.
An error with no outcome means the node could not answer, or the tree has not mined within the Payer's Timeout: the tree is neither adopted nor held, the record stays, and the next start asks again. A node that serves an unproven tree and cannot say who spent its coin is such an error with Async, so that no tree is adopted without a proof or the node's word on its coin; without Async the wait for the proof decides, and adopts only a tree that mines. The fee coin of a tree the node knows is out of the pool even so, which is right, since the node holds it spent. Recover may be run again for the same record: a coin the pool holds is not added twice, and a tree that is already the current one, or already held, is answered TreeAdopted or TreeHeld with nothing done.
One error comes with an outcome. Adopt is called before the tree is published (see Spend), so the publish can fail with the tree adopted and its record dropped. Recover then returns TreeAdopted with the tree and a *PublishError (errors.Is ErrPublish): nothing is left to recover, and the application repeats the publish alone, with Publish and Recovery.Tree. Running Recover again would not publish it, since the tree is then the current one.
A record also outlives a mint that failed after Prepare, since Spend puts the coin back and leaves the record alone; Recover then answers CoinReturned, or CoinSpent once the coin has paid for something else. It should not be dropped on Spend's error, which may come after the tree reached the leg.
The node's view is a moment's view. A tree handed to the settlement leg an instant before the run stopped may not have reached the node when Recover asks, and is then answered CoinReturned, with the coin back in the pool; the tree can still land afterwards. That is why a CoinReturned record is kept: the next start's Recover finds the tree on the chain, takes the coin, which the tree spent, out of the pool, takes the tree's change and adopts or holds the tree, so nothing is lost. An application that drops the record at CoinReturned is, for such a tree, where it was without Prepare: the tree, its change and its outputs are lost to it, and the spent coin stays in its pool.
Two things remain uncovered, both inside the same moment:
- Between the start that answered CoinReturned and the next, the coin is in the pool. If the tree lands in that time and the coin is taken to pay for a transaction, that transaction is refused (ErrRefused) and has to be built again. The refusal takes the spent coin out of the pool when the node shows the tree as its spender (see Payer.Settle). No coin or tree is lost by it; if the transaction was a funding tree, its own record is answered CoinSpent.
- A kept record is asked about on every start, and CoinReturned puts the coin back each time. If the application has since spent the coin in a transaction of its own, and that transaction in turn reached the leg an instant before a stop, the node may show the coin unspent once more and the coin goes back to the pool though it is spent. A transaction that then takes it is refused, or, reaching the node first, displaces the earlier one. That refusal takes the coin out again when the node names its spender by then, and otherwise the next Recover that answers CoinSpent does.
An application that restarts at once can wait a moment before it recovers, which makes both rarer.
A tree Fund paid for has no record, and Recover has nothing to say about it: see Spend.
Example ¶
Prepare hands the application each tree's record and fee coin before the tree reaches the settlement leg, and Recover settles a record a run left behind. Here the run stops while its tree waits for a block: the coin is spent and the state knows no tree. The block arrives, and the next start recovers the tree: it is adopted and published, and its change is in the pool.
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
}
chain := &testChain{known: map[string]*transaction.Transaction{}, accepted: map[string]bool{},
mined: map[string]uint32{}, refused: map[string]string{}, height: 700}
chain.srv = httptest.NewServer(http.HandlerFunc(chain.serve))
defer chain.srv.Close()
run, stop := context.WithCancel(ctx)
defer stop()
payer.Settler, payer.Asset, payer.Poll = stoppingLeg{chain.arcade(), stop}, chain.asset(), 10*time.Millisecond
state := &exampleRecords{prepared: map[string]examplePrepared{}}
treesOver := func(p *producer.Payer) *producer.Trees {
return &producer.Trees{
Payer: p, 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,
Facade: chain.facade(), Topic: "tm_vector_sample",
Prepare: state.Prepare,
}
}
_, _, err = treesOver(payer).Spend(run, 1)
fmt.Println("the run stopped:", errors.Is(err, context.Canceled))
fmt.Println("trees adopted:", len(state.all), "records kept:", len(state.prepared), "coins in the pool:", payer.Pool.Count())
// The block arrives while the application is down.
for txid := range state.prepared {
chain.mine(txid)
}
// The next start: the pool as it is on disk, and the records the state
// kept. Every record is recovered before the first Spend.
pool, err := bwallet.LoadPool(payer.Pool.Path())
if err != nil {
fmt.Println(err)
return
}
next := treesOver(&producer.Payer{
Pool: pool, Tip: 100, Keys: payer.Keys, Kept: &producer.Kept{}, Fees: mint.DefaultFees,
Settler: chain.arcade(), Asset: chain.asset(), Poll: 10 * time.Millisecond, Note: payer.Note,
})
for txid, rec := range state.prepared {
got, err := next.Recover(ctx, rec.Tree, rec.Coin)
if errors.Is(err, producer.ErrPublish) {
// The tree is adopted and its record dropped: only the publish
// is repeated, now or, with got.Tree noted, on a later start.
err = next.Publish(ctx, got.Tree)
}
if err != nil {
fmt.Println(err) // undecided: the record stays for the next start
continue
}
fmt.Println("recovered:", got.Outcome)
if got.Outcome == producer.CoinSpent {
delete(state.prepared, txid) // no tree: the application drops the record
}
// A CoinReturned record is kept for the next start: the tree may
// still land.
}
fmt.Println("trees adopted:", len(state.all), "records kept:", len(state.prepared), "coins in the pool:", pool.Count())
hex := regexp.MustCompile(`[0-9a-f]{64}`)
for _, l := range lines {
if !strings.Contains(l, "settling via") {
fmt.Println(hex.ReplaceAllString(l, "<tree>"))
}
}
Output: the run stopped: true trees adopted: 0 records kept: 1 coins in the pool: 0 recovered: tree adopted trees adopted: 1 records kept: 0 coins in the pool: 1 funding tree <tree>: 4 output(s) of 1000 sat funding tree <tree>: mined at height 701 funding tree <tree> is recovered funding tree published: admitted 1 output(s)
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. The pool pays with a coin of at least the tree's value and the fee floor. A failure before the tree reaches the settlement leg puts that coin back in the pool before Spend returns; once the tree has reached the leg the coin is spent and no later GiveBack returns it.
One failure before the leg does not put the coin back: a tree the leg refuses because its fee coin is already spent. When the refusal names another transaction as the coin's spender (a *nodeapi.SpentError for its outpoint, as publish.Arcade held to a node answers), or the Payer's Asset, when set, shows the coin spent by a transaction other than the tree, the coin is taken out of the pool (bwallet.Pool.Remove) and is not returned, with a note naming the spender: put back, it would be handed out again and fail the next build, until a later start's Recover took it out. A refusal for any other reason, a coin the node shows unspent or spent by the tree itself, and a coin the node cannot answer for all put the coin back as before. A tree minted ahead is treated the same when it is collected. Its Prepare record is left alone either way, and Recover answers CoinSpent for it. Payer.Settle does the same for the fee coin of any other transaction.
The pool-paid mint here takes its coin as Payer.Take does, so with no coin in the pool it first waits for a tree another Trees over the same pool is minting ahead, whose change may pay for this one. 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.
That order means a publish can fail with the tree already adopted. Spend then returns a *PublishError (errors.Is ErrPublish) and no tree: the tree is the current one, and a later Spend answers it without publishing it, so the application repeats the publish with Publish, in this run or a later one, and then calls Spend again. Any other error leaves no tree adopted.
With Ahead set, and not DryRun, a spend that leaves Ahead outputs or fewer on the tree it answers starts minting the next one, of Count outputs, once per tree. A pool-paid tree takes its fee coin and is signed here, on the caller's goroutine, so the reservation never races another Take; only a proven coin is taken, of at least the tree's value and the fee floor, never change Allow would let through, because the tree is settled on its own. The settlement (Payer.Settle), or Fund when it is set, then runs in the background under ctx, so ctx should be the producer's run, not one that ends with this call. Its notes are held and reported through Note by the Spend or Wait that collects it, which also takes the tree's change into the pool. At most one mint is in flight, and none is started while a tree minted ahead waits unused.
Between the moment a pool-paid mint ahead takes its coin and the moment it is collected, the coin is out of the pool and its change is not yet in it. A wallet with one coin has none in that time. A mint ahead that finds no coin for itself never waits: it is skipped with a note, and the tree is minted when it is needed. A take through a Payer over the same pool (Payer.Take, Payer.TakeAtLeast, and the mint a Spend makes on demand) that the pool cannot cover does wait: it collects the mint, as Wait does, and takes again, bounded by its context and its Payer's Timeout. Payer.Take has the rules.
When the current tree cannot cover need, Spend waits for a mint still in flight, then switches to the tree minted ahead if it is locked to Identity and has need outputs: it is adopted and published exactly as a new tree is, and nothing is minted. A mint ahead that failed is reported through Note, the fee coin it had not spent goes back to the pool, and the tree is minted here as it is with Ahead zero. A tree minted ahead that is too small waits for a later switch; one locked to an earlier identity is dropped.
The tree minted ahead is adopted and published only when it is used, never when it is minted: adopting it earlier would make it current and strand the outputs left on the tree carriers still spend, and publishing it before it is adopted would break the rule above. It lives in memory until then, so a crash before the switch leaves it on the chain but in neither the state nor the plane; its change is already in the pool, and only its funding outputs are stranded. An application that wants a sweep to take those too records Prepared in its own history.
A run can also stop between the moment a tree's fee coin leaves the pool and the moment the tree is adopted: while the tree waits for its block, or, minted ahead, for the switch. The pool is saved without the coin, and the state does not know the tree, so the coin, the tree's change and its outputs are all lost to the application, though the chain holds them. An application closes that gap with Prepare, which hands it the tree's record and the coin before the tree reaches the settlement leg, and with Recover, which it runs for each such record on the next start.
Neither covers a tree Fund pays for. The wallet behind Fund chooses the coins, signs and broadcasts inside one call, so there is no moment between the signing and the settlement leg for Prepare to be called in, and no pool coin to return: a run that stops after Fund has broadcast and before Adopt leaves a tree only that wallet knows.
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 test 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
func (*Trees) Wait ¶ added in v0.4.0
Wait waits for the mint ahead in flight, if there is one, and collects it as Spend does: its notes are reported, its change is taken into the pool, and the tree becomes the one Prepared answers. It returns the mint's error, which Note has already reported and which Spend recovers from by minting when a tree is needed, or ctx's error, which leaves the mint in flight. With nothing in flight it returns nil at once.
An application need not call it before it takes a fee: a take the pool cannot cover while the mint holds a coin collects the mint itself (see Payer.Take). Wait remains how a run that is ending collects the mint, so that the change reaches the pool and Prepared answers the tree.
Example ¶
With Ahead set, a spend that leaves the current tree with Ahead outputs or fewer mints the next tree in the background, and the spend the current tree cannot cover switches to it with no wait for a block. The tree minted ahead is adopted and published only at the switch. The example settles on a test chain in this process that mines what it is given.
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
}
chain := &testChain{known: map[string]*transaction.Transaction{}, accepted: map[string]bool{},
mined: map[string]uint32{}, refused: map[string]string{}, height: 700, mineOnSubmit: true}
chain.srv = httptest.NewServer(http.HandlerFunc(chain.serve))
defer chain.srv.Close()
payer.Settler, payer.Asset, payer.Poll = chain.arcade(), chain.asset(), 10*time.Millisecond
payer.Kept.Load = func(txid string) (*transaction.Transaction, error) {
chain.mu.Lock()
defer chain.mu.Unlock()
return chain.known[txid], nil
}
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,
Facade: chain.facade(), Topic: "tm_vector_sample",
Ahead: 2,
}
names := map[string]string{}
name := func(txid string) string {
if names[txid] == "" {
names[txid] = fmt.Sprintf("<tree %d>", len(names)+1)
}
return names[txid]
}
spend := func(need uint32) {
tree, first, err := trees.Spend(ctx, need)
if err != nil {
fmt.Println(err)
return
}
state.cur.Next += need // the application's half: the outputs are spent
fmt.Printf("spend %d: %s from output %d\n", need, name(tree.TxID().String()), first)
}
spend(2) // a tree is minted; 2 outputs are left, so the next is minted ahead
if err := trees.Wait(ctx); err != nil {
fmt.Println(err)
return
}
fmt.Println("minted ahead:", name(trees.Prepared().Txid), "trees adopted:", len(state.all))
spend(2) // the last two
spend(1) // the switch
fmt.Println("trees adopted:", len(state.all))
hex := regexp.MustCompile(`[0-9a-f]{64}`)
for _, l := range lines {
if !strings.Contains(l, "settling via") {
fmt.Println(hex.ReplaceAllStringFunc(l, name))
}
}
Output: spend 2: <tree 1> from output 0 minted ahead: <tree 2> trees adopted: 1 spend 2: <tree 1> from output 2 spend 1: <tree 2> from output 0 trees adopted: 2 funding tree <tree 1>: 4 output(s) of 1000 sat funding tree <tree 1>: mined at height 701 funding tree published: admitted 1 output(s) funding tree <tree 1> has 2 output(s) left, so the next is minted ahead funding tree <tree 2>: 4 output(s) of 1000 sat funding tree <tree 2>: mined at height 702 funding tree <tree 2> is minted ahead and waits for the switch switching to funding tree <tree 2>, minted ahead funding tree published: admitted 1 output(s)