bsv-rs 0.4.2

BSV SDK for Rust: primitives, the script interpreter, transactions and BEEF/SPV, BRC-42 wallets, BRC-103 auth, the overlay network. Reference parity with the TypeScript SDK, native and wasm32.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# BSV Script Module
> Bitcoin Script construction, parsing, execution, validation, and templates for BSV transactions

## Overview

This module provides complete Bitcoin Script functionality for the BSV SDK:
- Opcode definitions and lookup utilities
- Script parsing, serialization, and construction
- Specialized types for locking (output) and unlocking (input) scripts
- Script number encoding for stack operations
- Full script interpreter with spend validation
- Script templates for common transaction types (P2PKH, P2PK, Multisig, R-Puzzle, PushDrop)
- Script type detection (P2PKH, P2PK, P2SH, multisig, data, safe data carrier)
- Bitcoin P2PKH address encoding/decoding (Base58Check)
- BIP-276 typed script encoding
- Transaction interface traits for Script/Transaction module integration

Compatible with the TypeScript and Go SDKs through shared opcode values, serialization formats, and execution semantics.

## Files

| File | Purpose |
|------|---------|
| `mod.rs` | Module root; submodule declarations and public re-exports |
| `op.rs` | Opcode constants (u8) and name/value lookup functions via `LazyLock` HashMaps |
| `chunk.rs` | `ScriptChunk` type representing individual script elements |
| `script.rs` | Core `Script` class with parsing, serialization, builder methods, and type detection |
| `locking_script.rs` | `LockingScript` newtype wrapper for output scripts (scriptPubKey) with `to_address()` |
| `unlocking_script.rs` | `UnlockingScript` newtype wrapper for input scripts (scriptSig) |
| `address.rs` | `Address` type for P2PKH address parsing, creation, and Base58Check encoding |
| `bip276.rs` | BIP-276 typed script encoding (`encode_bip276` / `decode_bip276`) |
| `script_num.rs` | `ScriptNum` utilities for Bitcoin script number encoding |
| `evaluation_error.rs` | `ScriptEvaluationError` with full execution context for debugging |
| `spend.rs` | `Spend` interpreter for validating transaction spends |
| `flags.rs` | `ScriptFlags` (the node's 32-bit verification flag word, bitcoin-sv v1.2.2's bit values and names; `block(era)` the consensus word, `standard(era)` the mempool word), `ProtocolEra`, `ScriptFlagsError` |
| `template.rs` | `ScriptTemplate` trait, `SigningContext`, and signing utilities |
| `transaction.rs` | Transaction interface traits for Transaction module integration |
| `templates/mod.rs` | Templates module declarations and re-exports |
| `templates/p2pkh.rs` | P2PKH (Pay-to-Public-Key-Hash) template |
| `templates/p2pk.rs` | P2PK (Pay-to-Public-Key) template |
| `templates/multisig.rs` | Multisig (M-of-N) template using OP_CHECKMULTISIG |
| `templates/rpuzzle.rs` | R-Puzzle template for knowledge-based locking |
| `templates/pushdrop.rs` | PushDrop template for data envelope scripts with P2PK lock |

## Key Exports

```rust
// Core script types
pub use chunk::ScriptChunk;
pub use script::Script;
pub use locking_script::LockingScript;
pub use unlocking_script::UnlockingScript;
pub use script_num::ScriptNum;

// Address and BIP-276
pub use address::Address;
pub use bip276::*;  // encode_bip276, decode_bip276, BIP276_PREFIX, NETWORK_MAINNET, NETWORK_TESTNET

// Evaluation types
pub use evaluation_error::{ExecutionContext, ScriptEvaluationError, ScriptResource, ScriptResourceLimit};
pub use flags::{ProtocolEra, ScriptFlags, ScriptFlagsError};
pub use spend::{Spend, SpendParams};

// Template types
pub use template::{ScriptTemplate, ScriptTemplateUnlock, SignOutputs, SigningContext};

// Transaction interface types
pub use transaction::{
    SimpleUtxo, SpendValidation, TransactionContext, TransactionInputContext,
    TransactionOutputContext, UtxoProvider,
};

// Templates (via templates module)
pub use templates::{P2PKH, P2PK, Multisig, RPuzzle, RPuzzleType, PushDrop, LockPosition};
```

