Skip to main content

arete_hash/protocol/
builtins.rs

1//! ProgramSpecs for the builtin programs, so templates can call them like
2//! installed programs.
3//!
4//! System, SPL Token, Associated Token and Memo carry the registry's
5//! published ProgramSpecs (`system-program`, `spl-token`, `spl-ata` and
6//! `spl-memo`), which hash the same. Compute Budget has no registry package; its
7//! ProgramSpec is built from the IDL in `builtins/compute-budget.idl.json`.
8//! Each is pinned by hash: [`BuiltinProgram::program_spec_hash`] is fixed,
9//! and a test fails when an embedded spec no longer hashes to it.
10
11use std::sync::OnceLock;
12
13use crate::{
14    build_program_spec_v1_from_bytes, AccountResolutionV1, InstructionDefinitionV1, PdaSeedV1,
15    ProgramSpecV1,
16};
17
18use super::BuiltinProgram;
19
20const SYSTEM: &str = include_str!("builtins/system-program-canonical.program-spec.json");
21const SPL_TOKEN: &str = include_str!("builtins/spl-token.program-spec.json");
22const ASSOCIATED_TOKEN: &str = include_str!("builtins/spl-ata.program-spec.json");
23const MEMO: &str = include_str!("builtins/spl-memo.program-spec.json");
24const COMPUTE_BUDGET_IDL: &str = include_str!("builtins/compute-budget.idl.json");
25
26/// Other names agents and tools use for each builtin: registry package
27/// names and the program SDK names generated from them.
28const ALIASES: &[(&str, BuiltinProgram)] = &[
29    ("system-program", BuiltinProgram::System),
30    ("systemProgram", BuiltinProgram::System),
31    ("spl-token", BuiltinProgram::SplToken),
32    ("token", BuiltinProgram::SplToken),
33    ("spl-ata", BuiltinProgram::AssociatedToken),
34    ("splAta", BuiltinProgram::AssociatedToken),
35    ("ata", BuiltinProgram::AssociatedToken),
36    ("associated-token", BuiltinProgram::AssociatedToken),
37    (
38        "spl-associated-token-account",
39        BuiltinProgram::AssociatedToken,
40    ),
41    ("compute-budget", BuiltinProgram::ComputeBudget),
42    ("spl-memo", BuiltinProgram::Memo),
43    ("splMemo", BuiltinProgram::Memo),
44];
45
46fn normalize(name: &str) -> String {
47    name.chars()
48        .filter(|c| !matches!(c, '-' | '_'))
49        .flat_map(char::to_lowercase)
50        .collect()
51}
52
53impl BuiltinProgram {
54    /// Every builtin, in [`BuiltinProgram::ALL`] order.
55    pub const VALUES: [Self; 5] = [
56        Self::System,
57        Self::SplToken,
58        Self::AssociatedToken,
59        Self::ComputeBudget,
60        Self::Memo,
61    ];
62
63    /// The canonical name, as specs and `builtins(...)` write it.
64    pub const fn name(self) -> &'static str {
65        match self {
66            Self::System => "system",
67            Self::SplToken => "splToken",
68            Self::AssociatedToken => "associatedToken",
69            Self::ComputeBudget => "computeBudget",
70            Self::Memo => "memo",
71        }
72    }
73
74    /// The builtin with this canonical name.
75    pub fn from_name(name: &str) -> Option<Self> {
76        Self::VALUES
77            .into_iter()
78            .find(|builtin| builtin.name() == name)
79    }
80
81    /// The builtin another name refers to (`spl-ata`, `splAta` →
82    /// `associatedToken`), for error messages. `None` for a canonical name
83    /// written exactly, and for names that are no builtin.
84    pub fn from_alias(name: &str) -> Option<Self> {
85        if Self::from_name(name).is_some() {
86            return None;
87        }
88        let normalized = normalize(name);
89        Self::VALUES
90            .into_iter()
91            .find(|builtin| normalize(builtin.name()) == normalized)
92            .or_else(|| {
93                ALIASES
94                    .iter()
95                    .find(|(alias, _)| normalize(alias) == normalized)
96                    .map(|(_, builtin)| *builtin)
97            })
98    }
99
100    /// The registry package the ProgramSpec comes from, if any.
101    pub const fn registry_package(self) -> Option<&'static str> {
102        match self {
103            Self::System => Some("system-program"),
104            Self::SplToken => Some("spl-token"),
105            Self::AssociatedToken => Some("spl-ata"),
106            Self::ComputeBudget => None,
107            Self::Memo => Some("spl-memo"),
108        }
109    }
110
111    /// The pinned hash of [`BuiltinProgram::program_spec`].
112    pub const fn program_spec_hash(self) -> &'static str {
113        match self {
114            Self::System => {
115                "arete:h1:program-spec:sha256:a416fd5d908028d96d4e6445d1da3c7a60147e55711640c281d91f6602c7fb17"
116            }
117            Self::SplToken => {
118                "arete:h1:program-spec:sha256:c4660abfd3626b6f8c9161c159ce2ab856905e1455477e526aaceff95c56d544"
119            }
120            Self::AssociatedToken => {
121                "arete:h1:program-spec:sha256:8a2641d19db953d2945a6eb864c4135e0748f5467a5e298d5969972d44441ea4"
122            }
123            Self::ComputeBudget => {
124                "arete:h1:program-spec:sha256:66dcfb8f7fa0f43dcb6a9d4dda212134ad9fab15292ba73665c8cfca84ec1b5b"
125            }
126            Self::Memo => {
127                "arete:h1:program-spec:sha256:63fe09c92de2095d6f56683451058871e12c2f584f4d9ad35a53e6d85daf64a7"
128            }
129        }
130    }
131
132    /// The builtin's ProgramSpec, which templates resolve against.
133    pub fn program_spec(self) -> &'static ProgramSpecV1 {
134        static SPECS: OnceLock<[ProgramSpecV1; 5]> = OnceLock::new();
135        let specs = SPECS.get_or_init(|| Self::VALUES.map(build_spec));
136        &specs[self as usize]
137    }
138}
139
140fn build_spec(builtin: BuiltinProgram) -> ProgramSpecV1 {
141    let spec = match builtin {
142        BuiltinProgram::System => serde_json::from_str(SYSTEM),
143        BuiltinProgram::SplToken => serde_json::from_str(SPL_TOKEN),
144        BuiltinProgram::AssociatedToken => serde_json::from_str(ASSOCIATED_TOKEN),
145        BuiltinProgram::Memo => serde_json::from_str(MEMO),
146        BuiltinProgram::ComputeBudget => {
147            return build_program_spec_v1_from_bytes(COMPUTE_BUDGET_IDL.as_bytes(), None)
148                .expect("the embedded Compute Budget IDL builds");
149        }
150    };
151    spec.expect("an embedded builtin ProgramSpec parses")
152}
153
154/// Where the generated TypeScript builtin modules live, relative to the
155/// repository root.
156#[doc(hidden)]
157pub const BUILTIN_MODULES_TYPESCRIPT_PATH: &str = "typescript/protocol/src/builtins.generated.ts";
158
159/// The TypeScript module `builtins(...)` reads: each builtin's program id,
160/// pinned ProgramSpec hash and instruction shapes, in the shape generated
161/// program modules use. Regenerate with
162/// `cargo run -p arete-hash --example generate_builtin_modules`.
163#[doc(hidden)]
164pub fn builtin_modules_typescript() -> String {
165    let q = |text: &str| serde_json::to_string(text).expect("a string serializes");
166    let mut out = String::from(
167        "// Generated by `cargo run -p arete-hash --example generate_builtin_modules`\n\
168         // from the builtin ProgramSpecs in arete-hash. Do not edit.\n\n\
169         /** Each builtin program's id, pinned ProgramSpec hash and instruction shapes. */\n\
170         export const BUILTIN_PROGRAM_MODULES = {\n",
171    );
172    for builtin in BuiltinProgram::VALUES {
173        let spec = builtin.program_spec();
174        out.push_str(&format!(
175            "  {}: {{\n    builtin: {},\n    programId: {},\n    programSpecHash: {},\n    instructionShapes: {{\n",
176            builtin.name(),
177            q(builtin.name()),
178            q(builtin.program_id()),
179            q(builtin.program_spec_hash()),
180        ));
181        for ix in &spec.instructions {
182            let args = ix.args.iter().map(|arg| q(&arg.name)).collect::<Vec<_>>();
183            out.push_str(&format!(
184                "      {}: {{\n        args: [{}],\n        accounts: {{",
185                q(&ix.name),
186                args.join(", ")
187            ));
188            if ix.accounts.is_empty() {
189                out.push_str("},\n");
190            } else {
191                out.push('\n');
192                for account in &ix.accounts {
193                    let derive = match account_derivation(spec, ix, &account.resolution) {
194                        Some(names) => format!(
195                            "[{}]",
196                            names
197                                .iter()
198                                .map(|name| q(name))
199                                .collect::<Vec<_>>()
200                                .join(", ")
201                        ),
202                        None => "null".into(),
203                    };
204                    out.push_str(&format!(
205                        "          {}: {{ optional: {}, derive: {} }},\n",
206                        q(&account.name),
207                        account.is_optional,
208                        derive
209                    ));
210                }
211                out.push_str("        },\n");
212            }
213            out.push_str("      },\n");
214        }
215        out.push_str("    },\n  },\n");
216    }
217    out.push_str("} as const;\n");
218    out
219}
220
221/// The accounts an account's address derives from, as the template check
222/// derives it: `[]` for a fixed address or a PDA of literal and argument
223/// seeds, the seed accounts for a PDA over accounts, `None` when a template
224/// must supply it.
225fn account_derivation(
226    spec: &ProgramSpecV1,
227    ix: &InstructionDefinitionV1,
228    resolution: &AccountResolutionV1,
229) -> Option<Vec<String>> {
230    let seeds = |seeds: &[PdaSeedV1], dynamic_program: bool| {
231        if dynamic_program {
232            return None;
233        }
234        let mut accounts = Vec::new();
235        for seed in seeds {
236            match seed {
237                PdaSeedV1::Literal { .. } | PdaSeedV1::Bytes { .. } => {}
238                PdaSeedV1::ArgRef { arg_name, .. } => {
239                    if !ix.args.iter().any(|arg| arg.name == *arg_name) {
240                        return None;
241                    }
242                }
243                PdaSeedV1::AccountRef { account_name } => {
244                    if !accounts.contains(account_name) {
245                        accounts.push(account_name.clone());
246                    }
247                }
248            }
249        }
250        Some(accounts)
251    };
252    match resolution {
253        AccountResolutionV1::Known { .. } => Some(Vec::new()),
254        AccountResolutionV1::PdaRef { pda_name } => spec
255            .pdas
256            .get(pda_name)
257            .and_then(|pda| seeds(&pda.seeds, pda.program.is_some())),
258        AccountResolutionV1::PdaInline {
259            seeds: s, program, ..
260        } => seeds(s, program.is_some()),
261        AccountResolutionV1::Signer | AccountResolutionV1::UserProvided => None,
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn embedded_specs_hash_to_their_pins() {
271        for builtin in BuiltinProgram::VALUES {
272            let spec = builtin.program_spec();
273            assert_eq!(spec.program_id, builtin.program_id(), "{builtin:?}");
274            assert_eq!(
275                spec.hash().unwrap().to_string(),
276                builtin.program_spec_hash(),
277                "{builtin:?}"
278            );
279        }
280    }
281
282    #[test]
283    fn generated_typescript_is_current() {
284        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
285            .join("..")
286            .join(BUILTIN_MODULES_TYPESCRIPT_PATH);
287        let Ok(current) = std::fs::read_to_string(&path) else {
288            // Outside the repository (a packaged crate) there is nothing to compare.
289            return;
290        };
291        assert!(
292            current == builtin_modules_typescript(),
293            "{BUILTIN_MODULES_TYPESCRIPT_PATH} is stale: run `cargo run -p arete-hash --example generate_builtin_modules`"
294        );
295    }
296
297    #[test]
298    fn values_match_all_and_serde() {
299        for (builtin, name) in BuiltinProgram::VALUES.iter().zip(BuiltinProgram::ALL) {
300            assert_eq!(builtin.name(), name);
301            assert_eq!(
302                serde_json::to_value(builtin).unwrap(),
303                serde_json::Value::String(name.into())
304            );
305        }
306    }
307
308    #[test]
309    fn aliases_name_the_canonical_builtin() {
310        assert_eq!(
311            BuiltinProgram::from_alias("spl-ata"),
312            Some(BuiltinProgram::AssociatedToken)
313        );
314        assert_eq!(
315            BuiltinProgram::from_alias("splAta"),
316            Some(BuiltinProgram::AssociatedToken)
317        );
318        assert_eq!(
319            BuiltinProgram::from_alias("associated_token"),
320            Some(BuiltinProgram::AssociatedToken)
321        );
322        assert_eq!(
323            BuiltinProgram::from_alias("spl-token"),
324            Some(BuiltinProgram::SplToken)
325        );
326        assert_eq!(
327            BuiltinProgram::from_alias("compute-budget"),
328            Some(BuiltinProgram::ComputeBudget)
329        );
330        assert_eq!(BuiltinProgram::from_alias("associatedToken"), None);
331        assert_eq!(BuiltinProgram::from_alias("vault"), None);
332    }
333
334    #[test]
335    fn specs_name_the_canonical_builtin_for_an_alias() {
336        let fixture: serde_json::Value = serde_json::from_str(include_str!(
337            "../../../test-vectors/protocol/escrow-resolution.protocol-spec.json"
338        ))
339        .unwrap();
340
341        let mut spec = fixture.clone();
342        spec["programs"]["ata"] = serde_json::json!({ "kind": "builtin", "builtin": "spl-ata" });
343        let error = super::super::validate_protocol_spec_value(&spec).unwrap_err();
344        assert_eq!(error.code, "invalid-value");
345        assert_eq!(error.path, "/programs/ata/builtin");
346        assert_eq!(error.message, "'spl-ata' is the builtin 'associatedToken'");
347
348        let mut spec = fixture;
349        spec["steps"]["open"]["build"]["instructions"][0]["program"] = "splAta".into();
350        let error = super::super::validate_protocol_spec_value(&spec).unwrap_err();
351        assert_eq!(error.code, "unknown-program");
352        assert!(
353            error
354                .message
355                .contains("'splAta' is the builtin 'associatedToken'"),
356            "{}",
357            error.message
358        );
359    }
360
361    /// A template's account metas take their flags from the builtin
362    /// ProgramSpec, and the message merges them with the fee payer's. An
363    /// owner that is not the fee payer signs only if the spec says so.
364    #[test]
365    fn token_transfer_owner_signs_when_it_is_not_the_fee_payer() {
366        const PAYER: &str = "Payer11111111111111111111111111111111111111";
367        const OWNER: &str = "Owner11111111111111111111111111111111111111";
368        let spec = BuiltinProgram::SplToken.program_spec();
369        let transfer = spec
370            .instructions
371            .iter()
372            .find(|ix| ix.name == "Transfer")
373            .unwrap();
374        let supplied = |name: &str| match name {
375            "source" => "Source1111111111111111111111111111111111111",
376            "destination" => "Destination11111111111111111111111111111111",
377            "owner" => OWNER,
378            other => panic!("unexpected account '{other}'"),
379        };
380        let metas: Vec<_> = transfer
381            .accounts
382            .iter()
383            .map(|account| {
384                (
385                    supplied(&account.name),
386                    account.is_signer,
387                    account.is_writable,
388                )
389            })
390            .collect();
391        assert_eq!(
392            metas,
393            [
394                ("Source1111111111111111111111111111111111111", false, true),
395                ("Destination11111111111111111111111111111111", false, true),
396                (OWNER, true, false),
397            ]
398        );
399        let mut signers = vec![PAYER];
400        signers.extend(metas.iter().filter(|meta| meta.1).map(|meta| meta.0));
401        assert_eq!(signers, [PAYER, OWNER]);
402    }
403
404    /// Every single-authority account signs. A multisig authority does not,
405    /// so it cannot be used from a template, whose remaining accounts never
406    /// sign. Multisig members only sign the instructions that use the
407    /// multisig, never the one that creates it.
408    #[test]
409    fn token_authorities_are_signers() {
410        let spec = BuiltinProgram::SplToken.program_spec();
411        let signers: Vec<_> = spec
412            .instructions
413            .iter()
414            .flat_map(|ix| {
415                ix.accounts
416                    .iter()
417                    .filter(|account| account.is_signer)
418                    .map(move |account| (ix.name.as_str(), account.name.as_str()))
419            })
420            .collect();
421        assert_eq!(
422            signers,
423            [
424                ("Transfer", "owner"),
425                ("Approve", "owner"),
426                ("Revoke", "owner"),
427                ("SetAuthority", "authority"),
428                ("MintTo", "authority"),
429                ("Burn", "owner"),
430                ("CloseAccount", "owner"),
431                ("FreezeAccount", "authority"),
432                ("ThawAccount", "authority"),
433                ("TransferChecked", "owner"),
434                ("ApproveChecked", "owner"),
435                ("MintToChecked", "authority"),
436                ("BurnChecked", "owner"),
437                ("UnwrapLamports", "owner"),
438            ]
439        );
440        for ix in &spec.instructions {
441            for account in ix.accounts.iter().filter(|account| account.is_signer) {
442                assert_eq!(
443                    account.resolution,
444                    AccountResolutionV1::Signer,
445                    "{}.{}",
446                    ix.name,
447                    account.name
448                );
449            }
450        }
451    }
452
453    #[test]
454    fn associated_token_create_idempotent_names_its_accounts() {
455        let spec = BuiltinProgram::AssociatedToken.program_spec();
456        let ix = spec
457            .instructions
458            .iter()
459            .find(|ix| ix.name == "createIdempotent")
460            .unwrap();
461        assert_eq!(ix.discriminator, vec![1]);
462        let names: Vec<_> = ix.accounts.iter().map(|a| a.name.as_str()).collect();
463        assert_eq!(
464            names,
465            [
466                "fundingAddress",
467                "associatedTokenAccountAddress",
468                "walletAddress",
469                "tokenMintAddress",
470                "systemProgram",
471                "tokenProgram"
472            ]
473        );
474    }
475
476    #[test]
477    fn compute_budget_instructions_use_one_byte_discriminators() {
478        let spec = BuiltinProgram::ComputeBudget.program_spec();
479        let found: Vec<_> = spec
480            .instructions
481            .iter()
482            .map(|ix| (ix.name.as_str(), ix.discriminator.clone()))
483            .collect();
484        assert_eq!(
485            found,
486            [
487                ("requestHeapFrame", vec![1]),
488                ("setComputeUnitLimit", vec![2]),
489                ("setComputeUnitPrice", vec![3]),
490                ("setLoadedAccountsDataSizeLimit", vec![4]),
491            ]
492        );
493    }
494}