Skip to main content

palinurus_core/
pda.rs

1//! Solana Program Derived Address (PDA) derivation.
2//!
3//! A hand-rolled, `wasm32-wasip2`-friendly reimplementation of
4//! `solana_program::Pubkey::find_program_address` using only `sha2` (SHA-256)
5//! and `curve25519-dalek` (the off-curve check), so it compiles inside a WIT
6//! component where `solana-sdk`/`solana-program` cannot.
7//!
8//! ## Algorithm (matches `solana_program` exactly)
9//!
10//! A PDA is a 32-byte value that is **not** a valid ed25519 public key (i.e. it
11//! is "off-curve", so it has no associated private key and cannot be a signer).
12//! Canonical derivation (`solana_program::Pubkey::try_find_program_address`)
13//! appends the bump as the last seed, then the program id, then the `PDA_MARKER`
14//! domain separator, and tries bump seeds from 255 down to 1, returning the
15//! **highest** bump whose hash is off-curve:
16//!
17//! ```text
18//! for bump in 255..=1 (descending):
19//!     h = sha256(seed_1 || seed_2 || ... || bump || program_id || PDA_MARKER)
20//!     if h is NOT a valid ed25519 compressed point (off-curve):
21//!         return (h, bump)
22//! ```
23//!
24//! ~50% of 32-byte hashes are off-curve, so a result is always found — the
25//! no-result branch is practically unreachable (mirrors `find_program_address`
26//! panicking on `try_find_program_address`'s `None`).
27//!
28//! ## Why this matters for Palinurus
29//!
30//! Track C's `depin-attest` plugin commits sensor readings to Solana's
31//! Attestation Service (SAS). Every SAS account (Credential, Schema, Attestation)
32//! is a PDA under the SAS program, so the plugin must derive PDAs to build the
33//! `create_attestation` instruction. Doing this inside the WASM sandbox — without
34//! `solana-program` — is the single hardest piece of the "solana-sdk won't
35//! compile for wasm" trap this bounty warns about. This module is the de-risk
36//! spike for that: if `curve25519-dalek` + `sha2` compile to `wasm32-wasip2` and
37//! the derivation matches the reference algorithm, SAS-primary is confirmed.
38
39use crate::base58::Pubkey;
40use curve25519_dalek::edwards::CompressedEdwardsY;
41use sha2::{Digest, Sha256};
42
43/// Domain-separation marker appended to every PDA derivation hash, matching
44/// `solana_program::pubkey::PDA_MARKER` (v1.18 classic and v2.x both use it).
45/// The canonical on-chain derivation is
46/// `sha256(seed_1 || ... || bump || program_id || PDA_MARKER)` — the bump is the
47/// last seed (tried 255 → 1) and the marker follows the program id.
48const PDA_MARKER: &[u8; 21] = b"ProgramDerivedAddress";
49
50/// Returns `true` if `pk` is a valid ed25519 compressed point (on-curve).
51///
52/// A PDA must be **off-curve** (`is_on_curve == false`) so it has no associated
53/// private key. This mirrors `solana_program::Pubkey::is_on_curve`, which uses
54/// `curve25519_dalek::edwards::CompressedEdwardsY::decompress`.
55pub fn is_on_curve(pk: &Pubkey) -> bool {
56    CompressedEdwardsY(*pk.as_bytes()).decompress().is_some()
57}
58
59/// Derive the canonical (highest-bump) PDA for `seeds` under `program_id`.
60///
61/// Mirrors `solana_program::Pubkey::find_program_address`: the bump is appended
62/// as the last seed, then the program id and `PDA_MARKER` follow; bump seeds are
63/// tried 255 → 1 (descending) and the first (highest) off-curve hash wins.
64///
65/// Returns `(pda, bump_seed)`. Panics only if no off-curve hash is found across
66/// 255 bumps — practically unreachable (~0.5^255 probability).
67pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) {
68    // Matches solana_program::Pubkey::try_find_program_address: bump is the
69    // last seed, then program_id, then the PDA_MARKER domain separator. Bump
70    // tries 255 → 1 (solana's loop is `0..u8::MAX` starting at u8::MAX).
71    let program_bytes = program_id.as_bytes();
72    for bump in (1u8..=255).rev() {
73        let mut hasher = Sha256::new();
74        for seed in seeds {
75            hasher.update(seed);
76        }
77        hasher.update([bump]);
78        hasher.update(program_bytes);
79        hasher.update(PDA_MARKER);
80        let hash: [u8; 32] = hasher.finalize().into();
81        let pda = Pubkey::from_bytes(hash);
82        if !is_on_curve(&pda) {
83            return (pda, bump);
84        }
85    }
86    // See crate docs: ~50% of hashes are off-curve, so this is unreachable.
87    unreachable!("no off-curve PDA found across 255 bumps (probability ~0.5^255)");
88}
89
90#[cfg(test)]
91mod solana_oracle {
92    use super::*;
93    /// The consensus-critical on-chain derivation (`solana_program::Pubkey::
94    /// find_program_address`). If our hand-rolled `find_program_address` matches
95    /// this byte-for-byte, it produces real on-chain-valid PDAs.
96    #[test]
97    fn matches_canonical_solana_program_derivation_two_seeds() {
98        let program_id = Pubkey::from_bytes([0x42u8; 32]);
99        let seeds: &[&[u8]] = &[b"palinurus", b"depin-attest"];
100
101        let (ref_pda, ref_bump) =
102            solana_program::pubkey::Pubkey::find_program_address(seeds, &solana_program::pubkey::Pubkey::new_from_array(program_id.to_bytes()));
103        let ref_b58 = bs58::encode(ref_pda.to_bytes()).into_string();
104
105        let (my_pda, my_bump) = find_program_address(seeds, &program_id);
106
107        eprintln!(
108            "[oracle] solana-program ref: {ref_b58} bump {ref_bump} | ours: {my_pda} bump {my_bump}"
109        );
110        assert_eq!(my_pda.to_bytes(), ref_pda.to_bytes(), "PDA must match canonical solana-program derivation");
111        assert_eq!(my_bump, ref_bump, "bump must match canonical solana-program derivation");
112    }
113
114    #[test]
115    fn matches_canonical_solana_program_derivation_single_seed() {
116        let program_id = Pubkey::from_bytes([0x42u8; 32]);
117        let seeds: &[&[u8]] = &[b"palinurus"];
118
119        let (ref_pda, ref_bump) =
120            solana_program::pubkey::Pubkey::find_program_address(seeds, &solana_program::pubkey::Pubkey::new_from_array(program_id.to_bytes()));
121        let ref_b58 = bs58::encode(ref_pda.to_bytes()).into_string();
122
123        let (my_pda, my_bump) = find_program_address(seeds, &program_id);
124
125        eprintln!(
126            "[oracle] solana-program ref: {ref_b58} bump {ref_bump} | ours: {my_pda} bump {my_bump}"
127        );
128        assert_eq!(my_pda.to_bytes(), ref_pda.to_bytes(), "PDA must match canonical solana-program derivation");
129        assert_eq!(my_bump, ref_bump, "bump must match canonical solana-program derivation");
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    /// A fixed, arbitrary program id (32 × 0x42, base58
138    /// `5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf`) used across the property
139    /// tests. Its base58 form is cross-checked in the web3.js reference test below.
140    fn test_program_id() -> Pubkey {
141        Pubkey::from_bytes([0x42; 32])
142    }
143
144    #[test]
145    fn pda_is_off_curve() {
146        let program_id = test_program_id();
147        let (pda, _bump) = find_program_address(&[b"palinurus", b"depin-attest"], &program_id);
148        assert!(
149            !is_on_curve(&pda),
150            "derived PDA must be off-curve (no associated private key)"
151        );
152    }
153
154    #[test]
155    fn pda_is_deterministic() {
156        let program_id = test_program_id();
157        let (pda1, bump1) = find_program_address(&[b"palinurus"], &program_id);
158        let (pda2, bump2) = find_program_address(&[b"palinurus"], &program_id);
159        assert_eq!(pda1, pda2, "same seeds must yield the same PDA");
160        assert_eq!(bump1, bump2, "same seeds must yield the same bump");
161    }
162
163    #[test]
164    fn different_seeds_yield_different_pdas() {
165        let program_id = test_program_id();
166        let (pda_a, _) = find_program_address(&[b"palinurus"], &program_id);
167        let (pda_b, _) = find_program_address(&[b"oracle"], &program_id);
168        assert_ne!(pda_a, pda_b, "different seeds must yield different PDAs");
169    }
170
171    #[test]
172    fn different_programs_yield_different_pdas() {
173        let seeds: &[&[u8]] = &[b"palinurus"];
174        let prog_a = Pubkey::from_bytes([0x42; 32]);
175        let prog_b = Pubkey::from_bytes([0x43; 32]);
176        let (pda_a, _) = find_program_address(seeds, &prog_a);
177        let (pda_b, _) = find_program_address(seeds, &prog_b);
178        assert_ne!(
179            pda_a, pda_b,
180            "same seeds under different programs must yield different PDAs"
181        );
182    }
183
184    #[test]
185    fn bump_is_the_highest_off_curve() {
186        // find_program_address returns the highest bump (255→0 search) that is
187        // off-curve. Verify every bump strictly greater than the returned one is
188        // on-curve (otherwise that higher bump would have been returned), and the
189        // returned bump's hash equals the PDA.
190        let program_id = test_program_id();
191        let program_bytes = program_id.as_bytes();
192        let (pda, bump) = find_program_address(&[b"palinurus"], &program_id);
193
194        for higher_u16 in ((bump as u16) + 1)..=255u16 {
195            let higher = higher_u16 as u8;
196            let mut hasher = Sha256::new();
197            hasher.update(b"palinurus");
198            hasher.update([higher]);
199            hasher.update(program_bytes);
200            hasher.update(PDA_MARKER);
201            let h: [u8; 32] = hasher.finalize().into();
202            let h_pk = Pubkey::from_bytes(h);
203            assert!(
204                is_on_curve(&h_pk),
205                "bump {higher} is higher than returned bump {bump} but is off-curve — search order is wrong"
206            );
207        }
208
209        // The returned bump's hash must equal the PDA.
210        let mut hasher = Sha256::new();
211        hasher.update(b"palinurus");
212        hasher.update([bump]);
213        hasher.update(program_bytes);
214        hasher.update(PDA_MARKER);
215        let h: [u8; 32] = hasher.finalize().into();
216        assert_eq!(pda.to_bytes(), h, "PDA must equal sha256(seeds || bump || program || PDA_MARKER)");
217    }
218
219    #[test]
220    fn empty_seeds_still_derive_an_off_curve_pda() {
221        // Edge case: no seeds. Mirrors solana find_program_address(&[], &prog).
222        let program_id = test_program_id();
223        let (pda, _bump) = find_program_address(&[], &program_id);
224        assert!(!is_on_curve(&pda), "empty-seed PDA must still be off-curve");
225    }
226
227    /// Cross-check against the canonical Solana reference implementation
228    /// (`@solana/web3.js` `PublicKey.findProgramAddressSync`). The expected
229    /// `(base58, bump)` constants below were produced by an independent Node.js
230    /// script (`tools/verify-pda.mjs`, run 2026-07-19) using `@solana/web3.js`
231    /// for the same seeds + program id (32 × 0x42, base58
232    /// `5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf`). If this test passes, our
233    /// hand-rolled derivation matches the reference exactly — SAS-primary is
234    /// confirmed feasible (PDA derivation works inside a WIT component without
235    /// `solana-program`).
236    #[test]
237    fn matches_solana_web3_js_reference_two_seeds() {
238        // seeds = ["palinurus", "depin-attest"], program_id = 32 × 0x42
239        let expected_pda_b58: &str = "Et5CGkEYE5YqNYbReBAaBTmkZ8txz2D4kcjCVhWcvT2p";
240        let expected_bump: u8 = 252;
241
242        let program_id = test_program_id();
243        let (pda, bump) = find_program_address(&[b"palinurus", b"depin-attest"], &program_id);
244        assert_eq!(pda.to_string(), expected_pda_b58, "PDA base58 must match @solana/web3.js reference");
245        assert_eq!(bump, expected_bump, "bump must match @solana/web3.js reference");
246    }
247
248    /// Second reference vector (single seed) from the same `@solana/web3.js` run.
249    #[test]
250    fn matches_solana_web3_js_reference_single_seed() {
251        // seeds = ["palinurus"], program_id = 32 × 0x42
252        let expected_pda_b58: &str = "2Jbijq1B93p6CpXv2yz4hwoz1gn3hQLFHjy7zPobWWgo";
253        let expected_bump: u8 = 255;
254
255        let program_id = test_program_id();
256        let (pda, bump) = find_program_address(&[b"palinurus"], &program_id);
257        assert_eq!(pda.to_string(), expected_pda_b58, "PDA base58 must match @solana/web3.js reference");
258        assert_eq!(bump, expected_bump, "bump must match @solana/web3.js reference");
259    }
260}