## Core Types

### Opcodes (`op` module)

All Bitcoin Script opcodes defined as `u8` constants with `LazyLock` HashMaps for lookup:

```rust
// Push values: OP_0/OP_FALSE, OP_1..OP_16, OP_1NEGATE, OP_PUSHDATA1/2/4
// Direct push: OP_DATA_1..OP_DATA_75 (push exactly N bytes)
// Control: OP_IF, OP_NOTIF, OP_ELSE, OP_ENDIF, OP_VERIFY, OP_RETURN
// Stack: OP_DUP, OP_DROP, OP_SWAP, OP_OVER, OP_NIP, OP_PICK, OP_ROLL, etc.
// BSV re-enabled: OP_CAT, OP_SPLIT, OP_NUM2BIN, OP_BIN2NUM, OP_MUL, OP_DIV, OP_MOD
// Bitwise: OP_INVERT, OP_AND, OP_OR, OP_XOR, OP_LSHIFT, OP_RSHIFT
// Crypto: OP_HASH160, OP_HASH256, OP_SHA256, OP_RIPEMD160, OP_SHA1
// Signature: OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY
// Expansion NOPs: OP_NOP1..OP_NOP77 (0xb0-0xfc range)
// Template matching: OP_SMALLDATA, OP_SMALLINTEGER, OP_PUBKEYS, OP_PUBKEYHASH, OP_PUBKEY

pub fn name_to_opcode(name: &str) -> Option<u8>        // "OP_DUP" -> Some(0x76)
pub fn opcode_to_name(op: u8) -> Option<&'static str>  // 0x76 -> Some("OP_DUP")
```

Note: `OP_FALSE`/`OP_0` renders as `"0"` in ASM output, `OP_1NEGATE` renders as `"-1"`. The value-to-name map uses BSV names for dual-named opcodes (e.g., `OP_SPLIT` not `OP_SUBSTR`).

### ScriptChunk

Represents a single script element (opcode or data push):

```rust
pub struct ScriptChunk { pub op: u8, pub data: Option<Vec<u8>> }

impl ScriptChunk {
    pub fn new_opcode(op: u8) -> Self
    pub fn new_push(data: Vec<u8>) -> Self      // Auto-selects optimal push opcode
    pub fn new(op: u8, data: Option<Vec<u8>>) -> Self
    pub fn is_push_data(&self) -> bool          // True if op <= OP_16
    pub fn to_asm(&self) -> String
}
// Implements Default (OP_0), Debug, Clone, PartialEq, Eq
```

### Script

Core script type with lazy parsing and serialization caching using `RefCell`:

