acceptance

package
v0.14.1 Latest Latest
Warning

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

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

Documentation

Overview

Package acceptance decides how much evidence a payment needs before the receiver acts on it, and gathers that evidence.

A payment is worth what it pays only once it is mined: until then its payer can spend the same coins elsewhere. Waiting for a block on every payment makes every paid answer as slow as a block, while acting on a payment at once makes the receiver carry every double spend. The rule this package applies is a value discriminator:

  • a payment at or below a threshold takes the fast path: its ancestry verifies to mined proofs against the receiver's headers, it is well formed, final and pays what it must, the receiver broadcasts it itself, the network's broadcaster answers it accepted with no double-spend status, and the node's spend view names no other spender, through an optional conflict-watch window;
  • a payment above the threshold is held for confirmation: it is broadcast the same way, and acted on only once it is mined with a merkle proof that verifies against the receiver's headers;
  • a payment that fails a check is refused.

The threshold is in satoshis. It is a static value, or one converted from a fiat amount through a PriceSource the application supplies; a price that is unknown, zero or stale makes the threshold zero, so every payment is held: the package fails toward waiting, never toward trusting.

Many small payments add up, so the fast path is also bounded per payer and in total: an Exposure counts the satoshis taken on the fast path and not yet mined within a rolling window, and a payment that would carry either sum past its limit is held instead. The receiver decides, always: a payer may ask to be held, never to be fast.

A payment taken on the fast path is watched until it mines (Monitor). One that the network refuses, or whose input the node shows spent by another transaction, is reported to the application's hook and its payer flagged, so that the payer's later payments are held for confirmation. What else follows (revoking a service, telling an operator) is the application's.

The zero Policy holds every payment: a receiver that configures nothing waits for a block, as it did before this package.

Index

Constants

View Source
const (
	DefaultThresholdSats  uint64 = 25_000_000
	DefaultThresholdCents uint64 = 2_500
	DefaultWindow                = time.Hour
	DefaultTotalFactor    uint64 = 10
	DefaultWait                  = 10 * time.Second
	DefaultPoll                  = 500 * time.Millisecond
	DefaultMaxPriceAge           = time.Hour
	// SatsPerCoin is satoshis in one coin.
	SatsPerCoin uint64 = 100_000_000
)

Defaults DefaultPolicy uses. DefaultThresholdSats is 25 US dollars at a price of 100 US dollars a coin, set high on purpose: if the price is lower, the threshold is worth less than 25 dollars, which is the safe side. Review it against the price on a schedule.

Variables

View Source
var ErrNoHeaders = errors.New("acceptance: no headers to verify the payment against")

ErrNoHeaders is a Verifier with no headers to check a payment against.

View Source
var ErrNoPrice = errors.New("acceptance: no price")

ErrNoPrice is a source with no price to give.

Functions

This section is empty.

Types

type Ask

type Ask int

Ask is what a payer asks of the receiver. A payer may ask to be held; it may ask to be fast, which changes nothing: the receiver decides.

const (
	AskNone Ask = iota
	AskFast
	AskHold
)

type CachedPrice

type CachedPrice struct {
	Source PriceSource
	TTL    time.Duration
	// contains filtered or unexported fields
}

CachedPrice asks Source at most once per TTL and answers its last good price between asks. A failed ask is not cached: the last good price is answered, with its own date, so a Policy's MaxPriceAge still ends it, and with no good price yet the error is answered.

func (*CachedPrice) Price

func (c *CachedPrice) Price(ctx context.Context) (Price, error)

Price is the cached price, asking Source when the cache is older than TTL.

type Decision

type Decision int

Decision is what a receiver does with a payment.

const (
	// Refuse is a payment that fails a check: the receiver does not act on
	// it. It is the zero Decision, so a Verdict nobody filled in refuses.
	Refuse Decision = iota
	// Fast is a payment the receiver acts on now, on the fast path's
	// evidence, and watches until it mines.
	Fast
	// Hold is a payment the receiver acts on only once it is mined with a
	// proof against its headers.
	Hold
)

func (Decision) String

func (d Decision) String() string

type Event

