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 ¶
- Constants
- Variables
- func CheckBEEF(b []byte, bound int) error
- func IsEF(b []byte) bool
- func ParseBEEF(b []byte, bound int) (beef *transaction.Beef, tx *transaction.Transaction, txid *chainhash.Hash, ...)
- func ParseBUMP(b []byte, bound int) (mp *transaction.MerklePath, err error)
- func ParsePubKey(b []byte) (*ec.PublicKey, error)
- func ParsePubKeyHex(s string) (*ec.PublicKey, error)
- func ParseTransaction(b []byte, bound int) (tx *transaction.Transaction, err error)
- func RawTransaction(b []byte, bound int) ([]byte, error)
Examples ¶
Constants ¶
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 ¶
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.
var ErrPubKey = errors.New("guard: public key refused")
ErrPubKey is every refusal of ParsePubKey and ParsePubKeyHex.
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
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
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
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
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
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.