```rust
impl Script {
    // Constructors
    pub fn new() -> Self
    pub fn from_chunks(chunks: Vec<ScriptChunk>) -> Self
    pub fn from_asm(asm: &str) -> Result<Self>      // "OP_DUP OP_HASH160 <hex>..."
    pub fn from_hex(hex: &str) -> Result<Self>
    pub fn from_binary(bin: &[u8]) -> Result<Self>

    // Serialization
    pub fn to_asm(&self) -> String
    pub fn to_hex(&self) -> String
    pub fn to_binary(&self) -> Vec<u8>

    // Builder methods (chainable)
    pub fn write_opcode(&mut self, op: u8) -> &mut Self
    pub fn write_bin(&mut self, bin: &[u8]) -> &mut Self
    pub fn write_number(&mut self, num: i64) -> &mut Self
    pub fn write_script(&mut self, script: &Script) -> &mut Self

    // Manipulation
    pub fn remove_codeseparators(&mut self) -> &mut Self
    pub fn find_and_delete(&mut self, script: &Script) -> &mut Self
    pub fn set_chunk_opcode(&mut self, index: usize, op: u8) -> &mut Self

    // Inspection
    pub fn chunks(&self) -> Vec<ScriptChunk>
    pub fn truncated_push(&self) -> Option<usize>   // the first push cut short of its declared bytes (BAD_OPCODE when reached)
    pub fn len(&self) -> usize
    pub fn is_empty(&self) -> bool
    pub fn is_push_only(&self) -> bool
    pub fn is_locking_script(&self) -> bool   // Always false for base Script
    pub fn is_unlocking_script(&self) -> bool // Always false for base Script

    // Script Type Detection
    pub fn is_p2pkh(&self) -> bool             // OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG
    pub fn is_p2pk(&self) -> bool              // <33|65 bytes> OP_CHECKSIG
    pub fn is_p2sh(&self) -> bool              // OP_HASH160 <20> OP_EQUAL
    pub fn is_data(&self) -> bool              // OP_RETURN or OP_FALSE OP_RETURN
    pub fn is_safe_data_carrier(&self) -> bool // OP_FALSE OP_RETURN only (prunable, unspendable)
    pub fn is_multisig(&self) -> Option<(u8, u8)>  // Returns (M, N) if multisig
    pub fn opcode_to_small_int(op: u8) -> Option<u8>  // OP_1..OP_16 -> 1..16
    pub fn extract_pubkey_hash(&self) -> Option<[u8; 20]>  // From P2PKH scripts
    pub fn get_public_key(&self) -> Option<Vec<u8>>        // From P2PK scripts (matches Go PubKey())
    pub fn get_public_key_hex(&self) -> Option<String>     // Hex-encoded (matches Go PubKeyHex())
}
// Implements Default, Debug, Clone, PartialEq (by binary), Eq
```

### LockingScript / UnlockingScript

Newtype wrappers providing type safety for output scripts (scriptPubKey) and input scripts (scriptSig):

```rust
pub struct LockingScript(Script);   // is_locking_script() -> true
pub struct UnlockingScript(Script); // is_unlocking_script() -> true

// Both provide: new(), from_chunks(), from_asm(), from_hex(), from_binary(),
// from_script(), as_script(), into_script(), to_asm(), to_hex(), to_binary(),
// chunks(), len(), is_empty(), is_push_only()
// Both implement: Default, Debug, Clone, PartialEq, Eq, From<Script>

// LockingScript-specific:
impl LockingScript {
    pub fn to_address(&self) -> Option<Address>  // Extracts P2PKH address (mainnet), None for non-P2PKH
}
```

### Address

Bitcoin P2PKH address with Base58Check encoding (mainnet `0x00`, testnet `0x6f`):

```rust
impl Address {
    pub fn new_from_string(address: &str) -> Result<Self>
    pub fn new_from_public_key_hash(hash: &[u8], mainnet: bool) -> Result<Self>
    pub fn new_from_public_key(public_key: &PublicKey, mainnet: bool) -> Result<Self>
    pub fn public_key_hash(&self) -> &[u8]
    pub fn prefix(&self) -> u8
    pub fn is_mainnet(&self) -> bool
    pub fn is_valid_address(address: &str) -> bool
}
// Implements Display (Base58Check), FromStr, Debug, Clone, PartialEq, Eq
```

### BIP-276

Typed bitcoin script encoding (`bitcoin-script:<network><type><script><checksum>`):

```rust
pub const BIP276_PREFIX: &str = "bitcoin-script";
pub const NETWORK_MAINNET: u8 = 1;
pub const NETWORK_TESTNET: u8 = 2;

pub fn encode_bip276(network: u8, script_type: u8, script: &[u8]) -> String
pub fn decode_bip276(encoded: &str) -> Result<(u8, u8, Vec<u8>)>  // -> (network, type, data)
```

