02 · External hooks and data types
The SDK is a stateless protocol library: it owns message encoding, signature
verification, pricing, transaction construction, and protocol validation, while
the calling application owns persistence, concurrency, content storage,
transport, node broadcasting, and the observation of time and block heights.
There is no Verifier callback, no Store, no node hook, and no clock hook; every
external fact crosses the boundary either as an explicit method input, as a
protocol.Facts{Now, BlockHeight} value, or through the single signer port.
Signing and key custody: one port
The only key-custody capability in the SDK is the constrained protocol.Signer
port:
// package protocol
type Signer interface {
// PublicKey 返回本 Signer 固定的压缩公钥;workflow 构造时固定并验证它,
// 生命周期内不得变化。
PublicKey() PublicKey
// Sign 对 SDK 已构造好的 32 字节 digest 做 secp256k1 签名,返回不带
// 交易 sighash flag 的 low-S DER。Signer 绝不能自行哈希。
Sign(ctx context.Context, request SigningRequest) ([]byte, error)
}
// SigningRequest 携带 Purpose(wire_message / transaction)、WireKind 与 Digest;
// 只供 HSM/KMS 策略审计,不替代任何既定签名预映像。
The signer is a capability port, not a replaceable protocol policy. It can never supply a custom hash function, preimage, sighash flag, verifier, CBOR encoder, price rule, or role decision; all verification stays fixed inside the SDK. Local software keys enter through the only provided adapter:
signer, err := protocol.NewPrivateKeySigner(privateKey) // privateKey 为 *ec.PrivateKey
buyerWorkflow, err := buyer.NewWorkflow(signer)
sellerWorkflow, err := seller.NewWorkflow(signer)
arbiterWorkflow, err := arbiter.NewWorkflow(signer)
Each constructor fixes and validates the compressed public key derived from the signer; that key becomes the workflow's role-bound identity: every later method re-checks that supplied opening evidence belongs to this key's role before computing anything. The public key MUST NOT change during the workflow's lifetime.
The SDK never accepts a seed, key-export callback, or signature-verifier
callback either. Every ordinary message signature follows one fixed path inside
the SDK: the digest over the typed signing input is constructed once, handed to
the Signer, normalized to low-S DER, and re-verified against the fixed role key
before returning. Callers must not hash a second time before signing.
Transaction signatures always use the fixed MultisigPool sighash (ForkID|All)
and are never hashed a second time.
Public keys in a quote, opening proof, content request, or payment state are protocol evidence. Callers cannot replace participant verification or reconfigure the buyer/seller/arbiter roles: verification is fixed and not substitutable.
Persistence belongs to the application
There is no Store interface in the SDK. Workflows return opaque checkpoints and
verified values — for example buyer.OpeningCheckpoint,
buyer.PoolCheckpoint, buyer.AuthorizationCheckpoint,
seller.OpeningCheckpoint, seller.DeliveryCheckpoint, content.VerifiedQuote,
and pool.VerifiedOpening — and require them again as explicit arguments in
later steps. Applications persist their evidence bytes in their own database
keyed by RefundTemplateTxID (or by authorization ID), serialize concurrent
work per pool, and implement retries, outboxes, and crash recovery themselves;
the buyer package exposes restore entries that rebuild every checkpoint from
exact persisted evidence with full re-verification
(RestoreOpeningCheckpoint, RestorePoolCheckpoint,
RestoreAuthorizationCheckpoint). The SDK adds no locks, leases, mutexes, or
process-serialization of any kind: calling the same method twice concurrently
yields two independently valid results, and deduplication is an application
responsibility.
Content bytes are caller-supplied
The seller reads seed/block payload bytes from its own storage and passes them
as an ordered batch via seller.DeliveryCommand.ContentPayloads; the buyer
passes ordered content hashes via buyer.RequestContentCommand.ContentHashes
and verified seeds via Seed. The workflows derive every content kind from
evidence (a hash equal to the quote SeedHash is the seed, everything else must
be committed by that seed), verify hashes, seed structure, block membership,
expected lengths, quote terms, and request/delivery signatures against those
explicit bytes, and accept or reject the whole batch atomically.
Verification results return the verified payload batch as data
(PaymentPreparationResult.Payloads, in authorized order); saving it to final
storage is the application's job, and a failed save means the business step
must not be treated as complete.
Time and height facts are explicit inputs
The SDK has no clock injection and no node access, and it reads no system time.
Every time-sensitive call receives one explicit protocol.Facts{Now, BlockHeight}
value; operations that need only a time reject a zero Now, and operations that
need a height reject a zero BlockHeight. The SDK never queries a node for the
current height, never falls back to the system clock, and never fabricates a
value. A height-source outage must delay or reroute refund operations, never
fabricate a value.
Protocol input and result types
The role APIs accept command structs and return unified results with outbound Artifacts plus opaque checkpoints to persist first:
PrepareOpeningCommandcarries the verified quote, raw funding bytes, expiry locktime, fee rate, and seller/arbiter public keys;PreparePoolOpeningResultreturnsOutbound wire.ArtifactplusOpeningCheckpoint— persist the checkpoint before sending.RequestContentCommandcarries the verified quote, pool checkpoint, ordered content hashes, delivery deadline, and seed;RequestContentResultreturnsOutbound, the typedAuthorizationID, and anAuthorizationCheckpointholding the exact signed 003.VerifyDeliveryCommandverifies the exact Kind 6 delivery against the persisted authorization checkpoint;PaymentPreparationResultreturns the verified payloads, the single outbound Kind 7 credential, and an audit-only unsigned candidate.DeliveryCommand/DeliveryResulton the seller side return the outbound Kind 6 Artifact plus a lock-freeDeliveryCheckpointrecording exactly the protocol context (pool correlation ID, authorization ID, target payment sequence, absolute cumulative seller amount) needed later byCompletePayment. It carries no owner/lease/expiry semantics.- pool.UnsignedPayment and pool.SignedPayment distinguish locally rebuilt
unsigned states, detached signatures, and complete transactions. Workflow
methods return complete transaction bytes wrapped in
pool.VerifiedSignedTransactionfor the application to broadcast; nothing is ever named "submitted" or "accepted" inside the SDK.
The correlation field across these types is pool.RefundTemplateTxID — a
dedicated [32]byte type carrying the canonical TxID of the refund template
transaction without embedded role signatures (CDDL label
refund-template-txid). It is not a SHA-256 of raw bytes, not a byte-reversed
hash, and not the txid of the final broadcast refund transaction.
The wire package maps domain values to canonical Kind 1–11 CBOR as immutable
Artifacts whose Bytes() are transmitted and stored unchanged. The optional
transport package binds those exact bytes to the shared
/bitfs/wire/1.0.0 bitcoin-libp2p stream profile with unsigned-varint length
framing. Host lifecycle, routing, retries, HTTP, queues, databases, and browser
session policy remain application-owned; no transport may re-encode an
Artifact or add a hidden pool/session identity.
What is not an extension point
There is no verifier strategy, workflow clock, store/repository hook,
transaction engine hook, lease or locker, content source/sink, backend port,
private-key provider, or application-supplied transaction-ID calculator. Those
abstractions would allow a caller to replace business rules that define the
protocol, or would smuggle infrastructure side effects back into the SDK. Only
key custody crosses this boundary, once, at construction time through the
constrained protocol.Signer port; everything else flows through explicit
inputs, explicit facts, and returned results.