@btc-vision/btc-runtime 1.8.1 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -79
- package/package.json +8 -8
- package/runtime/buffer/BytesReader.ts +36 -45
- package/runtime/buffer/BytesWriter.ts +60 -20
- package/runtime/contracts/OP20.ts +643 -0
- package/runtime/contracts/OP_NET.ts +3 -3
- package/runtime/contracts/interfaces/IOP20.ts +15 -0
- package/runtime/contracts/interfaces/OP20InitParameters.ts +3 -1
- package/runtime/env/BlockchainEnvironment.ts +28 -7
- package/runtime/env/classes/Transaction.ts +1 -2
- package/runtime/env/global.ts +171 -23
- package/runtime/events/NetEvent.ts +1 -1
- package/runtime/events/predefined/{ApproveEvent.ts → ApprovedEvent.ts} +3 -3
- package/runtime/events/predefined/{MintEvent.ts → BurnedEvent.ts} +5 -6
- package/runtime/events/predefined/{TransferEvent.ts → MintedEvent.ts} +5 -6
- package/runtime/events/predefined/TransferredEvent.ts +18 -0
- package/runtime/events/predefined/index.ts +4 -4
- package/runtime/exports/index.ts +4 -4
- package/runtime/index.ts +13 -3
- package/runtime/math/abi.ts +10 -6
- package/runtime/math/bytes.ts +5 -17
- package/runtime/memory/AddressMemoryMap.ts +4 -5
- package/runtime/memory/KeyMerger.ts +7 -7
- package/runtime/memory/MapOfMap.ts +2 -7
- package/runtime/memory/Nested.ts +6 -8
- package/runtime/nested/PointerManager.ts +1 -1
- package/runtime/nested/codecs/AddressCodec.ts +1 -1
- package/runtime/nested/codecs/BooleanCodec.ts +1 -1
- package/runtime/nested/codecs/Ids.ts +1 -1
- package/runtime/nested/codecs/NumericCodec.ts +1 -1
- package/runtime/nested/codecs/StringCodec.ts +1 -1
- package/runtime/nested/codecs/VariableBytesCodec.ts +3 -3
- package/runtime/nested/storage/StorageMap.ts +3 -3
- package/runtime/nested/storage/StorageSet.ts +2 -2
- package/runtime/plugins/Plugin.ts +5 -7
- package/runtime/script/Bech32.ts +369 -0
- package/runtime/script/BitcoinAddresses.ts +208 -0
- package/runtime/script/BitcoinCodec.ts +395 -0
- package/runtime/script/Networks.ts +94 -0
- package/runtime/script/Opcodes.ts +155 -0
- package/runtime/script/Script.ts +463 -0
- package/runtime/script/ScriptUtils.ts +101 -0
- package/runtime/script/Segwit.ts +185 -0
- package/runtime/script/reader/ScriptReader.ts +247 -0
- package/runtime/secp256k1/ECPoint.ts +6 -12
- package/runtime/shared-libraries/TransferHelper.ts +72 -31
- package/runtime/storage/AdvancedStoredString.ts +1 -1
- package/runtime/storage/StoredAddress.ts +1 -1
- package/runtime/storage/StoredBoolean.ts +2 -4
- package/runtime/storage/StoredString.ts +6 -3
- package/runtime/storage/StoredU256.ts +1 -3
- package/runtime/storage/StoredU32.ts +1 -6
- package/runtime/storage/StoredU64.ts +1 -4
- package/runtime/storage/arrays/StoredBooleanArray.ts +7 -12
- package/runtime/storage/arrays/StoredPackedArray.ts +2 -7
- package/runtime/storage/maps/StoredMapU256.ts +5 -5
- package/runtime/types/Address.ts +49 -39
- package/runtime/types/SafeMath.ts +19 -18
- package/runtime/types/SafeMathI128.ts +14 -13
- package/runtime/utils/hex.ts +41 -19
- package/runtime/utils/index.ts +0 -2
- package/runtime/contracts/DeployableOP_20.ts +0 -415
- package/runtime/contracts/OP_20.ts +0 -9
- package/runtime/contracts/interfaces/IOP_20.ts +0 -19
- package/runtime/events/predefined/BurnEvent.ts +0 -14
- package/runtime/utils/b32.ts +0 -243
- package/runtime/utils/box.ts +0 -134
- package/runtime/utils/encodings.ts +0 -45
- /package/{LICENSE → LICENSE.md} +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { CsvP2wshResult, MultisigP2wshResult } from './ScriptUtils';
|
|
2
|
+
import { Segwit } from './Segwit';
|
|
3
|
+
import { BitcoinScript } from './Script';
|
|
4
|
+
import { sha256 } from '../env/global';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Ct provides constant-time comparison functions
|
|
8
|
+
* This is important for cryptographic operations to avoid timing attacks
|
|
9
|
+
*/
|
|
10
|
+
@final
|
|
11
|
+
export class Ct {
|
|
12
|
+
/**
|
|
13
|
+
* Compare two 32-byte arrays in constant time
|
|
14
|
+
* Returns true if they are equal, false otherwise
|
|
15
|
+
*
|
|
16
|
+
* The function always takes the same amount of time regardless of where
|
|
17
|
+
* the arrays differ, which prevents timing-based attacks
|
|
18
|
+
*/
|
|
19
|
+
@inline public static eq32(a: Uint8Array, b: Uint8Array): bool {
|
|
20
|
+
// XOR the lengths with 32 to detect length mismatches
|
|
21
|
+
let d: i32 = (a.length ^ 32) | (b.length ^ 32);
|
|
22
|
+
|
|
23
|
+
// Compare each byte, accumulating differences in d
|
|
24
|
+
for (let i = 0; i < 32; i++) {
|
|
25
|
+
// Handle arrays shorter than 32 bytes by treating missing bytes as 0
|
|
26
|
+
const ai: u8 = i < a.length ? a[i] : 0;
|
|
27
|
+
const bi: u8 = i < b.length ? b[i] : 0;
|
|
28
|
+
// XOR accumulates any differences without early exit
|
|
29
|
+
d |= ai ^ bi;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// d will be 0 only if all bytes matched
|
|
33
|
+
return d == 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* BitcoinAddresses provides high-level functions for creating and verifying
|
|
39
|
+
* various types of Bitcoin addresses, particularly those using witness scripts
|
|
40
|
+
*/
|
|
41
|
+
@final
|
|
42
|
+
export class BitcoinAddresses {
|
|
43
|
+
/**
|
|
44
|
+
* Create a witness script for a CSV (CheckSequenceVerify) timelock
|
|
45
|
+
* This script requires a certain number of blocks to pass before spending
|
|
46
|
+
*
|
|
47
|
+
* @param pubkey - The public key that can spend after the timelock
|
|
48
|
+
* @param csvBlocks - Number of blocks to wait (must be 0-65535)
|
|
49
|
+
* @returns The witness script bytes
|
|
50
|
+
*/
|
|
51
|
+
public static csvWitnessScript(pubkey: Uint8Array, csvBlocks: i32): Uint8Array {
|
|
52
|
+
return BitcoinScript.csvTimelock(pubkey, csvBlocks);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a P2WSH (Pay to Witness Script Hash) address with CSV timelock
|
|
57
|
+
* This creates both the address and the witness script needed to spend it
|
|
58
|
+
*
|
|
59
|
+
* @param pubkey - The public key that can spend after the timelock
|
|
60
|
+
* @param csvBlocks - Number of blocks to wait
|
|
61
|
+
* @param hrp - Human-readable part for the address (e.g., "bc" for mainnet)
|
|
62
|
+
* @returns Object containing both the address and witness script
|
|
63
|
+
*/
|
|
64
|
+
public static csvP2wshAddress(pubkey: Uint8Array, csvBlocks: i32, hrp: string): CsvP2wshResult {
|
|
65
|
+
const ws = BitcoinAddresses.csvWitnessScript(pubkey, csvBlocks);
|
|
66
|
+
const addr = Segwit.p2wsh(hrp, ws);
|
|
67
|
+
return new CsvP2wshResult(addr, ws);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Verify that a given address corresponds to a specific CSV timelock setup
|
|
72
|
+
* This is useful for validating that an address was created with expected parameters
|
|
73
|
+
*
|
|
74
|
+
* @param pubkey - The expected public key
|
|
75
|
+
* @param csvBlocks - The expected number of CSV blocks
|
|
76
|
+
* @param address - The address to verify
|
|
77
|
+
* @param hrp - Expected human-readable part
|
|
78
|
+
* @param strictMinimal - Whether to enforce strict minimal encoding rules
|
|
79
|
+
* @returns true if the address matches the expected parameters
|
|
80
|
+
*/
|
|
81
|
+
public static verifyCsvP2wshAddress(
|
|
82
|
+
pubkey: Uint8Array,
|
|
83
|
+
csvBlocks: i32,
|
|
84
|
+
address: string,
|
|
85
|
+
hrp: string,
|
|
86
|
+
strictMinimal: bool = true,
|
|
87
|
+
): bool {
|
|
88
|
+
// Try to decode the address - this replaces the try-catch block
|
|
89
|
+
const dec = Segwit.decodeOrNull(address);
|
|
90
|
+
if (!dec) return false;
|
|
91
|
+
|
|
92
|
+
// Verify it's a v0 witness program with 32-byte hash
|
|
93
|
+
if (dec.version != 0 || dec.hrp != hrp || dec.program.length != 32) return false;
|
|
94
|
+
|
|
95
|
+
// Reconstruct the witness script and verify it matches
|
|
96
|
+
const ws = BitcoinAddresses.csvWitnessScript(pubkey, csvBlocks);
|
|
97
|
+
|
|
98
|
+
// Parse the witness script to ensure it's well-formed
|
|
99
|
+
const rec = BitcoinScript.recognizeCsvTimelock(ws, strictMinimal);
|
|
100
|
+
if (!rec.ok || rec.csvBlocks != csvBlocks) return false;
|
|
101
|
+
|
|
102
|
+
// Compute the script hash and compare with the address program
|
|
103
|
+
const prog = sha256(ws);
|
|
104
|
+
return Ct.eq32(dec.program, prog);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a witness script for a multisig setup
|
|
109
|
+
* This creates a script requiring m-of-n signatures to spend
|
|
110
|
+
*
|
|
111
|
+
* @param m - Number of required signatures
|
|
112
|
+
* @param pubkeys - Array of public keys (n total)
|
|
113
|
+
* @returns The witness script bytes
|
|
114
|
+
*/
|
|
115
|
+
public static multisigWitnessScript(m: i32, pubkeys: Array<Uint8Array>): Uint8Array {
|
|
116
|
+
return BitcoinScript.multisig(m, pubkeys);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create a P2WSH multisig address
|
|
121
|
+
* This creates both the address and the witness script for a multisig setup
|
|
122
|
+
*
|
|
123
|
+
* @param m - Number of required signatures
|
|
124
|
+
* @param pubkeys - Array of public keys
|
|
125
|
+
* @param hrp - Human-readable part for the address
|
|
126
|
+
* @returns Object containing both the address and witness script
|
|
127
|
+
*/
|
|
128
|
+
public static multisigP2wshAddress(
|
|
129
|
+
m: i32,
|
|
130
|
+
pubkeys: Array<Uint8Array>,
|
|
131
|
+
hrp: string,
|
|
132
|
+
): MultisigP2wshResult {
|
|
133
|
+
const ws = BitcoinAddresses.multisigWitnessScript(m, pubkeys);
|
|
134
|
+
const addr = Segwit.p2wsh(hrp, ws);
|
|
135
|
+
return new MultisigP2wshResult(addr, ws);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Verify that a given address corresponds to a specific multisig setup
|
|
140
|
+
* This validates that an address was created with the expected m-of-n parameters
|
|
141
|
+
*
|
|
142
|
+
* @param m - Expected number of required signatures
|
|
143
|
+
* @param pubkeys - Expected array of public keys
|
|
144
|
+
* @param address - The address to verify
|
|
145
|
+
* @param hrp - Expected human-readable part
|
|
146
|
+
* @returns true if the address matches the expected parameters
|
|
147
|
+
*/
|
|
148
|
+
public static verifyMultisigP2wshAddress(
|
|
149
|
+
m: i32,
|
|
150
|
+
pubkeys: Array<Uint8Array>,
|
|
151
|
+
address: string,
|
|
152
|
+
hrp: string,
|
|
153
|
+
): bool {
|
|
154
|
+
// Decode the address safely
|
|
155
|
+
const dec = Segwit.decodeOrNull(address);
|
|
156
|
+
if (!dec) return false;
|
|
157
|
+
|
|
158
|
+
// Verify it's a v0 witness program with 32-byte hash
|
|
159
|
+
if (dec.version != 0 || dec.hrp != hrp || dec.program.length != 32) return false;
|
|
160
|
+
|
|
161
|
+
// Reconstruct the witness script and compare
|
|
162
|
+
const ws = BitcoinAddresses.multisigWitnessScript(m, pubkeys);
|
|
163
|
+
const prog = sha256(ws);
|
|
164
|
+
return Ct.eq32(dec.program, prog);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Create a Taproot (P2TR) key-path spend address
|
|
169
|
+
* This is the simplest form of Taproot address, spendable with a single key
|
|
170
|
+
*
|
|
171
|
+
* @param outputKeyX32 - The 32-byte X coordinate of the output key
|
|
172
|
+
* @param hrp - Human-readable part for the address
|
|
173
|
+
* @returns The Bech32m-encoded address
|
|
174
|
+
*/
|
|
175
|
+
public static p2trKeyPathAddress(outputKeyX32: Uint8Array, hrp: string): string {
|
|
176
|
+
return Segwit.p2tr(hrp, outputKeyX32);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create a Pay-to-Witness-Public-Key-Hash (P2WPKH) address
|
|
181
|
+
* @param pubkey - The public key (33 bytes compressed or 65 bytes uncompressed)
|
|
182
|
+
* @param hrp - Human-readable part (e.g., "bc" for mainnet)
|
|
183
|
+
* @returns The Bech32-encoded address
|
|
184
|
+
*/
|
|
185
|
+
public static p2wpkh(pubkey: Uint8Array, hrp: string): string {
|
|
186
|
+
return Segwit.p2wpkh(hrp, pubkey);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Verify that a given address corresponds to a specific Taproot output key
|
|
191
|
+
*
|
|
192
|
+
* @param outputKeyX32 - The expected 32-byte X coordinate
|
|
193
|
+
* @param address - The address to verify
|
|
194
|
+
* @param hrp - Expected human-readable part
|
|
195
|
+
* @returns true if the address matches the expected output key
|
|
196
|
+
*/
|
|
197
|
+
public static verifyP2trAddress(outputKeyX32: Uint8Array, address: string, hrp: string): bool {
|
|
198
|
+
// Decode the address safely
|
|
199
|
+
const dec = Segwit.decodeOrNull(address);
|
|
200
|
+
if (!dec) return false;
|
|
201
|
+
|
|
202
|
+
// Verify it's a v1 witness program with 32-byte key
|
|
203
|
+
if (dec.version != 1 || dec.hrp != hrp || dec.program.length != 32) return false;
|
|
204
|
+
|
|
205
|
+
// Compare the output key
|
|
206
|
+
return Ct.eq32(dec.program, outputKeyX32);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { BitcoinAddresses, Ct } from './BitcoinAddresses';
|
|
2
|
+
import { BytesWriter } from '../buffer/BytesWriter';
|
|
3
|
+
import { BytesReader } from '../buffer/BytesReader';
|
|
4
|
+
import { CsvPairCrossCheck, MultisigPairCrossCheck } from './ScriptUtils';
|
|
5
|
+
import { Segwit } from './Segwit';
|
|
6
|
+
import { sha256 } from '../env/global';
|
|
7
|
+
import { BitcoinScript } from './Script';
|
|
8
|
+
import { Revert } from '../types/Revert';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Result type for codec operations that can fail
|
|
12
|
+
* This provides detailed error information when operations fail
|
|
13
|
+
*/
|
|
14
|
+
@final
|
|
15
|
+
export class CodecResult<T> {
|
|
16
|
+
public readonly success: bool;
|
|
17
|
+
public readonly value: T | null;
|
|
18
|
+
public readonly error: string | null;
|
|
19
|
+
|
|
20
|
+
public constructor(success: bool, value: T | null, error: string | null) {
|
|
21
|
+
this.success = success;
|
|
22
|
+
this.value = value;
|
|
23
|
+
this.error = error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public static ok<T>(value: T): CodecResult<T> {
|
|
27
|
+
return new CodecResult<T>(true, value, null);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public static err<T>(error: string): CodecResult<T> {
|
|
31
|
+
return new CodecResult<T>(false, null, error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Represents a verified address read from a byte stream
|
|
37
|
+
*/
|
|
38
|
+
@final
|
|
39
|
+
export class VerifiedAddress {
|
|
40
|
+
public readonly address: string;
|
|
41
|
+
public readonly witnessScript: Uint8Array | null;
|
|
42
|
+
|
|
43
|
+
constructor(address: string, witnessScript: Uint8Array | null = null) {
|
|
44
|
+
this.address = address;
|
|
45
|
+
this.witnessScript = witnessScript;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* BitcoinCodec provides serialization and deserialization functions for
|
|
51
|
+
* various Bitcoin address types and witness scripts. All methods use
|
|
52
|
+
* explicit error handling suitable for AssemblyScript/WebAssembly.
|
|
53
|
+
*/
|
|
54
|
+
@final
|
|
55
|
+
export class BitcoinCodec {
|
|
56
|
+
/**
|
|
57
|
+
* Write a CSV P2WSH address and witness script to a byte stream
|
|
58
|
+
*
|
|
59
|
+
* @param out - The output writer to write to
|
|
60
|
+
* @param pubkey - The public key for the CSV timelock
|
|
61
|
+
* @param csvBlocks - Number of blocks for the timelock
|
|
62
|
+
* @param hrp - Human-readable part for the address
|
|
63
|
+
*/
|
|
64
|
+
public static writeCsvP2wsh(
|
|
65
|
+
out: BytesWriter,
|
|
66
|
+
pubkey: Uint8Array,
|
|
67
|
+
csvBlocks: i32,
|
|
68
|
+
hrp: string,
|
|
69
|
+
): void {
|
|
70
|
+
const res = BitcoinAddresses.csvP2wshAddress(pubkey, csvBlocks, hrp);
|
|
71
|
+
out.writeStringWithLength(res.address);
|
|
72
|
+
out.writeBytesWithLength(res.witnessScript);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read a CSV P2WSH address and verify it matches expected parameters
|
|
77
|
+
*
|
|
78
|
+
* @param inp - The input reader to read from
|
|
79
|
+
* @param pubkey - The expected public key
|
|
80
|
+
* @param csvBlocks - The expected CSV blocks
|
|
81
|
+
* @param hrp - The expected human-readable part
|
|
82
|
+
* @param strictMinimal - Whether to enforce strict minimal encoding
|
|
83
|
+
* @returns A result containing the verified address or an error
|
|
84
|
+
*/
|
|
85
|
+
public static readAndVerifyCsvP2wsh(
|
|
86
|
+
inp: BytesReader,
|
|
87
|
+
pubkey: Uint8Array,
|
|
88
|
+
csvBlocks: i32,
|
|
89
|
+
hrp: string,
|
|
90
|
+
strictMinimal: bool = true,
|
|
91
|
+
): CodecResult<VerifiedAddress> {
|
|
92
|
+
const addr = inp.readStringWithLength();
|
|
93
|
+
|
|
94
|
+
if (BitcoinAddresses.verifyCsvP2wshAddress(pubkey, csvBlocks, addr, hrp, strictMinimal)) {
|
|
95
|
+
const ws = BitcoinAddresses.csvWitnessScript(pubkey, csvBlocks);
|
|
96
|
+
return CodecResult.ok<VerifiedAddress>(new VerifiedAddress(addr, ws));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return CodecResult.err<VerifiedAddress>(
|
|
100
|
+
'CSV P2WSH verification failed: address does not match expected parameters',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Write a multisig P2WSH address and witness script to a byte stream
|
|
106
|
+
*
|
|
107
|
+
* @param out - The output writer
|
|
108
|
+
* @param m - Number of required signatures
|
|
109
|
+
* @param pubkeys - Array of public keys
|
|
110
|
+
* @param hrp - Human-readable part for the address
|
|
111
|
+
*/
|
|
112
|
+
public static writeMultisigP2wsh(
|
|
113
|
+
out: BytesWriter,
|
|
114
|
+
m: i32,
|
|
115
|
+
pubkeys: Array<Uint8Array>,
|
|
116
|
+
hrp: string,
|
|
117
|
+
): void {
|
|
118
|
+
const res = BitcoinAddresses.multisigP2wshAddress(m, pubkeys, hrp);
|
|
119
|
+
out.writeStringWithLength(res.address);
|
|
120
|
+
out.writeBytesWithLength(res.witnessScript);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Read and verify a multisig P2WSH address
|
|
125
|
+
*
|
|
126
|
+
* @param inp - The input reader
|
|
127
|
+
* @param m - Expected number of required signatures
|
|
128
|
+
* @param pubkeys - Expected array of public keys
|
|
129
|
+
* @param hrp - Expected human-readable part
|
|
130
|
+
* @returns A result containing the verified address or an error
|
|
131
|
+
*/
|
|
132
|
+
public static readAndVerifyMultisigP2wsh(
|
|
133
|
+
inp: BytesReader,
|
|
134
|
+
m: i32,
|
|
135
|
+
pubkeys: Array<Uint8Array>,
|
|
136
|
+
hrp: string,
|
|
137
|
+
): CodecResult<VerifiedAddress> {
|
|
138
|
+
const addr = inp.readStringWithLength();
|
|
139
|
+
|
|
140
|
+
if (BitcoinAddresses.verifyMultisigP2wshAddress(m, pubkeys, addr, hrp)) {
|
|
141
|
+
const ws = BitcoinAddresses.multisigWitnessScript(m, pubkeys);
|
|
142
|
+
return CodecResult.ok<VerifiedAddress>(new VerifiedAddress(addr, ws));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return CodecResult.err<VerifiedAddress>(
|
|
146
|
+
'Multisig P2WSH verification failed: address does not match expected parameters',
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Write a Taproot address to a byte stream
|
|
152
|
+
*
|
|
153
|
+
* @param out - The output writer
|
|
154
|
+
* @param outputKeyX32 - The 32-byte X coordinate of the output key
|
|
155
|
+
* @param hrp - Human-readable part for the address
|
|
156
|
+
*/
|
|
157
|
+
public static writeP2tr(out: BytesWriter, outputKeyX32: Uint8Array, hrp: string): void {
|
|
158
|
+
const addr = BitcoinAddresses.p2trKeyPathAddress(outputKeyX32, hrp);
|
|
159
|
+
out.writeStringWithLength(addr);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Read and verify a Taproot address
|
|
164
|
+
*
|
|
165
|
+
* @param inp - The input reader
|
|
166
|
+
* @param outputKeyX32 - Expected 32-byte X coordinate
|
|
167
|
+
* @param hrp - Expected human-readable part
|
|
168
|
+
* @returns A result containing the verified address or an error
|
|
169
|
+
*/
|
|
170
|
+
public static readAndVerifyP2tr(
|
|
171
|
+
inp: BytesReader,
|
|
172
|
+
outputKeyX32: Uint8Array,
|
|
173
|
+
hrp: string,
|
|
174
|
+
): CodecResult<VerifiedAddress> {
|
|
175
|
+
const addr = inp.readStringWithLength();
|
|
176
|
+
|
|
177
|
+
if (BitcoinAddresses.verifyP2trAddress(outputKeyX32, addr, hrp)) {
|
|
178
|
+
return CodecResult.ok<VerifiedAddress>(new VerifiedAddress(addr));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return CodecResult.err<VerifiedAddress>(
|
|
182
|
+
'P2TR verification failed: address does not match expected output key',
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read a P2WSH address/witness script pair and verify they match
|
|
188
|
+
*
|
|
189
|
+
* @param inp - The input reader
|
|
190
|
+
* @param hrp - Expected human-readable part
|
|
191
|
+
* @returns A result containing verification details
|
|
192
|
+
*/
|
|
193
|
+
public static readP2wshPairAndVerify(
|
|
194
|
+
inp: BytesReader,
|
|
195
|
+
hrp: string,
|
|
196
|
+
): CodecResult<VerifiedAddress> {
|
|
197
|
+
const address = inp.readStringWithLength();
|
|
198
|
+
const witnessScript = inp.readBytesWithLength();
|
|
199
|
+
|
|
200
|
+
// Decode the address
|
|
201
|
+
const dec = Segwit.decodeOrNull(address);
|
|
202
|
+
if (!dec) {
|
|
203
|
+
return CodecResult.err<VerifiedAddress>('Failed to decode address');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Verify it's a valid P2WSH address
|
|
207
|
+
if (dec.version != 0) {
|
|
208
|
+
return CodecResult.err<VerifiedAddress>('Invalid witness version: expected v0');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (dec.hrp != hrp) {
|
|
212
|
+
return CodecResult.err<VerifiedAddress>(
|
|
213
|
+
`HRP mismatch: expected ${hrp}, got ${dec.hrp}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (dec.program.length != 32) {
|
|
218
|
+
return CodecResult.err<VerifiedAddress>(
|
|
219
|
+
'Invalid program length: P2WSH must be 32 bytes',
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Verify the witness script hashes to the witness program
|
|
224
|
+
const prog = sha256(witnessScript);
|
|
225
|
+
if (!Ct.eq32(dec.program, prog)) {
|
|
226
|
+
return CodecResult.err<VerifiedAddress>('Witness script hash mismatch');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return CodecResult.ok<VerifiedAddress>(new VerifiedAddress(address, witnessScript));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Read a CSV P2WSH pair and cross-check all components
|
|
234
|
+
*
|
|
235
|
+
* @param inp - The input reader
|
|
236
|
+
* @param hrp - Expected human-readable part
|
|
237
|
+
* @param strictMinimal - Whether to enforce strict minimal encoding
|
|
238
|
+
* @returns Detailed cross-check results including extracted parameters
|
|
239
|
+
*/
|
|
240
|
+
public static readCsvP2wshPairAndCrossCheck(
|
|
241
|
+
inp: BytesReader,
|
|
242
|
+
hrp: string,
|
|
243
|
+
strictMinimal: bool = true,
|
|
244
|
+
): CsvPairCrossCheck {
|
|
245
|
+
const address = inp.readStringWithLength();
|
|
246
|
+
const witnessScript = inp.readBytesWithLength();
|
|
247
|
+
|
|
248
|
+
// Decode the address
|
|
249
|
+
const dec = Segwit.decodeOrNull(address);
|
|
250
|
+
if (!dec) {
|
|
251
|
+
return new CsvPairCrossCheck(false, address, witnessScript, -1, null);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Verify it's a valid P2WSH address
|
|
255
|
+
if (dec.version != 0 || dec.hrp != hrp || dec.program.length != 32) {
|
|
256
|
+
return new CsvPairCrossCheck(false, address, witnessScript, -1, null);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Verify the witness script hashes to the witness program
|
|
260
|
+
const prog = sha256(witnessScript);
|
|
261
|
+
if (!Ct.eq32(dec.program, prog)) {
|
|
262
|
+
return new CsvPairCrossCheck(false, address, witnessScript, -1, null);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Parse the witness script to extract CSV parameters
|
|
266
|
+
const rec = BitcoinScript.recognizeCsvTimelock(witnessScript, strictMinimal);
|
|
267
|
+
if (!rec.ok) {
|
|
268
|
+
return new CsvPairCrossCheck(false, address, witnessScript, -1, null);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Everything checks out
|
|
272
|
+
return new CsvPairCrossCheck(true, address, witnessScript, rec.csvBlocks, rec.pubkey);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Read a multisig P2WSH pair and cross-check all components
|
|
277
|
+
*
|
|
278
|
+
* @param inp - The input reader
|
|
279
|
+
* @param hrp - Expected human-readable part
|
|
280
|
+
* @param expectedM - Optional: verify the m value matches
|
|
281
|
+
* @param expectedPubkeys - Optional: verify the public keys match
|
|
282
|
+
* @param strictMinimal - Whether to enforce strict minimal encoding
|
|
283
|
+
* @returns Detailed cross-check results
|
|
284
|
+
*/
|
|
285
|
+
public static readMultisigP2wshPairAndCrossCheck(
|
|
286
|
+
inp: BytesReader,
|
|
287
|
+
hrp: string,
|
|
288
|
+
expectedM: i32 = -1,
|
|
289
|
+
expectedPubkeys: Array<Uint8Array> | null = null,
|
|
290
|
+
strictMinimal: bool = true,
|
|
291
|
+
): MultisigPairCrossCheck {
|
|
292
|
+
const address = inp.readStringWithLength();
|
|
293
|
+
const witnessScript = inp.readBytesWithLength();
|
|
294
|
+
|
|
295
|
+
// Decode the address
|
|
296
|
+
const dec = Segwit.decodeOrNull(address);
|
|
297
|
+
if (!dec) {
|
|
298
|
+
return new MultisigPairCrossCheck(false, 0, 0, address);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Verify it's a valid P2WSH address
|
|
302
|
+
if (dec.version != 0 || dec.hrp != hrp || dec.program.length != 32) {
|
|
303
|
+
return new MultisigPairCrossCheck(false, 0, 0, address);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Verify the witness script hashes to the witness program
|
|
307
|
+
const prog = sha256(witnessScript);
|
|
308
|
+
if (!Ct.eq32(dec.program, prog)) {
|
|
309
|
+
return new MultisigPairCrossCheck(false, 0, 0, address);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Parse the witness script to extract multisig parameters
|
|
313
|
+
const rec = BitcoinScript.recognizeMultisig(witnessScript, strictMinimal);
|
|
314
|
+
if (!rec.ok) {
|
|
315
|
+
return new MultisigPairCrossCheck(false, 0, 0, address);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check if m matches expected value (if provided)
|
|
319
|
+
if (expectedM >= 0 && rec.m != expectedM) {
|
|
320
|
+
return new MultisigPairCrossCheck(false, rec.m, rec.n, address);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Check if public keys match expected values (if provided)
|
|
324
|
+
if (expectedPubkeys !== null) {
|
|
325
|
+
if (!rec.pubkeys) throw new Revert('Public keys not found in multisig script');
|
|
326
|
+
|
|
327
|
+
if (!BitcoinCodec.comparePublicKeyArrays(rec.pubkeys, expectedPubkeys)) {
|
|
328
|
+
return new MultisigPairCrossCheck(false, rec.m, rec.n, address);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// All checks passed
|
|
333
|
+
return new MultisigPairCrossCheck(true, rec.m, rec.n, address);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Write a generic witness script and its address
|
|
338
|
+
*
|
|
339
|
+
* @param out - The output writer
|
|
340
|
+
* @param witnessScript - The witness script bytes
|
|
341
|
+
* @param hrp - Human-readable part for the address
|
|
342
|
+
*/
|
|
343
|
+
public static writeWitnessScriptAndAddress(
|
|
344
|
+
out: BytesWriter,
|
|
345
|
+
witnessScript: Uint8Array,
|
|
346
|
+
hrp: string,
|
|
347
|
+
): void {
|
|
348
|
+
const address = Segwit.p2wsh(hrp, witnessScript);
|
|
349
|
+
out.writeStringWithLength(address);
|
|
350
|
+
out.writeBytesWithLength(witnessScript);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Validate that a witness script is within size limits
|
|
355
|
+
* Bitcoin consensus rules limit witness scripts to 10,000 bytes
|
|
356
|
+
*
|
|
357
|
+
* @param witnessScript - The script to validate
|
|
358
|
+
* @returns true if the script is within limits
|
|
359
|
+
*/
|
|
360
|
+
public static isValidWitnessScriptSize(witnessScript: Uint8Array): bool {
|
|
361
|
+
return witnessScript.length <= 10000;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Compare two arrays of public keys for equality
|
|
366
|
+
* Uses constant-time comparison for each key
|
|
367
|
+
*
|
|
368
|
+
* @param a - First array of public keys
|
|
369
|
+
* @param b - Second array of public keys
|
|
370
|
+
* @returns true if arrays are identical
|
|
371
|
+
*/
|
|
372
|
+
private static comparePublicKeyArrays(a: Array<Uint8Array>, b: Array<Uint8Array>): bool {
|
|
373
|
+
// Check array lengths first
|
|
374
|
+
if (a.length != b.length) return false;
|
|
375
|
+
|
|
376
|
+
// Compare each key
|
|
377
|
+
for (let i = 0; i < a.length; i++) {
|
|
378
|
+
const keyA = a[i];
|
|
379
|
+
const keyB = b[i];
|
|
380
|
+
|
|
381
|
+
// Check key lengths
|
|
382
|
+
if (keyA.length != keyB.length) return false;
|
|
383
|
+
|
|
384
|
+
// Constant-time byte comparison
|
|
385
|
+
let diff = 0;
|
|
386
|
+
for (let j = 0; j < keyA.length; j++) {
|
|
387
|
+
diff |= keyA[j] ^ keyB[j];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (diff != 0) return false;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Revert } from '../types/Revert';
|
|
2
|
+
|
|
3
|
+
export enum Networks {
|
|
4
|
+
Unknown = -1,
|
|
5
|
+
Mainnet = 0,
|
|
6
|
+
Testnet = 1,
|
|
7
|
+
Regtest = 2,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@final
|
|
11
|
+
export class NetworkManager {
|
|
12
|
+
private readonly mainnet: Uint8Array;
|
|
13
|
+
private readonly testnet: Uint8Array;
|
|
14
|
+
private readonly regtest: Uint8Array;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
const mainnet = new Uint8Array(32);
|
|
18
|
+
mainnet.set([
|
|
19
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0xd6, 0x68, 0x9c, 0x08, 0x5a, 0xe1, 0x65, 0x83,
|
|
20
|
+
0x1e, 0x93, 0x4f, 0xf7, 0x63, 0xae, 0x46, 0xa2, 0xa6, 0xc1, 0x72, 0xb3, 0xf1, 0xb6,
|
|
21
|
+
0x0a, 0x8c, 0xe2, 0x6f,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const testnet = new Uint8Array(32);
|
|
25
|
+
testnet.set([
|
|
26
|
+
0x00, 0x00, 0x00, 0x00, 0x09, 0x33, 0xea, 0x01, 0xad, 0x0e, 0xe9, 0x84, 0x20, 0x97,
|
|
27
|
+
0x79, 0xba, 0xae, 0xc3, 0xce, 0xd9, 0x0f, 0xa3, 0xf4, 0x08, 0x71, 0x95, 0x26, 0xf8,
|
|
28
|
+
0xd7, 0x7f, 0x49, 0x43,
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const regtest = new Uint8Array(32);
|
|
32
|
+
regtest.set([
|
|
33
|
+
0x0f, 0x91, 0x88, 0xf1, 0x3c, 0xb7, 0xb2, 0xc7, 0x1f, 0x2a, 0x33, 0x5e, 0x3a, 0x4f,
|
|
34
|
+
0xc3, 0x28, 0xbf, 0x5b, 0xeb, 0x43, 0x60, 0x12, 0xaf, 0xca, 0x59, 0x0b, 0x1a, 0x11,
|
|
35
|
+
0x46, 0x6e, 0x22, 0x06,
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
this.mainnet = mainnet;
|
|
39
|
+
this.testnet = testnet;
|
|
40
|
+
this.regtest = regtest;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public hrp(n: Networks): string {
|
|
44
|
+
switch (n) {
|
|
45
|
+
case Networks.Mainnet:
|
|
46
|
+
return 'bc';
|
|
47
|
+
case Networks.Testnet:
|
|
48
|
+
return 'tb';
|
|
49
|
+
case Networks.Regtest:
|
|
50
|
+
return 'bcrt';
|
|
51
|
+
default:
|
|
52
|
+
throw new Revert('Unknown network');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public getChainId(network: Networks): Uint8Array {
|
|
57
|
+
const out = new Uint8Array(32);
|
|
58
|
+
switch (network) {
|
|
59
|
+
case Networks.Mainnet:
|
|
60
|
+
out.set(this.mainnet);
|
|
61
|
+
return out;
|
|
62
|
+
case Networks.Testnet:
|
|
63
|
+
out.set(this.testnet);
|
|
64
|
+
return out;
|
|
65
|
+
case Networks.Regtest:
|
|
66
|
+
out.set(this.regtest);
|
|
67
|
+
return out;
|
|
68
|
+
default:
|
|
69
|
+
throw new Revert('Unknown network');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public fromChainId(chainId: Uint8Array): Networks {
|
|
74
|
+
if (chainId.length !== 32) {
|
|
75
|
+
throw new Revert('Invalid chain id length');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (this.equals(chainId, this.mainnet)) return Networks.Mainnet;
|
|
79
|
+
if (this.equals(chainId, this.testnet)) return Networks.Testnet;
|
|
80
|
+
if (this.equals(chainId, this.regtest)) return Networks.Regtest;
|
|
81
|
+
|
|
82
|
+
throw new Revert('Unknown chain id');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private equals(a: Uint8Array, b: Uint8Array): boolean {
|
|
86
|
+
if (a.length !== b.length) return false;
|
|
87
|
+
for (let i = 0; i < a.length; i++) {
|
|
88
|
+
if (a[i] !== b[i]) return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const Network: NetworkManager = new NetworkManager();
|