paygate

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Overview

internal/paygate/auth_brc31.go — BRC-31 mutual auth session.

V1 simplification: the BRC-31 header is computed as

Brc31 session=<sid>; nonce=<hex>; pk=<hex>; sig=<hex>

where sig = secp256k1 ECDSA over the canonical request preamble. The full BRC-31 handshake (key exchange, session establishment via the /v1/auth path) is wired by the stub server in Task 13 and by the live integration tests; v1 does not implement the registration dance because PRD 2 § 7.1 inherits it from proof-generation-architecture.md without extending the contract.

internal/paygate/client.go — top-level Client + canonical 11-step submit lifecycle from PRD P1.8 "Submit lifecycle".

internal/paygate/credit_token_ledger.go — single-use credit_token store.

Schema:

CREATE TABLE credit_tokens (
  token       TEXT PRIMARY KEY,
  issued_at   TIMESTAMP NOT NULL,
  expires_at  TIMESTAMP NOT NULL,
  used_at     TIMESTAMP
);

Take performs the single-use enforcement atomically:

  1. Load token, check expiry → ErrCreditTokenExpired
  2. Check used_at IS NULL → ErrCreditTokenInvalid (already used)
  3. UPDATE used_at = now() → only one caller wins

Package paygate is the konareef runtime client for the PayGate ZK API (PRD 2). It implements the six PRD 4 § 2.1 / PRD P1.8 obligations and counter-signs the eight named acceptance checks M-1..M-8 plus the konareef-added M-6b first-fetch-window extension.

Public surface:

  • Client + Session
  • Open, SubmitFold, PollStatus, FetchResult
  • IdempotencyKey (PRD 2 § 6.5 errata-locked preimage)
  • Typed error model (Category + Code + IsRetryable)

All rejection paths return wrapped sentinel errors. Branch with errors.Is(err, paygate.ErrCircuitPinMismatch) for the canonical PRD P1.8 § 2 (Obligation 6) State C halt code; see errors.go for the closed enumeration of categories and codes inherited from PRD 2 § 6.4.

internal/paygate/errors.go — typed PRD 2 § 6.4 error model.

Every Err* sentinel is a *PaygateError; wrapped errors compare equal via errors.Is via the default pointer-equality unwrap. Code(err) and CategoryOf(err) extract the stable identifier strings for logging, telemetry, and golden-vector comparison.

internal/paygate/idempotency.go — PRD 2 § 6.5 errata-locked preimage.

preimage = ULEB128(len(circuit_id_bytes)) ‖ circuit_id_bytes

‖ be_u64(step_index)             (8 bytes, fixed)
‖ h_p                            (32 bytes, fixed)

Idempotency-Key = hex_lower( SHA-256(preimage) )

circuit_id_bytes is the UTF-8 NFC bytes of the circuit_id tstr. Per the PRD P1.8 normative-encoding rationale, ULEB128 length prefix + fixed-width big-endian step_index removes all delimiter ambiguity the upstream PRD 2 § 6.5 erratum (2026-06-06) flagged.

internal/paygate/idempotency_cache.go — PRD P1.8 WI-4 normative cache.

Schema:

CREATE TABLE idempotency_cache (
  brc31_session_id TEXT NOT NULL,
  idempotency_key  TEXT NOT NULL,
  job_id           TEXT NOT NULL,
  submitted_at     TIMESTAMP NOT NULL,
  expires_at       TIMESTAMP NOT NULL,
  PRIMARY KEY (brc31_session_id, idempotency_key)
);

Lookup is filter-on-now < expires_at; older rows count as cache miss for dedupe purposes (a resubmission past expiry is a billable fresh submission). Single-argument lookup is NOT a supported API per PRD P1.8 § "Idempotency cache schema (WI-4 normative)".

internal/paygate/jobs.go — async job lifecycle (submit / poll / fetch).

internal/paygate/manifest.go — discovery manifest (PRD 2 § 3.4).

internal/paygate/manifest_cache.go — discovery manifest refresh cadence per PRD P1.8 Obligation 2. Hard floor 60s, soft ceiling 1h (matches the result-retention window per PRD 2 § 4.4).

