Documentation
¶
Overview ¶
Package publish is the publisher's dual submission: one signed transaction, two encodings, two transports, and never one writer for both.
The settlement leg (TCPIngress, RPCSettler, Arcade) carries a mined transaction as EF bytes to a fabric ingress, or as standard hex to a node's RPC. The object leg (Facade) carries a BEEF to an overlay host's submit door. The shard-proxy locks a TCP connection's grammar from its first four bytes (0xBE 0xEF is a BRC-149 stream, E3 E1 F3 E8 is framed, anything else is bare transactions), so a BEEF written down the EF socket is not a mistake the peer reports, it is a poisoned stream. This package makes that a structural rule rather than a prose one: the two legs share no connection, no net.Conn and no writer, and this package exports nothing that takes both a *transaction.Transaction and a BEEF []byte. TestNoExportedAPITakesBoth walks the package's exported declarations to hold it, and TestLegsNeverShareASocket runs both legs against recording peers.
Index ¶
- Constants
- Variables
- func DefaultSettle(network string) (string, error)
- func IsBEEF(b []byte) bool
- func ParseSettler(spec string, opt SettleOptions) (Settler, *Arcade, error)
- type Arcade
- func (a *Arcade) Known(ctx context.Context, txid string) (bool, error)
- func (a *Arcade) Name() string
- func (a *Arcade) Ping(ctx context.Context) error
- func (a *Arcade) Proof(ctx context.Context, txid string) (*transaction.MerklePath, uint32, error)
- func (a *Arcade) Status(ctx context.Context, txid string) (*ArcadeStatus, error)
- func (a *Arcade) Submit(ctx context.Context, tx *transaction.Transaction) error
- type ArcadeStatus
- type Entry
- type Facade
- type Journal
- type RPCSettler
- type Result
- type SettleOptions
- type Settler
- type TCPIngress
Examples ¶
Constants ¶
const ( ArcadeMainnet = "https://arcade.gorillapool.io" ArcadeTestnet = "https://testnet.arcade.gorillapool.io" )
The public arcade installations GorillaPool runs, the default broadcaster: no key, ARC-compatible routes at the root (POST /tx, GET /tx/{txid}, GET /policy).
const DefaultVerdict = 15 * time.Second
DefaultVerdict is long enough for a network to see a transaction in the ordinary case and short enough that a publisher never waits long for it.
Variables ¶
var ErrArcadeRefused = errors.New("arcade: the network refused the transaction")
ErrArcadeRefused is a transaction arcade reports the network refused (REJECTED, DOUBLE_SPEND_ATTEMPTED). It never mines.
var ErrArcadeUnknown = errors.New("arcade: transaction not known")
ErrArcadeUnknown is a txid arcade does not hold. A transaction broadcast through some other path is not arcade's to report on.
var ErrBodyTooLarge = errors.New("publish: response body exceeds the bound")
ErrBodyTooLarge refuses a response above the bound.
var ErrNoEntry = errors.New("publish: no journal entry")
ErrNoEntry is returned by Read and Update for a sequence that has no file.
var ErrNotBEEF = errors.New("publish: body is not a BEEF")
ErrNotBEEF is returned by Submit for a body that does not start with a BEEF marker. Nothing else may go down the object leg.
var ErrSettleSpec = errors.New("publish: bad settlement specification")
ErrSettleSpec is a settlement specification that cannot be used.
Functions ¶
func DefaultSettle ¶ added in v0.14.0
DefaultSettle is the settlement specification for a network when an application configures none: arcade, "arcade:main" or "arcade:test". A regtest chain has no public broadcaster, so it has no default.
func IsBEEF ¶
IsBEEF reports whether b begins with a BEEF V1, V2 or Atomic BEEF marker (the SDK's constants, little-endian on the wire).
func ParseSettler ¶ added in v0.14.0
func ParseSettler(spec string, opt SettleOptions) (Settler, *Arcade, error)
ParseSettler reads a settlement specification:
arcade:main | arcade:test GorillaPool's public arcade (the default)
arcade:https://host an arcade installation, yours or another
arc:https://host[/v1] an ARC installation (opt-in), such as
https://arc.gorillapool.io or, with a Key,
https://arc.taal.com; /v1 is added to a
bare host
rpc:http://node:port a node's sendrawtransaction
tcp:host:port a fabric ingress, bare EF, no answer
and returns the leg, and the *Arcade when it is one (arcade and ARC answer the same API), for proofs and status.
Types ¶
type Arcade ¶
type Arcade struct {
Base string
// Key, when set, is sent as a bearer token; public arcade installations
// generally require one.
Key string
Client *http.Client
// Verdict bounds how long Submit waits for the network to accept or
// refuse a transaction arcade has taken. Zero means DefaultVerdict.
Verdict time.Duration
// Poll is the interval between status checks while waiting. Zero means
// one second.
Poll time.Duration
// Note, when set, receives a line a caller may want to show: a verdict
// that did not arrive inside the bound, or a backoff.
Note func(format string, args ...any)
// Asset, when set, is the node arcade's verdict is held to. Arcade has
// answered ACCEPTED_BY_NETWORK for a transaction whose input was already
// spent and mined; with Asset set, an input the node shows spent by
// another transaction is a refusal (errors.Is nodeapi.ErrDoubleSpent)
// whatever arcade says, and is checked while Submit waits too.
Asset *nodeapi.Asset
// Spends, when set, is that view in Asset's place: a spend view that
// needs no node, such as WhatsOnChain (nodeapi.WoC), so that the
// ACCEPTED-but-spent case stays refused without one.
Spends nodeapi.SpendSource
}
Arcade settles through an arcade installation's ARC-compatible API: the broadcaster a publisher reaches when it runs no node of its own.
Unlike the bare EF ingress it answers. POST /tx validates policy synchronously and says so, and GET /tx/{txid} reports the network's verdict and, once mined, the merkle path. That answer is what lets a publisher stop waiting for a block: the network's acceptance is the evidence a transaction will mine, and the proof can be collected later.
Only a transaction meant to be mined is ever given to a Settler. A carrier is non-final by construction and arcade would refuse it as such; nothing here ever sees one.
func (*Arcade) Known ¶ added in v0.14.0
Known reports whether arcade holds txid, in any status: true for a transaction it was sent, false for ErrArcadeUnknown.
func (*Arcade) Ping ¶
Ping reads GET /policy, which answers without a transaction, to show the installation is reachable and speaks ARC.
func (*Arcade) Proof ¶ added in v0.14.0
func (a *Arcade) Proof(ctx context.Context, txid string) (*transaction.MerklePath, uint32, error)
Proof is arcade's proof of a transaction it was sent, as a nodeapi.ProofSource: the merkle path once MINED, held to the checks a node's proof is (it parses through the BUMP guard, names txid, and agrees with the height arcade reports). A transaction not mined yet, or one arcade does not hold (ErrArcadeUnknown: it was broadcast some other way), is nodeapi.ErrNotMined, so that a caller asks another source; one the network refused is ErrArcadeRefused. Check the proof against your headers (nodeapi.Checked).
func (*Arcade) Submit ¶
func (a *Arcade) Submit(ctx context.Context, tx *transaction.Transaction) error
Submit gives arcade the transaction as EF and waits, briefly, for the network's verdict.
A 202 means arcade's policy check passed; scripts and fees are the network's to judge, so RECEIVED alone is not yet acceptance. Submit waits up to Verdict for the network to accept or refuse. A refusal is an error, which keeps a transaction the network will not mine from being published to anyone. No verdict inside the bound is not an error: the transaction is in arcade's hands and its later status will say, and a publisher that waited for it would be back to waiting for a block.
With Asset set, acceptance is not taken at arcade's word: an input the node shows spent by another transaction is the network's refusal, an error that wraps a *nodeapi.SpentError.
type ArcadeStatus ¶
type ArcadeStatus struct {
TxID string `json:"txid"`
TxStatus string `json:"txStatus"`
BlockHash string `json:"blockHash"`
BlockHeight uint32 `json:"blockHeight"`
MerklePath string `json:"merklePath"`
ExtraInfo string `json:"extraInfo"`
CompetingTxs []string `json:"competingTxs"`
}
ArcadeStatus is arcade's answer for one transaction.
func (*ArcadeStatus) Accepted ¶
func (s *ArcadeStatus) Accepted() bool
Accepted reports that the network, not only arcade's policy check, has taken the transaction.
func (*ArcadeStatus) Mined ¶
func (s *ArcadeStatus) Mined() bool
Mined reports a transaction arcade can prove.
func (*ArcadeStatus) Refused ¶
func (s *ArcadeStatus) Refused() bool
Refused reports a terminal refusal: the network will not mine this transaction as it stands.
func (*ArcadeStatus) Why ¶
func (s *ArcadeStatus) Why() string
Why is the refusal in words, with whatever arcade said about it.
type Entry ¶
type Entry struct {
// Acct is the publisher's account, such as a BRC-169 handle@domain, as
// the application writes it.
Acct string `json:"acct"`
// IdentityKey is the publisher's identity key, in the application's
// encoding.
IdentityKey string `json:"identityKey"`
// Seq is the transition's sequence number, the first half of the key.
Seq uint64 `json:"seq"`
// Kind is the application's label for the transition.
Kind string `json:"kind"`
// TxID is the id of the transaction the settlement leg sends, 64 hex
// characters, the second half of the key.
TxID string `json:"txid"`
// CarrierTxID is the id of the carrier the transition commits to, when
// it has one.
CarrierTxID string `json:"carrierTxid,omitempty"`
// EFSentAt is when the settlement leg took the transaction, and EFError
// why it did not.
EFSentAt *time.Time `json:"efSentAt,omitempty"`
EFError string `json:"efError,omitempty"`
// BEEFSentAt is when the object leg's host answered, BEEFSteak that
// answer as received, and BEEFError why the leg failed.
BEEFSentAt *time.Time `json:"beefSentAt,omitempty"`
BEEFSteak json.RawMessage `json:"beefSteak,omitempty"`
BEEFError string `json:"beefError,omitempty"`
// MinedAt is when the transaction was seen mined, and Height the block
// height it was mined at.
MinedAt *time.Time `json:"minedAt,omitempty"`
Height uint32 `json:"height,omitempty"`
}
Entry is one transition's record. The journal reads only Seq and TxID, which key the entry; every other field holds what the caller writes.
type Facade ¶
type Facade struct {
// Base is the facade's root with no path suffix.
Base string
// HTTP is optional; the default has a 30s timeout.
HTTP *http.Client
}
Facade is the object leg: POST <Base>/submit with a raw BEEF body and one topic. It owns its own *http.Client and touches no net.Conn of the settlement leg.
The POST is hand-rolled rather than go-sdk's HTTPSOverlayBroadcastFacilitator because that one sets x-topics to a JSON array, which not every overlay server reads: go-overlay-services before v1.3.6 splits the header on commas without trimming, so the brackets and quotes stay in the name and no topic matches. It also mis-decodes the wrapped answer. One bare name reads the same on every server.
func (*Facade) Submit ¶
Submit posts beef to the facade for exactly one topic.
One topic per call, because a Result is one topic's answer. Submit refuses an empty topic, or one containing a comma, space, tab, CR or LF, before it sends anything: a server splits x-topics on commas, and servers differ on whether they trim the parts, so "a, b" can reach the topic lookup as "a" and " b". The header is sent exactly as given.
Example ¶
The object leg carries a BEEF and nothing else. Facade.Submit checks the marker and the topic before it builds a request, so the refusals below never reach the network (overlay.example.com is never contacted).
package main
import (
"context"
"errors"
"fmt"
"github.com/bsv-blockchain/go-sdk/chainhash"
"github.com/bsv-blockchain/go-sdk/script"
"github.com/bsv-blockchain/go-sdk/transaction"
"github.com/lightwebinc/bcommon/publish"
)
func main() {
tx := transaction.NewTransaction()
nowhere := chainhash.Hash{}
tx.AddInput(&transaction.TransactionInput{SourceTXID: &nowhere, UnlockingScript: &script.Script{},
SequenceNumber: transaction.MaxTxInSequenceNum})
tx.AddOutput(&transaction.TransactionOutput{Satoshis: 1, LockingScript: &script.Script{script.OpTRUE}})
isTxid := true
tx.MerklePath = transaction.NewMerklePath(1, [][]*transaction.PathElement{{
{Offset: 0, Hash: tx.TxID(), Txid: &isTxid},
{Offset: 1, Duplicate: &isTxid},
}})
beef, err := tx.BEEF()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("BEEF:", publish.IsBEEF(beef), "raw transaction:", publish.IsBEEF(tx.Bytes()))
f := &publish.Facade{Base: "https://overlay.example.com"}
ctx := context.Background()
_, err = f.Submit(ctx, "tm_example", tx.Bytes())
fmt.Println(errors.Is(err, publish.ErrNotBEEF))
_, err = f.Submit(ctx, "tm_example,tm_other", beef)
fmt.Println(err)
}
Output: BEEF: true raw transaction: false true publish: topic "tm_example,tm_other" must be one name with no separators
type Journal ¶
type Journal struct {
// Dir holds the entries; Write creates it when it is missing.
Dir string
}
Journal keeps one record per transition of a publish's two legs, so what happened to a transition outlives the process that sent it.
Each entry is one file, <seq>-<txid>.json, mode 0600, written atomically, so an entry's update never rewrites another entry. The key is (seq, txid) rather than seq alone because a sequence retried after a failure leaves one entry per attempt. List refuses a .json file that is not a readable <seq>-<txid> entry rather than skip it, and Update may not change an entry's key.
func (Journal) List ¶
List returns every entry sorted by sequence, then txid. Directories, dotfiles (a write in progress) and names without a .json suffix are not entries and are passed over. A .json file that is not a readable <seq>-<txid> entry is an ERROR, not a skip: a stranded sequence is exactly the one an operator needs shown, and a diagnostic that quietly skipped the file it could not parse would report a clean history over a broken one.
type RPCSettler ¶
RPCSettler sends the standard serialisation through sendrawtransaction and returns the node's answer. This leg has an acknowledgement, which the TCP ingress does not.
func (*RPCSettler) Name ¶
func (r *RPCSettler) Name() string
Name identifies the leg in the journal.
func (*RPCSettler) Submit ¶
func (r *RPCSettler) Submit(ctx context.Context, tx *transaction.Transaction) error
Submit sends tx.Hex() and returns the node's error verbatim. A txid that comes back different from the one we computed is an error too: it would mean the node parsed different bytes than we signed.
type Result ¶
type Result struct {
// Admitted are the output indexes the topic manager admitted.
Admitted []uint32
// Retained are the input outpoints (as indexes) the manager kept.
Retained []uint32
// Duplicate is HTTP 200 with nothing admitted: the engine's answer for
// an object it already holds. It is a result, not an error, so a
// re-POST after a crash is idempotent.
Duplicate bool
// Raw is the whole response body, for the journal.
Raw json.RawMessage
}
Result is what the host answered for the one topic submitted.
type SettleOptions ¶ added in v0.14.0
type SettleOptions struct {
// Key is the bearer token for arcade or ARC (TAAL's ARC requires one).
Key string
// RPCUser, RPCPass and RPCID authenticate an rpc: leg.
RPCUser, RPCPass, RPCID string
// Client, when set, is the HTTP client of an arcade, arc or rpc leg.
Client *http.Client
// Spends, when set, is the spend view arcade's verdict is held to
// (Arcade.Spends); Asset, a node's (Arcade.Asset).
Spends nodeapi.SpendSource
Asset *nodeapi.Asset
// Note receives arcade's notes (Arcade.Note).
Note func(format string, args ...any)
}
SettleOptions are what a settlement specification does not carry.
type Settler ¶
type Settler interface {
Submit(ctx context.Context, tx *transaction.Transaction) error
Name() string
}
Settler delivers a signed transaction for mining. It takes the transaction itself so that a Settler can never be handed a BEEF: the only bytes a Settler ever writes are ones it encoded from a *transaction.Transaction.
type TCPIngress ¶
type TCPIngress struct {
// Addr is host:port of the ingress (8725 on a shard-proxy).
Addr string
// DialTimeout defaults to 5s.
DialTimeout time.Duration
// WriteTimeout defaults to 10s and is overridden by a ctx deadline.
WriteTimeout time.Duration
// contains filtered or unexported fields
}
TCPIngress writes the transaction's EF (BRC-30) bytes to a fabric ingress as a bare transaction stream. There is no acknowledgement: the journal and a later WaitMined are the evidence.
func (*TCPIngress) Name ¶
func (t *TCPIngress) Name() string
Name identifies the leg in the journal.
func (*TCPIngress) Submit ¶
func (t *TCPIngress) Submit(ctx context.Context, tx *transaction.Transaction) error
Submit dials a fresh connection, writes the EF bytes in one Write and closes. One Write because the stream is self-delimiting: the reader parses transaction structure with no length prefix, so a short write leaves it mid-transaction and everything after is garbage. A fresh connection per Submit because a poisoned stream then has nothing after it to poison; the cost is one handshake per transition, and transitions are rare.