type Event struct {
	Kind   EventKind
	Txid   string
	Payer  string
	Sats   uint64
	Height uint32
	// Why is the evidence in words; it may hold text another party wrote.
	Why string
}

Event is a payment taken on the fast path that the Monitor stopped watching, and why.

type EventKind

type EventKind int

EventKind is what became of a payment taken on the fast path.

const (
	// Confirmed is a payment mined with a proof against the headers: it is
	// money, and no longer counts against its payer.
	Confirmed EventKind = iota + 1
	// DoubleSpent is a payment whose input the node shows spent by another
	// transaction: it will never mine.
	DoubleSpent
	// Refused is a payment a broadcaster answers REJECTED.
	Refused
	// Unmined is a payment not mined within the Monitor's MaxAge. It may
	// still mine; it is reported once and no longer watched.
	Unmined
)

func (EventKind) String

func (k EventKind) String() string

type Exposure

type Exposure struct {
	// contains filtered or unexported fields
}

Exposure counts what the fast path has taken and not seen mined, per payer and in total, and the payers whose fast payment was double spent. It is safe for concurrent use. It is memory only: an application that restarts restores what it still watches with Charge and Flag.

func NewExposure

func NewExposure() *Exposure

NewExposure is an empty Exposure.

func (*Exposure) Charge

func (x *Exposure) Charge(payer, txid string, sats uint64, at time.Time)

Charge counts a fast payment taken earlier, at the time it was taken: what an application restores after a restart.

func (*Exposure) Flag

func (x *Exposure) Flag(payer, why string)

Flag marks a payer whose fast payment was double spent or refused: every later payment of theirs is held. why is kept for the verdict.

func (*Exposure) Flagged

func (x *Exposure) Flagged(payer string) (string, bool)

Flagged reports whether a payer is flagged, and why.

func (*Exposure) Release

func (x *Exposure) Release(txid string)

Release stops counting a payment: one decided fast and then not acted on, or one that has mined.

func (*Exposure) Unflag

func (x *Exposure) Unflag(payer string)

Unflag clears a payer's flag: an operator's decision.

func (*Exposure) Unmined

func (x *Exposure) Unmined(payer string, window time.Duration) (forPayer, total uint64)

Unmined is what the fast path has taken and not seen mined: for one payer, and in total. Charges older than window are not counted; zero counts them all.

type Monitor

type Monitor struct {
	Verifier *Verifier
	// MaxAge is how long a payment may stay unmined before it is reported
	// Unmined; zero never reports it.
	MaxAge time.Duration
	// Hook receives each event, outside the Monitor's lock.
	Hook func(Event)
	// contains filtered or unexported fields
}

Monitor watches the payments the fast path took until each mines or is lost. A lost payment (DoubleSpent, Refused, Unmined) flags its payer in the Verifier's Exposure, so the payer's later payments are held, and is reported to Hook: revoking what it bought, or telling an operator, is the application's. Sweep is one pass and is idempotent, so it runs on a timer; Run runs it on one.

func (*Monitor) Run

func (m *Monitor) Run(ctx context.Context, every time.Duration)

Run sweeps every interval until ctx ends.

func (*Monitor) Sweep

func (m *Monitor) Sweep(ctx context.Context) []Event

Sweep asks once after every watched payment and reports each that mined or was lost. It answers the events it reported.

func (*Monitor) Watch

func (m *Monitor) Watch(tx *transaction.Transaction, payer string, sats uint64)

Watch starts watching a payment the fast path took.

func (*Monitor) Watching

func (m *Monitor) Watching() []string

Watching is the txids still watched, sorted.

type Output

type Output struct {
	Vout   uint32
	Script []byte
	Sats   uint64
}

Output is one output a payment must hold: at index Vout, locked by Script (the key the receiver derived for the payment), at least Sats.

type Payment

type Payment struct {
	Tx    *transaction.Transaction
	Payer string
	Pays  []Output
	Ask   Ask
}

Payment is a payment offered to the receiver: the transaction, read from its BEEF so that its inputs carry their source transactions (guard first), the payer it is charged to, the outputs it must hold, and what the payer asked.

type Policy

