pyrxd.security — Typed primitives + errors

Security primitives for pyrxd.

This package contains the security foundation the rest of the SDK is built on: exception hierarchy, key-material wrappers, secure RNG helpers, and typed newtypes enforcing trust-boundary invariants.

Nothing in this package should ever log, print, or format raw key material.

class pyrxd.security.BlockHeight[source]

Bases: int

Non-negative block height with a generous sanity ceiling.

MAX: ClassVar[int] = 10000000
static __new__(cls, value)[source]
Parameters:

value (Any)

Return type:

BlockHeight

exception pyrxd.security.ConfirmationTimeoutError[source]

Bases: InsufficientConfirmationsError

A confirmation wait gave up before the tx reached the required depth.

A subclass of InsufficientConfirmationsError (and therefore of NetworkError) — which is exactly the distinction that class exists to make: “the tx is just shallow, retry / check the explorer” is a fundamentally different operator response from “the transport is broken”. A confirmation timeout is the former, so raising a bare NetworkError would tell the caller the wrong thing.

Carries txid and waited_s so a caller can render a resume hint. The txid is public chain data and is intentionally kept verbatim — it is the only thing that makes the failure actionable.

Parameters:
  • txid – the transaction that failed to reach required confirmations.

  • have – the last observed depth (0 if the tx was never seen).

  • required – the caller-supplied min_confirmations threshold.

  • waited_s – elapsed seconds on the injected clock when the wait gave up.

  • reason – why the wait stopped ("timeout", "max_iterations", …).

__init__(*, txid, have, required, waited_s, reason='timeout')[source]
Parameters:
Return type:

None

exception pyrxd.security.CovenantError[source]

Bases: RxdSdkError

Raised for covenant construction or verification failures.

class pyrxd.security.Hex20[source]

Bases: _FixedBytes

Exactly 20 raw bytes (e.g. a hash160 public-key hash).

class pyrxd.security.Hex32[source]

Bases: _FixedBytes

Exactly 32 raw bytes (e.g. a hash digest).

exception pyrxd.security.InsufficientConfirmationsError[source]

Bases: NetworkError

A tx exists but has fewer confirmations than the caller required.

Subclass of NetworkError so existing handlers still catch it, but catchable as a distinct class for wait-for-conf retry loops that need to discriminate “tx is just shallow, retry later” from “real transport error, fail fast”. The legacy substring match ("confirmations, required" in str(exc)) was fragile across reader implementations — this class is the typed replacement.

Parameters:
  • have – observed confirmation depth at read time (0 if unconfirmed).

  • required – the caller-supplied min_confirmations threshold.

  • detail – optional extra context appended in parentheses (e.g. why the wait gave up). Static description only — never key material.

__init__(*, have, required, detail=None)[source]
Parameters:
  • have (int)

  • required (int)

  • detail (str | None)

Return type:

None

exception pyrxd.security.InsufficientFundsError[source]

Bases: ValidationError

A pre-flight value check found less funding than the operation provably needs.

Deliberately a subclass of ValidationError: the SDK already raises a bare ValidationError("Insufficient funds…") from ~16 sites in wallet.py, hd/wallet.py, agent/watch_only.py and btc_wallet/payment.py, and every existing except ValidationError handler must keep catching this. The subclass only adds the machine-readable available / required / shortfall triple so a caller can say how much more is needed instead of substring-matching a message.

Not to be confused with pyrxd.transaction.transaction.InsufficientFunds, which is a bare ValueError raised by the low-level transaction builder and is not part of the RxdSdkError family. That one means “these inputs do not cover these outputs” at serialisation time; this one means “we checked before spending anything and the operation cannot succeed”. The two are not interchangeable and neither catches the other; new library code should raise this one.

Parameters:
  • message – static description — must not embed key material.

  • available – value the caller actually has, in the operation’s own units.

  • required – value the operation needs, same units.

__init__(message, *, available=None, required=None)[source]
Parameters:
  • message (str)

  • available (int | None)

  • required (int | None)

Return type:

None

property shortfall: int | None

required - available when both are known, else None.

exception pyrxd.security.KeyMaterialError[source]

Bases: RxdSdkError

Raised for errors touching private keys, mnemonics, or WIFs.

Constructors raising this error MUST NOT include the offending key material in the message — pass a static description only.

class pyrxd.security.Nbits[source]

Bases: bytes

The compact difficulty target (nBits) from a block header.

Wire format