internal/paygate/payment_brc29.go — PRD 2 § 5.1 + PRD P1.8 Obligation 3.

retail_sats = ceil(compute_cost_usd * margin_multiplier * (100_000_000 / bsv_usd_rate))

BEEFs constructed after schedule_effective_after has elapsed are non-refundable per PRD P1.8 Obligation 3. BuildBEEF returns ErrStaleScheduleBEEF in that case so the runtime bears the cost observably rather than silently absorbing it.

internal/paygate/pin_check.go — PRD P1.8 Obligation 6 State A/B/C state machine. Auto-refresh applies ONLY to State A; State C is a terminal halt with no in-session auto-adoption of the new vkey.

internal/paygate/pipelining.go — PRD P1.8 Obligation 1 durable-binding gate. Preparatory work for step i+1 MAY proceed in parallel, but a nova-fold submission for step i+1 MUST NOT precede durable binding of step i's accumulator+result.

internal/paygate/retry.go — PRD 2 § 6.6 retry semantics + Retry-After.

internal/paygate/session.go — per-publish-session state.

Index

Constants

View Source
const IdempotencyTTL = time.Hour

IdempotencyTTL is the default server-mirrored cache window (1h per PRD 2 § 6.5).

Variables

View Source
var (
	ErrAuthMissingBRC31   = &PaygateError{CodeStr: "AUTH_MISSING_BRC31", Cat: CategoryAuth, Retryabl: true}
	ErrAuthInvalidBRC31   = &PaygateError{CodeStr: "AUTH_INVALID_BRC31", Cat: CategoryAuth, Retryabl: true}
	ErrAuthSessionExpired = &PaygateError{CodeStr: "AUTH_SESSION_EXPIRED", Cat: CategoryAuth, Retryabl: true}
	ErrAuthScopeMismatch  = &PaygateError{CodeStr: "AUTH_SCOPE_MISMATCH", Cat: CategoryAuth, Retryabl: false}
)

auth sentinels.

View Source
var (
	ErrPaymentBEEFInvalid        = &PaygateError{CodeStr: "PAYMENT_BEEF_INVALID", Cat: CategoryPayment, Retryabl: true}
	ErrPaymentAmountInsufficient = &PaygateError{CodeStr: "PAYMENT_AMOUNT_INSUFFICIENT", Cat: CategoryPayment, Retryabl: true}
	ErrPaymentDoubleSpend        = &PaygateError{CodeStr: "PAYMENT_DOUBLE_SPEND", Cat: CategoryPayment, Retryabl: false}
	ErrCreditTokenInvalid        = &PaygateError{CodeStr: "PAYMENT_CREDIT_TOKEN_INVALID", Cat: CategoryPayment, Retryabl: false}
	ErrCreditTokenExpired        = &PaygateError{CodeStr: "PAYMENT_CREDIT_TOKEN_EXPIRED", Cat: CategoryPayment, Retryabl: false}
	// Non-PRD-2 but normative per PRD P1.8 Obligation 3:
	ErrStaleScheduleBEEF = &PaygateError{CodeStr: "ERR_STALE_SCHEDULE_BEEF", Cat: CategoryPayment, Retryabl: false}
)

payment sentinels.

View Source
var (
	ErrValidationSchema            = &PaygateError{CodeStr: "VALIDATION_SCHEMA", Cat: CategoryValidation, Retryabl: false}
	ErrValidationInputCapViolation = &PaygateError{CodeStr: "VALIDATION_INPUT_CAP_VIOLATION", Cat: CategoryValidation, Retryabl: false}
	ErrValidationUnknownCircuitID  = &PaygateError{CodeStr: "VALIDATION_UNKNOWN_CIRCUIT_ID", Cat: CategoryValidation, Retryabl: false}
	ErrValidationWitnessShape      = &PaygateError{CodeStr: "VALIDATION_WITNESS_SHAPE", Cat: CategoryValidation, Retryabl: true}
	ErrValidationOperationNotSupp  = &PaygateError{CodeStr: "VALIDATION_OPERATION_NOT_SUPPORTED", Cat: CategoryValidation, Retryabl: false}
	// Non-PRD-2 but normative per PRD P1.8 Obligation 6:
	ErrCircuitPinMismatch = &PaygateError{CodeStr: "ERR_CIRCUIT_PIN_MISMATCH", Cat: CategoryValidation, Retryabl: false}
)