type Policy struct {
	// ThresholdSats is the largest payment, in satoshis, the fast path
	// takes. With ThresholdCents set too, the smaller of the two applies.
	ThresholdSats uint64
	// ThresholdCents is the threshold in US cents, converted through
	// Price. A price that is unknown, zero or older than MaxPriceAge makes
	// the threshold zero: every payment is held.
	ThresholdCents uint64
	Price          PriceSource
	MaxPriceAge    time.Duration
	// Window is how long a fast payment counts against its payer and the
	// total once taken, unless it mines first; zero counts it until it
	// mines or is released.
	Window time.Duration
	// PayerLimit bounds the satoshis one payer has on the fast path and
	// not yet mined within Window; TotalLimit bounds them across every
	// payer. A payment that would pass either is held. Zero allows
	// nothing.
	PayerLimit uint64
	TotalLimit uint64
	// Agree is how many status sources must answer a fast payment
	// accepted; zero is one.
	Agree int
	// Wait bounds how long the fast path waits for that acceptance; zero
	// is DefaultWait. Watch is how long it keeps watching for a conflict
	// before it answers fast, zero for none; a Watch longer than Wait
	// extends Wait. Poll paces both; zero is DefaultPoll.
	Wait  time.Duration
	Watch time.Duration
	Poll  time.Duration
}

Policy is a receiver's rule for which payments are fast. The zero Policy holds every payment.

func DefaultPolicy

func DefaultPolicy() Policy

DefaultPolicy is the recommended policy over a static threshold: 25 dollars at 100 dollars a coin, each payer bounded to one threshold and all payers to ten, within an hour, no watch window.

func (Policy) Decide

func (p Policy) Decide(ctx context.Context, x *Exposure, r Request) Verdict

Decide weighs a payment's value against the policy and the exposure already taken. On Fast it charges the payment to x at once, so two payments decided together cannot both pass a limit; a caller that then does not act on it calls x.Release. It looks at nothing but value: the checks are the Verifier's.

func (Policy) Threshold

func (p Policy) Threshold(ctx context.Context) (uint64, Reason)

Threshold is the threshold in satoshis now, with the reason it is zero when a price was needed and not known.

type Price

type Price struct {
	CentsPerCoin uint64
	At           time.Time
}

Price is a coin's price in US cents, and when it was read. A zero At is a price with no date, which never goes stale: a static price an operator set and reviews.

type PriceSource

type PriceSource interface {
	Price(ctx context.Context) (Price, error)
}

PriceSource is where a threshold set in cents gets its price. The library names no live price service: an application wraps the one it trusts. Any error, or a zero price, makes the threshold zero, so every payment is held.

type ProofSource

type ProofSource interface {
	Proof(ctx context.Context, txid string) (*transaction.MerklePath, uint32, error)
}

ProofSource is where a mined transaction's proof is read (*nodeapi.Asset); an error is not mined yet, or not known.

type Reason

type Reason string

Reason is why a Verdict is what it is: a short fixed label, fit for a metric.

const (
	// Fast.
	ReasonAtOrBelow Reason = "at-or-below-threshold"
	ReasonMined     Reason = "mined"

	// Hold.
	ReasonAboveThreshold   Reason = "above-threshold"
	ReasonPriceUnknown     Reason = "price-unknown"
	ReasonPriceStale       Reason = "price-stale"
	ReasonPayerLimit       Reason = "payer-limit"
	ReasonTotalLimit       Reason = "total-limit"
	ReasonFlagged          Reason = "payer-flagged"
	ReasonAskedHold        Reason = "payer-asked-hold"
	ReasonNoExposure       Reason = "no-exposure-ledger"
	ReasonNoBroadcast      Reason = "no-broadcast-leg"
	ReasonBroadcastUnknown Reason = "broadcast-unconfirmed"
	ReasonNoVerdict        Reason = "no-network-verdict"
	ReasonConflict         Reason = "double-spend-attempted"
	ReasonSpendUnknown     Reason = "spend-view-unknown"
	ReasonNoTxid           Reason = "no-txid"
	ReasonAlreadyCharged   Reason = "already-charged"

	// Refuse.
	ReasonNothing        Reason = "pays-nothing"
	ReasonMalformed      Reason = "malformed"
	ReasonUnderpaid      Reason = "underpaid"
	ReasonWrongScript    Reason = "wrong-script"
	ReasonNotFinal       Reason = "not-final"
	ReasonOverspends     Reason = "outputs-exceed-inputs"
	ReasonSPV            Reason = "spv-failed"
	ReasonNetworkRefused Reason = "network-refused"
	ReasonDoubleSpent    Reason = "double-spent"
)