nBits is a 4-byte little-endian encoding of a uint32. When decoded:
  • exponent = nBits_uint32 >> 24 (high byte, little-endian: byte[3])

  • mantissa = nBits_uint32 & 0x007fffff (low 3 bytes)

  • target   = mantissa * 256^(exponent-3)

This type accepts the raw 4 wire bytes and validates the three conditions Bitcoin Core enforces on target-word parsing. A malformed nBits can be used to forge PoW (e.g. a negative target evaluates the comparison weirdly, a zero target is trivially satisfied, an over-large exponent shifts out of range). Rejecting these at the trust boundary protects every SPV check downstream.

static __new__(cls, value)[source]
Parameters:

value (Any)

Return type:

Nbits

exception pyrxd.security.NetworkError[source]

Bases: RxdSdkError

Raised for transport / RPC / network failures.

class pyrxd.security.Photons[source]

Bases: int

Non-negative integer amount in photons (RXD smallest unit), capped at Radiant max supply.

The cap is RADIANT_MAX_PHOTONS, so every value a Radiant node can put in an output constructs. It exists to catch the shapes a hostile or broken server can inject that an unbounded int would carry into coin selection — a value large enough to swamp any subtraction, or one parsed out of a field that was never a number.

MAX: ClassVar[int] = 2100000000000000000
static __new__(cls, value)[source]
Parameters:

value (Any)

Return type:

Photons

exception pyrxd.security.PolicyRejection[source]

Bases: CovenantError, NetworkError

Raised when a node rejects a transaction on a consensus/policy rule (e.g. mandatory-script-verify-flag-failed, dust, min-relay-fee, an ElectrumX code 1).

Surface this distinctly rather than letting a node rejection be reclassified as a plain NetworkError — that masking hid a critical dMint covenant-rejection bug for weeks (see docs/solutions/logic-errors/dmint-v1-mint-scriptsig-divergence.md). The masking harm was that the node’s reason was discarded, so a script failure was indistinguishable from a dropped socket.

Parentage: it inherits from both CovenantError (its original parent — keeps every except CovenantError handler working) and NetworkError (so the ~30 except NetworkError handlers that already wrap broadcast calls do not silently stop catching rejections now that this class is actually raised). A node rejection is not covenant-specific — a dust or min-relay-fee rejection has nothing to do with covenants — so the covenant-only parentage it shipped with was too narrow. Widening it here is the compatible fix; re-rooting it under a dedicated NodeRejection base would be the cleaner shape but is a breaking change for existing handlers.

Parameters:
  • message – sanitized, caller-safe description. Node messages are attacker-influencable text — run them through redact() and strip control characters before constructing this.

  • code – the RPC error code, when the server supplied one.

  • reason – the sanitized server reason on its own, for programmatic matching.

__init__(*args, code=None, reason=None)[source]
Parameters:
Return type:

None

class pyrxd.security.PrivateKeyMaterial[source]

Bases: SecretBytes

A SecretBytes whose contents are a valid secp256k1 private key.

Invariants enforced at construction:
  • length is exactly 32 bytes

  • integer value is in the valid scalar range [1, N-1]

__init__(data)[source]
Parameters:

data (bytes)

Return type:

None

classmethod from_wif(wif)[source]

Decode a WIF-encoded private key.

On failure raises KeyMaterialError WITHOUT embedding the input wif in the message — an attacker watching logs must not learn any part of the candidate key.

Implementation is self-contained (no heavy coincurve dependency) so the security module can be imported before the rest of the SDK.

Parameters:

wif (str)

Return type:

PrivateKeyMaterial

classmethod generate()[source]

Generate a fresh random private key using the secure RNG.

Return type:

PrivateKeyMaterial

class pyrxd.security.RawTx[source]

Bases: bytes

Raw transaction bytes.

Enforces the 64-byte Merkle-forgery defense: any candidate transaction must be strictly greater than 64 bytes. A 64-byte “transaction” can be forged from an internal Merkle-tree node, letting an attacker prove inclusion of bogus data. See audit finding 02-F-1 and Bitcoin BIP-141’s segwit commitment for the historical context (and the CVE-2017-12842 family for concrete exploits).

MIN_SIZE: ClassVar[int] = 65
static __new__(cls, value)[source]
Parameters:

value (Any)

Return type:

RawTx

exception pyrxd.security.RxdSdkError[source]

Bases: Exception

Base class for every exception raised by pyrxd.

Applying redact to each positional arg on construction defends against accidental key-material leakage when callers pass user-supplied values straight into the exception.

