001002003004005006007 008use anchor_lang::prelude::*;009use solana_sha256_hasher::hashv;010 011declare_id!("GGhZ8LJzNdx14SSUaNEy3YULu7ohWkYV7vBdLXSCE95y");012 013014pub const MIN_WINDOW_SECONDS: i64 = 60 * 60;015016pub const MAX_WINDOW_SECONDS: i64 = 60 * 60 * 24 * 366;017018pub const MAX_STATEMENT_LEN: usize = 280;019020021pub const COMMITMENT_DOMAIN: &[u8] = b"ante-plate-v1";022 023#[program]024pub mod ante {025 use super::*;026 027 028 pub fn open_observer(ctx: Context<OpenObserver>) -> Result<()> {029 let observer = &mut ctx.accounts.observer;030 observer.authority = ctx.accounts.authority.key();031 observer.plates_exposed = 0;032 observer.plates_developed = 0;033 observer.plates_fogged = 0;034 observer.opened_slot = Clock::get()?.slot;035 observer.bump = ctx.bumps.observer;036 Ok(())037 }038 039 040 pub fn expose_plate(041 ctx: Context<ExposePlate>,042 commitment: [u8; 32],043 window_seconds: i64,044 ) -> Result<()> {045 require!(commitment != [0; 32], AnteError::EmptyCommitment);046 validate_window(window_seconds)?;047 048 let clock = Clock::get()?;049 let observer = &mut ctx.accounts.observer;050 let plate = &mut ctx.accounts.plate;051 052 plate.observer = observer.key();053 plate.index = observer.plates_exposed;054 plate.commitment = commitment;055 plate.exposed_slot = clock.slot;056 plate.exposed_at = clock.unix_timestamp;057 plate.develop_by = clock058 .unix_timestamp059 .checked_add(window_seconds)060 .ok_or(AnteError::ArithmeticOverflow)?;061 plate.status = PlateStatus::Latent;062 plate.settled_slot = 0;063 plate.settled_at = 0;064 plate.salt = [0; 32];065 plate.statement = String::new();066 plate.bump = ctx.bumps.plate;067 068 observer.plates_exposed = observer069 .plates_exposed070 .checked_add(1)071 .ok_or(AnteError::ArithmeticOverflow)?;072 073 emit!(PlateExposed {074 observer: plate.observer,075 index: plate.index,076 commitment,077 exposed_slot: plate.exposed_slot,078 develop_by: plate.develop_by,079 });080 Ok(())081 }082 083 084 085 pub fn develop_plate(086 ctx: Context<DevelopPlate>,087 salt: [u8; 32],088 statement: String,089 ) -> Result<()> {090 let clock = Clock::get()?;091 let plate = &mut ctx.accounts.plate;092 093 check_developable(plate.status, clock.unix_timestamp, plate.develop_by)?;094 check_statement(&statement)?;095 require!(096 plate_commitment(&salt, statement.as_bytes()) == plate.commitment,097 AnteError::CommitmentMismatch098 );099 100 plate.salt = salt;101 plate.statement = statement;102 plate.status = PlateStatus::Developed;103 plate.settled_slot = clock.slot;104 plate.settled_at = clock.unix_timestamp;105 106 let observer = &mut ctx.accounts.observer;107 observer.plates_developed = observer108 .plates_developed109 .checked_add(1)110 .ok_or(AnteError::ArithmeticOverflow)?;111 112 emit!(PlateDeveloped {113 observer: plate.observer,114 index: plate.index,115 settled_slot: plate.settled_slot,116 });117 Ok(())118 }119 120 121 122 pub fn fog_plate(ctx: Context<FogPlate>) -> Result<()> {123 let clock = Clock::get()?;124 let plate = &mut ctx.accounts.plate;125 126 check_foggable(plate.status, clock.unix_timestamp, plate.develop_by)?;127 128 plate.status = PlateStatus::Fogged;129 plate.settled_slot = clock.slot;130 plate.settled_at = clock.unix_timestamp;131 132 let observer = &mut ctx.accounts.observer;133 observer.plates_fogged = observer134 .plates_fogged135 .checked_add(1)136 .ok_or(AnteError::ArithmeticOverflow)?;137 138 emit!(PlateFogged {139 observer: plate.observer,140 index: plate.index,141 settled_slot: plate.settled_slot,142 });143 Ok(())144 }145}146 147148149pub fn plate_commitment(salt: &[u8; 32], statement: &[u8]) -> [u8; 32] {150 hashv(&[COMMITMENT_DOMAIN, salt, statement]).to_bytes()151}152 153154pub fn validate_window(window_seconds: i64) -> Result<()> {155 require!(156 window_seconds >= MIN_WINDOW_SECONDS,157 AnteError::WindowTooShort158 );159 require!(160 window_seconds <= MAX_WINDOW_SECONDS,161 AnteError::WindowTooLong162 );163 Ok(())164}165 166167pub fn check_statement(statement: &str) -> Result<()> {168 require!(!statement.is_empty(), AnteError::EmptyStatement);169 require!(170 statement.len() <= MAX_STATEMENT_LEN,171 AnteError::StatementTooLong172 );173 Ok(())174}175 176177178pub fn check_developable(status: PlateStatus, now: i64, develop_by: i64) -> Result<()> {179 require!(status == PlateStatus::Latent, AnteError::PlateNotLatent);180 require!(now <= develop_by, AnteError::DevelopmentWindowClosed);181 Ok(())182}183 184185186pub fn check_foggable(status: PlateStatus, now: i64, develop_by: i64) -> Result<()> {187 require!(status == PlateStatus::Latent, AnteError::PlateNotLatent);188 require!(now > develop_by, AnteError::DevelopmentWindowOpen);189 Ok(())190}191 192#[derive(Accounts)]193pub struct OpenObserver<'info> {194 #[account(mut)]195 pub authority: Signer<'info>,196 #[account(197 init,198 payer = authority,199 space = 8 + Observer::INIT_SPACE,200 seeds = [b"observer", authority.key().as_ref()],201 bump202 )]203 pub observer: Account<'info, Observer>,204 pub system_program: Program<'info, System>,205}206 207#[derive(Accounts)]208pub struct ExposePlate<'info> {209 #[account(mut)]210 pub authority: Signer<'info>,211 #[account(212 mut,213 seeds = [b"observer", authority.key().as_ref()],214 bump = observer.bump,215 has_one = authority216 )]217 pub observer: Account<'info, Observer>,218 #[account(219 init,220 payer = authority,221 space = 8 + Plate::INIT_SPACE,222 seeds = [b"plate", observer.key().as_ref(), &observer.plates_exposed.to_le_bytes()],223 bump224 )]225 pub plate: Account<'info, Plate>,226 pub system_program: Program<'info, System>,227}228 229#[derive(Accounts)]230pub struct DevelopPlate<'info> {231 pub authority: Signer<'info>,232 #[account(233 mut,234 seeds = [b"observer", authority.key().as_ref()],235 bump = observer.bump,236 has_one = authority237 )]238 pub observer: Account<'info, Observer>,239 #[account(240 mut,241 seeds = [b"plate", observer.key().as_ref(), &plate.index.to_le_bytes()],242 bump = plate.bump,243 has_one = observer244 )]245 pub plate: Account<'info, Plate>,246}247 248#[derive(Accounts)]249pub struct FogPlate<'info> {250 #[account(251 mut,252 seeds = [b"observer", observer.authority.as_ref()],253 bump = observer.bump254 )]255 pub observer: Account<'info, Observer>,256 #[account(257 mut,258 seeds = [b"plate", observer.key().as_ref(), &plate.index.to_le_bytes()],259 bump = plate.bump,260 has_one = observer261 )]262 pub plate: Account<'info, Plate>,263}264 265266#[account]267#[derive(InitSpace)]268pub struct Observer {269 pub authority: Pubkey,270 pub plates_exposed: u32,271 pub plates_developed: u32,272 pub plates_fogged: u32,273 pub opened_slot: u64,274 pub bump: u8,275}276 277278279#[account]280#[derive(InitSpace)]281pub struct Plate {282 pub observer: Pubkey,283 pub index: u32,284 pub commitment: [u8; 32],285 pub exposed_slot: u64,286 pub exposed_at: i64,287 pub develop_by: i64,288 pub status: PlateStatus,289 pub settled_slot: u64,290 pub settled_at: i64,291 pub salt: [u8; 32],292 #[max_len(280)]293 pub statement: String,294 pub bump: u8,295}296 297#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, Debug, InitSpace)]298pub enum PlateStatus {299 Latent,300 Developed,301 Fogged,302}303 304#[event]305pub struct PlateExposed {306 pub observer: Pubkey,307 pub index: u32,308 pub commitment: [u8; 32],309 pub exposed_slot: u64,310 pub develop_by: i64,311}312 313#[event]314pub struct PlateDeveloped {315 pub observer: Pubkey,316 pub index: u32,317 pub settled_slot: u64,318}319 320#[event]321pub struct PlateFogged {322 pub observer: Pubkey,323 pub index: u32,324 pub settled_slot: u64,325}326 327#[error_code]328pub enum AnteError {329 #[msg("Commitment must not be zero")]330 EmptyCommitment,331 #[msg("Development window is shorter than one hour")]332 WindowTooShort,333 #[msg("Development window is longer than 366 days")]334 WindowTooLong,335 #[msg("Plate is no longer latent")]336 PlateNotLatent,337 #[msg("Development window has closed")]338 DevelopmentWindowClosed,339 #[msg("Development window is still open")]340 DevelopmentWindowOpen,341 #[msg("Statement must not be empty")]342 EmptyStatement,343 #[msg("Statement is longer than 280 bytes")]344 StatementTooLong,345 #[msg("Salt and statement do not match the commitment")]346 CommitmentMismatch,347 #[msg("Arithmetic overflow")]348 ArithmeticOverflow,349}350 351#[cfg(test)]352mod tests {353 use super::*;354 355 const SALT: [u8; 32] = [7; 32];356 const DEADLINE: i64 = 1_800_000_000;357 358 fn code_of(result: Result<()>) -> u32 {359 match result.expect_err("expected an error") {360 Error::AnchorError(error) => error.error_code_number,361 other => panic!("unexpected error: {other:?}"),362 }363 }364 365 fn code(error: AnteError) -> u32 {366 u32::from(error)367 }368 369 #[test]370 fn commitment_is_deterministic() {371 let first = plate_commitment(&SALT, b"SOL closes the week above 300");372 let second = plate_commitment(&SALT, b"SOL closes the week above 300");373 assert_eq!(first, second);374 }375 376 #[test]377 fn commitment_changes_with_salt_and_statement() {378 let base = plate_commitment(&SALT, b"rain on friday");379 assert_ne!(base, plate_commitment(&[8; 32], b"rain on friday"));380 assert_ne!(base, plate_commitment(&SALT, b"rain on saturday"));381 }382 383 #[test]384 fn commitment_is_domain_separated() {385 let plain = hashv(&[&SALT, b"rain on friday"]).to_bytes();386 assert_ne!(plain, plate_commitment(&SALT, b"rain on friday"));387 }388 389 #[test]390 fn window_bounds_are_inclusive() {391 assert!(validate_window(MIN_WINDOW_SECONDS).is_ok());392 assert!(validate_window(MAX_WINDOW_SECONDS).is_ok());393 assert_eq!(394 code_of(validate_window(MIN_WINDOW_SECONDS - 1)),395 code(AnteError::WindowTooShort)396 );397 assert_eq!(398 code_of(validate_window(MAX_WINDOW_SECONDS + 1)),399 code(AnteError::WindowTooLong)400 );401 assert_eq!(code_of(validate_window(-5)), code(AnteError::WindowTooShort));402 }403 404 #[test]405 fn statement_length_is_counted_in_bytes() {406 assert!(check_statement(&"a".repeat(MAX_STATEMENT_LEN)).is_ok());407 assert_eq!(408 code_of(check_statement(&"a".repeat(MAX_STATEMENT_LEN + 1))),409 code(AnteError::StatementTooLong)410 );411 412 assert_eq!(413 code_of(check_statement(&"é".repeat(141))),414 code(AnteError::StatementTooLong)415 );416 assert_eq!(code_of(check_statement("")), code(AnteError::EmptyStatement));417 }418 419 #[test]420 fn plate_has_room_for_the_longest_statement() {421 let fixed = 32 + 4 + 32 + 8 + 8 + 8 + 1 + 8 + 8 + 32 + 1;422 assert_eq!(Plate::INIT_SPACE, fixed + 4 + MAX_STATEMENT_LEN);423 }424 425 #[test]426 fn develop_is_allowed_until_the_deadline_second() {427 assert!(check_developable(PlateStatus::Latent, DEADLINE - 1, DEADLINE).is_ok());428 assert!(check_developable(PlateStatus::Latent, DEADLINE, DEADLINE).is_ok());429 assert_eq!(430 code_of(check_developable(PlateStatus::Latent, DEADLINE + 1, DEADLINE)),431 code(AnteError::DevelopmentWindowClosed)432 );433 }434 435 #[test]436 fn settled_plates_cannot_be_developed_again() {437 for status in [PlateStatus::Developed, PlateStatus::Fogged] {438 assert_eq!(439 code_of(check_developable(status, DEADLINE - 1, DEADLINE)),440 code(AnteError::PlateNotLatent)441 );442 }443 }444 445 #[test]446 fn fog_waits_for_the_deadline_to_pass() {447 assert_eq!(448 code_of(check_foggable(PlateStatus::Latent, DEADLINE, DEADLINE)),449 code(AnteError::DevelopmentWindowOpen)450 );451 assert!(check_foggable(PlateStatus::Latent, DEADLINE + 1, DEADLINE).is_ok());452 }453 454 #[test]455 fn developed_plates_cannot_be_fogged() {456 assert_eq!(457 code_of(check_foggable(PlateStatus::Developed, DEADLINE + 1, DEADLINE)),458 code(AnteError::PlateNotLatent)459 );460 assert_eq!(461 code_of(check_foggable(PlateStatus::Fogged, DEADLINE + 1, DEADLINE)),462 code(AnteError::PlateNotLatent)463 );464 }465 466 #[test]467 fn develop_and_fog_never_overlap() {468 for now in [DEADLINE - 1, DEADLINE, DEADLINE + 1] {469 let develop = check_developable(PlateStatus::Latent, now, DEADLINE).is_ok();470 let fog = check_foggable(PlateStatus::Latent, now, DEADLINE).is_ok();471 assert!(develop != fog, "exactly one path must be open at {now}");472 }473 }474}