Skip to main content

outscript/
script.rs

1//! The [`Script`] engine: holds a public key and generates output scripts for
2//! various named formats, caching results.
3
4use std::cell::RefCell;
5use std::collections::HashMap;
6
7use crate::hash::HashFn;
8use crate::insertable::{Format, b, ihash, ihash160, lookup, push, ttweak};
9use crate::out::Out;
10use crate::pubkey::PubKey;
11
12/// Returns the format definition for a name, mirroring the Go `Formats` table.
13pub fn format_def(name: &str) -> Option<Format> {
14    let f = match name {
15        "p2pkh" => vec![
16            b(&[0x76, 0xa9]),
17            push(ihash160(lookup("pubkey:comp"))),
18            b(&[0x88, 0xac]),
19        ],
20        "p2pukh" => vec![
21            b(&[0x76, 0xa9]),
22            push(ihash160(lookup("pubkey:uncomp"))),
23            b(&[0x88, 0xac]),
24        ],
25        "p2pk" => vec![push(lookup("pubkey:comp")), b(&[0xac])],
26        "p2puk" => vec![push(lookup("pubkey:uncomp")), b(&[0xac])],
27        "p2wpkh" => vec![b(&[0x00]), push(ihash160(lookup("pubkey:comp")))],
28        "p2tr" => vec![b(&[0x51]), push(ttweak(lookup("pubkey:comp")))],
29        "p2sh:p2pkh" => vec![b(&[0xa9]), push(ihash160(lookup("p2pkh"))), b(&[0x87])],
30        "p2sh:p2pukh" => vec![b(&[0xa9]), push(ihash160(lookup("p2pukh"))), b(&[0x87])],
31        "p2sh:p2pk" => vec![b(&[0xa9]), push(ihash160(lookup("p2pk"))), b(&[0x87])],
32        "p2sh:p2puk" => vec![b(&[0xa9]), push(ihash160(lookup("p2puk"))), b(&[0x87])],
33        "p2sh:p2wpkh" => vec![b(&[0xa9]), push(ihash160(lookup("p2wpkh"))), b(&[0x87])],
34        "p2wsh:p2pkh" => vec![b(&[0x00]), push(ihash(lookup("p2pkh"), &[HashFn::Sha256]))],
35        "p2wsh:p2pukh" => vec![b(&[0x00]), push(ihash(lookup("p2pukh"), &[HashFn::Sha256]))],
36        "p2wsh:p2pk" => vec![b(&[0x00]), push(ihash(lookup("p2pk"), &[HashFn::Sha256]))],
37        "p2wsh:p2puk" => vec![b(&[0x00]), push(ihash(lookup("p2puk"), &[HashFn::Sha256]))],
38        "p2wsh:p2wpkh" => vec![b(&[0x00]), push(ihash(lookup("p2wpkh"), &[HashFn::Sha256]))],
39        "eth" => vec![ihash(lookup("pubkey:uncomp"), &[HashFn::EtherHash])],
40        "massa_pubkey" => vec![b(&[0x00]), lookup("pubkey:ed25519")],
41        "massa" => vec![
42            b(&[0x00, 0x00]),
43            ihash(lookup("massa_pubkey"), &[HashFn::Blake3]),
44        ],
45        "solana" => vec![lookup("pubkey:ed25519")],
46        _ => return None,
47    };
48    Some(f)
49}
50
51/// All known format names (used by [`crate::out::get_outs`]).
52pub const ALL_FORMATS: &[&str] = &[
53    "p2pkh",
54    "p2pukh",
55    "p2pk",
56    "p2puk",
57    "p2wpkh",
58    "p2tr",
59    "p2sh:p2pkh",
60    "p2sh:p2pukh",
61    "p2sh:p2pk",
62    "p2sh:p2puk",
63    "p2sh:p2wpkh",
64    "p2wsh:p2pkh",
65    "p2wsh:p2pukh",
66    "p2wsh:p2pk",
67    "p2wsh:p2puk",
68    "p2wsh:p2wpkh",
69    "eth",
70    "massa_pubkey",
71    "massa",
72    "solana",
73];
74
75/// Typical formats available for each network (port of `FormatsPerNetwork`).
76pub fn formats_per_network(network: &str) -> Option<&'static [&'static str]> {
77    Some(match network {
78        "bitcoin" => &[
79            "p2tr",
80            "p2wpkh",
81            "p2sh:p2wpkh",
82            "p2puk",
83            "p2pk",
84            "p2pukh",
85            "p2pkh",
86        ],
87        "bitcoin-cash" => &["p2puk", "p2pk", "p2pukh", "p2pkh"],
88        "litecoin" => &["p2wpkh", "p2sh:p2wpkh", "p2puk", "p2pk", "p2pukh", "p2pkh"],
89        "dogecoin" => &["p2puk", "p2pk", "p2pukh", "p2pkh"],
90        "evm" => &["eth"],
91        "massa" => &["massa"],
92        "solana" => &["solana"],
93        _ => return None,
94    })
95}
96
97/// Holds a public key and caches generated output scripts for various formats.
98pub struct Script {
99    pubkey: PubKey,
100    cache: RefCell<HashMap<String, Vec<u8>>>,
101}
102
103impl Script {
104    /// Creates a new [`Script`] for the given public key.
105    pub fn new(pubkey: impl Into<PubKey>) -> Script {
106        Script {
107            pubkey: pubkey.into(),
108            cache: RefCell::new(HashMap::new()),
109        }
110    }
111
112    /// The underlying public key.
113    pub fn pubkey(&self) -> &PubKey {
114        &self.pubkey
115    }
116
117    /// Returns the byte value for the specified format name, generating and
118    /// caching it as needed.
119    pub fn generate(&self, name: &str) -> Result<Vec<u8>, String> {
120        if let Some(v) = self.cache.borrow().get(name) {
121            return Ok(v.clone());
122        }
123
124        // Special-case direct public-key access.
125        if matches!(
126            name,
127            "pubkey:pkix" | "pubkey:ed25519" | "pubkey:comp" | "pubkey:uncomp"
128        ) {
129            let res = self.pubkey.bytes_for(name)?;
130            self.cache
131                .borrow_mut()
132                .insert(name.to_string(), res.clone());
133            return Ok(res);
134        }
135
136        let format = format_def(name).ok_or_else(|| format!("unsupported format {name}"))?;
137        let mut out = Vec::new();
138        for piece in &format {
139            out.extend_from_slice(&piece.bytes(self)?);
140        }
141        self.cache
142            .borrow_mut()
143            .insert(name.to_string(), out.clone());
144        Ok(out)
145    }
146
147    /// Returns an [`Out`] for the requested format.
148    pub fn out(&self, name: &str) -> Result<Out, String> {
149        let buf = self.generate(name)?;
150        Ok(Out::make(name, buf, &[]))
151    }
152
153    /// Formats the key as an address using the given format and optional network
154    /// hints.
155    pub fn address(&self, script: &str, flags: &[&str]) -> Result<String, String> {
156        let out = self.out(script)?;
157        out.address(flags)
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::crypto::secp256k1::SecpPrivateKey;
165
166    fn key() -> SecpPrivateKey {
167        let mut s = [0u8; 32];
168        s.copy_from_slice(
169            &hex::decode("eb696a065ef48a2192da5b28b694f87544b30fae8327c4510137a922f32c6dcf")
170                .unwrap(),
171        );
172        SecpPrivateKey::from_bytes(&s).unwrap()
173    }
174
175    #[test]
176    fn generates_p2pkh_script() {
177        let s = Script::new(key().public_key());
178        let script = s.generate("p2pkh").unwrap();
179        // OP_DUP OP_HASH160 <20> ... OP_EQUALVERIFY OP_CHECKSIG
180        assert_eq!(script.len(), 25);
181        assert_eq!(&script[..3], &[0x76, 0xa9, 0x14]);
182        assert_eq!(&script[23..], &[0x88, 0xac]);
183    }
184
185    #[test]
186    fn generates_p2wpkh_and_p2tr() {
187        let s = Script::new(key().public_key());
188        let wpkh = s.generate("p2wpkh").unwrap();
189        assert_eq!(wpkh.len(), 22);
190        assert_eq!(&wpkh[..2], &[0x00, 0x14]);
191        let tr = s.generate("p2tr").unwrap();
192        assert_eq!(tr.len(), 34);
193        assert_eq!(&tr[..2], &[0x51, 0x20]);
194    }
195
196    #[test]
197    fn caching_is_consistent() {
198        let s = Script::new(key().public_key());
199        assert_eq!(
200            s.generate("p2sh:p2wpkh").unwrap(),
201            s.generate("p2sh:p2wpkh").unwrap()
202        );
203    }
204}