__init__(*args)[source]
Parameters:

args (Any)

Return type:

None

class pyrxd.security.Satoshis[source]

Bases: int

Non-negative integer amount in Bitcoin satoshis, capped at Bitcoin max supply.

Not for Radiant values — see RADIANT_MAX_PHOTONS and Photons.

MAX: ClassVar[int] = 2100000000000000
static __new__(cls, value)[source]
Parameters:

value (Any)

Return type:

Satoshis

class pyrxd.security.SecretBytes[source]

Bases: object

A wrapper around bytes that will not leak its contents on repr/str.

The internal storage is a bytearray so zeroize() can mutate it in place. unsafe_raw_bytes returns an immutable bytes copy.

__init__(data)[source]
Parameters:

data (bytes)

Return type:

None

classmethod from_hex(h)[source]

Construct from a hex string.

The hex string itself is not embedded in any error message — an attacker who can see the exception must not learn the invalid input was “0x…” with specific bytes.

Parameters:

h (str)

Return type:

SecretBytes

unsafe_raw_bytes()[source]

Return the raw secret bytes.

Named unsafe_* on purpose: every call site should be auditable with a single grep. Prefer SDK methods that consume SecretBytes directly over pulling the raw bytes out.

Return type:

bytes

zeroize()[source]

Best-effort overwrite of the internal buffer with zeros.

Python’s memory model does not guarantee that every copy of a string or bytes object is scrubbed; only the buffer we own is cleared. Call this when you know a key will not be used again.

Return type:

None

class pyrxd.security.SighashFlag[source]

Bases: int

A valid Radiant sighash flag byte.

SIGHASH_ALL: ClassVar[int] = 65
SIGHASH_ALL_ANYONECANPAY: ClassVar[int] = 193
SIGHASH_NONE: ClassVar[int] = 66
SIGHASH_NONE_ANYONECANPAY: ClassVar[int] = 194
SIGHASH_SINGLE: ClassVar[int] = 67
SIGHASH_SINGLE_ANYONECANPAY: ClassVar[int] = 195
static __new__(cls, value)[source]
Parameters:

value (Any)

Return type:

SighashFlag

exception pyrxd.security.SpvVerificationError[source]

Bases: RxdSdkError

Raised when an SPV proof (Merkle path, header chain) fails to verify.

class pyrxd.security.Txid[source]

Bases: str

A lowercase-hex transaction id (64 chars).

static __new__(cls, value)[source]
Parameters:

value (Any)

Return type:

Txid

exception pyrxd.security.ValidationError[source]

Bases: RxdSdkError

Raised when input fails a trust-boundary validation check.

pyrxd.security.redact(value)[source]

Return a redacted representation of value if it looks sensitive.

  • str longer than 8 chars that looks like key material -> "<redacted>"

  • bytes longer than 8 bytes -> "<redacted:Nb>"

  • other types -> returned unchanged

Warning

This matches the value as a whole. redact(wif) redacts; redact(f"bad wif {wif}") does not — the interpolated string is neither all-hex nor all-base58, so the heuristic declines and the secret passes through verbatim. So the idiom at the top of this module, raise KeyMaterialError(redact(bad_wif)), is the only defended shape, and raise ValidationError(f"bad wif {wif}") is undefended by construction.

Making this per-token instead is not the fix: nearly every English word longer than 8 characters is also a valid base58 string ("transaction" is), so per-token redaction would replace ordinary prose with <redacted>, and it would also swallow the public txids that errors like ConfirmationTimeoutError deliberately keep verbatim because they are the only thing that makes the failure actionable. The defence for the embedded shape is therefore the call-site discipline this module documents, enforced by tests/security/test_key_material_never_echoed.py — not a wider heuristic here.

Parameters:

value (Any)

Return type:

Any

pyrxd.security.secure_random_bytes(n)[source]

Return n cryptographically secure random bytes.

Thin wrapper over secrets.token_bytes(). Exists so callers have a single chokepoint to audit/mock and so the n <= 0 guard lives in one place.

Parameters:

n (int)

Return type:

bytes

pyrxd.security.secure_scalar_mod_n()[source]

Draw a uniform random scalar in [1, N-1] and return it wrapped.

Convenience wrapper combining pyrxd.security.rng.secure_scalar_bytes_mod_n() with PrivateKeyMaterial. Lives in this module (not rng) so rng has no dependency on PrivateKeyMaterial — that keeps the import graph acyclic.

Return type:

PrivateKeyMaterial