validation sentinels.

View Source
var (
	ErrComputeWorkerCrash       = &PaygateError{CodeStr: "COMPUTE_WORKER_CRASH", Cat: CategoryCompute, Retryabl: true}
	ErrComputeTimeout           = &PaygateError{CodeStr: "COMPUTE_TIMEOUT", Cat: CategoryCompute, Retryabl: true}
	ErrComputeResourceExhausted = &PaygateError{CodeStr: "COMPUTE_RESOURCE_EXHAUSTED", Cat: CategoryCompute, Retryabl: true}
)

compute sentinels.

View Source
var (
	ErrProofSelfVerifyFailed  = &PaygateError{CodeStr: "PROOF_SELF_VERIFY_FAILED", Cat: CategoryLogical, Retryabl: false}
	ErrProofCircuitDeprecated = &PaygateError{CodeStr: "PROOF_CIRCUIT_DEPRECATED", Cat: CategoryLogical, Retryabl: false}
)

logical sentinels.

View Source
var (
	ErrServiceUnavailable    = &PaygateError{CodeStr: "SERVICE_UNAVAILABLE", Cat: CategoryService, Retryabl: true}
	ErrServiceQueueSaturated = &PaygateError{CodeStr: "SERVICE_QUEUE_SATURATED", Cat: CategoryService, Retryabl: true}
	ErrResultExpired         = &PaygateError{CodeStr: "RESULT_EXPIRED", Cat: CategoryService, Retryabl: false}
)

service sentinels.

View Source
var ErrCircuitNotInRefreshedManifest = errors.New("paygate: refreshed manifest no longer advertises session circuit")

ErrCircuitNotInRefreshedManifest is returned by refreshManifestAndRevalidatePin when the freshly-fetched manifest no longer advertises the session's circuit. Surfacing this instead of silently allowing submission ensures a manifest that drops a circuit mid-session is treated as a hard failure for both the primary and credit-token retry paths.

View Source
var ErrIdempCacheMiss = errors.New("paygate: idempotency cache miss")

ErrIdempCacheMiss signals "no live cache row" — distinct from a PaygateError because it is internal to the client's bookkeeping.

View Source
var ErrSpeculativeFold = errors.New("paygate: speculative fold rejected (PRD P1.8 Obligation 1)")

ErrSpeculativeFold signals that the caller tried to submit step i+1 before step i's result was durably bound (PRD P1.8 Obligation 1).

Functions

func BackoffStep

func BackoffStep(attempt int) time.Duration

BackoffStep returns the exponential-backoff delay for retry attempt number attempt (0-indexed). 1s, 2s, 4s, …, capped at 60s.

func Code

func Code(err error) string

Code extracts the stable code string from err (or any wrapper). Returns "" for nil or non-PaygateError.

func CreditTokenFrom

func CreditTokenFrom(err error) string

CreditTokenFrom walks the error chain and returns the credit_token from the innermost *codedErr it finds. Returns "" if none.

func CreditTokenValidityFrom

func CreditTokenValidityFrom(err error) (issuedAt, expiresAt time.Time, ok bool)

CreditTokenValidityFrom walks the error chain and returns the server-declared (issued_at, expires_at, ok) for the credit_token, where ok is true only when BOTH timestamps are present on the failure envelope. Callers MUST treat ok == false as "no validity declared" and refuse to persist the token (round-2 B3 fail-closed).

func IdempotencyKey

func IdempotencyKey(circuitID string, stepIndex uint64, hP [32]byte) string

IdempotencyKey returns hex_lower(SHA-256(preimage)) — 64 lowercase hex ASCII characters, suitable for the `Idempotency-Key` request header.

func IdempotencyPreimage

func IdempotencyPreimage(circuitID string, stepIndex uint64, hP [32]byte) []byte

