diff --git a/Cargo.lock b/Cargo.lock index 92736cd..e473882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,10 +2,388 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rand_core", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "five8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f76610e969fa1784327ded240f1e28a3fd9520c9cec93b636fcf62dd37f772" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_const" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0f1728185f277989ca573a402716ae0beaaea3f76a8ff87ef9dd8fb19436c5" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "pastey" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "solana-address" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1384b52c435a750cc9c538760fc7bb472fd78e65a9900a2d07312c5bb335b72" +dependencies = [ + "curve25519-dalek", + "five8", + "five8_const", + "sha2-const-stable", + "solana-define-syscall 5.0.0", + "solana-program-error", + "solana-sha256-hasher", + "wincode", +] + +[[package]] +name = "solana-define-syscall" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738" + +[[package]] +name = "solana-define-syscall" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03aacdd7a61e2109887a7a7f046caebafce97ddf1150f33722eeac04f9039c73" + +[[package]] +name = "solana-hash" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b113239362cee7093bfb250467138f079a2a03673181dc15bff6ccd677912d" +dependencies = [ + "wincode", +] + +[[package]] +name = "solana-program-error" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f04fa578707b3612b095f0c8e19b66a1233f7c42ca8082fcb3b745afcc0add6" + +[[package]] +name = "solana-sha256-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" +dependencies = [ + "sha2", + "solana-define-syscall 4.0.1", + "solana-hash", +] + [[package]] name = "spl-nonce-interface" version = "0.1.0" +dependencies = [ + "solana-address", + "solana-hash", + "wincode", +] [[package]] name = "spl-nonce-program" version = "0.1.0" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wincode" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c754f1fc41250f2f742a27ba0fcc9f73df1dec23f6878490770855d43c322d" +dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror", + "wincode-derive", +] + +[[package]] +name = "wincode-derive" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e070787599c7c067b89598cd3eda440cca1b69eda9e0ff7c725fc8679ce9eb4" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/interface/Cargo.toml b/interface/Cargo.toml index 0327d4a..6e69b6c 100644 --- a/interface/Cargo.toml +++ b/interface/Cargo.toml @@ -1,20 +1,23 @@ [package] name = "spl-nonce-interface" version = "0.1.0" -description = "TBD" -authors = {workspace = true} -repository = {workspace = true} -homepage = {workspace = true} -license = {workspace = true} -edition = {workspace = true} +description = "Interface for the SPL Nonce program" +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } [lib] crate-type = ["rlib"] [package.metadata.solana] -program-id = "2iZvRhbVukqhBXdKTpjmY5w2omXQbziFq1r5WkxSJKFD" +program-id = "nonce34S3Viw97xQwWGpRWEGufiSpfVEAiFe7Lefv7y" [lints] workspace = true [dependencies] +solana-address = { version = "2.6.0", features = ["curve25519", "decode", "wincode"] } +solana-hash = { version = "4.3.0", features = ["wincode"] } +wincode = { version = "0.5.3", features = ["derive"] } diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs new file mode 100644 index 0000000..5b01416 --- /dev/null +++ b/interface/src/instruction.rs @@ -0,0 +1,121 @@ +use { + solana_address::Address, + wincode::{SchemaRead, SchemaWrite}, +}; + +/// Instructions supported by the SPL Nonce program. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NonceInstruction { + /// Initializes a nonce state account for an authority. + /// + /// The caller must first create and fund the nonce state account. Recommended to include + /// `solana_system_interface::instruction::create_account` and `Initialize` in the same + /// transaction so no other transaction can initialize the account first. + /// + /// On success, the program: + /// 1. Verifies the nonce state account is uninitialized, rent-exempt, and owned by + /// the nonce program. + /// 2. Derives the initial `nonce` as + /// `sha256("spl-nonce::init-v1" ‖ nonce_state_address ‖ slot_hashes[0])`. + /// 3. Writes `NonceState { nonce, authority }` into the account data. + /// + /// Instruction data: empty. + /// + /// Accounts required: + /// - `[writable]` Nonce state account + /// - `[]` Authority to store in the nonce state account + /// - `[]` `SlotHashes` sysvar + Initialize, + + /// Authorizes and executes a wrapped Solana transaction whose required signers are + /// `NonceAuthorityPda` accounts. + /// + /// Instruction data: serialized `solana_transaction::versioned::VersionedTransaction`. + /// All message variants supported by `VersionedTransaction` are accepted. + /// + /// Wrapped required signers are paired by index: + /// - `message.account_keys[i]`: `NonceAuthorityPda` promoted during CPI. + /// - `tx.signatures[i]`: wrapped-message signature from the matching authority address. + /// + /// On success, the program: + /// 1. Deserializes the transaction and sanitizes the wrapped message. + /// 2. Reads the authority stored in the nonce state account. + /// 3. Checks the passed nonce state account's authority signed the wrapped message. + /// 4. Checks the wrapped message's lifetime / recent blockhash field equals `state.nonce`. + /// 5. Verifies the outer transaction's only top-level instruction is `Submit`. + /// 6. For each wrapped required signer position `i`, requires + /// `NonceAuthorityPda(authority_i) == message.account_keys[i]` and verifies + /// `tx.signatures[i]` over the wrapped message with `authority_i`. + /// 7. Executes each `message.instructions` entry by CPI, using `invoke_signed` to promote + /// each authorized signer's corresponding `NonceAuthorityPda`. + /// 8. Derives and stores the next nonce as + /// `sha256("spl-nonce::v1" ‖ nonce_state ‖ old_nonce ‖ slot_hashes[0] ‖ sha256(signed_message_bytes))` + /// + /// Accounts required: + /// - `[writable]` Nonce state account whose nonce is consumed and advanced + /// - `[]` `SlotHashes` sysvar + /// - `[]` `Instructions` sysvar + /// - Required-signer authority addresses, ordered to match the wrapped required signers: + /// `NonceAuthorityPda(authority_i) == message.account_keys[i]`. + /// - Remaining: all accounts referenced by the wrapped message, in order, with `is_signer` + /// and `is_writable` flags matching the wrapped message. + Submit, + + /// Closes a nonce state account and refunds its lamports. + /// + /// Instruction data: [`CloseData`]. + /// + /// Runs only as an inner instruction of a wrapped transaction submitted through `Submit` + /// because nothing outside this program can sign for `NonceAuthorityPda`. + /// + /// Accounts required: + /// - `[signer]` `NonceAuthorityPda` + /// - `[writable]` Nonce state account + /// - `[writable]` Lamport recipient + Close, +} + +/// Data for [`NonceInstruction::Close`]. +#[derive(Clone, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] +pub struct CloseData { + /// Address that receives all lamports from the closed nonce account. + pub recipient: Address, +} + +impl TryFrom for NonceInstruction { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Initialize), + 1 => Ok(Self::Submit), + 2 => Ok(Self::Close), + _ => Err(()), + } + } +} + +impl From for u8 { + fn from(value: NonceInstruction) -> Self { + value as u8 + } +} + +#[cfg(test)] +mod tests { + use super::NonceInstruction; + + #[test] + fn discriminants_match() { + assert_eq!(u8::from(NonceInstruction::Initialize), 0); + assert_eq!(u8::from(NonceInstruction::Submit), 1); + assert_eq!(u8::from(NonceInstruction::Close), 2); + } + + #[test] + fn try_from_rejects_unknown() { + assert!(NonceInstruction::try_from(4).is_err()); + assert!(NonceInstruction::try_from(255).is_err()); + } +} diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 665308c..75cbdda 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -1,2 +1,8 @@ //! Interface for the Nonce program. #![no_std] + +pub mod instruction; +pub mod pda; +pub mod state; + +solana_address::declare_id!("nonce34S3Viw97xQwWGpRWEGufiSpfVEAiFe7Lefv7y"); diff --git a/interface/src/pda.rs b/interface/src/pda.rs new file mode 100644 index 0000000..504995e --- /dev/null +++ b/interface/src/pda.rs @@ -0,0 +1,26 @@ +use solana_address::Address; + +/// Nonce authority PDA. +/// +/// Program-owned runtime signer for an authority. `Submit` promotes it to `is_signer=true` +/// via `invoke_signed` wherever a wrapped instruction references it after the corresponding +/// authority has signed the wrapped message. +/// +/// Seeds: `["nonce-authority", authority, bump]` +pub struct NonceAuthorityPda; + +impl NonceAuthorityPda { + pub const SEED_PREFIX: &[u8] = b"nonce-authority"; + + #[inline(always)] + pub fn derive_address_and_bump(program_id: &Address, authority: &Address) -> (Address, u8) { + Address::derive_program_address(&[Self::SEED_PREFIX, authority.as_ref()], program_id) + .expect("failed to derive NonceAuthorityPda from authority") + } + + #[inline(always)] + pub fn derive_address(program_id: &Address, authority: &Address) -> Address { + let (address, _bump) = Self::derive_address_and_bump(program_id, authority); + address + } +} diff --git a/interface/src/state.rs b/interface/src/state.rs new file mode 100644 index 0000000..5e5b336 --- /dev/null +++ b/interface/src/state.rs @@ -0,0 +1,23 @@ +use { + solana_address::Address, + solana_hash::Hash, + wincode::{SchemaRead, SchemaWrite}, +}; + +/// On-chain state for a nonce account. Caller-created and owned by the nonce program. +/// +/// One authority can control any number of independent nonce state accounts. This is useful for +/// when that authority wants to prepare or submit more than one transaction concurrently. Each +/// account carries its own nonce, so consuming one nonce does not advance or invalidate +/// transactions prepared against another nonce state account. +#[derive(Clone, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] +pub struct NonceState { + /// Single-use value that prevents a signed message from being replayed. `Submit` requires this + /// to match the wrapped message's lifetime field: `lifetime_specifier` for `v1` messages or + /// `recent_blockhash` for `legacy` and `v0` messages. On success, `Submit` advances it to a + /// fresh hash over the prior nonce, `SlotHashes[0]`, and the wrapped message bytes. + pub nonce: Hash, + /// Address allowed to consume this nonce and advance its value. `Submit` verifies that this + /// address signed the wrapped transaction message. + pub authority: Address, +} diff --git a/program/Cargo.toml b/program/Cargo.toml index 7503f8b..1f6964c 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -12,7 +12,7 @@ edition = {workspace = true} crate-type = ["cdylib"] [package.metadata.solana] -program-id = "2iZvRhbVukqhBXdKTpjmY5w2omXQbziFq1r5WkxSJKFD" +program-id = "nonce34S3Viw97xQwWGpRWEGufiSpfVEAiFe7Lefv7y" [lints] workspace = true @@ -21,4 +21,3 @@ workspace = true [dev-dependencies] - diff --git a/scripts/solana.dic b/scripts/solana.dic index 18581f7..c130ba0 100644 --- a/scripts/solana.dic +++ b/scripts/solana.dic @@ -52,3 +52,6 @@ autogenerated pinocchio Pinocchio IDL +Ed25519 +tx +blockhash