### ScriptNum

Utilities for Bitcoin Script number encoding (little-endian sign-magnitude):

```rust
impl ScriptNum {
    pub fn from_bytes(bytes: &[u8], require_minimal: bool) -> Result<BigNumber>
    pub fn to_bytes(value: &BigNumber) -> Vec<u8>
    pub fn is_minimally_encoded(bytes: &[u8]) -> bool
    pub fn cast_to_bool(bytes: &[u8]) -> bool
    pub fn minimally_encode(bytes: &[u8]) -> Vec<u8>
}
```

### Spend

Full script interpreter for validating transaction spends:

```rust
pub struct SpendParams {
    pub source_txid: [u8; 32],
    pub source_output_index: u32,
    pub source_satoshis: u64,
    pub locking_script: LockingScript,
    pub transaction_version: i32,
    pub other_inputs: Vec<TxInput>,
    pub outputs: Vec<TxOutput>,
    pub input_index: usize,
    pub unlocking_script: UnlockingScript,
    pub input_sequence: u32,
    pub lock_time: u32,
    pub memory_limit: Option<usize>,  // the LOCAL budget: None = 32 MB (default mode) or 100 MB on the node's count (under a word)
}

impl Spend {
    pub fn new(params: SpendParams) -> Self          // the TypeScript SDK's default mode
    pub fn set_flags(&mut self, flags: ScriptFlags)  // the node's word: every rule re-derived at the reference's site under its version gate
    pub fn flags(&self) -> Option<ScriptFlags>
    pub fn set_require_minimal(&mut self, require: bool)    // overrides, also after set_flags
    pub fn set_require_push_only(&mut self, require: bool)  // overrides, also after set_flags
    pub fn set_utxo_after_chronicle(&mut self, after: bool)  // the Chronicle opcodes' gate; overrides the word's bit
    pub fn memory_limit(&self) -> usize                      // the local budget in force (0.3.30)
    pub fn stack_memory_policy(&self) -> Option<usize>       // the mempool path's verdict figure in force (0.3.30)
    pub fn set_stack_memory_policy(&mut self, bytes: usize)  // the node's -maxstackmemoryusagepolicy, 0 = none (0.3.30)
    pub fn reset(&mut self)
    pub fn validate(&mut self) -> Result<bool, ScriptEvaluationError>  // refuses a word `check` refuses first
    pub fn step(&mut self) -> Result<bool, ScriptEvaluationError>  // Single instruction
}
```

Two ways to run it. Without a word: strict for version ≤ 1 (MINIMALDATA, LOW_S,
CLEANSTACK, NULLDUMMY), relaxed for version ≥ 2, push-only unlocking scripts at
every version, no NULLFAIL (ts-sdk `isRelaxed()`). With a word: `ScriptFlags::block(era)`
is what a mining node applies, `ScriptFlags::standard(era)` what a relaying node with
default policy applies; NULLFAIL, MINIMALIF, DISCOURAGE_UPGRADABLE_NOPS and
COMPRESSED_PUBKEYTYPE exist only under a word. A consensus oracle selects the block
word. The gates, their sites in bitcoin-sv v1.2.2 and what is not modeled are the
table on `ScriptFlags` in `flags.rs`; the seven witness transactions are
`tests/script_flags_witnesses.rs`.

