pushdrop

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: 13 Imported by: 0

Documentation

Overview

Package pushdrop is the key derivation an application locks its PushDrop outputs under, and the tagged PushDrop those outputs carry: a leading tag field, the application's own fields, and the wallet's signature over them.

Every derivation here is counterparty Anyone with forSelf=true. Measured at go-sdk v1.5.2, that is the only setting under which a reader holding just the identity key can recompute the locking key AND the embedded signature verifies under it, so a reader checks an output without asking its producer anything.

A Derivation is frozen at an application's first mint: the protocol and key id are hashed into every derived key (BRC-43), so changing either orphans every output ever locked under it. The application owns its Derivation and its tags; nothing here names one.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrDerivation = errors.New("pushdrop: invalid derivation")

ErrDerivation is what Validate refuses a derivation with.

View Source
var ErrNonCanonical = errors.New("pushdrop: not the canonical encoding")

ErrNonCanonical is a lock-before PushDrop written other than the one way Lock writes it.

View Source
var ErrNotTagged = errors.New("pushdrop: not a tagged PushDrop")

ErrNotTagged reports a script that is not the tagged PushDrop DecodeTagged was asked for.

Functions

func Anyone

func Anyone() wallet.Counterparty

Anyone is the counterparty every derivation here uses. A zero-value Counterparty is not the same thing (the SDK's GetPublicKey and CreateSignature default it differently) and is never passed.

func CheckCanonical added in v0.5.0

func CheckCanonical(s *script.Script) error

CheckCanonical refuses, as ErrNonCanonical, a script that is not exactly the lock-before PushDrop Lock writes for the key and fields go-sdk decodes from it: the key pushed directly in its one canonical compressed encoding (guard.ParsePubKey), OP_CHECKSIG, each field in its minimal push (OP_0, OP_1 to OP_16 and OP_1NEGATE for the values they stand for, a direct push up to 75 bytes, then OP_PUSHDATA1, 2 and 4), then OP_2DROP per pair of fields and OP_DROP for one left over, and nothing else.

go-sdk's decoder reads looser forms too: a wider push, a trailing opcode, drops written one at a time, an uncompressed key or a compressed key whose x is at or above the field prime. Each is another script for the same fields, so a reader that keys on script bytes, or on key bytes, would see two outputs where there is one.

func Fields added in v0.6.0

func Fields(s []byte) ([][]byte, bool)

Fields reads s as a lock-before PushDrop, leniently: a 33-byte push, OP_CHECKSIG, then every data push up to the first opcode that is not one, the field signature included when the lock carries one. Every field is a slice of s.

It decides nothing about the script: whether s is the canonical one is decided by rebuilding it from these fields (Script) and comparing bytes, which is how a host holds a script to one encoding without trusting a decoder to refuse every other.

A data push here is OP_0, a direct push or OP_PUSHDATA1, 2 or 4. OP_1 to OP_16 and OP_1NEGATE are not, although the canonical lock writes a one-byte field of 1 to 16 or 0x81 with them: such a field ends the reading, the rebuild then differs, and a host that compares refuses the script. A tag or a record is never one of those bytes alone. OP_0 reads as an empty field, and rebuilds as OP_0.

func FirstPush added in v0.6.0

func FirstPush(s []byte) ([]byte, bool)

FirstPush reads the push that follows a lock-before PushDrop's key and OP_CHECKSIG, and nothing else of the script: the script starts with 0x21, 33 bytes and 0xac, then one push, an opcode 0x01 to 0x4b or OP_PUSHDATA1, 2 or 4 with its little-endian length, every declared byte present. It allocates nothing and returns a slice of s.

It is the first look a classifier takes at an output: which record or tag the first field claims. A script that claims something is then held to the exact script rebuilt from its fields (Script).

func Script added in v0.6.0

func Script(key *ec.PublicKey, fields [][]byte, sig []byte) []byte

Script is the one canonical lock-before PushDrop for key, fields and an optional field signature, exactly as Lock writes it: the 33-byte compressed key, OP_CHECKSIG, each field and then the signature as one minimal push, and the fewest OP_2DROP then OP_DROP that clear them. A nil sig writes an unsigned lock.

A host compares a script it checks with this, byte for byte.

Types

type Derivation

type Derivation struct {
	Protocol wallet.Protocol
	KeyID    string
}

Derivation is one key under an application's BRC-43 protocol: the protocol (security level and name) and the key id within it, which together give the invoice number "<level>-<name>-<key id>".

func (Derivation) ExpectedLockingKey

func (d Derivation) ExpectedLockingKey(identity *ec.PublicKey) (*ec.PublicKey, error)

ExpectedLockingKey is the reader's side of the derivation: from the anyone root, identity's key under d. It equals what identity's own wallet produces with counterparty Anyone and forSelf=true, which is the key Lock locks to.

func (Derivation) Lock

func (d Derivation) Lock(ctx context.Context, w wallet.Interface, originator string, fields [][]byte, sign bool) (*script.Script, error)

Lock builds a lock-before PushDrop of fields through the wallet, locked to the wallet's key under d with counterparty Anyone and forSelf=true. With sign, the wallet's signature over the fields concatenated follows them as one more field; without it the output carries the fields alone.

The fields slice is freshly allocated on every call because the SDK appends the signature into whatever capacity it is handed, and a reused buffer would carry one output's signature into the next. The field bytes are copied too, so the SDK is never handed a buffer the caller still holds.

Example

A producer locks a signed, tagged PushDrop through its wallet; a reader holding only the producer's identity key recomputes the locking key, decodes the output and checks the field signature. The locking key is the independent vector's objectLockingKeyHex in testdata/vectors/transactions-v1.json.

package main

import (
	"context"
	"encoding/hex"
	"errors"
	"fmt"

	"github.com/bsv-blockchain/go-sdk/wallet"

	"github.com/lightwebinc/bcommon/goldentest"
	"github.com/lightwebinc/bcommon/pushdrop"
)

// The derivation and tag the library's test vectors use. They belong to no
// application, are reserved for tests (docs/registry.md), and an application
// supplies its own.
var (
	exampleDerivation = pushdrop.Derivation{
		Protocol: wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryApp, Protocol: "vector sample"},
		KeyID:    "object",
	}
	exampleTag = []byte{'v', 'x', 0x01}
)