The reasons.

type Request

type Request struct {
	Payer string
	Txid  string
	Sats  uint64
	Ask   Ask
}

Request is a payment as Decide weighs it: who pays, how much, and its txid, by which a fast payment is charged to its payer.

type SpendView

type SpendView interface {
	Spender(ctx context.Context, txid string, vout uint32) (string, error)
}

SpendView is the node's view of an output (*nodeapi.Asset): the txid that spent it, "" with a nil error only when the node says it is unspent, and an error for any other answer.

type StaticPrice

type StaticPrice uint64

StaticPrice is a fixed price, reviewed by its operator.

func (StaticPrice) Price

func (s StaticPrice) Price(context.Context) (Price, error)

Price is the fixed price, undated.

type StatusSource

type StatusSource interface {
	Status(ctx context.Context, txid string) (*publish.ArcadeStatus, error)
}

StatusSource is a broadcaster's view of one transaction, as arcade's GET /tx/{txid} answers it (*publish.Arcade).

type Verdict

type Verdict struct {
	Decision Decision
	Reason   Reason
	// Detail is the reason in words, for a log; it may hold text another
	// party wrote (a broadcaster's answer), so filter it before a terminal.
	Detail string
	// Sats is what the payment pays the receiver, and Threshold the
	// threshold it was held to (zero when it was never reached).
	Sats      uint64
	Threshold uint64
}

Verdict is a decision on one payment and why.

func (Verdict) String

func (v Verdict) String() string

type Verifier

type Verifier struct {
	Policy   Policy
	Exposure *Exposure
	// Headers are the receiver's own headers, which every proof is checked
	// against.
	Headers chaintracker.ChainTracker
	// Settler is the leg the receiver broadcasts on; without one, no
	// payment is fast.
	Settler publish.Settler
	// Status are the broadcasters asked for the network's verdict; Policy
	// .Agree of them must answer accepted. Spends, when set, is the node's
	// spend view, which must name no other spender and must answer for
	// every input. Proofs is where Confirm reads a proof.
	Status []StatusSource
	Spends SpendView
	Proofs ProofSource
}

Verifier gathers a payment's evidence under a Policy.

func (*Verifier) Accept

func (v *Verifier) Accept(ctx context.Context, p Payment) (Verdict, error)

Accept checks a payment and decides on it:

  1. the checks every payment passes (Check): well formed, final, pays every Output, spends no more than it has, and its ancestry verifies to mined proofs against Headers;
  2. Decide on its value; a mined payment that verifies is Fast at once;
  3. the receiver broadcasts it, its unmined ancestors first, whatever the decision, so that a held payment mines too;
  4. for a Fast decision, the network's verdict: Agree status sources answer it accepted with no double-spend status and no competing transaction, and the spend view names no other spender of any input, through Policy.Watch.

A payment that loses its fast evidence for a reason that may pass (no verdict yet, a spend view that does not answer) is held; one the network refuses, or whose input is spent by another transaction, is refused. The error is only for a Verifier that cannot decide at all, or a context that ended.

func (*Verifier) Check

func (v *Verifier) Check(ctx context.Context, p Payment) (uint64, *Verdict)

Check is the checks every payment passes before its value is weighed. It answers what the payment pays the receiver, and a refusal, or nil when it passes.

func (*Verifier) Confirm

Confirm waits for tx to mine and answers its proof, checked against Headers. While it waits, an input the spend view shows spent by another transaction ends the wait with a *nodeapi.SpentError (errors.Is nodeapi.ErrDoubleSpent). ctx bounds the wait.

Jump to

Keyboard shortcuts

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