The memory budget (0.3.30, bsv-rs#30). `memory_limit` is LOCAL: exhausting it is a
`ScriptResourceLimit`, never a verdict, and `Some(n)` wins in every mode. The default
mode keeps 32 MB for each stack apart, the bytes alone. Under a word the count is the
node's (`LimitedStack`: one budget over both stacks, `STACK_ELEMENT_OVERHEAD` = 32
bytes per element, every growth charged before it happens, an unlocking script's alt
stack left charged) and `None` selects 100 MB (`WORD_MEMORY_LIMIT`, the node's policy
default); on the mempool path the node's stack memory policy is a verdict besides
(`Stack size limit exceeded: …`, `SCRIPT_ERR_STACK_SIZE`), and `OP_NUM2BIN`'s size is
bounded by `INT32_MAX` (`SCRIPT_ERR_PUSH_SIZE`). On the block path a node's budget
is its operator's mandatory `-maxstackmemoryusageconsensus` (bitcoin-sv v1.2.2 does not
start without it, `src/bitcoind.cpp:140-157`; 0 = `INT64_MAX`,
`src/configscriptpolicy.cpp:280-283`), no constant, so the local budget declines above
100 MB; `memory_limit: Some(usize::MAX)` models a node run with 0. A decline is an `Err`
whose `resource_limit` is `Some`: never read it as invalid. The witnesses are
`tests/script_stack_memory_witness.rs`.

### ScriptFlags / ProtocolEra (`flags.rs`)

```rust
pub enum ProtocolEra { PostGenesis, PostChronicle }   // mainnet 620,538 / 943,816; ProtocolEra::mainnet(height)
pub struct ScriptFlags(u32);                          // the reference's bit values (script_flags.h)
impl ScriptFlags {
    pub const P2SH, STRICTENC, DERSIG, LOW_S, NULLDUMMY, SIGPUSHONLY, MINIMALDATA, DISCOURAGE_UPGRADABLE_NOPS,
              CLEANSTACK, CHECKLOCKTIMEVERIFY, CHECKSEQUENCEVERIFY, MINIMALIF, NULLFAIL, COMPRESSED_PUBKEYTYPE,
              SIGHASH_FORKID, GENESIS, UTXO_AFTER_GENESIS, CHRONICLE, UTXO_AFTER_CHRONICLE: Self;
    pub fn block(era: ProtocolEra) -> Self             // 0x3D462F post-Chronicle
    pub fn standard(era: ProtocolEra) -> Self          // 0x3D47FF post-Chronicle
    pub fn bits(self) -> u32
    pub fn from_bits(bits: u32) -> Result<Self, ScriptFlagsError>          // unknown bits refused
    pub fn from_names<'a>(names: impl IntoIterator<Item = &'a str>) -> Result<Self, ScriptFlagsError>
    pub fn names(self) -> Vec<&'static str>
    pub fn contains / union / without
    pub fn check(self) -> Result<(), ScriptFlagsError>  // what the interpreter cannot honor, what the reference refuses
    pub fn enforce_non_malleability(self, tx_version: i32) -> bool  // interpreter.cpp:40-44
    pub fn requires_push_only(self, tx_version: i32) -> bool        // interpreter.cpp:2321-2334
}
```

### ScriptEvaluationError

Rich error type capturing full execution state at failure:

```rust
pub struct ScriptEvaluationError {
    pub message: String,
    pub source_txid: String,
    pub source_output_index: u32,
    pub context: ExecutionContext,  // UnlockingScript or LockingScript
    pub program_counter: usize,
    pub stack: Vec<Vec<u8>>,
    pub alt_stack: Vec<Vec<u8>>,
    pub if_stack: Vec<bool>,
    pub stack_mem: usize,
    pub alt_stack_mem: usize,
}
// Implements Display, Error, Debug, Clone
```

## Script Templates

### ScriptTemplate Trait & Types

```rust
pub trait ScriptTemplate {
    fn lock(&self, params: &[u8]) -> Result<LockingScript>;
}

pub enum SignOutputs { All, None, Single }

pub struct SigningContext<'a> { pub raw_tx, pub input_index, pub source_satoshis, pub locking_script }
impl SigningContext<'_> {
    pub fn new(...) -> Self
    pub fn compute_sighash(&self, scope: u32) -> Result<[u8; 32]>
}

pub struct ScriptTemplateUnlock { sign_fn, estimate_length_fn }
impl ScriptTemplateUnlock {
    pub fn sign(&self, context: &SigningContext) -> Result<UnlockingScript>
    pub fn estimate_length(&self) -> usize
}

pub fn compute_sighash_scope(sign_outputs: SignOutputs, anyone_can_pay: bool) -> u32
```