func main() {
	ctx := context.Background()
	// The fixed TEST key (32 bytes of 0x42). The SDK's proto wallet stands
	// in for the producer's BRC-100 wallet: Lock needs only its key calls.
	w, err := wallet.NewCompletedProtoWallet(goldentest.FixedKey())
	if err != nil {
		fmt.Println(err)
		return
	}
	lock, err := exampleDerivation.Lock(ctx, w, "example.com", [][]byte{exampleTag, []byte("hello")}, true)
	if err != nil {
		fmt.Println(err)
		return
	}

	// The reader's side.
	identity := goldentest.FixedKey().PubKey()
	want, err := exampleDerivation.ExpectedLockingKey(identity)
	if err != nil {
		fmt.Println(err)
		return
	}
	out, err := pushdrop.DecodeTagged(lock, exampleTag, 2)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("locking key:", hex.EncodeToString(out.LockingKey.Compressed()))
	fmt.Println("derived by the reader:", want.IsEqual(out.LockingKey))
	fmt.Printf("fields: %q\n", out.Fields)
	fmt.Println("signature verifies:", out.VerifySignature())

	// Another tag is not this application's output.
	_, err = pushdrop.DecodeTagged(lock, []byte{'v', 'x', 0x02}, 2)
	fmt.Println("another tag refused:", errors.Is(err, pushdrop.ErrNotTagged))
}
Output:
locking key: 0324d6d6ee75173da1ca8d964d3889792c8911d66c3290a76c367413f5a68e5446
derived by the reader: true
fields: ["vx\x01" "hello"]
signature verifies: true
another tag refused: true