IdempotencyPreimage returns the byte-exact preimage from PRD 2 § 6.5 errata. circuitID is NFC-normalised internally before the byte sequence is computed, so canonically-equivalent Unicode forms (composed vs decomposed) always produce the same preimage and the same idempotency key. Callers SHOULD pass NFC-normalised circuit IDs already, but the function is defensive: an upstream component that forgets to normalise cannot produce a divergent key and double-bill.

func IsRetryable

func IsRetryable(err error) bool

IsRetryable returns the per-code retry hint from PRD 2 § 6.6. Caller MAY override based on other signals (e.g. Retry-After header).

func ParseRetryAfter

func ParseRetryAfter(h http.Header, now time.Time) time.Duration

ParseRetryAfter reads a `Retry-After` header per RFC 9110 §10.2.3 (delta-seconds OR HTTP-date). Returns 0 if absent or unparseable.

func RetailSats

func RetailSats(computeUSD float64, marginMultiplier int, bsvUSDRate float64) uint64

RetailSats computes ceil(usd * margin * 100_000_000 / rate).

func Sha256HexFor

func Sha256HexFor(body []byte) string

Sha256HexFor returns lowercase-hex SHA-256(body); exported for tests.

Types

type BEEF

type BEEF struct {
	CircuitID string
	Op        string
	Sats      uint64
	WireBytes []byte
}

BEEF is the constructed BRC-29 payment payload.

Wire-byte construction is delegated to the existing konareef secp256k1 publisher signing code (internal/identity/); BEEF.WireBytes is the finalised CBOR-ish payload to be hex-encoded into the X-Payment header. V1 ships a stub WireBytes that satisfies the conformance stub server; full BSV transaction graph is added by the live BEEF builder ticket (out of scope for v1 stub).

func BuildBEEF

func BuildBEEF(m *Manifest, circuitID, op string, bsvUSDRate float64) (*BEEF, error)

BuildBEEF constructs the BEEF for (circuitID, op) against the manifest's CURRENT active schedule. Returns ErrStaleScheduleBEEF when the manifest's schedule_effective_after has elapsed (PRD P1.8 Obligation 3 non-refundable contract).

type BRC31Identity

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

BRC31Identity is the persistent secp256k1 keypair used to authenticate to PayGate ZK. Distinct from the publisher identity per PRD P1.8 § 1.

func LoadOrCreateBRC31Identity

func LoadOrCreateBRC31Identity(path string) (*BRC31Identity, error)

LoadOrCreateBRC31Identity reads the secp256k1 private key from path, or generates and persists a new one if path does not exist. Mode 0600.

func (*BRC31Identity) PubKeyHex

func (id *BRC31Identity) PubKeyHex() string

PubKeyHex returns the compressed-SEC pubkey as lowercase hex.

func (*BRC31Identity) Sign

func (id *BRC31Identity) Sign(msg []byte) []byte

Sign returns the DER-encoded ECDSA signature of msg under the identity key.

type BRC31Session

type BRC31Session struct {
	ID       string
	Identity *BRC31Identity
}

BRC31Session is one BRC-31-scoped session with its server-issued ID.

func (*BRC31Session) AuthorizationHeader

func (s *BRC31Session) AuthorizationHeader(preamble []byte) string

AuthorizationHeader produces the per-request Brc31 header value for the given canonical preamble (e.g. method+path bytes per the wire spec). Nonce is fresh per call.

type Category

type Category string

Category is the closed enumeration from PRD 2 § 6.4.

const (
	CategoryAuth       Category = "auth"
	CategoryPayment    Category = "payment"
	CategoryValidation Category = "validation"
	CategoryCompute    Category = "compute"
	CategoryLogical    Category = "logical"
	CategoryService    Category = "service"
)

func CategoryOf

func CategoryOf(err error) Category

CategoryOf returns the closed-enumeration category for err.

type Circuit