### P2PKH / P2PK / Multisig / RPuzzle / PushDrop

See `templates/CLAUDE.md` for detailed template documentation.

Summary: each template provides `lock()` (via `ScriptTemplate` trait or direct), `unlock()` (returns `ScriptTemplateUnlock`), and `sign_with_sighash()` (direct signing). PushDrop also provides `decode()` and `estimate_unlocking_length()`.

## Transaction Interface Traits

Traits for Transaction module integration (implemented by the `transaction` module):

```rust
pub trait TransactionInputContext   // source_txid, output_index, sequence, satoshis, locking_script, unlocking_script
pub trait TransactionOutputContext  // satoshis, locking_script
pub trait TransactionContext        // version, inputs, outputs, lock_time + default impls
pub trait SpendValidation           // validate_input, validate_all_inputs
pub trait UtxoProvider              // get_utxo(txid, index) -> Option<(u64, LockingScript)>
pub struct SimpleUtxo               // Test implementation of TransactionOutputContext
```

## Implementation Notes

- **Lazy Parsing**: Scripts cache raw bytes; chunks parsed on demand via `ensure_parsed()`
- **Caching**: Raw bytes and hex strings are cached and invalidated on mutation
- **PUSHDATA**: Uses smallest encoding (direct push 0x01-0x4b, PUSHDATA1/2/4)
- **BSV Opcodes**: All BSV re-enabled opcodes supported (CAT, SPLIT, MUL, DIV, MOD, etc.)
- **Disabled before Chronicle only**: OP_2MUL, OP_2DIV (disabled), OP_VER, OP_VERIF, OP_VERNOTIF (BAD_OPCODE when executed, skipped when not), 0xb3–0xb7 (NOPs); for a post-Chronicle UTXO (`UTXO_AFTER_CHRONICLE`, or version > 1 by default) all run per bitcoin-sv v1.2.2 (0.3.27). Undefined opcodes 0xba–0xff executed, a truncated push reached, a second OP_ELSE: failures in every mode.
- **ASM Rendering**: `OP_FALSE`/`OP_0` renders as `"0"`, not `"OP_0"` in `to_asm()`

### Script Interpreter Configuration (spend.rs)

```rust
const MAX_SCRIPT_ELEMENT_SIZE: usize = 1024 * 1024 * 1024;  // 1GB (BSV unlimited)
const DEFAULT_MEMORY_LIMIT: usize = 32_000_000;              // 32MB, the default mode's local budget
const WORD_MEMORY_LIMIT: usize = DEFAULT_STACK_MEMORY_USAGE_POLICY;  // 100MB, under a word
const MAX_MULTISIG_KEY_COUNT: i64 = i32::MAX as i64;
const REQUIRE_MINIMAL_PUSH: bool = true;        // the default mode, version <= 1 only
const REQUIRE_PUSH_ONLY_UNLOCKING: bool = true;  // the default mode, every version
const REQUIRE_LOW_S_SIGNATURES: bool = true;     // the default mode, version <= 1 only
const REQUIRE_CLEAN_STACK: bool = true;          // the default mode, version <= 1 only
// NULLDUMMY: the default mode, version <= 1 only. NULLFAIL, MINIMALIF, DISCOURAGE_UPGRADABLE_NOPS,
// COMPRESSED_PUBKEYTYPE: only under a word (`set_flags`), derived by `ScriptFlags::gates`.
```

## Related Documentation

- `../CLAUDE.md` - Root SDK documentation
- `../primitives/CLAUDE.md` - Primitives module (encoding, hashing)
- `../primitives/bsv/CLAUDE.md` - BSV primitives (sighash, transaction signatures)
- `templates/CLAUDE.md` - Templates module documentation