func (Derivation) Unlocker

func (d Derivation) Unlocker(ctx context.Context, w wallet.Interface, originator string) Template

Unlocker returns the template that spends an output Lock locked under d, through the wallet, signing all outputs. The SDK's pushdrop.Unlocker does not satisfy transaction.UnlockingScriptTemplate on its own (its Sign takes an int and its EstimateLength takes nothing), so Template adapts it.

func (Derivation) Validate

func (d Derivation) Validate() error

Validate applies the BRC-43 rules go-sdk v1.5.2's key deriver applies to a protocol and key id, in the SDK's order, so an application can refuse a derivation that will never produce a key when it builds one rather than at its first mint. The SDK stays the authority: no method here calls Validate, and each derivation still meets the SDK's own check.

The name is measured as the SDK measures it, trimmed and lower-cased, so " Sample " passes here because the SDK derives it as "sample".

Example

Validate applies the SDK's BRC-43 rules when a derivation is built, rather than at the first mint.

package main

import (
	"fmt"

	"github.com/bsv-blockchain/go-sdk/wallet"

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

// The derivation and tag the library's test vectors use. They belong to no
// application, are reserved for tests (docs/registry.md), and an application
// supplies its own.
var exampleDerivation = pushdrop.Derivation{
	Protocol: wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryApp, Protocol: "vector sample"},
	KeyID:    "object",
}

func main() {
	for _, d := range []pushdrop.Derivation{
		exampleDerivation,
		{Protocol: wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryApp, Protocol: "abcd"}, KeyID: "object"},
		{Protocol: wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryApp, Protocol: "vector_sample"}, KeyID: "object"},
		{Protocol: wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryApp, Protocol: "vector sample"}},
	} {
		fmt.Println(d.Validate())
	}
}
Output:
<nil>
pushdrop: invalid derivation: protocol name "abcd" is under 5 characters
pushdrop: invalid derivation: protocol name "vector_sample" has '_'; only letters, digits and spaces
pushdrop: invalid derivation: empty key id

type Tagged

type Tagged struct {
	// Fields are the fields Lock was given, tag first. The signature is not
	// one of them.
	Fields [][]byte
	// LockingKey is the key the output is locked to.
	LockingKey *ec.PublicKey
	// Signature is Lock's DER signature over sha256 of Fields concatenated.
	Signature []byte
}

Tagged is a decoded signed PushDrop whose first field is a tag.

func DecodeTagged

func DecodeTagged(s *script.Script, tag []byte, nfields int) (*Tagged, error)

DecodeTagged parses what Lock writes with sign set: nfields fields, tag included, the first equal to tag, then the signature. Only the lock-before layout decodes, which is the layout Lock writes; a lock-after script from some other producer is refused rather than guessed at. What the fields after the tag must hold is the caller's to check.

A script of the caller's tag that is not the one encoding Lock writes (CheckCanonical) is refused with ErrNonCanonical, not ErrNotTagged: it is the caller's output written another way, and a reader skipping it as somebody else's would miss that.

func (*Tagged) Signed

func (t *Tagged) Signed() []byte

Signed returns the bytes Lock signed: the fields concatenated.

func (*Tagged) VerifySignature

func (t *Tagged) VerifySignature() bool

VerifySignature checks the embedded signature under the locking key. The wallet signs sha256 of the concatenated fields, so that is what is verified.

type Template

type Template struct {
	U *sdkpushdrop.Unlocker
}

Template adapts the SDK's pushdrop.Unlocker to transaction.UnlockingScriptTemplate.

func (Template) EstimateLength

func (t Template) EstimateLength(_ *transaction.Transaction, _ uint32) uint32

EstimateLength is the unlocker's own estimate: one DER signature push.

func (Template) Sign

func (t Template) Sign(tx *transaction.Transaction, inputIndex uint32) (*script.Script, error)

Sign signs input inputIndex of tx through the wallet.

Jump to

Keyboard shortcuts

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