type Circuit struct {
	VkeySha256      string                 `json:"vkey_sha256"`
	VkeyURL         string                 `json:"vkey_url"`
	Pricing         map[string]Schedule    `json:"pricing"`
	InputCaps       map[string]interface{} `json:"input_caps"`
	DeprecatedAfter *time.Time             `json:"deprecated_after"`
}

Circuit is the per-circuit block from PRD 2 § 3.4.

type Client

type Client struct {
	BaseURL       string
	HTTP          *http.Client
	IdentityPath  string
	StateDBPath   string
	ManifestCache *ManifestCache
	Resolver      *vkeystore.Resolver
	SaltBackend   saltstore.Backend
	PinChecker    PinCheck // interface; defaults to *PinChecker in production wiring
	IdempCache    *IdempCache
	TokenLedger   *TokenLedger

	// SubmitMaxRetries bounds the SubmitFold Retry-After loop.
	// Zero means default (2). One initial attempt PLUS up to N retries.
	SubmitMaxRetries int
}

Client is the konareef runtime's PayGate ZK API consumer.

func (*Client) FetchResult

func (c *Client) FetchResult(ctx context.Context, sess *Session, jobID string) (*ResultBody, error)

FetchResult issues GET /v1/jobs/{job_id}/result. A 410 Gone surfaces ErrResultExpired per PRD 2 § 4.4.

func (*Client) Open

func (c *Client) Open(ctx context.Context, in OpenSessionInput) (*Session, error)

Open performs the pre-session ritual: refresh manifest, pin-check (State A/B/C), create BRC-31 session (v1-simplified — within-process only; see Non-goals), load Type-D salt if applicable, prepare the pipelining gate. Returns the ready Session.

func (*Client) PollStatus

func (c *Client) PollStatus(ctx context.Context, sess *Session, jobID string) (*StatusResp, error)

PollStatus issues GET /v1/jobs/{job_id}.

func (*Client) SubmitFold

func (c *Client) SubmitFold(ctx context.Context, sess *Session, in FoldStepInput) (*ResultBody, error)

SubmitFold executes the canonical 11-step lifecycle from PRD P1.8 "Submit lifecycle":

  1. ensureFreshManifest
  2. assertNotSpeculative
  3. (witness/public-inputs supplied by caller)
  4. idempKey
  5. constructBEEF (or take credit_token)
  6. submit
  7. handle 202 / 4xx / 5xx
  8. on 5xx with credit_token: ledger.Persist
  9. poll status loop
  10. fetch result
  11. durableBind

func (*Client) SubmitFoldWithCredit

func (c *Client) SubmitFoldWithCredit(ctx context.Context, sess *Session, in FoldStepInput, creditToken string) (*ResultBody, error)

SubmitFoldWithCredit retries a previously-failed step using the supplied credit_token. M-4 / M-8 exercise this path.

Round-3 B1 + B2 contract: the credit-token retry path MUST be no weaker than the primary path. That means:

  • the manifest is re-fetched AND the State A/B/C pin check re-runs on a rotated vkey_sha256 (shared refreshManifestAndRevalidatePin helper) — a credit-retry MUST fail closed across a mid-session vkey rotation, exactly like SubmitFold;
  • the post-submission IdempotencyCache.Insert error is fatal — swallowing it would leave the runtime reporting success while no durable (brc31_session_id, idempotency_key) → job_id binding exists, which the pipelining gate would then permit a later replay to slip past as a fresh BEEF for the same logical fold (duplicate-payment risk that round-2 B5 was meant to close).

type DisclosurePolicy

type DisclosurePolicy string

DisclosurePolicy selects whether the runtime needs to look up a Type-D session salt from saltstore (Type-D) or skip salt lookup entirely because the bundle discloses the salt itself (Type-C). Required field — Open returns an error if it is the zero value.

const (
	// DisclosureTypeC indicates the bundle discloses salt; no saltstore lookup.
	DisclosureTypeC DisclosurePolicy = "C"
	// DisclosureTypeD indicates the runtime MUST load salt from saltstore; missing is fatal.
	DisclosureTypeD DisclosurePolicy = "D"
)

type FoldStep

type FoldStep struct {
	StepIndex   uint64
	JobID       string
	Accumulator []byte
	BoundAt     time.Time
}

