Documentation
¶
Overview ¶
Package mint builds the mined transactions an application's state lives on: a transition of its state token, a funding tree, and a plain payment, each with a fee input and a change output supplied by the caller.
Every lock script arrives as a parameter and every input arrives with the template that signs it, so the package knows no application's derivation or tags and holds no key material. What it owns is the fee logic: a loop that signs to measure the size and rebuilds at the rate until the fee covers it.
A carrier is not built here: it is never mined and pays no fee, so package carrier builds it.
Index ¶
- Constants
- Variables
- func FundingTree(lock *script.Script, count int, sats uint64, fee Input, change *script.Script, ...) (*transaction.Transaction, error)
- func Payment(ctx context.Context, dest *script.Script, sats uint64, fee Input, ...) (*transaction.Transaction, error)
- func Transition(lock *script.Script, sats uint64, prev *Input, fee Input, ...) (*transaction.Transaction, error)
- type Fees
- type Input
- type Rate
Examples ¶
Constants ¶
const MaxFundingOutputs = 1023
MaxFundingOutputs is the most funding outputs one funding tree holds, its change outputs aside. A sweep of every funding output of a tree with one fee input then spends at most 1024 inputs, the least a host that admits a sweep must accept, so the kill switch for a whole tree is one transaction every such host takes. Every carrier's BEEF carries its whole tree as well, so an application usually wants a tree far smaller.
Variables ¶
var ( ErrInsufficient = errors.New("mint: fee input cannot cover the outputs and the fee") ErrNoChange = errors.New("mint: nil change script") // ErrTreeTooLarge is a funding tree of more than MaxFundingOutputs // funding outputs. ErrTreeTooLarge = errors.New("mint: a funding tree holds at most 1023 funding outputs") )
var DefaultFees = Fees{Rate: Rate{Sats: 100, Bytes: 1000}, Floor: 100, Dust: 100, MaxRate: Rate{Sats: 100, Bytes: 1000}}
DefaultFees is the network's rate, 100 satoshis per 1000 bytes (the miningFee GorillaPool's and TAAL's ARC and GorillaPool's arcade publish), never more: MaxRate is the same rate, so a configured or live rate above it is lowered to it. Every transaction pays at least 100 satoshis, what a 1000-byte transaction pays at that rate, and change under 100 satoshis is added to the fee rather than kept.
var ErrFeeTooHigh = errors.New("mint: fee above the per-transaction maximum")
ErrFeeTooHigh is a fee above Fees.Max.
var ErrRate = errors.New("mint: bad fee rate")
ErrRate is a rate that cannot be computed: satoshis over zero bytes, or a fee that overflows 64 bits.
var LegacyFees = Fees{SatPerByte: 1, Floor: 250}
LegacyFees is one satoshi per byte with a 250 satoshi floor: DefaultFees until the network rate became the default. A test vector that pins its fee amounts names it, so a change of default never moves a pinned byte.
var NetworkFees = Fees{Rate: Rate{Sats: 100, Bytes: 1000}, Floor: 1, Dust: 1}
NetworkFees is the network's rate with no padding: 100 satoshis per 1000 bytes, a floor of one satoshi, and every change of a satoshi or more kept. A 225-byte payment pays 23 satoshis.
Functions ¶
func FundingTree ¶
func FundingTree(lock *script.Script, count int, sats uint64, fee Input, change *script.Script, fees Fees) (*transaction.Transaction, error)
FundingTree builds and signs a funding tree: count outputs of sats each, locked with lock, plus change. Each carrier spends one; a mined spend of any of them by the owner (a kill switch) is a transaction the caller builds with the unlocker for lock. count is 1 to MaxFundingOutputs; a larger one is refused with an error wrapping ErrTreeTooLarge.
Example ¶
A funding tree on the test chain: four funding outputs of 1,000 satoshis under the application's funding lock, paid for by the coin, with the fee settled by the fee loop and the change sent back to the payer. The tree is not mined, so it verifies through its proven parent, and the state an application keeps about it carries the BEEF that proves it.
package main
import (
"context"
"fmt"
"github.com/bsv-blockchain/go-sdk/chainhash"
"github.com/bsv-blockchain/go-sdk/script"
"github.com/bsv-blockchain/go-sdk/transaction"
"github.com/bsv-blockchain/go-sdk/transaction/template/p2pkh"
"github.com/bsv-blockchain/go-sdk/wallet"
"github.com/lightwebinc/bcommon/carrier"
"github.com/lightwebinc/bcommon/funding"
"github.com/lightwebinc/bcommon/goldentest"
"github.com/lightwebinc/bcommon/mint"
"github.com/lightwebinc/bcommon/pushdrop"
"github.com/lightwebinc/bcommon/verify"
)
// testCoin is a stand-in mined coin on a test chain that exists only in this
// process: one P2PKH output of 50,000 satoshis to the fixed test key, placed
// by a stand-in proof at offset 1 of a two-transaction block at height 90.
// The tracker knows that block's merkle root and nothing else, so SPV runs
// against it exactly as it would against a reader's own header source.
func testCoin() (*transaction.Transaction, *goldentest.Tracker, error) {
addr, err := script.NewAddressFromPublicKey(goldentest.FixedKey().PubKey(), false)
if err != nil {
return nil, nil, err
}
lock, err := p2pkh.Lock(addr)
if err != nil {
return nil, nil, err
}
coin := transaction.NewTransaction()
nowhere := chainhash.Hash(goldentest.Fill(0x11))
coin.AddInput(&transaction.TransactionInput{SourceTXID: &nowhere, UnlockingScript: &script.Script{},
SequenceNumber: transaction.MaxTxInSequenceNum})
coin.AddOutput(&transaction.TransactionOutput{Satoshis: 50000, LockingScript: 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},
}})
root, err := coin.MerklePath.ComputeRoot(coin.TxID())
if err != nil {
return nil, nil, err
}
return coin, &goldentest.Tracker{Roots: map[uint32]string{90: root.String()}, Tip: 100}, nil
}
// A funding tree on the test chain: four funding outputs of 1,000 satoshis
// under the application's funding lock, paid for by the coin, with the fee
// settled by the fee loop and the change sent back to the payer. The tree
// is not mined, so it verifies through its proven parent, and the state an
// application keeps about it carries the BEEF that proves it.
func main() {
ctx := context.Background()
coin, tracker, err := testCoin()
if err != nil {
fmt.Println(err)
return
}
payer, err := p2pkh.Unlock(goldentest.FixedKey(), nil)
if err != nil {
fmt.Println(err)
return
}
w, err := wallet.NewCompletedProtoWallet(goldentest.FixedKey())
if err != nil {
fmt.Println(err)
return
}
// The application's carrier parameters: its derivation and funding tag.
// These are the test vectors' own, reserved for tests.
params := carrier.Params{
Derivation: pushdrop.Derivation{
Protocol: wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryApp, Protocol: "vector sample"},
KeyID: "object",
},
FundingTag: []byte{'v', 'x', 0x02},
}
lock, err := carrier.FundingLock(ctx, w, "example.com", params)
if err != nil {
fmt.Println(err)
return
}
fee := mint.Input{Tx: coin, Vout: 0, Unlocker: payer}
change := coin.Outputs[0].LockingScript
tree, err := mint.FundingTree(lock, 4, 1000, fee, change, mint.DefaultFees)
if err != nil {
fmt.Println(err)
return
}
for i, out := range tree.Outputs {
_, isFunding := carrier.DecodeFunding(out.LockingScript, params.FundingTag)
fmt.Printf("output %d: %d sats, funding=%t\n", i, out.Satoshis, isFunding)
}
fmt.Println("fee:", 50000-tree.TotalOutputSatoshis(), "sats for", tree.Size(), "bytes")
verdict, err := verify.Check(ctx, tree, tracker)
fmt.Println("proves through its parent:", verdict == verify.Passed, err)
beef, err := funding.KeepBEEF(tree, nil)
if err != nil {
fmt.Println(err)
return
}
state := funding.Tree{Txid: tree.TxID().String(), RawHex: tree.Hex(), BeefHex: beef, Sats: 1000, Count: 4}
fmt.Println("carriers it can fund:", state.Remaining())
}
Output: output 0: 1000 sats, funding=true output 1: 1000 sats, funding=true output 2: 1000 sats, funding=true output 3: 1000 sats, funding=true output 4: 45900 sats, funding=false fee: 100 sats for 388 bytes proves through its parent: true <nil> carriers it can fund: 4
func Payment ¶
func Payment(ctx context.Context, dest *script.Script, sats uint64, fee Input, change *script.Script, fees Fees) (*transaction.Transaction, error)
Payment builds and signs a simple payment: one output of sats to dest, change back to the payer. It is the BRC-29 leg: the destination is a key derived from the recipient's identity, and the transaction itself is an ordinary P2PKH spend.
ctx is unused: nothing here reaches the network, and a template that signs through a wallet carries its own context. It is kept so the signature stays stable for callers if a later version needs it.
func Transition ¶
func Transition(lock *script.Script, sats uint64, prev *Input, fee Input, change *script.Script, fees Fees) (*transaction.Transaction, error)
Transition builds and signs a state token transaction: one output of sats locked with lock.
Inputs: the previous token output when prev is not nil (an update spends its predecessor; a create spends none), then the fee input. Outputs: the token at index 0, then change. Change below the dust threshold (Fees.Dust, the floor when unset) is dropped rather than left as dust.
prev.Unlocker is required. The key that signs the previous token is not always the one the new lock names (after a rotation the predecessor signs and the successor is locked to), so which one it is stays the caller's decision rather than a default here.
Types ¶
type Fees ¶
type Fees struct {
// SatPerByte is a whole number of satoshis per byte, read only when
// Rate is zero.
SatPerByte uint64
// Floor is the least fee a transaction pays, in satoshis.
Floor uint64
// Rate is the fee rate. Bytes must be at least 1 when Sats is set.
Rate Rate
// Dust is the least change kept as an output; change below it is
// added to the fee. Zero means Floor, the rule before Dust existed.
Dust uint64
// MaxRate, when set, caps Rate: a rate above it is lowered to it. It
// bounds what a fee rate from someone else's policy endpoint can cost.
MaxRate Rate
// Max, when set, is the most one transaction may pay in fee. A fee
// above it is refused with ErrFeeTooHigh: overpaying is lost money, so
// it fails closed.
Max uint64
}
Fees is the fee policy: a rate on the signed transaction's size, a floor under the fee, the change below which change is dropped, and the bounds that keep a bad rate from draining a wallet.
The rate is Rate, satoshis per bytes exactly as miners publish it (ARC's miningFee), computed in integer arithmetic and rounded up. SatPerByte is the older whole-number form and is read only when Rate is zero, as Rate{SatPerByte, 1}; a Fees written before Rate existed means what it meant. The fee for a size is ceil(size*Sats/Bytes), at least Floor, with the rate first lowered to MaxRate when MaxRate is set; above Max it is refused with ErrFeeTooHigh rather than paid.
type Input ¶
type Input struct {
Tx *transaction.Transaction
Vout uint32
Unlocker transaction.UnlockingScriptTemplate
}
Input is one input the caller funds a transaction with: its source transaction, the output index, and the template that signs it. The template is what keeps key material out of this package: an embedded wallet's funding unlocker signs through wallet.CreateSignature.
type Rate ¶ added in v0.14.0
Rate is a fee rate exactly as miners publish it: Sats satoshis per Bytes bytes (ARC's policy.miningFee, whose field names the JSON form keeps, so a policy answer pastes into a configuration). {1, 1} is one satoshi a byte, {100, 1000} is 100 satoshis a kilobyte, {1, 1000000} one satoshi a megabyte. Every computation on it is integer arithmetic; nothing here converts it to a float.
func ParseRate ¶ added in v0.14.0
ParseRate reads a rate written as sats/bytes ("100/1000") or as a whole number of satoshis a byte ("1", which is 1/1). Bytes must be at least 1.
func (Rate) Clamp ¶ added in v0.14.0
Clamp is r held to [lo, hi]: lo when r is below it, hi when r is above it. A zero bound is no bound.
func (Rate) Cmp ¶ added in v0.14.0
Cmp compares two rates by value: -1 when r is the lower, 0 when they are equal (100/1000 equals 1/10), +1 when r is the higher. A rate over zero bytes compares as the highest there is, so a guard never mistakes it for a cheap one.
func (Rate) Fee ¶ added in v0.14.0
Fee is ceil(size * Sats / Bytes), with no floor. Bytes zero, or a fee that does not fit in 64 bits, is an error wrapping ErrRate.