Skip to main content

bsv_rs/script/
bip276.rs

1//! BIP-276 script encoding for typed bitcoin-related data.
2//!
3//! BIP-276 proposes a scheme for encoding typed bitcoin related data in a user-friendly way.
4//! See <https://github.com/moneybutton/bips/blob/master/bip-0276.mediawiki>
5//!
6//! # Format
7//!
8//! ```text
9//! bitcoin-script:<network_hex><script_type_hex><script_hex><checksum_hex>
10//! ```
11//!
12//! Where:
13//! - `bitcoin-script` is the fixed prefix
14//! - `network_hex` is the network byte as 2-char lowercase hex (e.g., `01` for mainnet)
15//! - `script_type_hex` is the script type byte as 2-char lowercase hex (e.g., `01` for version 1)
16//! - `script_hex` is the script data as lowercase hex
17//! - `checksum_hex` is the first 4 bytes of SHA256d of the payload (everything before the checksum),
18//!   encoded as 8-char lowercase hex
19//!
20//! # Example
21//!
22//! ```rust
23//! use bsv_rs::script::bip276::{encode_bip276, decode_bip276, NETWORK_MAINNET, NETWORK_TESTNET};
24//!
25//! let encoded = encode_bip276(NETWORK_MAINNET, 1, b"fake script");
26//! assert_eq!(encoded, "bitcoin-script:010166616b65207363726970746f0cd86a");
27//!
28//! let (network, script_type, data) = decode_bip276(&encoded).unwrap();
29//! assert_eq!(network, NETWORK_MAINNET);
30//! assert_eq!(script_type, 1);
31//! assert_eq!(data, b"fake script");
32//! ```
33
34use crate::primitives::{from_hex, sha256d, to_hex};
35use crate::{Error, Result};
36
37/// The standard BIP-276 prefix for bitcoin scripts.
38pub const BIP276_PREFIX: &str = "bitcoin-script";
39
40/// Network byte for mainnet.
41pub const NETWORK_MAINNET: u8 = 1;
42
43/// Network byte for testnet.
44pub const NETWORK_TESTNET: u8 = 2;
45
46/// Encode script data in BIP-276 format.
47///
48/// The format is: `bitcoin-script:<network_hex><script_type_hex><script_hex><checksum_hex>`
49///
50/// The checksum is the first 4 bytes of the double-SHA256 hash of the payload
51/// (everything before the checksum), encoded as lowercase hex.
52///
53/// # Arguments
54///
55/// * `network` - Network byte (e.g., `NETWORK_MAINNET` or `NETWORK_TESTNET`)
56/// * `script_type` - Script type byte (e.g., `1` for current version)
57/// * `script` - Raw script bytes to encode
58///
59/// # Returns
60///
61/// The BIP-276 encoded string.
62///
63/// # Example
64///
65/// ```rust
66/// use bsv_rs::script::bip276::{encode_bip276, NETWORK_MAINNET};
67///
68/// let encoded = encode_bip276(NETWORK_MAINNET, 1, b"fake script");
69/// assert_eq!(encoded, "bitcoin-script:010166616b65207363726970746f0cd86a");
70/// ```
71pub fn encode_bip276(network: u8, script_type: u8, script: &[u8]) -> String {
72    let payload = format!(
73        "{}:{:02x}{:02x}{}",
74        BIP276_PREFIX,
75        network,
76        script_type,
77        to_hex(script)
78    );
79    let checksum = sha256d(payload.as_bytes());
80    let checksum_hex = to_hex(&checksum[..4]);
81    format!("{}{}", payload, checksum_hex)
82}
83
84/// Decode a BIP-276 encoded string.
85///
86/// Validates that the string starts with the `bitcoin-script:` prefix and that
87/// the checksum is correct.
88///
89/// # Arguments
90///
91/// * `encoded` - The BIP-276 encoded string
92///
93/// # Returns
94///
95/// A tuple of `(network, script_type, script_bytes)` on success.
96///
97/// # Errors
98///
99/// Returns an error if:
100/// - The string is too short or does not contain the expected prefix
101/// - The hex data is invalid
102/// - The checksum does not match
103///
104/// # Example
105///
106/// ```rust
107/// use bsv_rs::script::bip276::{decode_bip276, NETWORK_MAINNET};
108///
109/// let (network, script_type, data) = decode_bip276(
110///     "bitcoin-script:010166616b65207363726970746f0cd86a"
111/// ).unwrap();
112/// assert_eq!(network, NETWORK_MAINNET);
113/// assert_eq!(script_type, 1);
114/// assert_eq!(data, b"fake script");
115/// ```
116pub fn decode_bip276(encoded: &str) -> Result<(u8, u8, Vec<u8>)> {
117    // Check for the prefix followed by ':'
118    let prefix_with_colon = format!("{}:", BIP276_PREFIX);
119    if !encoded.starts_with(&prefix_with_colon) {
120        return Err(Error::Bip276Error(format!(
121            "invalid prefix: expected '{}'",
122            BIP276_PREFIX
123        )));
124    }
125
126    let after_prefix = &encoded[prefix_with_colon.len()..];
127
128    // We need at least 4 hex chars (network + script_type) + 8 hex chars (checksum) = 12 chars
129    if after_prefix.len() < 12 {
130        return Err(Error::Bip276Error("input too short".to_string()));
131    }
132
133    // Parse network byte (first 2 hex chars after prefix)
134    let network_hex = &after_prefix[..2];
135    let network_bytes = from_hex(network_hex)
136        .map_err(|_| Error::Bip276Error(format!("invalid network hex: '{}'", network_hex)))?;
137    let network = network_bytes[0];
138
139    // Parse script_type byte (next 2 hex chars)
140    let script_type_hex = &after_prefix[2..4];
141    let script_type_bytes = from_hex(script_type_hex).map_err(|_| {
142        Error::Bip276Error(format!("invalid script type hex: '{}'", script_type_hex))
143    })?;
144    let script_type = script_type_bytes[0];
145
146    // The remaining data is script_hex + 8-char checksum
147    let data_and_checksum = &after_prefix[4..];
148    if data_and_checksum.len() < 8 {
149        return Err(Error::Bip276Error(
150            "input too short for checksum".to_string(),
151        ));
152    }
153
154    let script_hex = &data_and_checksum[..data_and_checksum.len() - 8];
155    let provided_checksum = &data_and_checksum[data_and_checksum.len() - 8..];
156
157    // Decode script data
158    let script_data = from_hex(script_hex)
159        .map_err(|_| Error::Bip276Error(format!("invalid script hex: '{}'", script_hex)))?;
160
161    // Compute expected checksum: SHA256d of the payload (everything before the checksum)
162    let payload = &encoded[..encoded.len() - 8];
163    let checksum = sha256d(payload.as_bytes());
164    let expected_checksum = to_hex(&checksum[..4]);
165
166    if provided_checksum != expected_checksum {
167        return Err(Error::Bip276Error("invalid checksum".to_string()));
168    }
169
170    Ok((network, script_type, script_data))
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_encode_mainnet() {
179        let encoded = encode_bip276(NETWORK_MAINNET, 1, b"fake script");
180        assert_eq!(encoded, "bitcoin-script:010166616b65207363726970746f0cd86a");
181    }
182
183    #[test]
184    fn test_encode_testnet() {
185        let encoded = encode_bip276(NETWORK_TESTNET, 1, b"fake script");
186        assert_eq!(encoded, "bitcoin-script:020166616b65207363726970742577a444");
187    }
188
189    #[test]
190    fn test_decode_valid() {
191        let (network, script_type, data) =
192            decode_bip276("bitcoin-script:010166616b65207363726970746f0cd86a").unwrap();
193        assert_eq!(network, NETWORK_MAINNET);
194        assert_eq!(script_type, 1);
195        assert_eq!(data, b"fake script");
196    }
197
198    #[test]
199    fn test_roundtrip() {
200        let original_data = b"hello world script data";
201        let encoded = encode_bip276(NETWORK_MAINNET, 1, original_data);
202        let (network, script_type, data) = decode_bip276(&encoded).unwrap();
203        assert_eq!(network, NETWORK_MAINNET);
204        assert_eq!(script_type, 1);
205        assert_eq!(data, original_data);
206    }
207
208    #[test]
209    fn test_roundtrip_testnet() {
210        let original_data = b"\x76\xa9\x14";
211        let encoded = encode_bip276(NETWORK_TESTNET, 2, original_data);
212        let (network, script_type, data) = decode_bip276(&encoded).unwrap();
213        assert_eq!(network, NETWORK_TESTNET);
214        assert_eq!(script_type, 2);
215        assert_eq!(data, original_data);
216    }
217
218    #[test]
219    fn test_decode_invalid_prefix() {
220        let result = decode_bip276("invalid-prefix:010166616b65207363726970746f0cd86a");
221        assert!(result.is_err());
222        match result {
223            Err(Error::Bip276Error(msg)) => {
224                assert!(msg.contains("invalid prefix"));
225            }
226            _ => panic!("expected Bip276Error"),
227        }
228    }
229
230    #[test]
231    fn test_decode_invalid_checksum() {
232        // Valid format but wrong checksum (last 8 chars changed)
233        let result = decode_bip276("bitcoin-script:010166616b65207363726970746f0cd8");
234        assert!(result.is_err());
235    }
236
237    #[test]
238    fn test_decode_too_short() {
239        let result = decode_bip276("bitcoin-script:01");
240        assert!(result.is_err());
241    }
242
243    #[test]
244    fn test_decode_empty_script() {
245        // Encode empty script and verify roundtrip
246        let encoded = encode_bip276(NETWORK_MAINNET, 1, b"");
247        let (network, script_type, data) = decode_bip276(&encoded).unwrap();
248        assert_eq!(network, NETWORK_MAINNET);
249        assert_eq!(script_type, 1);
250        assert!(data.is_empty());
251    }
252
253    #[test]
254    fn test_roundtrip_various_data() {
255        // Test with binary data
256        let data: Vec<u8> = (0..=255).collect();
257        let encoded = encode_bip276(NETWORK_MAINNET, 1, &data);
258        let (network, script_type, decoded) = decode_bip276(&encoded).unwrap();
259        assert_eq!(network, NETWORK_MAINNET);
260        assert_eq!(script_type, 1);
261        assert_eq!(decoded, data);
262    }
263}