FoldStep records the durable binding from a /result fetch.

type FoldStepInput

type FoldStepInput struct {
	StepIndex    uint64
	PublicInputs map[string]interface{}
	Witness      map[string]interface{}
	HP           [32]byte // SHA-256 of canonical public-input vector
	BSVUSDRate   float64  // for BEEF sizing
}

FoldStepInput is the runtime-side input to one fold submission.

type IdempCache

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

IdempCache is the SQLite-backed `(brc31_session_id, idempotency_key)` → job_id store.

func OpenIdempCache

func OpenIdempCache(dbPath string) (*IdempCache, error)

OpenIdempCache opens or creates the SQLite database at dbPath and migrates the schema.

func (*IdempCache) Close

func (c *IdempCache) Close() error

Close releases the underlying database handle.

func (*IdempCache) Insert

func (c *IdempCache) Insert(sessionID, idemKey, jobID string, submittedAt time.Time) error

Insert records (sessionID, idemKey) → jobID with submitted_at and a derived expires_at = submitted_at + IdempotencyTTL.

func (*IdempCache) Lookup

func (c *IdempCache) Lookup(sessionID, idemKey string) (string, error)

Lookup returns the cached job_id for (sessionID, idemKey) if a row exists AND now < expires_at. Otherwise ErrIdempCacheMiss.

Single-argument lookup is intentionally NOT supported per PRD P1.8 WI-4: the server-side cache key is two-tuple, so the client mirror MUST be two-tuple. A future caller cannot accidentally collapse two distinct sessions presenting identical idempotency keys.

type LedgerEntry

type LedgerEntry struct {
	Token     string
	IssuedAt  time.Time
	ExpiresAt time.Time
	UsedAt    *time.Time
}

LedgerEntry is one durable credit_token record.

type Manifest

type Manifest struct {
	Service                string             `json:"service"`
	Version                string             `json:"version"`
	Operations             []string           `json:"operations"`
	Circuits               map[string]Circuit `json:"circuits"`
	MarginMultiplier       int                `json:"margin_multiplier"`
	ScheduleRevision       int                `json:"schedule_revision"`
	ScheduleEffectiveAfter *time.Time         `json:"schedule_effective_after"`
	Signature              map[string]string  `json:"signature"`
}

Manifest mirrors PRD 2 § 3.4. Unknown fields are preserved as raw JSON in RawExtra so a future PRD 2 field bump does not require a client recompile to ignore.

func ParseManifest

func ParseManifest(raw []byte) (*Manifest, error)

ParseManifest parses the JSON body of GET /v1/.well-known/x402-info.

func (*Manifest) ActiveSchedule

func (m *Manifest) ActiveSchedule(circuitID, op string) (Schedule, error)

ActiveSchedule returns the pricing row for (circuitID, op). Returns an error if either lookup fails.

func (*Manifest) IsScheduleStale

func (m *Manifest) IsScheduleStale(now time.Time) bool

IsScheduleStale returns true when ScheduleEffectiveAfter is non-nil and now >= ScheduleEffectiveAfter; PRD 2 § 5.3 calls this the transition-in-progress condition and PRD P1.8 Obligation 3 makes BEEFs built after this point non-refundable.

type ManifestCache

type ManifestCache struct {
	BaseURL string // base URL (no trailing slash) of the PayGate ZK service
	HTTP    *http.Client
	Cadence time.Duration
	// contains filtered or unexported fields
}

ManifestCache caches the discovery manifest in memory with refresh cadence. Safe for concurrent use.

func (*ManifestCache) EffectiveCadence

func (mc *ManifestCache) EffectiveCadence() time.Duration

EffectiveCadence returns the configured cadence clamped to [60s, 1h]. An unset / zero Cadence resolves to defaultRefreshCadence (15 min).

func (*ManifestCache) Get

func (mc *ManifestCache) Get(ctx context.Context) (*Manifest, error)

Get returns the current Manifest, refetching if the cached entry is past the effective cadence.

func (*ManifestCache) Refresh

func (mc *ManifestCache) Refresh(ctx context.Context) (*Manifest, error)

