diff --git a/Cargo.lock b/Cargo.lock index dfe518608..3821d6903 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2740,6 +2740,40 @@ dependencies = [ "solana-pubkey", ] +[[package]] +name = "solana-address" +version = "0.1.0" +dependencies = [ + "anyhow", + "arbitrary", + "borsh", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "five8", + "five8_const", + "num-traits", + "rand", + "serde", + "serde_derive", + "solana-account-info", + "solana-address", + "solana-atomic-u64", + "solana-cpi", + "solana-define-syscall", + "solana-example-mocks", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-hash", + "solana-instruction", + "solana-program-error", + "solana-sanitize", + "solana-sha256-hasher", + "solana-system-interface", + "strum", + "strum_macros", +] + [[package]] name = "solana-address-lookup-table-interface" version = "2.2.2" @@ -3577,35 +3611,8 @@ dependencies = [ name = "solana-pubkey" version = "2.4.0" dependencies = [ - "anyhow", - "arbitrary", - "borsh", - "bs58", - "bytemuck", - "bytemuck_derive", - "curve25519-dalek", - "five8", - "five8_const", - "num-traits", "rand", - "serde", - "serde_derive", - "solana-account-info", - "solana-atomic-u64", - "solana-cpi", - "solana-define-syscall", - "solana-example-mocks", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-hash", - "solana-instruction", - "solana-program-error", - "solana-pubkey", - "solana-sanitize", - "solana-sha256-hasher", - "solana-system-interface", - "strum", - "strum_macros", + "solana-address", ] [[package]] @@ -3728,11 +3735,11 @@ dependencies = [ "getrandom 0.2.15", "js-sys", "log", + "solana-address", "solana-hash", "solana-instruction", "solana-keypair", "solana-message", - "solana-pubkey", "solana-signature", "solana-signer", "solana-transaction", diff --git a/Cargo.toml b/Cargo.toml index fdfbf4484..bf7b28c35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "account", "account-info", + "address", "address-lookup-table-interface", "atomic-u64", "big-mod-exp", @@ -206,6 +207,7 @@ signal-hook = "0.3.17" siphasher = "0.3.11" solana-account = { path = "account", version = "2.2.1" } solana-account-info = { path = "account-info", version = "2.2.1" } +solana-address = { path = "address", version = "0.1.0" } solana-address-lookup-table-interface = { path = "address-lookup-table-interface", version = "2.2.2" } solana-atomic-u64 = { path = "atomic-u64", version = "2.2.1" } solana-big-mod-exp = { path = "big-mod-exp", version = "2.2.1" } diff --git a/account/src/lib.rs b/account/src/lib.rs index 2aac84de8..f4edae6de 100644 --- a/account/src/lib.rs +++ b/account/src/lib.rs @@ -33,7 +33,7 @@ pub mod state_traits; #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "2SUJNHbXMPWrsSXmDTFc4VHx2XQ85fT5Leabefh5Nwe7") + frozen_abi(digest = "62EqVoynUFvuui7DVfqWCvZP7bxKGJGioeSBnWrdjRME") )] #[cfg_attr( feature = "serde", @@ -70,7 +70,7 @@ mod account_serialize { #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "2SUJNHbXMPWrsSXmDTFc4VHx2XQ85fT5Leabefh5Nwe7") + frozen_abi(digest = "62EqVoynUFvuui7DVfqWCvZP7bxKGJGioeSBnWrdjRME") )] #[derive(serde_derive::Serialize)] #[serde(rename_all = "camelCase")] diff --git a/address/Cargo.toml b/address/Cargo.toml new file mode 100644 index 000000000..0e6f28f4b --- /dev/null +++ b/address/Cargo.toml @@ -0,0 +1,83 @@ +[package] +name = "solana-address" +description = "Solana account addresses" +documentation = "https://docs.rs/solana-address" +version = "0.1.0" +rust-version = "1.81.0" +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[features] +atomic = ["dep:solana-atomic-u64"] +borsh = ["dep:borsh", "std"] +bytemuck = ["dep:bytemuck", "dep:bytemuck_derive"] +curve25519 = ["dep:curve25519-dalek", "error", "sha2"] +decode = ["dep:five8", "dep:five8_const", "error"] +dev-context-only-utils = ["dep:arbitrary", "rand"] +error = ["dep:num-traits", "dep:solana-program-error"] +frozen-abi = [ + "dep:solana-frozen-abi", + "dep:solana-frozen-abi-macro", + "dep:solana-program-error", + "std", +] +rand = ["dep:rand", "atomic", "std"] +sanitize = ["dep:solana-sanitize"] +serde = ["dep:serde", "dep:serde_derive"] +sha2 = ["dep:solana-sha256-hasher", "error"] +std = ["decode"] +syscalls = ["dep:solana-define-syscall", "error"] + +[dependencies] +arbitrary = { workspace = true, features = ["derive"], optional = true } +borsh = { workspace = true, optional = true } +bytemuck = { workspace = true, optional = true } +bytemuck_derive = { workspace = true, optional = true } +five8 = { workspace = true, optional = true } +five8_const = { workspace = true, optional = true } +num-traits = { workspace = true, optional = true } +rand = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +serde_derive = { workspace = true, optional = true } +solana-atomic-u64 = { workspace = true, optional = true } +solana-frozen-abi = { workspace = true, features = ["frozen-abi"], optional = true } +solana-frozen-abi-macro = { workspace = true, features = ["frozen-abi"], optional = true } +solana-program-error = { workspace = true, optional = true } +solana-sanitize = { workspace = true, optional = true } + +[target.'cfg(not(target_os = "solana"))'.dependencies] +curve25519-dalek = { workspace = true, optional = true } +solana-sha256-hasher = { workspace = true, features = ["sha2"], optional = true } + +[target.'cfg(target_os = "solana")'.dependencies] +solana-define-syscall = { workspace = true, optional = true } +solana-sha256-hasher = { workspace = true, optional = true } + +[dev-dependencies] +anyhow = { workspace = true } +solana-account-info = { path = "../account-info" } +solana-address = { path = ".", features = [ + "atomic", + "borsh", + "curve25519", + "decode", + "dev-context-only-utils", + "error", + "sanitize", + "std", + "syscalls" +] } +solana-cpi = { path = "../cpi" } +solana-example-mocks = { path = "../example-mocks" } +solana-hash = { workspace = true } +solana-instruction = { path = "../instruction", features = ["borsh"] } +solana-program-error = { workspace = true, features = ["borsh"] } +solana-system-interface = { workspace = true, features = ["bincode"] } +strum = { workspace = true } +strum_macros = { workspace = true } + +[lints] +workspace = true diff --git a/address/src/error.rs b/address/src/error.rs new file mode 100644 index 000000000..840b265b6 --- /dev/null +++ b/address/src/error.rs @@ -0,0 +1,148 @@ +#[cfg(feature = "serde")] +use serde_derive::Serialize; +use { + core::{convert::Infallible, fmt}, + num_traits::{FromPrimitive, ToPrimitive}, + solana_program_error::ProgramError, +}; + +// Use strum when testing to ensure our FromPrimitive +// impl is exhaustive +#[cfg_attr(test, derive(strum_macros::FromRepr, strum_macros::EnumIter))] +#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AddressError { + /// Length of the seed is too long for address generation + MaxSeedLengthExceeded, + InvalidSeeds, + IllegalOwner, +} + +impl ToPrimitive for AddressError { + #[inline] + fn to_i64(&self) -> Option { + Some(match *self { + AddressError::MaxSeedLengthExceeded => AddressError::MaxSeedLengthExceeded as i64, + AddressError::InvalidSeeds => AddressError::InvalidSeeds as i64, + AddressError::IllegalOwner => AddressError::IllegalOwner as i64, + }) + } + #[inline] + fn to_u64(&self) -> Option { + self.to_i64().map(|x| x as u64) + } +} + +impl FromPrimitive for AddressError { + #[inline] + fn from_i64(n: i64) -> Option { + if n == AddressError::MaxSeedLengthExceeded as i64 { + Some(AddressError::MaxSeedLengthExceeded) + } else if n == AddressError::InvalidSeeds as i64 { + Some(AddressError::InvalidSeeds) + } else if n == AddressError::IllegalOwner as i64 { + Some(AddressError::IllegalOwner) + } else { + None + } + } + #[inline] + fn from_u64(n: u64) -> Option { + Self::from_i64(n as i64) + } +} + +impl core::error::Error for AddressError {} + +impl fmt::Display for AddressError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AddressError::MaxSeedLengthExceeded => { + f.write_str("Length of the seed is too long for address generation") + } + AddressError::InvalidSeeds => { + f.write_str("Provided seeds do not result in a valid address") + } + AddressError::IllegalOwner => f.write_str("Provided owner is not allowed"), + } + } +} + +impl From for AddressError { + fn from(error: u64) -> Self { + match error { + 0 => AddressError::MaxSeedLengthExceeded, + 1 => AddressError::InvalidSeeds, + 2 => AddressError::IllegalOwner, + _ => panic!("Unsupported AddressError"), + } + } +} + +impl From for ProgramError { + fn from(error: AddressError) -> Self { + match error { + AddressError::MaxSeedLengthExceeded => Self::MaxSeedLengthExceeded, + AddressError::InvalidSeeds => Self::InvalidSeeds, + AddressError::IllegalOwner => Self::IllegalOwner, + } + } +} + +// Use strum when testing to ensure our FromPrimitive +// impl is exhaustive +#[cfg_attr(test, derive(strum_macros::FromRepr, strum_macros::EnumIter))] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ParseAddressError { + WrongSize, + Invalid, +} + +impl ToPrimitive for ParseAddressError { + #[inline] + fn to_i64(&self) -> Option { + Some(match *self { + ParseAddressError::WrongSize => ParseAddressError::WrongSize as i64, + ParseAddressError::Invalid => ParseAddressError::Invalid as i64, + }) + } + #[inline] + fn to_u64(&self) -> Option { + self.to_i64().map(|x| x as u64) + } +} + +impl FromPrimitive for ParseAddressError { + #[inline] + fn from_i64(n: i64) -> Option { + if n == ParseAddressError::WrongSize as i64 { + Some(ParseAddressError::WrongSize) + } else if n == ParseAddressError::Invalid as i64 { + Some(ParseAddressError::Invalid) + } else { + None + } + } + #[inline] + fn from_u64(n: u64) -> Option { + Self::from_i64(n as i64) + } +} + +impl core::error::Error for ParseAddressError {} + +impl fmt::Display for ParseAddressError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ParseAddressError::WrongSize => f.write_str("String is the wrong size"), + ParseAddressError::Invalid => f.write_str("Invalid Base58 string"), + } + } +} + +impl From for ParseAddressError { + fn from(_: Infallible) -> Self { + unreachable!("Infallible uninhabited"); + } +} diff --git a/address/src/hasher.rs b/address/src/hasher.rs new file mode 100644 index 000000000..922b91ec6 --- /dev/null +++ b/address/src/hasher.rs @@ -0,0 +1,140 @@ +use { + crate::ADDRESS_BYTES, + core::{ + cell::Cell, + hash::{BuildHasher, Hasher}, + mem, + }, + rand::{thread_rng, Rng}, +}; + +/// A faster, but less collision resistant hasher for addresses. +/// +/// Specialized hasher that uses a random 8 bytes subslice of the +/// address as the hash value. Should not be used when collisions +/// might be used to mount DOS attacks. +/// +/// Using this results in about 4x faster lookups in a typical hashmap. +#[derive(Default)] +pub struct AddressHasher { + offset: usize, + state: u64, +} + +impl Hasher for AddressHasher { + #[inline] + fn finish(&self) -> u64 { + self.state + } + #[inline] + fn write(&mut self, bytes: &[u8]) { + debug_assert_eq!( + bytes.len(), + ADDRESS_BYTES, + "This hasher is intended to be used with addresses and nothing else" + ); + // This slice/unwrap can never panic since offset is < ADDRESS_BYTES - mem::size_of::() + let chunk: &[u8; mem::size_of::()] = bytes + [self.offset..self.offset + mem::size_of::()] + .try_into() + .unwrap(); + self.state = u64::from_ne_bytes(*chunk); + } +} + +/// A builder for faster, but less collision resistant hasher for addresses. +/// +/// Initializes `AddressHasher` instances that use an 8-byte +/// slice of the address as the hash value. Should not be used when +/// collisions might be used to mount DOS attacks. +/// +/// Using this results in about 4x faster lookups in a typical hashmap. +#[derive(Clone)] +pub struct AddressHasherBuilder { + offset: usize, +} + +impl Default for AddressHasherBuilder { + /// Default construct the AddressHasherBuilder. + /// + /// The position of the slice is determined initially + /// through random draw and then by incrementing a thread-local + /// This way each hashmap can be expected to use a slightly different + /// slice. This is essentially the same mechanism as what is used by + /// `RandomState` + fn default() -> Self { + std::thread_local!(static OFFSET: Cell = { + let mut rng = thread_rng(); + Cell::new(rng.gen_range(0..ADDRESS_BYTES - mem::size_of::())) + }); + + let offset = OFFSET.with(|offset| { + let mut next_offset = offset.get() + 1; + if next_offset > ADDRESS_BYTES - mem::size_of::() { + next_offset = 0; + } + offset.set(next_offset); + next_offset + }); + AddressHasherBuilder { offset } + } +} + +impl BuildHasher for AddressHasherBuilder { + type Hasher = AddressHasher; + #[inline] + fn build_hasher(&self) -> Self::Hasher { + AddressHasher { + offset: self.offset, + state: 0, + } + } +} + +#[cfg(test)] +mod tests { + use { + super::AddressHasherBuilder, + crate::Address, + core::hash::{BuildHasher, Hasher}, + }; + #[test] + fn test_address_hasher_builder() { + let key = Address::new_unique(); + let builder = AddressHasherBuilder::default(); + let mut hasher1 = builder.build_hasher(); + let mut hasher2 = builder.build_hasher(); + hasher1.write(key.as_array()); + hasher2.write(key.as_array()); + assert_eq!( + hasher1.finish(), + hasher2.finish(), + "Hashers made with same builder should be identical" + ); + // Make sure that when we make new builders we get different slices + // chosen for hashing + let builder2 = AddressHasherBuilder::default(); + for _ in 0..64 { + let mut hasher3 = builder2.build_hasher(); + hasher3.write(key.as_array()); + std::dbg!(hasher1.finish()); + std::dbg!(hasher3.finish()); + if hasher1.finish() != hasher3.finish() { + return; + } + } + panic!("Hashers built with different builder should be different due to random offset"); + } + + #[test] + fn test_address_hasher() { + let key1 = Address::new_unique(); + let key2 = Address::new_unique(); + let builder = AddressHasherBuilder::default(); + let mut hasher1 = builder.build_hasher(); + let mut hasher2 = builder.build_hasher(); + hasher1.write(key1.as_array()); + hasher2.write(key2.as_array()); + assert_ne!(hasher1.finish(), hasher2.finish()); + } +} diff --git a/address/src/lib.rs b/address/src/lib.rs new file mode 100644 index 000000000..903c6dafc --- /dev/null +++ b/address/src/lib.rs @@ -0,0 +1,638 @@ +//! Address representation for Solana. +//! +//! An address is a sequence of 32 bytes, often shown as a base58 encoded string +//! (e.g. 14grJpemFaf88c8tiVb77W7TYg2W3ir6pfkKz3YjhhZ5). + +#![no_std] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(feature = "frozen-abi", feature(min_specialization))] +#![allow(clippy::arithmetic_side_effects)] + +#[cfg(feature = "error")] +pub mod error; +#[cfg(feature = "rand")] +mod hasher; +#[cfg(any(feature = "curve25519", feature = "syscalls"))] +pub mod syscalls; + +#[cfg(feature = "sha2")] +use crate::error::AddressError; +#[cfg(feature = "decode")] +use crate::error::ParseAddressError; +#[cfg(all(feature = "rand", not(target_os = "solana")))] +pub use crate::hasher::{AddressHasher, AddressHasherBuilder}; + +#[cfg(feature = "std")] +extern crate std; +#[cfg(feature = "dev-context-only-utils")] +use arbitrary::Arbitrary; +#[cfg(feature = "bytemuck")] +use bytemuck_derive::{Pod, Zeroable}; +#[cfg(feature = "decode")] +use core::str::FromStr; +use core::{ + array, + convert::TryFrom, + hash::{Hash, Hasher}, +}; +#[cfg(feature = "serde")] +use serde_derive::{Deserialize, Serialize}; +#[cfg(feature = "std")] +use std::vec::Vec; +#[cfg(feature = "borsh")] +use { + borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, + std::string::ToString, +}; + +/// Number of bytes in an address. +pub const ADDRESS_BYTES: usize = 32; +/// maximum length of derived `Address` seed +pub const MAX_SEED_LEN: usize = 32; +/// Maximum number of seeds +pub const MAX_SEEDS: usize = 16; +#[cfg(feature = "decode")] +/// Maximum string length of a base58 encoded address. +const MAX_BASE58_LEN: usize = 44; + +#[cfg(feature = "sha2")] +const PDA_MARKER: &[u8; 21] = b"ProgramDerivedAddress"; + +/// The address of a [Solana account][acc]. +/// +/// Some account addresses are [ed25519] public keys, with corresponding secret +/// keys that are managed off-chain. Often, though, account addresses do not +/// have corresponding secret keys — as with [_program derived +/// addresses_][pdas] — or the secret key is not relevant to the operation +/// of a program, and may have even been disposed of. As running Solana programs +/// can not safely create or manage secret keys, the full [`Keypair`] is not +/// defined in `solana-program` but in `solana-sdk`. +/// +/// [acc]: https://solana.com/docs/core/accounts +/// [ed25519]: https://ed25519.cr.yp.to/ +/// [pdas]: https://solana.com/docs/core/cpi#program-derived-addresses +/// [`Keypair`]: https://docs.rs/solana-sdk/latest/solana_sdk/signer/keypair/struct.Keypair.html +#[repr(transparent)] +#[cfg_attr(feature = "frozen-abi", derive(solana_frozen_abi_macro::AbiExample))] +#[cfg_attr( + feature = "borsh", + derive(BorshSerialize, BorshDeserialize), + borsh(crate = "borsh") +)] +#[cfg_attr(all(feature = "borsh", feature = "std"), derive(BorshSchema))] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))] +#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))] +pub struct Address(pub(crate) [u8; 32]); + +#[cfg(feature = "sanitize")] +impl solana_sanitize::Sanitize for Address {} + +#[cfg(feature = "decode")] +impl FromStr for Address { + type Err = ParseAddressError; + + fn from_str(s: &str) -> Result { + use five8::DecodeError; + if s.len() > MAX_BASE58_LEN { + return Err(ParseAddressError::WrongSize); + } + let mut bytes = [0; ADDRESS_BYTES]; + five8::decode_32(s, &mut bytes).map_err(|e| match e { + DecodeError::InvalidChar(_) => ParseAddressError::Invalid, + DecodeError::TooLong + | DecodeError::TooShort + | DecodeError::LargestTermTooHigh + | DecodeError::OutputTooLong => ParseAddressError::WrongSize, + })?; + Ok(Address(bytes)) + } +} + +/// Custom impl of Hash for Address. +/// +/// This allows us to skip hashing the length of the address +/// which is always the same anyway. +impl Hash for Address { + fn hash(&self, state: &mut H) { + state.write(self.as_array()); + } +} + +impl From<&Address> for Address { + #[inline] + fn from(value: &Address) -> Self { + *value + } +} + +impl From<[u8; 32]> for Address { + #[inline] + fn from(from: [u8; 32]) -> Self { + Self(from) + } +} + +impl TryFrom<&[u8]> for Address { + type Error = array::TryFromSliceError; + + #[inline] + fn try_from(address: &[u8]) -> Result { + <[u8; 32]>::try_from(address).map(Self::from) + } +} + +#[cfg(feature = "std")] +impl TryFrom> for Address { + type Error = Vec; + + #[inline] + fn try_from(address: Vec) -> Result { + <[u8; 32]>::try_from(address).map(Self::from) + } +} +#[cfg(feature = "decode")] +impl TryFrom<&str> for Address { + type Error = ParseAddressError; + fn try_from(s: &str) -> Result { + Address::from_str(s) + } +} + +// If target_os = "solana", then this panics so there are no dependencies. +// When target_os != "solana", this should be opt-in so users +// don't need the curve25519 dependency. +#[cfg(any(target_os = "solana", feature = "curve25519"))] +#[allow(clippy::used_underscore_binding)] +pub fn bytes_are_curve_point>(_bytes: T) -> bool { + #[cfg(not(target_os = "solana"))] + { + let Ok(compressed_edwards_y) = + curve25519_dalek::edwards::CompressedEdwardsY::from_slice(_bytes.as_ref()) + else { + return false; + }; + compressed_edwards_y.decompress().is_some() + } + #[cfg(target_os = "solana")] + unimplemented!(); +} + +impl Address { + pub const fn new_from_array(address_array: [u8; 32]) -> Self { + Self(address_array) + } + + #[cfg(feature = "decode")] + /// Decode a string into an `Address`, usable in a const context + pub const fn from_str_const(s: &str) -> Self { + let id_array = five8_const::decode_32_const(s); + Address::new_from_array(id_array) + } + + #[cfg(feature = "atomic")] + /// Create an unique `Address` for tests and benchmarks. + pub fn new_unique() -> Self { + use solana_atomic_u64::AtomicU64; + static I: AtomicU64 = AtomicU64::new(1); + type T = u32; + const COUNTER_BYTES: usize = core::mem::size_of::(); + let mut b = [0u8; ADDRESS_BYTES]; + #[cfg(feature = "std")] + let mut i = I.fetch_add(1) as T; + #[cfg(not(feature = "std"))] + let i = I.fetch_add(1) as T; + // use big endian representation to ensure that recent unique addresses + // are always greater than less recent unique addresses. + b[0..COUNTER_BYTES].copy_from_slice(&i.to_be_bytes()); + // fill the rest of the address with pseudorandom numbers to make + // data statistically similar to real addresses. + #[cfg(feature = "std")] + { + let mut hash = std::hash::DefaultHasher::new(); + for slice in b[COUNTER_BYTES..].chunks_mut(COUNTER_BYTES) { + hash.write_u32(i); + i += 1; + slice.copy_from_slice(&hash.finish().to_ne_bytes()[0..COUNTER_BYTES]); + } + } + // if std is not available, just replicate last byte of the counter. + // this is not as good as a proper hash, but at least it is uniform + #[cfg(not(feature = "std"))] + { + for b in b[COUNTER_BYTES..].iter_mut() { + *b = (i & 0xFF) as u8; + } + } + Self::from(b) + } + + // If target_os = "solana", then the solana_sha256_hasher crate will use + // syscalls which bring no dependencies. + // When target_os != "solana", this should be opt-in so users + // don't need the sha2 dependency. + #[cfg(feature = "sha2")] + pub fn create_with_seed( + base: &Address, + seed: &str, + owner: &Address, + ) -> Result { + if seed.len() > MAX_SEED_LEN { + return Err(AddressError::MaxSeedLengthExceeded); + } + + let owner = owner.as_ref(); + if owner.len() >= PDA_MARKER.len() { + let slice = &owner[owner.len() - PDA_MARKER.len()..]; + if slice == PDA_MARKER { + return Err(AddressError::IllegalOwner); + } + } + let hash = solana_sha256_hasher::hashv(&[base.as_ref(), seed.as_ref(), owner]); + Ok(Address::from(hash.to_bytes())) + } + + pub const fn to_bytes(self) -> [u8; 32] { + self.0 + } + + /// Return a reference to the `Address`'s byte array. + #[inline(always)] + pub const fn as_array(&self) -> &[u8; 32] { + &self.0 + } + + // If target_os = "solana", then this panics so there are no dependencies. + // When target_os != "solana", this should be opt-in so users + // don't need the curve25519 dependency. + #[cfg(any(target_os = "solana", feature = "curve25519"))] + pub fn is_on_curve(&self) -> bool { + bytes_are_curve_point(self) + } +} + +impl AsRef<[u8]> for Address { + fn as_ref(&self) -> &[u8] { + &self.0[..] + } +} + +impl AsMut<[u8]> for Address { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0[..] + } +} + +#[cfg(feature = "decode")] +fn write_as_base58(f: &mut core::fmt::Formatter, p: &Address) -> core::fmt::Result { + let mut out = [0u8; MAX_BASE58_LEN]; + let len = five8::encode_32(&p.0, &mut out) as usize; + // any sequence of base58 chars is valid utf8 + let as_str = unsafe { core::str::from_utf8_unchecked(&out[..len]) }; + f.write_str(as_str) +} + +#[cfg(feature = "decode")] +impl core::fmt::Debug for Address { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write_as_base58(f, self) + } +} + +#[cfg(feature = "decode")] +impl core::fmt::Display for Address { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write_as_base58(f, self) + } +} + +/// Convenience macro to define a static `Address` value. +/// +/// Input: a single literal base58 string representation of an `Address`. +/// +/// # Example +/// +/// ``` +/// use std::str::FromStr; +/// use solana_address::{address, Address}; +/// +/// static ID: Address = address!("My11111111111111111111111111111111111111111"); +/// +/// let my_id = Address::from_str("My11111111111111111111111111111111111111111").unwrap(); +/// assert_eq!(ID, my_id); +/// ``` +#[macro_export] +macro_rules! address { + ($input:literal) => { + $crate::Address::from_str_const($input) + }; +} + +#[cfg(test)] +mod tests { + use { + super::*, core::str::from_utf8, num_traits::FromPrimitive, std::string::String, + strum::IntoEnumIterator, + }; + + fn encode_address(address: &[u8; 32]) -> String { + let mut buffer = [0u8; 44]; + let count = five8::encode_32(address, &mut buffer); + from_utf8(&buffer[..count as usize]).unwrap().to_string() + } + + #[test] + fn test_new_unique() { + assert!(Address::new_unique() != Address::new_unique()); + } + + #[test] + fn address_fromstr() { + let address = Address::new_unique(); + let mut address_base58_str = encode_address(&address.0); + + assert_eq!(address_base58_str.parse::
(), Ok(address)); + + address_base58_str.push_str(&encode_address(&address.0)); + assert_eq!( + address_base58_str.parse::
(), + Err(ParseAddressError::WrongSize) + ); + + address_base58_str.truncate(address_base58_str.len() / 2); + assert_eq!(address_base58_str.parse::
(), Ok(address)); + + address_base58_str.truncate(address_base58_str.len() / 2); + assert_eq!( + address_base58_str.parse::
(), + Err(ParseAddressError::WrongSize) + ); + + let mut address_base58_str = encode_address(&address.0); + assert_eq!(address_base58_str.parse::
(), Ok(address)); + + // throw some non-base58 stuff in there + address_base58_str.replace_range(..1, "I"); + assert_eq!( + address_base58_str.parse::
(), + Err(ParseAddressError::Invalid) + ); + + // too long input string + // longest valid encoding + let mut too_long = encode_address(&[255u8; ADDRESS_BYTES]); + // and one to grow on + too_long.push('1'); + assert_eq!( + too_long.parse::
(), + Err(ParseAddressError::WrongSize) + ); + } + + #[test] + fn test_create_with_seed() { + assert!( + Address::create_with_seed(&Address::new_unique(), "☉", &Address::new_unique()).is_ok() + ); + assert_eq!( + Address::create_with_seed( + &Address::new_unique(), + from_utf8(&[127; MAX_SEED_LEN + 1]).unwrap(), + &Address::new_unique() + ), + Err(AddressError::MaxSeedLengthExceeded) + ); + assert!(Address::create_with_seed( + &Address::new_unique(), + "\ + \u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\ + ", + &Address::new_unique() + ) + .is_ok()); + // utf-8 abuse ;) + assert_eq!( + Address::create_with_seed( + &Address::new_unique(), + "\ + x\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\ + ", + &Address::new_unique() + ), + Err(AddressError::MaxSeedLengthExceeded) + ); + + assert!(Address::create_with_seed( + &Address::new_unique(), + from_utf8(&[0; MAX_SEED_LEN]).unwrap(), + &Address::new_unique(), + ) + .is_ok()); + + assert!( + Address::create_with_seed(&Address::new_unique(), "", &Address::new_unique(),).is_ok() + ); + + assert_eq!( + Address::create_with_seed( + &Address::default(), + "limber chicken: 4/45", + &Address::default(), + ), + Ok("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq" + .parse() + .unwrap()) + ); + } + + #[test] + fn test_create_program_address() { + let exceeded_seed = &[127; MAX_SEED_LEN + 1]; + let max_seed = &[0; MAX_SEED_LEN]; + let exceeded_seeds: &[&[u8]] = &[ + &[1], + &[2], + &[3], + &[4], + &[5], + &[6], + &[7], + &[8], + &[9], + &[10], + &[11], + &[12], + &[13], + &[14], + &[15], + &[16], + &[17], + ]; + let max_seeds: &[&[u8]] = &[ + &[1], + &[2], + &[3], + &[4], + &[5], + &[6], + &[7], + &[8], + &[9], + &[10], + &[11], + &[12], + &[13], + &[14], + &[15], + &[16], + ]; + let program_id = Address::from_str("BPFLoaderUpgradeab1e11111111111111111111111").unwrap(); + let public_key = Address::from_str("SeedPubey1111111111111111111111111111111111").unwrap(); + + assert_eq!( + Address::create_program_address(&[exceeded_seed], &program_id), + Err(AddressError::MaxSeedLengthExceeded) + ); + assert_eq!( + Address::create_program_address(&[b"short_seed", exceeded_seed], &program_id), + Err(AddressError::MaxSeedLengthExceeded) + ); + assert!(Address::create_program_address(&[max_seed], &program_id).is_ok()); + assert_eq!( + Address::create_program_address(exceeded_seeds, &program_id), + Err(AddressError::MaxSeedLengthExceeded) + ); + assert!(Address::create_program_address(max_seeds, &program_id).is_ok()); + assert_eq!( + Address::create_program_address(&[b"", &[1]], &program_id), + Ok("BwqrghZA2htAcqq8dzP1WDAhTXYTYWj7CHxF5j7TDBAe" + .parse() + .unwrap()) + ); + assert_eq!( + Address::create_program_address(&["☉".as_ref(), &[0]], &program_id), + Ok("13yWmRpaTR4r5nAktwLqMpRNr28tnVUZw26rTvPSSB19" + .parse() + .unwrap()) + ); + assert_eq!( + Address::create_program_address(&[b"Talking", b"Squirrels"], &program_id), + Ok("2fnQrngrQT4SeLcdToJAD96phoEjNL2man2kfRLCASVk" + .parse() + .unwrap()) + ); + assert_eq!( + Address::create_program_address(&[public_key.as_ref(), &[1]], &program_id), + Ok("976ymqVnfE32QFe6NfGDctSvVa36LWnvYxhU6G2232YL" + .parse() + .unwrap()) + ); + assert_ne!( + Address::create_program_address(&[b"Talking", b"Squirrels"], &program_id).unwrap(), + Address::create_program_address(&[b"Talking"], &program_id).unwrap(), + ); + } + + #[test] + fn test_address_off_curve() { + // try a bunch of random input, all successful generated program + // addresses must land off the curve and be unique + let mut addresses = std::vec![]; + for _ in 0..1_000 { + let program_id = Address::new_unique(); + let bytes1 = rand::random::<[u8; 10]>(); + let bytes2 = rand::random::<[u8; 32]>(); + if let Ok(program_address) = + Address::create_program_address(&[&bytes1, &bytes2], &program_id) + { + assert!(!program_address.is_on_curve()); + assert!(!addresses.contains(&program_address)); + addresses.push(program_address); + } + } + } + + #[test] + fn test_find_program_address() { + for _ in 0..1_000 { + let program_id = Address::new_unique(); + let (address, bump_seed) = + Address::find_program_address(&[b"Lil'", b"Bits"], &program_id); + assert_eq!( + address, + Address::create_program_address(&[b"Lil'", b"Bits", &[bump_seed]], &program_id) + .unwrap() + ); + } + } + + fn address_from_seed_by_marker(marker: &[u8]) -> Result { + let key = Address::new_unique(); + let owner = Address::default(); + + let mut to_fake = owner.to_bytes().to_vec(); + to_fake.extend_from_slice(marker); + + let seed = from_utf8(&to_fake[..to_fake.len() - 32]).expect("not utf8"); + let base = &Address::try_from(&to_fake[to_fake.len() - 32..]).unwrap(); + + Address::create_with_seed(&key, seed, base) + } + + #[test] + fn test_create_with_seed_rejects_illegal_owner() { + assert_eq!( + address_from_seed_by_marker(PDA_MARKER), + Err(AddressError::IllegalOwner) + ); + assert!(address_from_seed_by_marker(&PDA_MARKER[1..]).is_ok()); + } + + #[test] + fn test_address_error_from_primitive_exhaustive() { + for variant in AddressError::iter() { + let variant_i64 = variant.clone() as i64; + assert_eq!( + AddressError::from_repr(variant_i64 as usize), + AddressError::from_i64(variant_i64) + ); + assert_eq!(AddressError::from(variant_i64 as u64), variant); + } + } + + #[test] + fn test_parse_address_error_from_primitive_exhaustive() { + for variant in ParseAddressError::iter() { + let variant_i64 = variant as i64; + assert_eq!( + ParseAddressError::from_repr(variant_i64 as usize), + ParseAddressError::from_i64(variant_i64) + ); + } + } + + #[test] + fn test_as_array() { + let bytes = [1u8; 32]; + let key = Address::from(bytes); + assert_eq!(key.as_array(), &bytes); + assert_eq!(key.as_array(), &key.to_bytes()); + // Sanity check: ensure the pointer is the same. + assert_eq!(key.as_array().as_ptr(), key.0.as_ptr()); + } + + #[test] + fn test_address_macro() { + const ADDRESS: Address = + Address::from_str_const("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq"); + assert_eq!( + address!("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq"), + ADDRESS + ); + assert_eq!( + Address::from_str("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq").unwrap(), + ADDRESS + ); + } +} diff --git a/address/src/syscalls.rs b/address/src/syscalls.rs new file mode 100644 index 000000000..066eca496 --- /dev/null +++ b/address/src/syscalls.rs @@ -0,0 +1,447 @@ +#[cfg(all(not(target_os = "solana"), feature = "curve25519"))] +use crate::bytes_are_curve_point; +#[cfg(any(target_os = "solana", feature = "curve25519"))] +use crate::error::AddressError; +use crate::Address; +#[cfg(target_os = "solana")] +/// Syscall definitions used by `solana_address`. +pub use solana_define_syscall::definitions::{ + sol_create_program_address, sol_log_pubkey, sol_try_find_program_address, +}; + +/// Copied from `solana_program::entrypoint::SUCCESS` +/// to avoid a `solana_program` dependency +#[cfg(target_os = "solana")] +const SUCCESS: u64 = 0; + +impl Address { + #[cfg(any(target_os = "solana", feature = "std"))] + /// Log a `Address` from a program + pub fn log(&self) { + #[cfg(target_os = "solana")] + unsafe { + sol_log_pubkey(self.as_ref() as *const _ as *const u8) + }; + + #[cfg(not(target_os = "solana"))] + std::println!("{}", std::string::ToString::to_string(&self)); + } + + /// Find a valid [program derived address][pda] and its corresponding bump seed. + /// + /// [pda]: https://solana.com/docs/core/cpi#program-derived-addresses + /// + /// Program derived addresses (PDAs) are account keys that only the program, + /// `program_id`, has the authority to sign. The address is of the same form + /// as a Solana `Address`, except they are ensured to not be on the ed25519 + /// curve and thus have no associated private key. When performing + /// cross-program invocations the program can "sign" for the key by calling + /// [`invoke_signed`] and passing the same seeds used to generate the + /// address, along with the calculated _bump seed_, which this function + /// returns as the second tuple element. The runtime will verify that the + /// program associated with this address is the caller and thus authorized + /// to be the signer. + /// + /// [`invoke_signed`]: https://docs.rs/solana-program/latest/solana_program/program/fn.invoke_signed.html + /// + /// The `seeds` are application-specific, and must be carefully selected to + /// uniquely derive accounts per application requirements. It is common to + /// use static strings and other addresses as seeds. + /// + /// Because the program address must not lie on the ed25519 curve, there may + /// be seed and program id combinations that are invalid. For this reason, + /// an extra seed (the bump seed) is calculated that results in a + /// point off the curve. The bump seed must be passed as an additional seed + /// when calling `invoke_signed`. + /// + /// The processes of finding a valid program address is by trial and error, + /// and even though it is deterministic given a set of inputs it can take a + /// variable amount of time to succeed across different inputs. This means + /// that when called from an on-chain program it may incur a variable amount + /// of the program's compute budget. Programs that are meant to be very + /// performant may not want to use this function because it could take a + /// considerable amount of time. Programs that are already at risk + /// of exceeding their compute budget should call this with care since + /// there is a chance that the program's budget may be occasionally + /// and unpredictably exceeded. + /// + /// As all account addresses accessed by an on-chain Solana program must be + /// explicitly passed to the program, it is typical for the PDAs to be + /// derived in off-chain client programs, avoiding the compute cost of + /// generating the address on-chain. The address may or may not then be + /// verified by re-deriving it on-chain, depending on the requirements of + /// the program. This verification may be performed without the overhead of + /// re-searching for the bump key by using the [`create_program_address`] + /// function. + /// + /// [`create_program_address`]: Address::create_program_address + /// + /// **Warning**: Because of the way the seeds are hashed there is a potential + /// for program address collisions for the same program id. The seeds are + /// hashed sequentially which means that seeds {"abcdef"}, {"abc", "def"}, + /// and {"ab", "cd", "ef"} will all result in the same program address given + /// the same program id. Since the chance of collision is local to a given + /// program id, the developer of that program must take care to choose seeds + /// that do not collide with each other. For seed schemes that are susceptible + /// to this type of hash collision, a common remedy is to insert separators + /// between seeds, e.g. transforming {"abc", "def"} into {"abc", "-", "def"}. + /// + /// # Panics + /// + /// Panics in the statistically improbable event that a bump seed could not be + /// found. Use [`try_find_program_address`] to handle this case. + /// + /// [`try_find_program_address`]: Address::try_find_program_address + /// + /// Panics if any of the following are true: + /// + /// - the number of provided seeds is greater than, _or equal to_, [`crate::MAX_SEEDS`], + /// - any individual seed's length is greater than [`crate::MAX_SEED_LEN`]. + /// + /// # Examples + /// + /// This example illustrates a simple case of creating a "vault" account + /// which is derived from the payer account, but owned by an on-chain + /// program. The program derived address is derived in an off-chain client + /// program, which invokes an on-chain Solana program that uses the address + /// to create a new account owned and controlled by the program itself. + /// + /// By convention, the on-chain program will be compiled for use in two + /// different contexts: both on-chain, to interpret a custom program + /// instruction as a Solana transaction; and off-chain, as a library, so + /// that clients can share the instruction data structure, constructors, and + /// other common code. + /// + /// First the on-chain Solana program: + /// + /// ``` + /// # use borsh::{BorshSerialize, BorshDeserialize}; + /// # use solana_account_info::{next_account_info, AccountInfo}; + /// # use solana_program_error::ProgramResult; + /// # use solana_cpi::invoke_signed; + /// # use solana_address::Address; + /// # use solana_system_interface::instruction::create_account; + /// // The custom instruction processed by our program. It includes the + /// // PDA's bump seed, which is derived by the client program. This + /// // definition is also imported into the off-chain client program. + /// // The computed address of the PDA will be passed to this program via + /// // the `accounts` vector of the `Instruction` type. + /// #[derive(BorshSerialize, BorshDeserialize, Debug)] + /// # #[borsh(crate = "borsh")] + /// pub struct InstructionData { + /// pub vault_bump_seed: u8, + /// pub lamports: u64, + /// } + /// + /// // The size in bytes of a vault account. The client program needs + /// // this information to calculate the quantity of lamports necessary + /// // to pay for the account's rent. + /// pub static VAULT_ACCOUNT_SIZE: u64 = 1024; + /// + /// // The entrypoint of the on-chain program, as provided to the + /// // `entrypoint!` macro. + /// fn process_instruction( + /// program_id: &Address, + /// accounts: &[AccountInfo], + /// instruction_data: &[u8], + /// ) -> ProgramResult { + /// let account_info_iter = &mut accounts.iter(); + /// let payer = next_account_info(account_info_iter)?; + /// // The vault PDA, derived from the payer's address + /// let vault = next_account_info(account_info_iter)?; + /// + /// let mut instruction_data = instruction_data; + /// let instr = InstructionData::deserialize(&mut instruction_data)?; + /// let vault_bump_seed = instr.vault_bump_seed; + /// let lamports = instr.lamports; + /// let vault_size = VAULT_ACCOUNT_SIZE; + /// + /// // Invoke the system program to create an account while virtually + /// // signing with the vault PDA, which is owned by this caller program. + /// invoke_signed( + /// &create_account( + /// &payer.key, + /// &vault.key, + /// lamports, + /// vault_size, + /// program_id, + /// ), + /// &[ + /// payer.clone(), + /// vault.clone(), + /// ], + /// // A slice of seed slices, each seed slice being the set + /// // of seeds used to generate one of the PDAs required by the + /// // callee program, the final seed being a single-element slice + /// // containing the `u8` bump seed. + /// &[ + /// &[ + /// b"vault", + /// payer.key.as_ref(), + /// &[vault_bump_seed], + /// ], + /// ] + /// )?; + /// + /// Ok(()) + /// } + /// ``` + /// + /// The client program: + /// + /// ``` + /// # use borsh::{BorshSerialize, BorshDeserialize}; + /// # use solana_example_mocks::{solana_sdk, solana_rpc_client}; + /// # use solana_address::Address; + /// # use solana_instruction::{AccountMeta, Instruction}; + /// # use solana_hash::Hash; + /// # use solana_sdk::{ + /// # signature::Keypair, + /// # signature::{Signer, Signature}, + /// # transaction::Transaction, + /// # }; + /// # use solana_rpc_client::rpc_client::RpcClient; + /// # use std::convert::TryFrom; + /// # use anyhow::Result; + /// # + /// # #[derive(BorshSerialize, BorshDeserialize, Debug)] + /// # #[borsh(crate = "borsh")] + /// # struct InstructionData { + /// # pub vault_bump_seed: u8, + /// # pub lamports: u64, + /// # } + /// # + /// # pub static VAULT_ACCOUNT_SIZE: u64 = 1024; + /// # + /// fn create_vault_account( + /// client: &RpcClient, + /// program_id: Address, + /// payer: &Keypair, + /// ) -> Result<()> { + /// // Derive the PDA from the payer account, a string representing the unique + /// // purpose of the account ("vault"), and the address of our on-chain program. + /// let (vault_address, vault_bump_seed) = Address::find_program_address( + /// &[b"vault", payer.pubkey().as_ref()], + /// &program_id + /// ); + /// + /// // Get the amount of lamports needed to pay for the vault's rent + /// let vault_account_size = usize::try_from(VAULT_ACCOUNT_SIZE)?; + /// let lamports = client.get_minimum_balance_for_rent_exemption(vault_account_size)?; + /// + /// // The on-chain program's instruction data, imported from that program's crate. + /// let instr_data = InstructionData { + /// vault_bump_seed, + /// lamports, + /// }; + /// + /// // The accounts required by both our on-chain program and the system program's + /// // `create_account` instruction, including the vault's address. + /// let accounts = vec![ + /// AccountMeta::new(payer.pubkey(), true), + /// AccountMeta::new(vault_address, false), + /// AccountMeta::new(solana_system_interface::program::ID, false), + /// ]; + /// + /// // Create the instruction by serializing our instruction data via borsh + /// let instruction = Instruction::new_with_borsh( + /// program_id, + /// &instr_data, + /// accounts, + /// ); + /// + /// let blockhash = client.get_latest_blockhash()?; + /// + /// let transaction = Transaction::new_signed_with_payer( + /// &[instruction], + /// Some(&payer.pubkey()), + /// &[payer], + /// blockhash, + /// ); + /// + /// client.send_and_confirm_transaction(&transaction)?; + /// + /// Ok(()) + /// } + /// # let program_id = Address::new_unique(); + /// # let payer = Keypair::new(); + /// # let client = RpcClient::new(String::new()); + /// # + /// # create_vault_account(&client, program_id, &payer)?; + /// # + /// # Ok::<(), anyhow::Error>(()) + /// ``` + // If target_os = "solana", then the function will use + // syscalls which bring no dependencies. + // When target_os != "solana", this should be opt-in so users + // don't need the curve25519 dependency. + #[cfg(any(target_os = "solana", feature = "curve25519"))] + pub fn find_program_address(seeds: &[&[u8]], program_id: &Address) -> (Address, u8) { + Self::try_find_program_address(seeds, program_id) + .unwrap_or_else(|| panic!("Unable to find a viable program address bump seed")) + } + + /// Find a valid [program derived address][pda] and its corresponding bump seed. + /// + /// [pda]: https://solana.com/docs/core/cpi#program-derived-addresses + /// + /// The only difference between this method and [`find_program_address`] + /// is that this one returns `None` in the statistically improbable event + /// that a bump seed cannot be found; or if any of `find_program_address`'s + /// preconditions are violated. + /// + /// See the documentation for [`find_program_address`] for a full description. + /// + /// [`find_program_address`]: Address::find_program_address + // If target_os = "solana", then the function will use + // syscalls which bring no dependencies. + // When target_os != "solana", this should be opt-in so users + // don't need the curve25519 dependency. + #[cfg(any(target_os = "solana", feature = "curve25519"))] + #[allow(clippy::same_item_push)] + pub fn try_find_program_address( + seeds: &[&[u8]], + program_id: &Address, + ) -> Option<(Address, u8)> { + // Perform the calculation inline, calling this from within a program is + // not supported + #[cfg(not(target_os = "solana"))] + { + let mut bump_seed = [u8::MAX]; + for _ in 0..u8::MAX { + { + let mut seeds_with_bump = seeds.to_vec(); + seeds_with_bump.push(&bump_seed); + match Self::create_program_address(&seeds_with_bump, program_id) { + Ok(address) => return Some((address, bump_seed[0])), + Err(AddressError::InvalidSeeds) => (), + _ => break, + } + } + bump_seed[0] -= 1; + } + None + } + // Call via a system call to perform the calculation + #[cfg(target_os = "solana")] + { + let mut bytes = [0; 32]; + let mut bump_seed = u8::MAX; + let result = unsafe { + crate::syscalls::sol_try_find_program_address( + seeds as *const _ as *const u8, + seeds.len() as u64, + program_id as *const _ as *const u8, + &mut bytes as *mut _ as *mut u8, + &mut bump_seed as *mut _ as *mut u8, + ) + }; + match result { + SUCCESS => Some((Address::from(bytes), bump_seed)), + _ => None, + } + } + } + + /// Create a valid [program derived address][pda] without searching for a bump seed. + /// + /// [pda]: https://solana.com/docs/core/cpi#program-derived-addresses + /// + /// Because this function does not create a bump seed, it may unpredictably + /// return an error for any given set of seeds and is not generally suitable + /// for creating program derived addresses. + /// + /// However, it can be used for efficiently verifying that a set of seeds plus + /// bump seed generated by [`find_program_address`] derives a particular + /// address as expected. See the example for details. + /// + /// See the documentation for [`find_program_address`] for a full description + /// of program derived addresses and bump seeds. + /// + /// [`find_program_address`]: Address::find_program_address + /// + /// # Examples + /// + /// Creating a program derived address involves iteratively searching for a + /// bump seed for which the derived [`Address`] does not lie on the ed25519 + /// curve. This search process is generally performed off-chain, with the + /// [`find_program_address`] function, after which the client passes the + /// bump seed to the program as instruction data. + /// + /// Depending on the application requirements, a program may wish to verify + /// that the set of seeds, plus the bump seed, do correctly generate an + /// expected address. + /// + /// The verification is performed by appending to the other seeds one + /// additional seed slice that contains the single `u8` bump seed, calling + /// `create_program_address`, checking that the return value is `Ok`, and + /// that the returned `Address` has the expected value. + /// + /// ``` + /// # use solana_address::Address; + /// # let program_id = Address::new_unique(); + /// let (expected_pda, bump_seed) = Address::find_program_address(&[b"vault"], &program_id); + /// let actual_pda = Address::create_program_address(&[b"vault", &[bump_seed]], &program_id)?; + /// assert_eq!(expected_pda, actual_pda); + /// # Ok::<(), anyhow::Error>(()) + /// ``` + // If target_os = "solana", then the function will use + // syscalls which bring no dependencies. + // When target_os != "solana", this should be opt-in so users + // don't need the curve225519 dep. + #[cfg(any(target_os = "solana", feature = "curve25519"))] + pub fn create_program_address( + seeds: &[&[u8]], + program_id: &Address, + ) -> Result { + use crate::MAX_SEEDS; + + if seeds.len() > MAX_SEEDS { + return Err(AddressError::MaxSeedLengthExceeded); + } + for seed in seeds.iter() { + use crate::MAX_SEED_LEN; + + if seed.len() > MAX_SEED_LEN { + return Err(AddressError::MaxSeedLengthExceeded); + } + } + + // Perform the calculation inline, calling this from within a program is + // not supported + #[cfg(not(target_os = "solana"))] + { + use crate::PDA_MARKER; + + let mut hasher = solana_sha256_hasher::Hasher::default(); + for seed in seeds.iter() { + hasher.hash(seed); + } + hasher.hashv(&[program_id.as_ref(), PDA_MARKER]); + let hash = hasher.result(); + + if bytes_are_curve_point(hash) { + return Err(AddressError::InvalidSeeds); + } + + Ok(Address::from(hash.to_bytes())) + } + // Call via a system call to perform the calculation + #[cfg(target_os = "solana")] + { + let mut bytes = [0; 32]; + let result = unsafe { + crate::syscalls::sol_create_program_address( + seeds as *const _ as *const u8, + seeds.len() as u64, + program_id as *const _ as *const u8, + &mut bytes as *mut _ as *mut u8, + ) + }; + match result { + SUCCESS => Ok(Address::from(bytes)), + _ => Err(result.into()), + } + } + } +} diff --git a/genesis-config/src/lib.rs b/genesis-config/src/lib.rs index 2c945ad0f..bc181f93f 100644 --- a/genesis-config/src/lib.rs +++ b/genesis-config/src/lib.rs @@ -49,7 +49,7 @@ pub const UNUSED_DEFAULT: u64 = 1024; #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "D9VFRSj4fodCuKFC9omQY2zY2Uw8wo6SzJFLeMJaVigm") + frozen_abi(digest = "3tUUJkZiUUGfuNCXbDuDR6KCQYPsh3m3cPw5vVUSt113") )] #[cfg_attr( feature = "serde", diff --git a/message/src/legacy.rs b/message/src/legacy.rs index 91e1d9e7b..637883ad6 100644 --- a/message/src/legacy.rs +++ b/message/src/legacy.rs @@ -66,7 +66,7 @@ fn compile_instructions(ixs: &[Instruction], keys: &[Pubkey]) -> Vec Option { - Some(match *self { - PubkeyError::MaxSeedLengthExceeded => PubkeyError::MaxSeedLengthExceeded as i64, - PubkeyError::InvalidSeeds => PubkeyError::InvalidSeeds as i64, - PubkeyError::IllegalOwner => PubkeyError::IllegalOwner as i64, - }) - } - #[inline] - fn to_u64(&self) -> Option { - self.to_i64().map(|x| x as u64) - } -} - -impl FromPrimitive for PubkeyError { - #[inline] - fn from_i64(n: i64) -> Option { - if n == PubkeyError::MaxSeedLengthExceeded as i64 { - Some(PubkeyError::MaxSeedLengthExceeded) - } else if n == PubkeyError::InvalidSeeds as i64 { - Some(PubkeyError::InvalidSeeds) - } else if n == PubkeyError::IllegalOwner as i64 { - Some(PubkeyError::IllegalOwner) - } else { - None - } - } - #[inline] - fn from_u64(n: u64) -> Option { - Self::from_i64(n as i64) - } -} - -impl core::error::Error for PubkeyError {} - -impl fmt::Display for PubkeyError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - PubkeyError::MaxSeedLengthExceeded => { - f.write_str("Length of the seed is too long for address generation") - } - PubkeyError::InvalidSeeds => { - f.write_str("Provided seeds do not result in a valid address") - } - PubkeyError::IllegalOwner => f.write_str("Provided owner is not allowed"), - } - } -} - -impl From for PubkeyError { - fn from(error: u64) -> Self { - match error { - 0 => PubkeyError::MaxSeedLengthExceeded, - 1 => PubkeyError::InvalidSeeds, - 2 => PubkeyError::IllegalOwner, - _ => panic!("Unsupported PubkeyError"), - } - } -} - -impl From for ProgramError { - fn from(error: PubkeyError) -> Self { - match error { - PubkeyError::MaxSeedLengthExceeded => Self::MaxSeedLengthExceeded, - PubkeyError::InvalidSeeds => Self::InvalidSeeds, - PubkeyError::IllegalOwner => Self::IllegalOwner, - } - } -} - -/// The address of a [Solana account][acc]. -/// -/// Some account addresses are [ed25519] public keys, with corresponding secret -/// keys that are managed off-chain. Often, though, account addresses do not -/// have corresponding secret keys — as with [_program derived -/// addresses_][pdas] — or the secret key is not relevant to the operation -/// of a program, and may have even been disposed of. As running Solana programs -/// can not safely create or manage secret keys, the full [`Keypair`] is not -/// defined in `solana-program` but in `solana-sdk`. -/// -/// [acc]: https://solana.com/docs/core/accounts -/// [ed25519]: https://ed25519.cr.yp.to/ -/// [pdas]: https://solana.com/docs/core/cpi#program-derived-addresses -/// [`Keypair`]: https://docs.rs/solana-sdk/latest/solana_sdk/signer/keypair/struct.Keypair.html -#[repr(transparent)] -#[cfg_attr(feature = "frozen-abi", derive(solana_frozen_abi_macro::AbiExample))] -#[cfg_attr( - feature = "borsh", - derive(BorshSerialize, BorshDeserialize), - borsh(crate = "borsh") -)] -#[cfg_attr(all(feature = "borsh", feature = "std"), derive(BorshSchema))] -#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] -#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))] -#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)] -#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))] -pub struct Pubkey(pub(crate) [u8; 32]); - -/// Custom impl of Hash for Pubkey -/// allows us to skip hashing the length of the pubkey -/// which is always the same anyway -impl Hash for Pubkey { - fn hash(&self, state: &mut H) { - state.write(self.as_array()); - } -} - -#[cfg(all(feature = "rand", not(target_os = "solana")))] -mod hasher { - use { - crate::PUBKEY_BYTES, - core::{ - cell::Cell, - hash::{BuildHasher, Hasher}, - mem, - }, - rand::{thread_rng, Rng}, - }; - - /// A faster, but less collision resistant hasher for pubkeys. - /// - /// Specialized hasher that uses a random 8 bytes subslice of the - /// pubkey as the hash value. Should not be used when collisions - /// might be used to mount DOS attacks. - /// - /// Using this results in about 4x faster lookups in a typical hashmap. - #[derive(Default)] - pub struct PubkeyHasher { - offset: usize, - state: u64, - } - - impl Hasher for PubkeyHasher { - #[inline] - fn finish(&self) -> u64 { - self.state - } - #[inline] - fn write(&mut self, bytes: &[u8]) { - debug_assert_eq!( - bytes.len(), - PUBKEY_BYTES, - "This hasher is intended to be used with pubkeys and nothing else" - ); - // This slice/unwrap can never panic since offset is < PUBKEY_BYTES - mem::size_of::() - let chunk: &[u8; mem::size_of::()] = bytes - [self.offset..self.offset + mem::size_of::()] - .try_into() - .unwrap(); - self.state = u64::from_ne_bytes(*chunk); - } - } - - /// A builder for faster, but less collision resistant hasher for pubkeys. - /// - /// Initializes `PubkeyHasher` instances that use an 8-byte - /// slice of the pubkey as the hash value. Should not be used when - /// collisions might be used to mount DOS attacks. - /// - /// Using this results in about 4x faster lookups in a typical hashmap. - #[derive(Clone)] - pub struct PubkeyHasherBuilder { - offset: usize, - } - - impl Default for PubkeyHasherBuilder { - /// Default construct the PubkeyHasherBuilder. - /// - /// The position of the slice is determined initially - /// through random draw and then by incrementing a thread-local - /// This way each hashmap can be expected to use a slightly different - /// slice. This is essentially the same mechanism as what is used by - /// `RandomState` - fn default() -> Self { - std::thread_local!(static OFFSET: Cell = { - let mut rng = thread_rng(); - Cell::new(rng.gen_range(0..PUBKEY_BYTES - mem::size_of::())) - }); - - let offset = OFFSET.with(|offset| { - let mut next_offset = offset.get() + 1; - if next_offset > PUBKEY_BYTES - mem::size_of::() { - next_offset = 0; - } - offset.set(next_offset); - next_offset - }); - PubkeyHasherBuilder { offset } - } - } - - impl BuildHasher for PubkeyHasherBuilder { - type Hasher = PubkeyHasher; - #[inline] - fn build_hasher(&self) -> Self::Hasher { - PubkeyHasher { - offset: self.offset, - state: 0, - } - } - } - - #[cfg(test)] - mod tests { - use { - super::PubkeyHasherBuilder, - crate::Pubkey, - core::hash::{BuildHasher, Hasher}, - }; - #[test] - fn test_pubkey_hasher_builder() { - let key = Pubkey::new_unique(); - let builder = PubkeyHasherBuilder::default(); - let mut hasher1 = builder.build_hasher(); - let mut hasher2 = builder.build_hasher(); - hasher1.write(key.as_array()); - hasher2.write(key.as_array()); - assert_eq!( - hasher1.finish(), - hasher2.finish(), - "Hashers made with same builder should be identical" - ); - // Make sure that when we make new builders we get different slices - // chosen for hashing - let builder2 = PubkeyHasherBuilder::default(); - for _ in 0..64 { - let mut hasher3 = builder2.build_hasher(); - hasher3.write(key.as_array()); - std::dbg!(hasher1.finish()); - std::dbg!(hasher3.finish()); - if hasher1.finish() != hasher3.finish() { - return; - } - } - panic!("Hashers built with different builder should be different due to random offset"); - } - - #[test] - fn test_pubkey_hasher() { - let key1 = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - let builder = PubkeyHasherBuilder::default(); - let mut hasher1 = builder.build_hasher(); - let mut hasher2 = builder.build_hasher(); - hasher1.write(key1.as_array()); - hasher2.write(key2.as_array()); - assert_ne!(hasher1.finish(), hasher2.finish()); - } - } -} -#[cfg(all(feature = "rand", not(target_os = "solana")))] -pub use hasher::{PubkeyHasher, PubkeyHasherBuilder}; - -impl solana_sanitize::Sanitize for Pubkey {} - -// Use strum when testing to ensure our FromPrimitive -// impl is exhaustive -#[cfg_attr(test, derive(strum_macros::FromRepr, strum_macros::EnumIter))] -#[cfg_attr(feature = "serde", derive(Serialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ParsePubkeyError { - WrongSize, - Invalid, -} - -impl ToPrimitive for ParsePubkeyError { - #[inline] - fn to_i64(&self) -> Option { - Some(match *self { - ParsePubkeyError::WrongSize => ParsePubkeyError::WrongSize as i64, - ParsePubkeyError::Invalid => ParsePubkeyError::Invalid as i64, - }) - } - #[inline] - fn to_u64(&self) -> Option { - self.to_i64().map(|x| x as u64) - } -} - -impl FromPrimitive for ParsePubkeyError { - #[inline] - fn from_i64(n: i64) -> Option { - if n == ParsePubkeyError::WrongSize as i64 { - Some(ParsePubkeyError::WrongSize) - } else if n == ParsePubkeyError::Invalid as i64 { - Some(ParsePubkeyError::Invalid) - } else { - None - } - } - #[inline] - fn from_u64(n: u64) -> Option { - Self::from_i64(n as i64) - } -} - -impl core::error::Error for ParsePubkeyError {} - -impl fmt::Display for ParsePubkeyError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ParsePubkeyError::WrongSize => f.write_str("String is the wrong size"), - ParsePubkeyError::Invalid => f.write_str("Invalid Base58 string"), - } - } -} - -impl From for ParsePubkeyError { - fn from(_: Infallible) -> Self { - unreachable!("Infallible uninhabited"); - } -} - -impl FromStr for Pubkey { - type Err = ParsePubkeyError; - - fn from_str(s: &str) -> Result { - use five8::DecodeError; - if s.len() > MAX_BASE58_LEN { - return Err(ParsePubkeyError::WrongSize); - } - let mut bytes = [0; PUBKEY_BYTES]; - five8::decode_32(s, &mut bytes).map_err(|e| match e { - DecodeError::InvalidChar(_) => ParsePubkeyError::Invalid, - DecodeError::TooLong - | DecodeError::TooShort - | DecodeError::LargestTermTooHigh - | DecodeError::OutputTooLong => ParsePubkeyError::WrongSize, - })?; - Ok(Pubkey(bytes)) - } -} - -impl From<&Pubkey> for Pubkey { - #[inline] - fn from(value: &Pubkey) -> Self { - *value - } -} - -impl From<[u8; 32]> for Pubkey { - #[inline] - fn from(from: [u8; 32]) -> Self { - Self(from) - } -} - -impl TryFrom<&[u8]> for Pubkey { - type Error = array::TryFromSliceError; - - #[inline] - fn try_from(pubkey: &[u8]) -> Result { - <[u8; 32]>::try_from(pubkey).map(Self::from) - } -} - -#[cfg(feature = "std")] -impl TryFrom> for Pubkey { - type Error = Vec; - - #[inline] - fn try_from(pubkey: Vec) -> Result { - <[u8; 32]>::try_from(pubkey).map(Self::from) - } -} - -impl TryFrom<&str> for Pubkey { - type Error = ParsePubkeyError; - fn try_from(s: &str) -> Result { - Pubkey::from_str(s) - } -} - // If target_os = "solana", then this panics so there are no dependencies. // When target_os != "solana", this should be opt-in so users // don't need the curve25519 dependency. #[cfg(any(target_os = "solana", feature = "curve25519"))] -#[allow(clippy::used_underscore_binding)] -pub fn bytes_are_curve_point>(_bytes: T) -> bool { - #[cfg(not(target_os = "solana"))] - { - let Ok(compressed_edwards_y) = - curve25519_dalek::edwards::CompressedEdwardsY::from_slice(_bytes.as_ref()) - else { - return false; - }; - compressed_edwards_y.decompress().is_some() - } - #[cfg(target_os = "solana")] - unimplemented!(); -} - -impl Pubkey { - pub const fn new_from_array(pubkey_array: [u8; 32]) -> Self { - Self(pubkey_array) - } - - /// Decode a string into a Pubkey, usable in a const context - pub const fn from_str_const(s: &str) -> Self { - let id_array = five8_const::decode_32_const(s); - Pubkey::new_from_array(id_array) - } - - /// unique Pubkey for tests and benchmarks. - pub fn new_unique() -> Self { - use solana_atomic_u64::AtomicU64; - static I: AtomicU64 = AtomicU64::new(1); - type T = u32; - const COUNTER_BYTES: usize = mem::size_of::(); - let mut b = [0u8; PUBKEY_BYTES]; - #[cfg(feature = "std")] - let mut i = I.fetch_add(1) as T; - #[cfg(not(feature = "std"))] - let i = I.fetch_add(1) as T; - // use big endian representation to ensure that recent unique pubkeys - // are always greater than less recent unique pubkeys. - b[0..COUNTER_BYTES].copy_from_slice(&i.to_be_bytes()); - // fill the rest of the pubkey with pseudorandom numbers to make - // data statistically similar to real pubkeys. - #[cfg(feature = "std")] - { - let mut hash = std::hash::DefaultHasher::new(); - for slice in b[COUNTER_BYTES..].chunks_mut(COUNTER_BYTES) { - hash.write_u32(i); - i += 1; - slice.copy_from_slice(&hash.finish().to_ne_bytes()[0..COUNTER_BYTES]); - } - } - // if std is not available, just replicate last byte of the counter. - // this is not as good as a proper hash, but at least it is uniform - #[cfg(not(feature = "std"))] - { - for b in b[COUNTER_BYTES..].iter_mut() { - *b = (i & 0xFF) as u8; - } - } - Self::from(b) - } - - // If target_os = "solana", then the solana_sha256_hasher crate will use - // syscalls which bring no dependencies. - // When target_os != "solana", this should be opt-in so users - // don't need the sha2 dependency. - #[cfg(any(target_os = "solana", feature = "sha2"))] - pub fn create_with_seed( - base: &Pubkey, - seed: &str, - owner: &Pubkey, - ) -> Result { - if seed.len() > MAX_SEED_LEN { - return Err(PubkeyError::MaxSeedLengthExceeded); - } - - let owner = owner.as_ref(); - if owner.len() >= PDA_MARKER.len() { - let slice = &owner[owner.len() - PDA_MARKER.len()..]; - if slice == PDA_MARKER { - return Err(PubkeyError::IllegalOwner); - } - } - let hash = solana_sha256_hasher::hashv(&[base.as_ref(), seed.as_ref(), owner]); - Ok(Pubkey::from(hash.to_bytes())) - } - - /// Find a valid [program derived address][pda] and its corresponding bump seed. - /// - /// [pda]: https://solana.com/docs/core/cpi#program-derived-addresses - /// - /// Program derived addresses (PDAs) are account keys that only the program, - /// `program_id`, has the authority to sign. The address is of the same form - /// as a Solana `Pubkey`, except they are ensured to not be on the ed25519 - /// curve and thus have no associated private key. When performing - /// cross-program invocations the program can "sign" for the key by calling - /// [`invoke_signed`] and passing the same seeds used to generate the - /// address, along with the calculated _bump seed_, which this function - /// returns as the second tuple element. The runtime will verify that the - /// program associated with this address is the caller and thus authorized - /// to be the signer. - /// - /// [`invoke_signed`]: https://docs.rs/solana-program/latest/solana_program/program/fn.invoke_signed.html - /// - /// The `seeds` are application-specific, and must be carefully selected to - /// uniquely derive accounts per application requirements. It is common to - /// use static strings and other pubkeys as seeds. - /// - /// Because the program address must not lie on the ed25519 curve, there may - /// be seed and program id combinations that are invalid. For this reason, - /// an extra seed (the bump seed) is calculated that results in a - /// point off the curve. The bump seed must be passed as an additional seed - /// when calling `invoke_signed`. - /// - /// The processes of finding a valid program address is by trial and error, - /// and even though it is deterministic given a set of inputs it can take a - /// variable amount of time to succeed across different inputs. This means - /// that when called from an on-chain program it may incur a variable amount - /// of the program's compute budget. Programs that are meant to be very - /// performant may not want to use this function because it could take a - /// considerable amount of time. Programs that are already at risk - /// of exceeding their compute budget should call this with care since - /// there is a chance that the program's budget may be occasionally - /// and unpredictably exceeded. - /// - /// As all account addresses accessed by an on-chain Solana program must be - /// explicitly passed to the program, it is typical for the PDAs to be - /// derived in off-chain client programs, avoiding the compute cost of - /// generating the address on-chain. The address may or may not then be - /// verified by re-deriving it on-chain, depending on the requirements of - /// the program. This verification may be performed without the overhead of - /// re-searching for the bump key by using the [`create_program_address`] - /// function. - /// - /// [`create_program_address`]: Pubkey::create_program_address - /// - /// **Warning**: Because of the way the seeds are hashed there is a potential - /// for program address collisions for the same program id. The seeds are - /// hashed sequentially which means that seeds {"abcdef"}, {"abc", "def"}, - /// and {"ab", "cd", "ef"} will all result in the same program address given - /// the same program id. Since the chance of collision is local to a given - /// program id, the developer of that program must take care to choose seeds - /// that do not collide with each other. For seed schemes that are susceptible - /// to this type of hash collision, a common remedy is to insert separators - /// between seeds, e.g. transforming {"abc", "def"} into {"abc", "-", "def"}. - /// - /// # Panics - /// - /// Panics in the statistically improbable event that a bump seed could not be - /// found. Use [`try_find_program_address`] to handle this case. - /// - /// [`try_find_program_address`]: Pubkey::try_find_program_address - /// - /// Panics if any of the following are true: - /// - /// - the number of provided seeds is greater than, _or equal to_, [`MAX_SEEDS`], - /// - any individual seed's length is greater than [`MAX_SEED_LEN`]. - /// - /// # Examples - /// - /// This example illustrates a simple case of creating a "vault" account - /// which is derived from the payer account, but owned by an on-chain - /// program. The program derived address is derived in an off-chain client - /// program, which invokes an on-chain Solana program that uses the address - /// to create a new account owned and controlled by the program itself. - /// - /// By convention, the on-chain program will be compiled for use in two - /// different contexts: both on-chain, to interpret a custom program - /// instruction as a Solana transaction; and off-chain, as a library, so - /// that clients can share the instruction data structure, constructors, and - /// other common code. - /// - /// First the on-chain Solana program: - /// - /// ``` - /// # use borsh::{BorshSerialize, BorshDeserialize}; - /// # use solana_account_info::{next_account_info, AccountInfo}; - /// # use solana_program_error::ProgramResult; - /// # use solana_cpi::invoke_signed; - /// # use solana_pubkey::Pubkey; - /// # use solana_system_interface::instruction::create_account; - /// // The custom instruction processed by our program. It includes the - /// // PDA's bump seed, which is derived by the client program. This - /// // definition is also imported into the off-chain client program. - /// // The computed address of the PDA will be passed to this program via - /// // the `accounts` vector of the `Instruction` type. - /// #[derive(BorshSerialize, BorshDeserialize, Debug)] - /// # #[borsh(crate = "borsh")] - /// pub struct InstructionData { - /// pub vault_bump_seed: u8, - /// pub lamports: u64, - /// } - /// - /// // The size in bytes of a vault account. The client program needs - /// // this information to calculate the quantity of lamports necessary - /// // to pay for the account's rent. - /// pub static VAULT_ACCOUNT_SIZE: u64 = 1024; - /// - /// // The entrypoint of the on-chain program, as provided to the - /// // `entrypoint!` macro. - /// fn process_instruction( - /// program_id: &Pubkey, - /// accounts: &[AccountInfo], - /// instruction_data: &[u8], - /// ) -> ProgramResult { - /// let account_info_iter = &mut accounts.iter(); - /// let payer = next_account_info(account_info_iter)?; - /// // The vault PDA, derived from the payer's address - /// let vault = next_account_info(account_info_iter)?; - /// - /// let mut instruction_data = instruction_data; - /// let instr = InstructionData::deserialize(&mut instruction_data)?; - /// let vault_bump_seed = instr.vault_bump_seed; - /// let lamports = instr.lamports; - /// let vault_size = VAULT_ACCOUNT_SIZE; - /// - /// // Invoke the system program to create an account while virtually - /// // signing with the vault PDA, which is owned by this caller program. - /// invoke_signed( - /// &create_account( - /// &payer.key, - /// &vault.key, - /// lamports, - /// vault_size, - /// &program_id, - /// ), - /// &[ - /// payer.clone(), - /// vault.clone(), - /// ], - /// // A slice of seed slices, each seed slice being the set - /// // of seeds used to generate one of the PDAs required by the - /// // callee program, the final seed being a single-element slice - /// // containing the `u8` bump seed. - /// &[ - /// &[ - /// b"vault", - /// payer.key.as_ref(), - /// &[vault_bump_seed], - /// ], - /// ] - /// )?; - /// - /// Ok(()) - /// } - /// ``` - /// - /// The client program: - /// - /// ``` - /// # use borsh::{BorshSerialize, BorshDeserialize}; - /// # use solana_example_mocks::{solana_sdk, solana_rpc_client}; - /// # use solana_pubkey::Pubkey; - /// # use solana_instruction::{AccountMeta, Instruction}; - /// # use solana_hash::Hash; - /// # use solana_sdk::{ - /// # signature::Keypair, - /// # signature::{Signer, Signature}, - /// # transaction::Transaction, - /// # }; - /// # use solana_rpc_client::rpc_client::RpcClient; - /// # use std::convert::TryFrom; - /// # use anyhow::Result; - /// # - /// # #[derive(BorshSerialize, BorshDeserialize, Debug)] - /// # #[borsh(crate = "borsh")] - /// # struct InstructionData { - /// # pub vault_bump_seed: u8, - /// # pub lamports: u64, - /// # } - /// # - /// # pub static VAULT_ACCOUNT_SIZE: u64 = 1024; - /// # - /// fn create_vault_account( - /// client: &RpcClient, - /// program_id: Pubkey, - /// payer: &Keypair, - /// ) -> Result<()> { - /// // Derive the PDA from the payer account, a string representing the unique - /// // purpose of the account ("vault"), and the address of our on-chain program. - /// let (vault_pubkey, vault_bump_seed) = Pubkey::find_program_address( - /// &[b"vault", payer.pubkey().as_ref()], - /// &program_id - /// ); - /// - /// // Get the amount of lamports needed to pay for the vault's rent - /// let vault_account_size = usize::try_from(VAULT_ACCOUNT_SIZE)?; - /// let lamports = client.get_minimum_balance_for_rent_exemption(vault_account_size)?; - /// - /// // The on-chain program's instruction data, imported from that program's crate. - /// let instr_data = InstructionData { - /// vault_bump_seed, - /// lamports, - /// }; - /// - /// // The accounts required by both our on-chain program and the system program's - /// // `create_account` instruction, including the vault's address. - /// let accounts = vec![ - /// AccountMeta::new(payer.pubkey(), true), - /// AccountMeta::new(vault_pubkey, false), - /// AccountMeta::new(solana_system_interface::program::ID, false), - /// ]; - /// - /// // Create the instruction by serializing our instruction data via borsh - /// let instruction = Instruction::new_with_borsh( - /// program_id, - /// &instr_data, - /// accounts, - /// ); - /// - /// let blockhash = client.get_latest_blockhash()?; - /// - /// let transaction = Transaction::new_signed_with_payer( - /// &[instruction], - /// Some(&payer.pubkey()), - /// &[payer], - /// blockhash, - /// ); - /// - /// client.send_and_confirm_transaction(&transaction)?; - /// - /// Ok(()) - /// } - /// # let program_id = Pubkey::new_unique(); - /// # let payer = Keypair::new(); - /// # let client = RpcClient::new(String::new()); - /// # - /// # create_vault_account(&client, program_id, &payer)?; - /// # - /// # Ok::<(), anyhow::Error>(()) - /// ``` - // If target_os = "solana", then the function will use - // syscalls which bring no dependencies. - // When target_os != "solana", this should be opt-in so users - // don't need the curve25519 dependency. - #[cfg(any(target_os = "solana", feature = "curve25519"))] - pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) { - Self::try_find_program_address(seeds, program_id) - .unwrap_or_else(|| panic!("Unable to find a viable program address bump seed")) - } - - /// Find a valid [program derived address][pda] and its corresponding bump seed. - /// - /// [pda]: https://solana.com/docs/core/cpi#program-derived-addresses - /// - /// The only difference between this method and [`find_program_address`] - /// is that this one returns `None` in the statistically improbable event - /// that a bump seed cannot be found; or if any of `find_program_address`'s - /// preconditions are violated. - /// - /// See the documentation for [`find_program_address`] for a full description. - /// - /// [`find_program_address`]: Pubkey::find_program_address - // If target_os = "solana", then the function will use - // syscalls which bring no dependencies. - // When target_os != "solana", this should be opt-in so users - // don't need the curve25519 dependency. - #[cfg(any(target_os = "solana", feature = "curve25519"))] - #[allow(clippy::same_item_push)] - pub fn try_find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Option<(Pubkey, u8)> { - // Perform the calculation inline, calling this from within a program is - // not supported - #[cfg(not(target_os = "solana"))] - { - let mut bump_seed = [u8::MAX]; - for _ in 0..u8::MAX { - { - let mut seeds_with_bump = seeds.to_vec(); - seeds_with_bump.push(&bump_seed); - match Self::create_program_address(&seeds_with_bump, program_id) { - Ok(address) => return Some((address, bump_seed[0])), - Err(PubkeyError::InvalidSeeds) => (), - _ => break, - } - } - bump_seed[0] -= 1; - } - None - } - // Call via a system call to perform the calculation - #[cfg(target_os = "solana")] - { - let mut bytes = [0; 32]; - let mut bump_seed = u8::MAX; - let result = unsafe { - crate::syscalls::sol_try_find_program_address( - seeds as *const _ as *const u8, - seeds.len() as u64, - program_id as *const _ as *const u8, - &mut bytes as *mut _ as *mut u8, - &mut bump_seed as *mut _ as *mut u8, - ) - }; - match result { - SUCCESS => Some((Pubkey::from(bytes), bump_seed)), - _ => None, - } - } - } - - /// Create a valid [program derived address][pda] without searching for a bump seed. - /// - /// [pda]: https://solana.com/docs/core/cpi#program-derived-addresses - /// - /// Because this function does not create a bump seed, it may unpredictably - /// return an error for any given set of seeds and is not generally suitable - /// for creating program derived addresses. - /// - /// However, it can be used for efficiently verifying that a set of seeds plus - /// bump seed generated by [`find_program_address`] derives a particular - /// address as expected. See the example for details. - /// - /// See the documentation for [`find_program_address`] for a full description - /// of program derived addresses and bump seeds. - /// - /// [`find_program_address`]: Pubkey::find_program_address - /// - /// # Examples - /// - /// Creating a program derived address involves iteratively searching for a - /// bump seed for which the derived [`Pubkey`] does not lie on the ed25519 - /// curve. This search process is generally performed off-chain, with the - /// [`find_program_address`] function, after which the client passes the - /// bump seed to the program as instruction data. - /// - /// Depending on the application requirements, a program may wish to verify - /// that the set of seeds, plus the bump seed, do correctly generate an - /// expected address. - /// - /// The verification is performed by appending to the other seeds one - /// additional seed slice that contains the single `u8` bump seed, calling - /// `create_program_address`, checking that the return value is `Ok`, and - /// that the returned `Pubkey` has the expected value. - /// - /// ``` - /// # use solana_pubkey::Pubkey; - /// # let program_id = Pubkey::new_unique(); - /// let (expected_pda, bump_seed) = Pubkey::find_program_address(&[b"vault"], &program_id); - /// let actual_pda = Pubkey::create_program_address(&[b"vault", &[bump_seed]], &program_id)?; - /// assert_eq!(expected_pda, actual_pda); - /// # Ok::<(), anyhow::Error>(()) - /// ``` - // If target_os = "solana", then the function will use - // syscalls which bring no dependencies. - // When target_os != "solana", this should be opt-in so users - // don't need the curve225519 dep. - #[cfg(any(target_os = "solana", feature = "curve25519"))] - pub fn create_program_address( - seeds: &[&[u8]], - program_id: &Pubkey, - ) -> Result { - if seeds.len() > MAX_SEEDS { - return Err(PubkeyError::MaxSeedLengthExceeded); - } - for seed in seeds.iter() { - if seed.len() > MAX_SEED_LEN { - return Err(PubkeyError::MaxSeedLengthExceeded); - } - } - - // Perform the calculation inline, calling this from within a program is - // not supported - #[cfg(not(target_os = "solana"))] - { - let mut hasher = solana_sha256_hasher::Hasher::default(); - for seed in seeds.iter() { - hasher.hash(seed); - } - hasher.hashv(&[program_id.as_ref(), PDA_MARKER]); - let hash = hasher.result(); - - if bytes_are_curve_point(hash) { - return Err(PubkeyError::InvalidSeeds); - } - - Ok(Pubkey::from(hash.to_bytes())) - } - // Call via a system call to perform the calculation - #[cfg(target_os = "solana")] - { - let mut bytes = [0; 32]; - let result = unsafe { - crate::syscalls::sol_create_program_address( - seeds as *const _ as *const u8, - seeds.len() as u64, - program_id as *const _ as *const u8, - &mut bytes as *mut _ as *mut u8, - ) - }; - match result { - SUCCESS => Ok(Pubkey::from(bytes)), - _ => Err(result.into()), - } - } - } - - pub const fn to_bytes(self) -> [u8; 32] { - self.0 - } - - /// Return a reference to the `Pubkey`'s byte array. - #[inline(always)] - pub const fn as_array(&self) -> &[u8; 32] { - &self.0 - } - - // If target_os = "solana", then this panics so there are no dependencies. - // When target_os != "solana", this should be opt-in so users - // don't need the curve25519 dependency. - #[cfg(any(target_os = "solana", feature = "curve25519"))] - pub fn is_on_curve(&self) -> bool { - bytes_are_curve_point(self) - } - - /// Log a `Pubkey` from a program - pub fn log(&self) { - #[cfg(target_os = "solana")] - unsafe { - crate::syscalls::sol_log_pubkey(self.as_ref() as *const _ as *const u8) - }; - - #[cfg(all(not(target_os = "solana"), feature = "std"))] - std::println!("{}", std::string::ToString::to_string(&self)); - } -} - -impl AsRef<[u8]> for Pubkey { - fn as_ref(&self) -> &[u8] { - &self.0[..] - } -} - -impl AsMut<[u8]> for Pubkey { - fn as_mut(&mut self) -> &mut [u8] { - &mut self.0[..] - } -} - -fn write_as_base58(f: &mut fmt::Formatter, p: &Pubkey) -> fmt::Result { - let mut out = [0u8; MAX_BASE58_LEN]; - let len = five8::encode_32(&p.0, &mut out) as usize; - // any sequence of base58 chars is valid utf8 - let as_str = unsafe { from_utf8_unchecked(&out[..len]) }; - f.write_str(as_str) -} - -impl fmt::Debug for Pubkey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write_as_base58(f, self) - } -} +pub use solana_address::bytes_are_curve_point; +#[cfg(target_os = "solana")] +pub use solana_address::syscalls; +pub use solana_address::{ + address as pubkey, + error::{AddressError as PubkeyError, ParseAddressError as ParsePubkeyError}, + Address as Pubkey, ADDRESS_BYTES as PUBKEY_BYTES, MAX_SEEDS, MAX_SEED_LEN, +}; +#[cfg(all(feature = "rand", not(target_os = "solana")))] +pub use solana_address::{ + AddressHasher as PubkeyHasher, AddressHasherBuilder as PubkeyHasherBuilder, +}; -impl fmt::Display for Pubkey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write_as_base58(f, self) - } +/// New random `Pubkey` for tests and benchmarks. +#[cfg(all(feature = "rand", not(target_os = "solana")))] +pub fn new_rand() -> Pubkey { + Pubkey::from(rand::random::<[u8; PUBKEY_BYTES]>()) } /// Convenience macro to declare a static public key and functions to interact with it. @@ -1023,9 +50,9 @@ impl fmt::Display for Pubkey { /// ``` #[macro_export] macro_rules! declare_id { - ($address:expr) => { + ($pubkey:expr) => { /// The const program ID. - pub const ID: $crate::Pubkey = $crate::Pubkey::from_str_const($address); + pub const ID: $crate::Pubkey = $crate::Pubkey::from_str_const($pubkey); /// Returns `true` if given pubkey is the program ID. // TODO make this const once `derive_const` makes it out of nightly @@ -1050,9 +77,9 @@ macro_rules! declare_id { /// Same as [`declare_id`] except that it reports that this ID has been deprecated. #[macro_export] macro_rules! declare_deprecated_id { - ($address:expr) => { + ($pubkey:expr) => { /// The const program ID. - pub const ID: $crate::Pubkey = $crate::Pubkey::from_str_const($address); + pub const ID: $crate::Pubkey = $crate::Pubkey::from_str_const($pubkey); /// Returns `true` if given pubkey is the program ID. // TODO make this const once `derive_const` makes it out of nightly @@ -1076,323 +103,3 @@ macro_rules! declare_deprecated_id { } }; } - -/// Convenience macro to define a static public key. -/// -/// Input: a single literal base58 string representation of a Pubkey. -/// -/// # Example -/// -/// ``` -/// use std::str::FromStr; -/// use solana_pubkey::{pubkey, Pubkey}; -/// -/// static ID: Pubkey = pubkey!("My11111111111111111111111111111111111111111"); -/// -/// let my_id = Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap(); -/// assert_eq!(ID, my_id); -/// ``` -#[macro_export] -macro_rules! pubkey { - ($input:literal) => { - $crate::Pubkey::from_str_const($input) - }; -} - -/// New random Pubkey for tests and benchmarks. -#[cfg(all(feature = "rand", not(target_os = "solana")))] -pub fn new_rand() -> Pubkey { - Pubkey::from(rand::random::<[u8; PUBKEY_BYTES]>()) -} - -#[cfg(test)] -mod tests { - use {super::*, core::str::from_utf8, strum::IntoEnumIterator}; - - #[test] - fn test_new_unique() { - assert!(Pubkey::new_unique() != Pubkey::new_unique()); - } - - #[test] - fn pubkey_fromstr() { - let pubkey = Pubkey::new_unique(); - let mut pubkey_base58_str = bs58::encode(pubkey.0).into_string(); - - assert_eq!(pubkey_base58_str.parse::(), Ok(pubkey)); - - pubkey_base58_str.push_str(&bs58::encode(pubkey.0).into_string()); - assert_eq!( - pubkey_base58_str.parse::(), - Err(ParsePubkeyError::WrongSize) - ); - - pubkey_base58_str.truncate(pubkey_base58_str.len() / 2); - assert_eq!(pubkey_base58_str.parse::(), Ok(pubkey)); - - pubkey_base58_str.truncate(pubkey_base58_str.len() / 2); - assert_eq!( - pubkey_base58_str.parse::(), - Err(ParsePubkeyError::WrongSize) - ); - - let mut pubkey_base58_str = bs58::encode(pubkey.0).into_string(); - assert_eq!(pubkey_base58_str.parse::(), Ok(pubkey)); - - // throw some non-base58 stuff in there - pubkey_base58_str.replace_range(..1, "I"); - assert_eq!( - pubkey_base58_str.parse::(), - Err(ParsePubkeyError::Invalid) - ); - - // too long input string - // longest valid encoding - let mut too_long = bs58::encode(&[255u8; PUBKEY_BYTES]).into_string(); - // and one to grow on - too_long.push('1'); - assert_eq!(too_long.parse::(), Err(ParsePubkeyError::WrongSize)); - } - - #[test] - fn test_create_with_seed() { - assert!( - Pubkey::create_with_seed(&Pubkey::new_unique(), "☉", &Pubkey::new_unique()).is_ok() - ); - assert_eq!( - Pubkey::create_with_seed( - &Pubkey::new_unique(), - from_utf8(&[127; MAX_SEED_LEN + 1]).unwrap(), - &Pubkey::new_unique() - ), - Err(PubkeyError::MaxSeedLengthExceeded) - ); - assert!(Pubkey::create_with_seed( - &Pubkey::new_unique(), - "\ - \u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\ - ", - &Pubkey::new_unique() - ) - .is_ok()); - // utf-8 abuse ;) - assert_eq!( - Pubkey::create_with_seed( - &Pubkey::new_unique(), - "\ - x\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\u{10FFFF}\ - ", - &Pubkey::new_unique() - ), - Err(PubkeyError::MaxSeedLengthExceeded) - ); - - assert!(Pubkey::create_with_seed( - &Pubkey::new_unique(), - from_utf8(&[0; MAX_SEED_LEN]).unwrap(), - &Pubkey::new_unique(), - ) - .is_ok()); - - assert!( - Pubkey::create_with_seed(&Pubkey::new_unique(), "", &Pubkey::new_unique(),).is_ok() - ); - - assert_eq!( - Pubkey::create_with_seed( - &Pubkey::default(), - "limber chicken: 4/45", - &Pubkey::default(), - ), - Ok("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq" - .parse() - .unwrap()) - ); - } - - #[test] - fn test_create_program_address() { - let exceeded_seed = &[127; MAX_SEED_LEN + 1]; - let max_seed = &[0; MAX_SEED_LEN]; - let exceeded_seeds: &[&[u8]] = &[ - &[1], - &[2], - &[3], - &[4], - &[5], - &[6], - &[7], - &[8], - &[9], - &[10], - &[11], - &[12], - &[13], - &[14], - &[15], - &[16], - &[17], - ]; - let max_seeds: &[&[u8]] = &[ - &[1], - &[2], - &[3], - &[4], - &[5], - &[6], - &[7], - &[8], - &[9], - &[10], - &[11], - &[12], - &[13], - &[14], - &[15], - &[16], - ]; - let program_id = Pubkey::from_str("BPFLoaderUpgradeab1e11111111111111111111111").unwrap(); - let public_key = Pubkey::from_str("SeedPubey1111111111111111111111111111111111").unwrap(); - - assert_eq!( - Pubkey::create_program_address(&[exceeded_seed], &program_id), - Err(PubkeyError::MaxSeedLengthExceeded) - ); - assert_eq!( - Pubkey::create_program_address(&[b"short_seed", exceeded_seed], &program_id), - Err(PubkeyError::MaxSeedLengthExceeded) - ); - assert!(Pubkey::create_program_address(&[max_seed], &program_id).is_ok()); - assert_eq!( - Pubkey::create_program_address(exceeded_seeds, &program_id), - Err(PubkeyError::MaxSeedLengthExceeded) - ); - assert!(Pubkey::create_program_address(max_seeds, &program_id).is_ok()); - assert_eq!( - Pubkey::create_program_address(&[b"", &[1]], &program_id), - Ok("BwqrghZA2htAcqq8dzP1WDAhTXYTYWj7CHxF5j7TDBAe" - .parse() - .unwrap()) - ); - assert_eq!( - Pubkey::create_program_address(&["☉".as_ref(), &[0]], &program_id), - Ok("13yWmRpaTR4r5nAktwLqMpRNr28tnVUZw26rTvPSSB19" - .parse() - .unwrap()) - ); - assert_eq!( - Pubkey::create_program_address(&[b"Talking", b"Squirrels"], &program_id), - Ok("2fnQrngrQT4SeLcdToJAD96phoEjNL2man2kfRLCASVk" - .parse() - .unwrap()) - ); - assert_eq!( - Pubkey::create_program_address(&[public_key.as_ref(), &[1]], &program_id), - Ok("976ymqVnfE32QFe6NfGDctSvVa36LWnvYxhU6G2232YL" - .parse() - .unwrap()) - ); - assert_ne!( - Pubkey::create_program_address(&[b"Talking", b"Squirrels"], &program_id).unwrap(), - Pubkey::create_program_address(&[b"Talking"], &program_id).unwrap(), - ); - } - - #[test] - fn test_pubkey_off_curve() { - // try a bunch of random input, all successful generated program - // addresses must land off the curve and be unique - let mut addresses = std::vec![]; - for _ in 0..1_000 { - let program_id = Pubkey::new_unique(); - let bytes1 = rand::random::<[u8; 10]>(); - let bytes2 = rand::random::<[u8; 32]>(); - if let Ok(program_address) = - Pubkey::create_program_address(&[&bytes1, &bytes2], &program_id) - { - assert!(!program_address.is_on_curve()); - assert!(!addresses.contains(&program_address)); - addresses.push(program_address); - } - } - } - - #[test] - fn test_find_program_address() { - for _ in 0..1_000 { - let program_id = Pubkey::new_unique(); - let (address, bump_seed) = - Pubkey::find_program_address(&[b"Lil'", b"Bits"], &program_id); - assert_eq!( - address, - Pubkey::create_program_address(&[b"Lil'", b"Bits", &[bump_seed]], &program_id) - .unwrap() - ); - } - } - - fn pubkey_from_seed_by_marker(marker: &[u8]) -> Result { - let key = Pubkey::new_unique(); - let owner = Pubkey::default(); - - let mut to_fake = owner.to_bytes().to_vec(); - to_fake.extend_from_slice(marker); - - let seed = from_utf8(&to_fake[..to_fake.len() - 32]).expect("not utf8"); - let base = &Pubkey::try_from(&to_fake[to_fake.len() - 32..]).unwrap(); - - Pubkey::create_with_seed(&key, seed, base) - } - - #[test] - fn test_create_with_seed_rejects_illegal_owner() { - assert_eq!( - pubkey_from_seed_by_marker(PDA_MARKER), - Err(PubkeyError::IllegalOwner) - ); - assert!(pubkey_from_seed_by_marker(&PDA_MARKER[1..]).is_ok()); - } - - #[test] - fn test_pubkey_error_from_primitive_exhaustive() { - for variant in PubkeyError::iter() { - let variant_i64 = variant.clone() as i64; - assert_eq!( - PubkeyError::from_repr(variant_i64 as usize), - PubkeyError::from_i64(variant_i64) - ); - assert_eq!(PubkeyError::from(variant_i64 as u64), variant); - } - } - - #[test] - fn test_parse_pubkey_error_from_primitive_exhaustive() { - for variant in ParsePubkeyError::iter() { - let variant_i64 = variant as i64; - assert_eq!( - ParsePubkeyError::from_repr(variant_i64 as usize), - ParsePubkeyError::from_i64(variant_i64) - ); - } - } - - #[test] - fn test_pubkey_macro() { - const PK: Pubkey = Pubkey::from_str_const("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq"); - assert_eq!(pubkey!("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq"), PK); - assert_eq!( - Pubkey::from_str("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq").unwrap(), - PK - ); - } - - #[test] - fn test_as_array() { - let bytes = [1u8; 32]; - let key = Pubkey::from(bytes); - assert_eq!(key.as_array(), &bytes); - assert_eq!(key.as_array(), &key.to_bytes()); - // Sanity check: ensure the pointer is the same. - assert_eq!(key.as_array().as_ptr(), key.0.as_ptr()); - } -} diff --git a/pubkey/src/syscalls.rs b/pubkey/src/syscalls.rs deleted file mode 100644 index 13baad1c0..000000000 --- a/pubkey/src/syscalls.rs +++ /dev/null @@ -1,4 +0,0 @@ -/// Syscall definitions used by `solana_pubkey`. -pub use solana_define_syscall::definitions::{ - sol_create_program_address, sol_log_pubkey, sol_try_find_program_address, -}; diff --git a/scripts/check-nits.sh b/scripts/check-nits.sh index 0a5371ce0..55daeca00 100755 --- a/scripts/check-nits.sh +++ b/scripts/check-nits.sh @@ -17,10 +17,11 @@ declare prints=( # Parts of the tree that are expected to be print free declare print_free_tree=( ':**.rs' + ':^address/src/hasher.rs' + ':^address/src/syscalls.rs' ':^logger/src/lib.rs' ':^msg/src/lib.rs' ':^program-option/src/lib.rs' - ':^pubkey/src/lib.rs' ':^sysvar/src/program_stubs.rs' ':^**bin**.rs' ':^**bench**.rs' diff --git a/sdk-wasm-js/Cargo.toml b/sdk-wasm-js/Cargo.toml index 1c047bd56..2f4ea3b66 100644 --- a/sdk-wasm-js/Cargo.toml +++ b/sdk-wasm-js/Cargo.toml @@ -19,11 +19,11 @@ crate-type = ["cdylib", "rlib"] [dependencies] bincode = { workspace = true } +solana-address = { workspace = true, features = ["curve25519", "sha2", "std"] } solana-hash = { workspace = true } solana-instruction = { workspace = true, features = ["std"] } solana-keypair = { workspace = true } solana-message = { workspace = true } -solana-pubkey = { workspace = true, features = ["curve25519", "sha2", "std"] } solana-signature = { workspace = true } solana-signer = { workspace = true } solana-transaction = { workspace = true, features = ["bincode", "verify"] } diff --git a/sdk-wasm-js/src/pubkey.rs b/sdk-wasm-js/src/address.rs similarity index 78% rename from sdk-wasm-js/src/pubkey.rs rename to sdk-wasm-js/src/address.rs index 3531fd7da..b44e3cdd5 100644 --- a/sdk-wasm-js/src/pubkey.rs +++ b/sdk-wasm-js/src/address.rs @@ -1,4 +1,4 @@ -//! Pubkey wrapper +//! Address wrapper use { crate::display_to_jsvalue, @@ -8,7 +8,7 @@ use { #[wasm_bindgen] #[derive(Debug, PartialEq, Eq, Clone)] -pub struct Pubkey(pub(crate) solana_pubkey::Pubkey); +pub struct Address(pub(crate) solana_address::Address); fn js_value_to_seeds_vec(array_of_uint8_arrays: &[JsValue]) -> Result>, JsValue> { let vec_vec_u8 = array_of_uint8_arrays @@ -29,21 +29,21 @@ fn js_value_to_seeds_vec(array_of_uint8_arrays: &[JsValue]) -> Result Result { if let Some(base58_str) = value.as_string() { base58_str - .parse::() + .parse::() .map(Self) .map_err(display_to_jsvalue) } else if let Some(uint8_array) = value.dyn_ref::() { - solana_pubkey::Pubkey::try_from(uint8_array.to_vec()) + solana_address::Address::try_from(uint8_array.to_vec()) .map(Self) - .map_err(|err| JsValue::from(std::format!("Invalid Uint8Array pubkey: {err:?}"))) + .map_err(|err| JsValue::from(std::format!("Invalid Uint8Array address: {err:?}"))) } else if let Some(array) = value.dyn_ref::() { let mut bytes = std::vec![]; let iterator = js_sys::try_iter(&array.values())?.expect("array to be iterable"); @@ -58,11 +58,11 @@ impl Pubkey { } return Err(std::format!("Invalid array argument: {:?}", x).into()); } - solana_pubkey::Pubkey::try_from(bytes) + solana_address::Address::try_from(bytes) .map(Self) - .map_err(|err| JsValue::from(std::format!("Invalid Array pubkey: {err:?}"))) + .map_err(|err| JsValue::from(std::format!("Invalid Array address: {err:?}"))) } else if value.is_undefined() { - Ok(Self(solana_pubkey::Pubkey::default())) + Ok(Self(solana_address::Address::default())) } else { Err("Unsupported argument".into()) } @@ -73,13 +73,13 @@ impl Pubkey { std::string::ToString::to_string(&self.0) } - /// Check if a `Pubkey` is on the ed25519 curve. + /// Check if a `Address` is on the ed25519 curve. pub fn isOnCurve(&self) -> bool { self.0.is_on_curve() } - /// Checks if two `Pubkey`s are equal - pub fn equals(&self, other: &Pubkey) -> bool { + /// Checks if two `Address`s are equal + pub fn equals(&self, other: &Self) -> bool { self.0 == other.0 } @@ -88,9 +88,9 @@ impl Pubkey { self.0.to_bytes().into() } - /// Derive a Pubkey from another Pubkey, string seed, and a program id + /// Derive an Address from anothern Address, string seed, and a program id pub fn createWithSeed(base: &Self, seed: &str, owner: &Self) -> Result { - solana_pubkey::Pubkey::create_with_seed(&base.0, seed, &owner.0) + solana_address::Address::create_with_seed(&base.0, seed, &owner.0) .map(Self) .map_err(display_to_jsvalue) } @@ -99,14 +99,14 @@ impl Pubkey { pub fn createProgramAddress( seeds: std::boxed::Box<[JsValue]>, program_id: &Self, - ) -> Result { + ) -> Result { let seeds_vec = js_value_to_seeds_vec(&seeds)?; let seeds_slice = seeds_vec .iter() .map(|seed| seed.as_slice()) .collect::>(); - solana_pubkey::Pubkey::create_program_address(seeds_slice.as_slice(), &program_id.0) + solana_address::Address::create_program_address(seeds_slice.as_slice(), &program_id.0) .map(Self) .map_err(display_to_jsvalue) } @@ -114,7 +114,7 @@ impl Pubkey { /// Find a valid program address /// /// Returns: - /// * `[PubKey, number]` - the program address and bump seed + /// * `[Address, number]` - the program address and bump seed pub fn findProgramAddress( seeds: std::boxed::Box<[JsValue]>, program_id: &Self, @@ -126,7 +126,7 @@ impl Pubkey { .collect::>(); let (address, bump_seed) = - solana_pubkey::Pubkey::find_program_address(seeds_slice.as_slice(), &program_id.0); + solana_address::Address::find_program_address(seeds_slice.as_slice(), &program_id.0); let result = Array::new_with_length(2); result.set(0, Self(address).into()); diff --git a/sdk-wasm-js/src/instruction.rs b/sdk-wasm-js/src/instruction.rs index 2190b48b1..a036d8b70 100644 --- a/sdk-wasm-js/src/instruction.rs +++ b/sdk-wasm-js/src/instruction.rs @@ -3,7 +3,7 @@ //! (ref: https://github.com/rustwasm/wasm-bindgen/issues/111) #![allow(non_snake_case)] -use {crate::pubkey::Pubkey, wasm_bindgen::prelude::*}; +use {crate::address::Address, wasm_bindgen::prelude::*}; /// wasm-bindgen version of the Instruction struct. /// This duplication is required until https://github.com/rustwasm/wasm-bindgen/issues/3671 @@ -16,7 +16,7 @@ pub struct Instruction(pub(crate) solana_instruction::Instruction); impl Instruction { /// Create a new `Instruction` #[wasm_bindgen(constructor)] - pub fn constructor(program_id: Pubkey) -> Self { + pub fn constructor(program_id: Address) -> Self { Instruction(solana_instruction::Instruction::new_with_bytes( program_id.0, &[], @@ -40,14 +40,14 @@ pub struct AccountMeta(pub(crate) solana_instruction::AccountMeta); #[wasm_bindgen] impl AccountMeta { /// Create a new writable `AccountMeta` - pub fn newWritable(pubkey: Pubkey, is_signer: bool) -> Self { - AccountMeta(solana_instruction::AccountMeta::new(pubkey.0, is_signer)) + pub fn newWritable(address: Address, is_signer: bool) -> Self { + AccountMeta(solana_instruction::AccountMeta::new(address.0, is_signer)) } /// Create a new readonly `AccountMeta` - pub fn newReadonly(pubkey: Pubkey, is_signer: bool) -> Self { + pub fn newReadonly(address: Address, is_signer: bool) -> Self { AccountMeta(solana_instruction::AccountMeta::new_readonly( - pubkey.0, is_signer, + address.0, is_signer, )) } } diff --git a/sdk-wasm-js/src/keypair.rs b/sdk-wasm-js/src/keypair.rs index b76ebbc9d..e525e1760 100644 --- a/sdk-wasm-js/src/keypair.rs +++ b/sdk-wasm-js/src/keypair.rs @@ -1,4 +1,4 @@ -use {crate::pubkey::Pubkey, solana_signer::Signer, wasm_bindgen::prelude::*}; +use {crate::address::Address, solana_signer::Signer, wasm_bindgen::prelude::*}; #[wasm_bindgen] #[derive(Debug)] @@ -25,9 +25,9 @@ impl Keypair { .map_err(|e| e.to_string().into()) } - /// Return the `Pubkey` for this `Keypair` + /// Return the `Address` for this `Keypair` #[wasm_bindgen(js_name = pubkey)] - pub fn js_pubkey(&self) -> Pubkey { - Pubkey(self.0.pubkey()) + pub fn js_pubkey(&self) -> Address { + Address(self.0.pubkey()) } } diff --git a/sdk-wasm-js/src/lib.rs b/sdk-wasm-js/src/lib.rs index 0db27db2a..7d7755e0e 100644 --- a/sdk-wasm-js/src/lib.rs +++ b/sdk-wasm-js/src/lib.rs @@ -1,21 +1,16 @@ //! solana-program Javascript interface #![cfg(target_arch = "wasm32")] -use log::Level; -pub use { - solana_hash::*, - solana_instruction::*, - solana_keypair::*, - solana_pubkey::*, - solana_transaction::*, +use { + log::Level, wasm_bindgen::prelude::{wasm_bindgen, JsValue}, }; +pub mod address; pub mod hash; pub mod instruction; pub mod keypair; pub mod message; -pub mod pubkey; pub mod transaction; /// Initialize Javascript logging and panic handler diff --git a/sdk-wasm-js/src/transaction.rs b/sdk-wasm-js/src/transaction.rs index 8fb0a152f..869897b52 100644 --- a/sdk-wasm-js/src/transaction.rs +++ b/sdk-wasm-js/src/transaction.rs @@ -2,7 +2,7 @@ #![allow(non_snake_case)] use { crate::{ - hash::Hash, instruction::Instruction, keypair::Keypair, message::Message, pubkey::Pubkey, + address::Address, hash::Hash, instruction::Instruction, keypair::Keypair, message::Message, }, wasm_bindgen::prelude::{wasm_bindgen, JsValue}, }; @@ -18,7 +18,7 @@ pub struct Transaction(pub(crate) solana_transaction::Transaction); impl Transaction { /// Create a new `Transaction` #[wasm_bindgen(constructor)] - pub fn constructor(instructions: Vec, payer: Option) -> Self { + pub fn constructor(instructions: Vec, payer: Option
) -> Self { let instructions = instructions.into_iter().map(|x| x.0).collect::>(); Transaction(solana_transaction::Transaction::new_with_payer( &instructions, diff --git a/sdk-wasm-js/tests/pubkey.mjs b/sdk-wasm-js/tests/address.mjs similarity index 62% rename from sdk-wasm-js/tests/pubkey.mjs rename to sdk-wasm-js/tests/address.mjs index 342273360..061156535 100644 --- a/sdk-wasm-js/tests/pubkey.mjs +++ b/sdk-wasm-js/tests/address.mjs @@ -1,60 +1,60 @@ import { expect } from "chai"; -import { solana_program_init, Pubkey } from "crate"; +import { solana_program_init, Address } from "crate"; solana_program_init(); // TODO: wasm_bindgen doesn't currently support exporting constants const MAX_SEED_LEN = 32; -describe("Pubkey", function () { +describe("Address", function () { it("invalid", () => { expect(() => { - new Pubkey([ + new Address([ 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); }).to.throw(); expect(() => { - new Pubkey([ + new Address([ 'invalid', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); }).to.throw(); expect(() => { - new Pubkey( + new Address( "0x300000000000000000000000000000000000000000000000000000000000000000000" ); }).to.throw(); expect(() => { - new Pubkey( + new Address( "0x300000000000000000000000000000000000000000000000000000000000000" ); }).to.throw(); expect(() => { - new Pubkey( + new Address( "135693854574979916511997248057056142015550763280047535983739356259273198796800000" ); }).to.throw(); expect(() => { - new Pubkey("12345"); + new Address("12345"); }).to.throw(); }); it("toString", () => { - const key = new Pubkey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); + const key = new Address("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); expect(key.toString()).to.eq("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); - const key2 = new Pubkey("1111111111111111111111111111BukQL"); + const key2 = new Address("1111111111111111111111111111BukQL"); expect(key2.toString()).to.eq("1111111111111111111111111111BukQL"); - const key3 = new Pubkey("11111111111111111111111111111111"); + const key3 = new Address("11111111111111111111111111111111"); expect(key3.toString()).to.eq("11111111111111111111111111111111"); - const key4 = new Pubkey([ + const key4 = new Address([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); @@ -62,7 +62,7 @@ describe("Pubkey", function () { }); it("toBytes", () => { - const key = new Pubkey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); + const key = new Address("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); expect(key.toBytes()).to.deep.equal( new Uint8Array([ 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -70,7 +70,7 @@ describe("Pubkey", function () { ]) ); - const key2 = new Pubkey(); + const key2 = new Address(); expect(key2.toBytes()).to.deep.equal( new Uint8Array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -80,26 +80,26 @@ describe("Pubkey", function () { }); it("isOnCurve", () => { - let onCurve = new Pubkey("J4NYrSRccTUGXP7wmFwiByakqWKZb5RwpiAoskpgAQRb"); + let onCurve = new Address("J4NYrSRccTUGXP7wmFwiByakqWKZb5RwpiAoskpgAQRb"); expect(onCurve.isOnCurve()).to.be.true; - let offCurve = new Pubkey("12rqwuEgBYiGhBrDJStCiqEtzQpTTiZbh7teNVLuYcFA"); + let offCurve = new Address("12rqwuEgBYiGhBrDJStCiqEtzQpTTiZbh7teNVLuYcFA"); expect(offCurve.isOnCurve()).to.be.false; }); it("equals", () => { - const arrayKey = new Pubkey([ + const arrayKey = new Address([ 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); - const base58Key = new Pubkey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); + const base58Key = new Address("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); expect(arrayKey.equals(base58Key)).to.be.true; }); it("createWithSeed", async () => { - const defaultPublicKey = new Pubkey("11111111111111111111111111111111"); - const derivedKey = Pubkey.createWithSeed( + const defaultPublicKey = new Address("11111111111111111111111111111111"); + const derivedKey = Address.createWithSeed( defaultPublicKey, "limber chicken: 4/45", defaultPublicKey @@ -107,75 +107,75 @@ describe("Pubkey", function () { expect( derivedKey.equals( - new Pubkey("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq") + new Address("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq") ) ).to.be.true; }); it("createProgramAddress", async () => { - const programId = new Pubkey("BPFLoader1111111111111111111111111111111111"); - const publicKey = new Pubkey("SeedPubey1111111111111111111111111111111111"); + const programId = new Address("BPFLoader1111111111111111111111111111111111"); + const publicKey = new Address("SeedPubey1111111111111111111111111111111111"); - let programAddress = Pubkey.createProgramAddress( + let programAddress = Address.createProgramAddress( [Buffer.from("", "utf8"), Buffer.from([1])], programId ); expect( programAddress.equals( - new Pubkey("3gF2KMe9KiC6FNVBmfg9i267aMPvK37FewCip4eGBFcT") + new Address("3gF2KMe9KiC6FNVBmfg9i267aMPvK37FewCip4eGBFcT") ) ).to.be.true; - programAddress = Pubkey.createProgramAddress( + programAddress = Address.createProgramAddress( [Buffer.from("☉", "utf8")], programId ); expect( programAddress.equals( - new Pubkey("7ytmC1nT1xY4RfxCV2ZgyA7UakC93do5ZdyhdF3EtPj7") + new Address("7ytmC1nT1xY4RfxCV2ZgyA7UakC93do5ZdyhdF3EtPj7") ) ).to.be.true; - programAddress = Pubkey.createProgramAddress( + programAddress = Address.createProgramAddress( [Buffer.from("Talking", "utf8"), Buffer.from("Squirrels", "utf8")], programId ); expect( programAddress.equals( - new Pubkey("HwRVBufQ4haG5XSgpspwKtNd3PC9GM9m1196uJW36vds") + new Address("HwRVBufQ4haG5XSgpspwKtNd3PC9GM9m1196uJW36vds") ) ).to.be.true; - programAddress = Pubkey.createProgramAddress( + programAddress = Address.createProgramAddress( [publicKey.toBytes()], programId ); expect( programAddress.equals( - new Pubkey("GUs5qLUfsEHkcMB9T38vjr18ypEhRuNWiePW2LoK4E3K") + new Address("GUs5qLUfsEHkcMB9T38vjr18ypEhRuNWiePW2LoK4E3K") ) ).to.be.true; - const programAddress2 = Pubkey.createProgramAddress( + const programAddress2 = Address.createProgramAddress( [Buffer.from("Talking", "utf8")], programId ); expect(programAddress.equals(programAddress2)).to.eq(false); expect(() => { - Pubkey.createProgramAddress([Buffer.alloc(MAX_SEED_LEN + 1)], programId); + Address.createProgramAddress([Buffer.alloc(MAX_SEED_LEN + 1)], programId); }).to.throw(); }); it("findProgramAddress", async () => { - const programId = new Pubkey("BPFLoader1111111111111111111111111111111111"); - let [programAddress, nonce] = Pubkey.findProgramAddress( + const programId = new Address("BPFLoader1111111111111111111111111111111111"); + let [programAddress, nonce] = Address.findProgramAddress( [Buffer.from("", "utf8")], programId ); expect( programAddress.equals( - Pubkey.createProgramAddress( + Address.createProgramAddress( [Buffer.from("", "utf8"), Buffer.from([nonce])], programId ) diff --git a/sdk-wasm-js/tests/transaction.mjs b/sdk-wasm-js/tests/transaction.mjs index 46ebc7f39..b06e98db5 100644 --- a/sdk-wasm-js/tests/transaction.mjs +++ b/sdk-wasm-js/tests/transaction.mjs @@ -2,7 +2,7 @@ import { expect } from "chai"; import { solana_program_init, AccountMeta, - Pubkey, + Address, Keypair, Hash, Instruction, @@ -32,8 +32,8 @@ describe("Transaction", function () { ]) ); - const programId = new Pubkey("11111111111111111111111111111111"); - const dst = new Pubkey("11111111111111111111111111111112"); + const programId = new Address("11111111111111111111111111111111"); + const dst = new Address("11111111111111111111111111111112"); const instructionData = new Uint8Array([2, 0, 0, 0, 123, 0, 0, 0, 0, 0, 0, 0]); const recentBlockhash = new Hash( diff --git a/transaction/src/lib.rs b/transaction/src/lib.rs index 37f344fec..0bc14bbf5 100644 --- a/transaction/src/lib.rs +++ b/transaction/src/lib.rs @@ -176,7 +176,7 @@ const NONCED_TX_MARKER_IX_INDEX: u8 = 0; #[cfg_attr( feature = "frozen-abi", derive(solana_frozen_abi_macro::AbiExample), - solana_frozen_abi_macro::frozen_abi(digest = "76BDTr3Xm3VP7h4eSiw6pZHKc5yYewDufyia3Yedh6GG") + solana_frozen_abi_macro::frozen_abi(digest = "KSndwV1Ezw3xDX3Mz4Sg2vY22dx9mGTCFzo1RxbwaV8") )] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] #[derive(Debug, PartialEq, Default, Eq, Clone)] diff --git a/vote-interface/src/state/vote_state_1_14_11.rs b/vote-interface/src/state/vote_state_1_14_11.rs index 250ba8bd0..f68cd11b4 100644 --- a/vote-interface/src/state/vote_state_1_14_11.rs +++ b/vote-interface/src/state/vote_state_1_14_11.rs @@ -7,7 +7,7 @@ const DEFAULT_PRIOR_VOTERS_OFFSET: usize = 82; #[cfg_attr( feature = "frozen-abi", - solana_frozen_abi_macro::frozen_abi(digest = "HF4NfshaLg9e93RURYWTJRowtRrpLf5mWiF4G2Gnfu2r"), + solana_frozen_abi_macro::frozen_abi(digest = "2rjXSWaNeAdoUNJDC5otC7NPR1qXHvLMuAs5faE4DPEt"), derive(solana_frozen_abi_macro::AbiExample) )] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] diff --git a/vote-interface/src/state/vote_state_v3.rs b/vote-interface/src/state/vote_state_v3.rs index c09c79a21..3ba8b26b6 100644 --- a/vote-interface/src/state/vote_state_v3.rs +++ b/vote-interface/src/state/vote_state_v3.rs @@ -23,7 +23,7 @@ use { #[cfg_attr( feature = "frozen-abi", - frozen_abi(digest = "4cUA9matKNYX1R9TceL4D14w23AfLXVjhZaStwuermQX"), + frozen_abi(digest = "pZqasQc6duzMYzpzU7eriHH9cMXmubuUP4NmCrkWZjt"), derive(AbiExample) )] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]