guard

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package guard checks bytes someone else supplied before the SDK is allowed to allocate for them or to trust them: a BRC-74 BUMP, a BEEF (BRC-62, BRC-96, and the Atomic BEEF of BRC-95 around either), a raw transaction or the Extended Format (BRC-30) of one, and a compressed public key.

A reader that sizes a slice from a wire count can be asked, by a handful of bytes, for an allocation that ends the process with an out-of-memory no recover() sees. go-sdk v1.5.2 bounds its own counts, which is why that version is the floor, but the protection belongs in the library that reads the bytes, not in a pin: the rule for length-prefixed data from any other party is that a declared length is a claim, checked against the bytes present before it sizes memory. Every walk here allocates nothing, bounds every count and length by the bytes that remain, and must end exactly at the last byte.

A public key has the same shape of problem in another form: go-sdk accepts a compressed key whose x coordinate is at or above the field prime and keeps the alias bytes, so one point has two encodings. ParsePubKey accepts only the one canonical encoding.

Index

Examples

Constants

View Source
const DefaultBound = 64 << 20

DefaultBound is the size bound this module's own callers pass for a BEEF or a transaction: 64 MiB, the bound hostset puts on a whole lookup answer, so a BEEF inside an answer cannot have been larger. An application calling the guard directly passes the bound it was prepared to read.

Variables

View Source
var ErrBEEF = errors.New("guard: BEEF refused")

ErrBEEF is every refusal of CheckBEEF and ParseBEEF that the walk makes; the SDK's own refusals of a BEEF the walk passed are returned as the SDK words them.

View Source
var ErrPubKey = errors.New("guard: public key refused")

ErrPubKey is every refusal of ParsePubKey and ParsePubKeyHex.

View Source
var ErrTransaction = errors.New("guard: transaction refused")

ErrTransaction is every refusal of ParseTransaction that the walk makes.

Functions

func CheckBEEF added in v0.5.0

func CheckBEEF(b []byte, bound int) error

CheckBEEF walks a BEEF of at most bound bytes and refuses, as ErrBEEF, anything whose declared counts or lengths overrun the bytes present or that leaves bytes over. It admits BEEF V1 (BRC-62), BEEF V2 (BRC-96) with its txid-only entries, and Atomic BEEF (BRC-95) around either, and allocates nothing. After it returns nil no count in the BEEF can ask the SDK for more than the bytes hold.

