proofofwork

package
v0.0.0-...-e8f74ff Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: AGPL-3.0 Imports: 10 Imported by: 0

Documentation

Index

Constants

View Source
const AlgorithmSHA256LeadingZeros = "sha256-leading-zeros-v1"

AlgorithmSHA256LeadingZeros names the only proof-of-work scheme we issue today: find a solution such that SHA-256(challenge || ":" || solution) has at least `difficulty` leading zero bits. The name travels in the challenge response and clients reject values they don't recognize rather than guessing, which is what makes a future scheme additive instead of breaking.

View Source
const DefaultDifficulty = 0

DefaultDifficulty is where the dial sits normally. Zero: the mechanism ships mandatory and the work ships at nothing, so raising the price of an anonymous identity later needs no client release.

View Source
const MaxDifficulty = 26

MaxDifficulty is the absolute ceiling on the work we will ever ask a client for. Clients are expected to honour any difficulty up to this and to surface an error above it, so a server-side bug can't wedge an overlay in a hash loop. Enforced at mint time regardless of what the difficulty function returns.

It is a refusal threshold, not a usable setting. 2^26 expected hashes is well over a minute in prism's Python client, and challengeTTL gives the whole handshake 60 seconds — so a difficulty near this ceiling doesn't converge at all: the client solves, gets ErrChallengeExpired (the TTL is checked before the work), fetches another and repeats. The band that actually works is roughly up to 22; anything beyond that needs challengeTTL raised in the same change.

Variables

View Source
var (
	// ErrInvalidConfig is returned at construction time, never per-request.
	ErrInvalidConfig = errors.New("invalid proof-of-work configuration")

	ErrMalformedChallenge = errors.New("malformed challenge")
	ErrBadSignature       = errors.New("bad challenge signature")
	ErrChallengeExpired   = errors.New("challenge expired")
	ErrIPMismatch         = errors.New("challenge was issued to a different ip")
	ErrUserIDMismatch     = errors.New("challenge was issued to a different user id")
	ErrInsufficientWork   = errors.New("solution does not meet the required difficulty")

	// ErrUnsupportedAlgorithm means we signed it but cannot check it: a newer
	// revision minted it under a scheme this one doesn't implement.
	ErrUnsupportedAlgorithm = errors.New("challenge uses an unsupported algorithm")
)

Functions

func GenerateSigningKey

func GenerateSigningKey() (string, error)

GenerateSigningKey returns a fresh base64-encoded signing key. Development only. A key generated at startup dies with the process, which invalidates every outstanding challenge on restart — a 60-second window that a local client just retries through, but not something to run in production, where the key is a secret so that it also survives a revision rollout.

func ParseSigningKeys

func ParseSigningKeys(encoded []string) ([][]byte, error)

ParseSigningKeys decodes base64 signing keys from config. The first key signs every challenge we mint and all of them are accepted on the way back in, so rotation is: prepend the new key, deploy, drop the old one a TTL later. Blank entries are skipped — the config format is newline-delimited and gets edited by hand.

func RejectionReason

func RejectionReason(err error) string

RejectionReason maps an error from ParseChallenge or Check to a bounded label safe to use as a metric attribute. A dial with no gauge isn't tunable, and the split by cause is what tells a difficulty change apart from a client bug.

Types

type Challenge

type Challenge struct {
	Value      string
	Algorithm  string
	Difficulty int
	ExpiresIn  time.Duration
}

Challenge is a minted proof-of-work challenge, ready to be handed to a client. Value is opaque to the client; everything the client needs in order to solve it is spelled out in the other fields.

type DifficultyFunc

type DifficultyFunc func(DifficultyInput) int

DifficultyFunc is evaluated once per minted challenge. A function rather than a constant on purpose: per-IP issuance volume, blocklist state and global load are all things we'd want to price in without shipping anything to clients.

func BuildDifficultyFunc

func BuildDifficultyFunc(globalFloor int) (DifficultyFunc, error)

BuildDifficultyFunc returns the difficulty policy. Today every caller gets the global floor: the signals worth escalating on want real issuance-per-IP data to calibrate against, and there is no live client producing any yet. They plug in here, and they may only raise the number — BuildIssueChallenge clamps the result to MaxDifficulty.

type DifficultyInput

type DifficultyInput struct {
	// IPHash is the sha256 of the caller's IP.
	IPHash string
	// ClientType is the *normalized* client type (ports.Client.Type), not
	// the raw header. It is client-supplied either way, so it may only ever
	// raise the cost, never lower it below the global floor.
	ClientType string
}

DifficultyInput is everything the difficulty policy may key on. All of it is knowable server-side at challenge time, which is the point — nothing about the dial should need a client release.

type IssueChallenge

type IssueChallenge func(userID string, ipHash string, clientType string) (Challenge, error)

IssueChallenge mints a challenge bound to the userId the caller intends to log in as and to their ip hash. clientType must be the *normalized* client type: it is client-supplied, so it may only ever raise the cost, never lower it.

func BuildIssueChallenge

func BuildIssueChallenge(keys [][]byte, difficultyFor DifficultyFunc, nowFunc func() time.Time) (IssueChallenge, error)

BuildIssueChallenge returns the challenge minting half of the scheme.

type ParseChallenge

type ParseChallenge func(challenge string) (SignedChallenge, error)

ParseChallenge recovers a challenge we minted from the blob a client presents. Every check it makes is a pure function of that blob, so a value coming back carries observations that hold however the verdict goes, and a failure means there was nothing to observe. Anything reading the clock or caller input is on SignedChallenge.Check.

func BuildParseChallenge

func BuildParseChallenge(keys [][]byte, nowFunc func() time.Time) (ParseChallenge, error)

BuildParseChallenge returns the parsing half of the verifying side.

type SignedChallenge

type SignedChallenge interface {
	// Difficulty is read from the signed payload, never re-derived: the dial
	// can move between mint and login, so re-deriving would report a
	// difficulty the client was never asked for.
	Difficulty() int

	// Age is minted-until-now, so it spans both round trips and the client's
	// own solve. Negative when our clock stepped backwards, and unbounded
	// above once past challengeTTL.
	Age() time.Duration

	// Check verifies freshness, the bindings and the work. Every failure
	// wraps one of the sentinel errors above.
	//
	// userID must be the raw value that becomes the session's identity_key,
	// compared byte for byte against what was minted. Normalizing on one
	// side only would reject correct solutions.
	Check(solution string, userID string, ipHash string) error
}

SignedChallenge is a challenge signed by one of our keys, naming a scheme we implement. Not a verdict on the caller's solution — that is Check.

An interface only so callers can fake it; nothing outside this package can build one.

Jump to

Keyboard shortcuts

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