treasury

package
v1.799.2 Latest Latest
Warning

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

Go to latest
Published: Jul 12, 2026 License: Apache-2.0, MIT Imports: 14 Imported by: 0

Documentation

Overview

Package treasury is the chain-backed credit ledger's mint authority boundary.

The recurring C1 money-mint vulnerability class exists because credits are a mutable DB number any code path can increment. This package removes that by construction: a credit exists ONLY as HUSD (Hanzo USD, ERC-20 on the Hanzo EVM) minted by the treasury key — which lives in KMS, never in this process. Commerce can only REQUEST a mint (treasury.Mint); the request is refused unless the caller carries proven mint authority (mintauth), and the value is created by a treasury-signed on-chain transaction, not a DB write.

This file (derive.go) is Step 1: deterministic per-org on-chain addresses. The same (master seed, orgID) yields the same EVM address byte-for-byte across processes and restarts, so an org's HUSD balance has one stable home on chain; distinct orgs get distinct addresses with cryptographic certainty. The master seed is KMS-sourced (like the treasury key) and never persisted.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNotConfigured = husd.ErrNotConfigured
	// ErrInFlight: an issuance for this key exists and is mid-submit (pending, no
	// tx). The caller must retry after the in-flight attempt resolves — resending
	// would risk a double mint. Not an error the ledger invented value.
	ErrInFlight = errors.New("treasury: mint already in flight for this idempotency key")
	// ErrNotAuthorized re-exports the mintauth refusal for callers.
	ErrNotAuthorized = mintauth.ErrNotAuthorized
)

Errors.

View Source
var ErrEmptyOrg = errors.New("treasury: org id must not be empty")

ErrEmptyOrg is returned when an empty org id is passed to derivation.

View Source
var ErrSeedTooShort = fmt.Errorf("treasury: master derivation seed must be >= %d bytes", minSeedLen)

ErrSeedTooShort is returned when the master derivation seed is missing or has insufficient entropy. Fail closed: no seed ⇒ no addresses ⇒ no mint target.

Functions

func AddressForKey

func AddressForKey(privHex string) (string, error)

AddressForKey returns the lowercased 0x EVM address controlled by a secp256k1 private key (hex, with or without 0x). It is the inverse of "what address does this signer send FROM" — used to locate the treasury's own on-chain address (settlement destination, Step 5) and to reconcile a derived org account's address against the key that will sign its settlement transfers.

func AddressForOrg

func AddressForOrg(masterSeed []byte, orgID string) (string, error)

AddressForOrg is the mint-side convenience: it derives only the org's on-chain address, discarding the private key (minting a credit only needs where to send the HUSD). Use DeriveAccount when you also need the settlement-signing key.

func AmountWeiForCents

func AmountWeiForCents(cents int64, decimals int) (*big.Int, error)

AmountWeiForCents exposes the cents→wei conversion for callers verifying a mint amount against the chain (e.g. the indexer reconciliation).

func IssuanceID

func IssuanceID(idemKey string) string

IssuanceID is the deterministic storage id for an idempotency key. Distinct keys → distinct ids; the same key → the same id, so a replay lands on the same row and concurrent duplicates collapse.

func SeedFromHex

func SeedFromHex(s string) ([]byte, error)

SeedFromHex decodes a hex master seed (with or without 0x) sourced from KMS (env HUSD_ORG_DERIVATION_SEED). It enforces the minimum entropy so a misconfigured deploy fails closed rather than deriving weak addresses.

Types

type Account

type Account struct {
	OrgID   string
	Address string // 0x-prefixed, lowercased for stable indexing/matching
	// contains filtered or unexported fields
}