It is a structural check, not a second parser: whether the transactions are valid, the proofs prove them and an Atomic BEEF holds its subject are the SDK's to decide, and SPV's after it. It is stricter than the SDK in three places, each a shape no BEEF this module writes has: trailing bytes, a BEEF of no transactions, and a transaction of no inputs (which is also the extended-format marker's shape) are refused.

func IsEF added in v0.5.3

func IsEF(b []byte) bool

IsEF reports whether b carries the Extended Format marker after its version. It says nothing of whether the rest of b is well formed.

func ParseBEEF added in v0.5.0

func ParseBEEF(b []byte, bound int) (beef *transaction.Beef, tx *transaction.Transaction, txid *chainhash.Hash, err error)

ParseBEEF is CheckBEEF, then the SDK's transaction.ParseBeef under a recover, so a malformed BEEF is an error and never a crash. Its results are ParseBeef's: the BEEF, its subject transaction (which may be nil) and the subject's txid.

Example

ParseBEEF walks a BEEF before the SDK parses it: every count and length it declares must fit the bytes present, the walk must end at the last byte, and the whole must be within the caller's bound.

package main

import (
	"fmt"

	"github.com/lightwebinc/bcommon/guard"
)

func main() {
	// Thirteen bytes: BEEF V1, then a BUMP count of 2^63.
	hostile := []byte{0x01, 0x00, 0xbe, 0xef, 0xff, 0, 0, 0, 0, 0, 0, 0, 0x80}
	_, _, _, err := guard.ParseBEEF(hostile, guard.DefaultBound)
	fmt.Println(err)

	// A BEEF V1 whose one transaction declares four billion inputs.
	manyInputs := append([]byte{0x01, 0x00, 0xbe, 0xef, 0x00, 0x01, 0x01, 0, 0, 0, 0xfe, 0xff, 0xff, 0xff, 0xff}, make([]byte, 64)...)
	_, _, _, err = guard.ParseBEEF(manyInputs, guard.DefaultBound)
	fmt.Println(err)
}
Output:
guard: BEEF refused: declares 9223372036854775808 BUMPs, 0 bytes remain
guard: BEEF refused: transaction 0: declares 4294967295 inputs, 64 bytes remain

func ParseBUMP

func ParseBUMP(b []byte, bound int) (mp *transaction.MerklePath, err error)

ParseBUMP guards and parses one BRC-74 BUMP of at most bound bytes. The bound is the caller's, since only the caller knows how large an answer it was prepared to read; a bound of zero or less admits nothing.

The parse runs under a recover, so a malformed proof is an error and never a crash.

Example

ParseBUMP walks a BRC-74 BUMP someone else supplied, allocating nothing, and hands it to the SDK only once every count it declares fits the bytes present. The bound is the caller's: how large an answer it was prepared to read.

package main

import (
	"crypto/sha256"
	"fmt"

	"github.com/bsv-blockchain/go-sdk/chainhash"
	"github.com/bsv-blockchain/go-sdk/transaction"

	"github.com/lightwebinc/bcommon/guard"
)

func main() {
	// A well-formed proof: a transaction at offset 1 of a two-transaction
	// block at height 90.
	txid := chainhash.Hash(sha256.Sum256([]byte("a transaction")))
	sibling := chainhash.Hash(sha256.Sum256([]byte("its neighbour")))
	isTxid := true
	good := transaction.NewMerklePath(90, [][]*transaction.PathElement{{
		{Offset: 0, Hash: &sibling},
		{Offset: 1, Hash: &txid, Txid: &isTxid},
	}}).Bytes()

	mp, err := guard.ParseBUMP(good, 1<<20)
	fmt.Println("good:", len(good), "bytes, height", mp.BlockHeight, err)

	// Seven bytes that declare four billion leaves: the SDK is never asked
	// to size a slice for them.
	hostile := []byte{0x5a, 0x01, 0xfe, 0xff, 0xff, 0xff, 0xff}
	_, err = guard.ParseBUMP(hostile, 1<<20)
	fmt.Println("hostile:", err)

	_, err = guard.ParseBUMP(append(good, 0x00), 1<<20)
	fmt.Println("trailing:", err)

	_, err = guard.ParseBUMP(good, 16)
	fmt.Println("over the bound:", err)
}
Output:
good: 71 bytes, height 90 <nil>
hostile: bump level 0 declares 4294967295 leaves, 0 bytes remain
trailing: bump has 1 trailing bytes
over the bound: bump is 71 bytes, max 16

func ParsePubKey added in v0.5.0

func ParsePubKey(b []byte) (*ec.PublicKey, error)

ParsePubKey accepts exactly one encoding of a point: 33 bytes, the prefix 0x02 or 0x03, and an x coordinate below the field prime that names a point on the curve. Only then is the key handed to the SDK.

go-sdk v1.5.2 accepts a compressed key whose x is at or above the prime (02 || p+1 is an alias of the point with x = 1), keeps x unreduced and writes the alias bytes back, so one point has two encodings and IsEqual calls them two keys. No private key is known for such a key, so it cannot sign, but anything keyed by key bytes before a signature is checked (a pin, an index, a de-duplication) would treat the alias as a second key. An uncompressed or hybrid encoding is refused too: a key from the wire in this module is always compressed, and a second encoding of the same point is the same problem.

Example

ParsePubKey takes a compressed key only in its one canonical encoding. 02 || p+1 names the same point as the key with x = 1, and go-sdk reads it.

package main

import (
	"encoding/hex"
	"fmt"

	ec "github.com/bsv-blockchain/go-sdk/primitives/ec"

	"github.com/lightwebinc/bcommon/guard"
)

func main() {
	x1, _ := hex.DecodeString("020000000000000000000000000000000000000000000000000000000000000001")
	alias, _ := hex.DecodeString("02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc30")

	k, err := guard.ParsePubKey(x1)
	fmt.Println(hex.EncodeToString(k.Compressed()) == hex.EncodeToString(x1), err)

	_, err = guard.ParsePubKey(alias)
	fmt.Println(err)
	sdk, err := ec.PublicKeyFromBytes(alias)
	fmt.Println("go-sdk:", err == nil, sdk.X.Cmp(k.X) != 0)
}
Output:
true <nil>
guard: public key refused: x is not below the field prime
go-sdk: true true

func ParsePubKeyHex added in v0.5.0

func ParsePubKeyHex(s string) (*ec.PublicKey, error)

ParsePubKeyHex is ParsePubKey over a hex string, which must be exactly the key's 66 hex digits.

func ParseTransaction added in v0.5.0

func ParseTransaction(b []byte, bound int) (tx *transaction.Transaction, err error)

ParseTransaction walks one raw transaction of at most bound bytes as CheckBEEF walks each of a BEEF's, refusing as ErrTransaction, then parses it with the SDK under a recover. An Extended Format transaction is refused as one of no inputs; RawTransaction turns it into the raw form first.

func RawTransaction added in v0.5.3

func RawTransaction(b []byte, bound int) ([]byte, error)

RawTransaction returns the raw transaction b is, given either the raw form or the Extended Format (BRC-30) that extends it, of at most bound bytes. A raw b is walked as ParseTransaction walks it and returned as it came; an Extended Format b is walked the same way, its marker and each input's previous satoshis and locking script included, and its raw form returned in a new slice no longer than b. The txid is the raw form's.

The previous outputs an Extended Format transaction carries are dropped: they are the sender's claim about other transactions, which nothing here can check, and whoever signs against an input reads the output it spends from that output's own transaction.

Every refusal is ErrTransaction, and the walk bounds every count and length by the bytes present before anything is allocated.

Types

This section is empty.

Jump to

Keyboard shortcuts

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