Refresh forces a refetch regardless of cadence. Used by the schedule_effective_after transition probe in M-1 and by manual operator triggers.

type OpenSessionInput

type OpenSessionInput struct {
	CircuitID        string
	LineageID        [16]byte         // Type-D only; from pod manifest; used for saltstore lookup
	DisclosurePolicy DisclosurePolicy // REQUIRED: "C" or "D"
}

OpenSessionInput drives Open().

DisclosurePolicy is REQUIRED. For Type-D, LineageID MUST be set and saltstore.ErrSaltNotFound is FATAL — Open returns the wrapped error and does NOT open the session. For Type-C, LineageID is ignored and saltstore is not consulted (the bundle carries the salt).

type PRDError

type PRDError struct {
	Code                 string                 `json:"code"`
	Category             Category               `json:"category"`
	Message              string                 `json:"message"`
	Details              map[string]interface{} `json:"details"`
	Retryable            bool                   `json:"retryable"`
	CreditToken          string                 `json:"credit_token"`
	CreditTokenIssuedAt  *time.Time             `json:"credit_token_issued_at,omitempty"`
	CreditTokenExpiresAt *time.Time             `json:"credit_token_expires_at,omitempty"`
	RetryAfterSeconds    int                    `json:"retry_after_seconds"`
}

PRDError mirrors PRD 2 § 6.3 envelope.

CreditTokenIssuedAt / CreditTokenExpiresAt carry the server-declared validity window for `credit_token` (round-2 B3). When the server omits either, the client treats the token as un-trustworthy and does NOT persist a synthesised "now + 1h" placeholder — that placeholder would let the runtime keep a credit_token alive past server-side expiry or reject a still-valid one early. The fields are zero-valued for non-payment failures.

type PaygateError

type PaygateError struct {
	CodeStr  string
	Cat      Category
	Retryabl bool
}

PaygateError is the concrete sentinel type.

func (*PaygateError) Error

func (e *PaygateError) Error() string

Error implements the error interface and returns the stable code string.

type PinCheck

type PinCheck interface {
	Check(ctx context.Context, circuitID, manifestPin string) (PinState, error)
}

PinCheck is the minimal interface SubmitFold's Open path consumes. Default production wiring is *PinChecker (vkeystore-backed). Tests can supply a noopPinChecker via newSmokeClient or Options.PinChecker.

type PinChecker

type PinChecker struct {
	Resolver *vkeystore.Resolver
	Domain   string // discovery manifest's paygate-zk service domain
}

PinChecker runs the pre-session pin check against the discovery manifest's vkey_sha256 commitment.

func (*PinChecker) Check

func (pc *PinChecker) Check(ctx context.Context, circuitID, manifestPin string) (PinState, error)

Check applies the State A/B/C machine to (circuitID, manifestPin). Returns the state reached and any halt error.

  • State B: cached vkey bytes hash-verify against manifestPin. Proceed. Round-2 B2 fix: this goes through LocalCache.Lookup so the cached vkey.bin is hashed and compared, NOT a bare path-existence check. A corrupted/tampered vkey.bin under the expected directory now surfaces ErrCircuitPinMismatch (or ErrVkeyCorrupt for malformed metadata) instead of silently proceeding to fold submission.
  • State C: cached vkey hash diverges from manifestPin. Terminal halt with ErrCircuitPinMismatch; cache is NOT updated.
  • State A: no local cache. Resolver is invoked; if the resolved vkey's hash matches manifestPin, cache it and report State A. If the resolved vkey hashes DIFFERENTLY from manifestPin, vkeystore returns ErrCircuitPinMismatch and we propagate it.

type PinState

type PinState int

PinState reports which branch of the §6 state machine fired.

const (
	PinStateUnknown PinState = iota
	PinStateA                // no local vkey for this circuit_id; resolver invoked
	PinStateB                // cached vkey matches manifest; PROCEED
	PinStateC                // cached vkey diverges from manifest; HALT (terminal)
)

func (PinState) String

func (s PinState) String() string

String returns the single-letter state label.

type PipeliningGate

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