Account is a per-org on-chain HUSD holding: a deterministic EVM address (where the org's HUSD balance lives — safe to expose and index) and the secp256k1 signing key for it. The private key is held in memory only, never persisted and never logged; it is needed only to SIGN settlement transfers (org → treasury) in Step 5. Minting only ever needs the Address.

func DeriveAccount

func DeriveAccount(masterSeed []byte, orgID string) (Account, error)

DeriveAccount deterministically derives an org's HUSD account from a master seed, BIP-32-style: I = HMAC-SHA512(seed, "hanzo-husd-org-v1:"||orgID||ctr), take the left 256 bits as the secp256k1 scalar. The counter increments only on the astronomically rare event that the scalar is not a valid key (>= curve order or zero) — so derivation is total and deterministic. Distinct orgIDs produce distinct scalars (HMAC collision resistance) hence distinct addresses; the mapping is collision-safe for arbitrary string org ids (no lossy string→uint32 BIP-44 index).

func (Account) PrivateKeyHex

func (a Account) PrivateKeyHex() string

PrivateKeyHex returns the account's secp256k1 private key as hex (no 0x). It is the settlement-signing key; callers must treat it as a secret and must never log or persist it.

type Bucket

type Bucket string

Bucket names which billing bucket a mint funds. On-chain HUSD is fungible and a Transfer event carries no tag, so the credit-vs-prepaid distinction is recorded HERE, off-chain, at issuance time; the indexer (step 3) reads it back to tag the projected ledger deposit (billing/bucket classifies from that tag).

const (
	// BucketCredit is a non-cash grant (welcome/starter/promo). Spendable on
	// non-GPU usage only (billing/bucket.Credit).
	BucketCredit Bucket = "credit"
	// BucketPrepaid is real money paid in (card top-up, crypto payin, migration
	// of a real balance). The only bucket GPUs may draw from (billing/bucket.Prepaid).
	BucketPrepaid Bucket = "prepaid"
)

func (Bucket) LedgerTag

func (b Bucket) LedgerTag() string

LedgerTag maps a bucket to the transaction Tags string the indexer writes so billing/bucket.DepositKind classifies the projected deposit into the same bucket. Credit → "credit:husd" (a grant prefix bucket.DepositKind treats as Credit); Prepaid → "husd" (real money, treated as Prepaid).

func (Bucket) Valid

func (b Bucket) Valid() bool

Valid reports whether b is a known bucket.

type Issuance

type Issuance struct {
	Id      string `json:"id"`
	IdemKey string `json:"idemKey"`
	OrgID   string `json:"orgId"`
	// Subject is the off-chain ledger DestinationId this mint credits (an IAM
	// "owner/name" per-user key, or the org slug for org-pooled billing). On-chain
	// HUSD is per-ORG (one derived address per OrgID), so many subjects share one
	// address; Subject records which sub-ledger the projected credit lands in.
	// Defaults to OrgID (org-pooled) when the caller leaves it empty.
	Subject     string `json:"subject"`
	OrgAddress  string `json:"orgAddress"`
	AmountCents int64  `json:"amountCents"`
	Bucket      Bucket `json:"bucket"`
	Reason      string `json:"reason"`
	// Test marks a mint on the test chain (matches the ledger's Test partition so
	// the balance read finds it). A test-mode org mints test HUSD; a live org mints
	// live HUSD — the projected credit's Test flag MUST equal the balance read's.
	Test      bool           `json:"test"`
	ChainID   int64          `json:"chainId"`
	TokenAddr string         `json:"tokenAddr"`
	TxHash    string         `json:"txHash,omitempty"`
	Status    IssuanceStatus `json:"status"`
	CreatedAt time.Time      `json:"createdAt"`
	MintedAt  time.Time      `json:"mintedAt,omitempty"`
}

Issuance is the durable, idempotent record of ONE mint request. Its storage id is deterministic in the idempotency key so concurrent duplicate requests collapse onto one row (the money-safe primitive; see billing/bucket + gift card design). It is also the audit trail: every credit that exists traces to exactly one Issuance and its on-chain TxHash.

type IssuanceStatus

type IssuanceStatus string

IssuanceStatus tracks a mint's lifecycle. An issuance is the off-chain, idempotent record of a mint request keyed by its idempotency key.

const (
	// StatusPending: the record was created and we are (or a crashed prior
	// attempt was) submitting the on-chain tx. A pending record with no TxHash is
	// treated as in-flight — never resubmitted blindly.
	StatusPending IssuanceStatus = "pending"
	// StatusMinted: the on-chain tx was submitted; TxHash is set. Replays return
	// the stored receipt with no new tx.
	StatusMinted IssuanceStatus = "minted"
	// StatusFailed: submission failed before broadcast (validation/signing); the
	// key may be retried.
	StatusFailed IssuanceStatus = "failed"
)

type IssuanceStore

type IssuanceStore interface {
	// CreateIfAbsent records iss iff its Id is not already present. Returns
	// created==true with iss when this call created it, or created==false with the
	// pre-existing record otherwise. Exactly one concurrent caller sees created==true.
	CreateIfAbsent(ctx context.Context, iss *Issuance) (created bool, existing *Issuance, err error)
	// Update persists status/txHash/mintedAt on an existing issuance.
	Update(ctx context.Context, iss *Issuance) error
	// Get loads an issuance by id (nil,nil if absent).
	Get(ctx context.Context, id string) (*Issuance, error)
}

IssuanceStore persists issuances. It is an interface (not a concrete datastore) so the mint service's logic is unit-tested with an in-memory fake (CGO-free, no sqlite) and wired to the real per-org SQLite in production. The store MUST make CreateIfAbsent atomic on Issuance.Id: concurrent same-id calls yield exactly one created==true (the deterministic-id ON CONFLICT upsert).

type MintRequest

type MintRequest struct {
	OrgID string
	// Subject is the ledger DestinationId to credit (defaults to OrgID when empty —
	// org-pooled billing). See Issuance.Subject.
	Subject     string
	AmountCents int64
	Bucket      Bucket
	Reason      string
	IdemKey     string
	// Test routes the mint to the test partition of the ledger (must match the
	// balance read's Test flag). Set from org.TestMode() by the caller.
	Test bool
}

MintRequest is a request to bring AmountCents of HUSD into existence for an org. IdemKey makes it exactly-once: the same key mints at most one on-chain tx.

type Option

type Option func(*Treasury)

Option configures a Treasury.

func WithClock

func WithClock(now func() time.Time) Option

WithClock overrides the clock (tests inject a fixed time).

func WithTransfer

func WithTransfer(fn TransferFunc) Option

WithTransfer overrides the on-chain transfer function (tests inject a fake).

type Receipt

type Receipt struct {
	IdemKey     string
	OrgID       string
	Subject     string
	OrgAddress  string
	AmountCents int64
	Bucket      Bucket
	Reason      string
	Test        bool
	ChainID     int64
	TxHash      string
	Replayed    bool // true if the key had already minted; no new tx was sent
}

Receipt is the result of a mint: the on-chain tx and where the value landed.

type TransferFunc

type TransferFunc func(ctx context.Context, t blockchain.TokenTransfer) (string, error)

TransferFunc signs+submits a treasury ERC-20 transfer, returning the tx hash. Defaults to util/blockchain.TransferToken (KMS-keyed, geth signing in the linked sub-module); injectable so unit tests need no chain.

type Treasury

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

Treasury is the mint service. Construct with New; call Mint.

func New

func New(cfg husd.Config, masterSeed []byte, store IssuanceStore, opts ...Option) *Treasury

New builds a Treasury. cfg is the HUSD config (KMS-injected; must be Configured for real mints). masterSeed is the KMS-sourced org-address derivation seed. store persists issuances. By default the on-chain transfer is util/blockchain.TransferToken (requires the geth signer sub-module linked).

func (*Treasury) Mint

func (t *Treasury) Mint(ctx context.Context, req MintRequest) (*Receipt, error)

Mint brings AmountCents of HUSD into existence for req.OrgID by a treasury-signed on-chain transfer to the org's derived address. It is:

  • authorized: refused unless mintauth.Require(ctx) passes (a gated HTTP caller must carry proven mint authority; crons/migrations are ungated).
  • idempotent: the same IdemKey mints at most one on-chain tx; a replay returns the stored receipt (Replayed=true) with no new tx.
  • fail-closed: no token/key configured ⇒ ErrNotConfigured (no DB fallback, no silent mint). The value is created ONLY by the KMS-keyed transfer.

Directories

Path Synopsis
Package datastorestore is the production treasury.IssuanceStore: it persists HUSD mint issuances to the per-org SQLite ledger via models/husdissuance.
Package datastorestore is the production treasury.IssuanceStore: it persists HUSD mint issuances to the per-org SQLite ledger via models/husdissuance.

Jump to

Keyboard shortcuts

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