PipeliningGate tracks the last durably-bound step index.

func (*PipeliningGate) AssertNotSpeculative

func (g *PipeliningGate) AssertNotSpeculative(stepIndex uint64) error

AssertNotSpeculative checks that stepIndex is the very-next step after the last durably-bound step OR the last durably-bound step itself (idempotent replay). Step 0 is always allowed (genesis); thereafter the request MUST be exactly lastStep or lastStep+1.

func (*PipeliningGate) DurablyBind

func (g *PipeliningGate) DurablyBind(stepIndex uint64, jobID string, accumulator []byte)

DurablyBind records the receipt of step stepIndex's /result. Only after this call may stepIndex+1 be submitted.

type ResultBody

type ResultBody struct {
	AccumulatorOut     []byte                 `json:"accumulator_out"`
	StepProof          []byte                 `json:"step_proof"`
	PublicInputsEchoed map[string]interface{} `json:"public_inputs_echoed"`
	CostMeta           map[string]interface{} `json:"cost_meta"`
	JobID              string                 `json:"job_id"`
}

ResultBody mirrors PRD 2 § 3.3 for nova-fold.

JobID is a konareef-local extension (PRD 2 § 3.3 erratum recommendation — the published spec returns it only in the 202 Accepted submit response, but tracking it on the result envelope makes M-3 / M-4 / M-6b assertions substantially simpler. The stub server echoes it; live PayGate servers MAY omit it and callers should fall back to the Idempotency-Key cache.

type Schedule

type Schedule struct {
	ComputeUSD     float64 `json:"compute_usd"`
	RetailUSD      float64 `json:"retail_usd"`
	AvailableAfter string  `json:"available_after,omitempty"`
}

Schedule is one (compute_usd, retail_usd) pricing row.

type Session

type Session struct {
	BRC31Session      *BRC31Session
	DiscoveryManifest *Manifest
	ManifestFetchedAt time.Time
	CircuitID         string
	PinnedVkeyHash    string // hex, mirrors manifest's vkey_sha256
	CreditTokens      *TokenLedger
	IdempotencyCache  *IdempCache
	LastDurableStep   *FoldStep
	Gate              *PipeliningGate
	LineageID         [16]byte
	TypeDSalt         [32]byte
}

Session is the per-publish-session bag of mutable state.

type StatusResp

type StatusResp struct {
	JobID      string    `json:"job_id"`
	Status     string    `json:"status"`
	ProofType  string    `json:"proof_type"`
	CircuitID  string    `json:"circuit_id"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`
	Error      *PRDError `json:"error"`
	RetryAfter int       `json:"-"` // populated from header
}

StatusResp mirrors PRD 2 § 3.2.

type SubmitResp

type SubmitResp struct {
	JobID       string    `json:"job_id"`
	SubmittedAt time.Time `json:"submitted_at"`
}

SubmitResp mirrors PRD 2 § 3.1 202 Accepted body.

type TokenLedger

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

TokenLedger persists credit_tokens issued in failed-job response bodies.

func OpenTokenLedger

func OpenTokenLedger(dbPath string) (*TokenLedger, error)

OpenTokenLedger opens or creates the SQLite database at dbPath and migrates the schema.

func (*TokenLedger) Close

func (l *TokenLedger) Close() error

Close releases the underlying database handle.

func (*TokenLedger) Persist

func (l *TokenLedger) Persist(token string, issuedAt, expiresAt time.Time) error

Persist records a freshly-issued credit_token. issuedAt + expiresAt are read from the server's failure-body envelope.

func (*TokenLedger) Take

func (l *TokenLedger) Take(token string) (*LedgerEntry, error)

Take consumes a credit_token atomically. Returns ErrCreditTokenInvalid for unknown OR already-used tokens; ErrCreditTokenExpired for expired tokens. The single-use enforcement is the UPDATE … WHERE used_at IS NULL row count.

Directories

Path Synopsis
Package stubserver — handlers + writers for the in-process stub PayGate.
Package stubserver — handlers + writers for the in-process stub PayGate.

Jump to

Keyboard shortcuts

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