diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cad774..325ebed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,7 @@ name: CI on: pull_request: branches: ["main"] + types: [opened, synchronize, reopened, ready_for_review] workflow_dispatch: env: @@ -54,9 +55,6 @@ jobs: - name: Build Tape Solana SBF program run: cargo build-sbf --manifest-path=program/Cargo.toml --verbose - - name: Build Example Solana SBF program - run: cargo build-sbf --manifest-path=example/Cargo.toml --verbose - - name: Run tests run: cargo test --verbose diff --git a/Cargo.lock b/Cargo.lock index f099a9c..2a37958 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1517,7 +1517,7 @@ dependencies = [ "derivation-path", "ed25519-dalek", "hmac 0.12.1", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -4214,9 +4214,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", @@ -4295,9 +4295,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -5003,7 +5003,7 @@ dependencies = [ "serde", "serde_bytes", "serde_derive", - "sha2 0.10.8", + "sha2 0.10.9", "sha3", "solana-account-info", "solana-atomic-u64", @@ -5346,7 +5346,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_with", - "sha2 0.10.8", + "sha2 0.10.9", "sha3", "siphasher", "solana-account", @@ -5444,7 +5444,7 @@ version = "2.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4028550a372b5ce9514941fb1a04cfac9aa66f76fd9684a7b11ef37ac586e492" dependencies = [ - "sha2 0.10.8", + "sha2 0.10.9", "solana-define-syscall", "solana-hash", ] @@ -6006,7 +6006,7 @@ checksum = "8c1f05593b7ca9eac7caca309720f2eafb96355e037e6d373b909a80fe7b69b9" dependencies = [ "proc-macro2", "quote", - "sha2 0.10.8", + "sha2 0.10.9", "syn 2.0.100", "thiserror 1.0.69", ] @@ -6115,7 +6115,7 @@ checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e" dependencies = [ "proc-macro2", "quote", - "sha2 0.10.8", + "sha2 0.10.9", "syn 2.0.100", ] @@ -6616,6 +6616,7 @@ dependencies = [ "num_enum", "packx", "solana-program", + "solana-sha256-hasher", "spl-associated-token-account 6.0.0", "spl-token 4.0.2", "steel", @@ -6652,29 +6653,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tape-example" -version = "0.2.1" -dependencies = [ - "bincode", - "brine-tree", - "bytemuck_derive", - "half", - "litesvm", - "litesvm-token", - "mpl-token-metadata", - "pretty-hex", - "rand 0.8.5", - "solana-compute-budget", - "solana-program", - "solana-sdk", - "spl-associated-token-account 6.0.0", - "spl-token 4.0.2", - "steel", - "tape-api", - "tape-program", -] - [[package]] name = "tape-network" version = "0.2.1" @@ -6724,6 +6702,7 @@ dependencies = [ "bincode", "brine-tree", "bytemuck_derive", + "chrono", "crankx", "half", "litesvm", @@ -6732,6 +6711,9 @@ dependencies = [ "packx", "pretty-hex", "rand 0.8.5", + "serde", + "serde_json", + "sha2 0.10.9", "solana-client", "solana-compute-budget", "solana-program", diff --git a/Cargo.toml b/Cargo.toml index c99a83e..80ea967 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = [ "api", "client", "cli", "network", "program", "example" ] +members = [ "api", "client", "cli", "network", "program"] [workspace.package] version = "0.2.1" @@ -36,6 +36,7 @@ solana-sdk = "=2.1" solana-transaction-status = "=2.1" solana-account-decoder = "=2.1" solana-transaction-status-client-types = "=2.1" +solana-sha256-hasher = "=2.1" steel = { version="4.0.0", features = ["spl"] } litesvm = "0.5.0" diff --git a/README.md b/README.md index a912f76..5ffdd7b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # TAPEDRIVE + [![crates.io](https://img.shields.io/crates/v/tapedrive-cli.svg?style=flat)](https://crates.io/crates/tapedrive-cli) **Decentralized object storage** on Solana. It works by compressing your data into tiny on-chain proofs. A network of miners then solve challenges in parallel to secure your data. It's entirely on Solana, so there's no need for side-chains or consensus overhead. @@ -29,6 +30,7 @@ tapedrive write -r https://example.com/path/to/a/remote/file ``` #### Read + ``` tapedrive read ``` @@ -67,11 +69,11 @@ Whether you're writing a message, a file, or something else, tapedrive compresse When you want to retrieve your data, tapedrive reads the tape sequentially from the tape network or blockchain to reassemble the original data. ----------------------- +--- ## Tape://Net -Beyond reading and writing, users can participate in the tape network. There are 3 primary functions, all can run on the same machine. +Beyond reading and writing, users can participate in the tape network. There are 3 primary functions, all can run on the same machine. image @@ -82,7 +84,6 @@ At minimum, each node must run the [archive](#archiving), but from there you can > [!Important] > We have an easy install script for running a **full node**, learn more [here](https://github.com/tapedrive-io/deploy). - ## Archiving If you'd like to either run a public gateway or a miner, you'll need an archiver. You can run one with the following command. @@ -115,13 +116,14 @@ The web service allows users to fetch data using a JSON RPC protocol similar to The following methods currently exist. - ### getHealth + Retrieves the last persisted block height and drift. **Parameters**: None (empty object `{}`) **Returns**: + ```text { "last_processed_slot": , @@ -130,6 +132,7 @@ Retrieves the last persisted block height and drift. ``` **Example**: + ```bash curl -X POST http://127.0.0.1:3000/api \ -H 'Content-Type: application/json' \ @@ -137,6 +140,7 @@ curl -X POST http://127.0.0.1:3000/api \ ``` **Response**: + ```text { "jsonrpc": "2.0", @@ -149,9 +153,11 @@ curl -X POST http://127.0.0.1:3000/api \ ``` ### getTapeAddress + Retrieves the Solana pubkey (tape address) for a given tape number. **Parameters**: + ```text { "tape_number": @@ -161,6 +167,7 @@ Retrieves the Solana pubkey (tape address) for a given tape number. **Returns**: Base-58-encoded Solana pubkey as a string. **Example**: + ```bash curl -X POST http://127.0.0.1:3000/api \ -H 'Content-Type: application/json' \ @@ -168,6 +175,7 @@ curl -X POST http://127.0.0.1:3000/api \ ``` **Response**: + ```text { "jsonrpc": "2.0", @@ -177,9 +185,11 @@ curl -X POST http://127.0.0.1:3000/api \ ``` ### getTapeNumber + Retrieves the numeric tape ID for a given Solana pubkey (tape address). **Parameters**: + ```text { "tape_address": @@ -189,6 +199,7 @@ Retrieves the numeric tape ID for a given Solana pubkey (tape address). **Returns**: Tape number as a number. **Example**: + ```bash curl -X POST http://127.0.0.1:3000/api \ -H 'Content-Type: application/json' \ @@ -196,6 +207,7 @@ curl -X POST http://127.0.0.1:3000/api \ ``` **Response**: + ```text { "jsonrpc": "2.0", @@ -205,9 +217,11 @@ curl -X POST http://127.0.0.1:3000/api \ ``` ### getSegment + Fetches a single segment’s data by tape address and segment number. **Parameters**: + ```text { "tape_address": , @@ -218,6 +232,7 @@ Fetches a single segment’s data by tape address and segment number. **Returns**: Base64-encoded string of the segment’s raw bytes. **Example**: + ```bash curl -X POST http://127.0.0.1:3000/api \ -H 'Content-Type: application/json' \ @@ -225,6 +240,7 @@ curl -X POST http://127.0.0.1:3000/api \ ``` **Response**: + ```text { "jsonrpc": "2.0", @@ -234,9 +250,11 @@ curl -X POST http://127.0.0.1:3000/api \ ``` ### getTape + Retrieves all segments and their data for a given tape address. **Parameters**: + ```text { "tape_address": @@ -244,6 +262,7 @@ Retrieves all segments and their data for a given tape address. ``` **Returns**: Array of objects, each containing: + ```text [ { @@ -254,6 +273,7 @@ Retrieves all segments and their data for a given tape address. ``` **Example**: + ```bash curl -X POST http://127.0.0.1:3000/api \ -H 'Content-Type: application/json' \ @@ -261,6 +281,7 @@ curl -X POST http://127.0.0.1:3000/api \ ``` **Response**: + ```text { "jsonrpc": "2.0", @@ -279,7 +300,9 @@ curl -X POST http://127.0.0.1:3000/api \ ``` ## Contributing + Fork, PR, or suggest: + - Faster writes/reads (turbo mode). - Encryption. diff --git a/api/Cargo.toml b/api/Cargo.toml index 6158322..ab6a0ba 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -25,3 +25,4 @@ spl-associated-token-account.workspace = true brine-tree.workspace = true array-const-fn-init.workspace = true const-crypto.workspace = true +solana-sha256-hasher.workspace = true diff --git a/api/src/instruction/miner.rs b/api/src/instruction/miner.rs index 3cdbf88..261cac7 100644 --- a/api/src/instruction/miner.rs +++ b/api/src/instruction/miner.rs @@ -50,7 +50,7 @@ pub fn build_register_ix( name: &str ) -> Instruction { let name = utils::to_name(name); - let (miner_address, _bump) = miner_pda(signer, name); + let (miner_address, _bump) = miner_find_pda(&signer, name); Instruction { program_id: crate::ID, @@ -58,8 +58,8 @@ pub fn build_register_ix( AccountMeta::new(signer, true), AccountMeta::new(miner_address, false), AccountMeta::new_readonly(solana_program::system_program::ID, false), - AccountMeta::new_readonly(sysvar::rent::ID, false), AccountMeta::new_readonly(sysvar::slot_hashes::ID, false), + AccountMeta::new_readonly(sysvar::clock::ID, false) ], data: Register { name, @@ -85,6 +85,8 @@ pub fn build_mine_ix( AccountMeta::new(tape, false), AccountMeta::new_readonly(ARCHIVE_ADDRESS, false), AccountMeta::new_readonly(sysvar::slot_hashes::ID, false), + AccountMeta::new_readonly(sysvar::clock::ID, false) + ], data: Mine { pow, @@ -115,7 +117,7 @@ pub fn build_claim_ix( } } -pub fn build_close_ix( +pub fn build_unregister_ix( signer: Pubkey, miner: Pubkey, ) -> Instruction { @@ -124,7 +126,6 @@ pub fn build_close_ix( accounts: vec![ AccountMeta::new(signer, true), AccountMeta::new(miner, false), - AccountMeta::new_readonly(solana_program::system_program::ID, false), ], data: Unregister {}.to_bytes(), } diff --git a/api/src/instruction/program.rs b/api/src/instruction/program.rs index ae5ffe1..58d881d 100644 --- a/api/src/instruction/program.rs +++ b/api/src/instruction/program.rs @@ -35,30 +35,25 @@ pub fn build_initialize_ix( let (block_pda, _block_bump) = block_pda(); let (mint_pda, _mint_bump) = mint_pda(); let (treasury_pda, _treasury_bump) = treasury_pda(); - let (treasury_ata, _treasury_ata_bump) = treasury_ata(); - let (metadata_pda, _metadata_bump) = metadata_pda(mint_pda); + let (treasury_ata, _treasury_ata_bump) = treasury_find_ata(); + let (metadata_pda, _metadata_bump) = metadata_find_pda(mint_pda); let name = utils::to_name("genesis"); - let (tape_pda, _tape_bump) = tape_pda(signer, &name); - let (writer_pda, _writer_bump) = writer_pda(tape_pda); + let (tape_pda, _tape_bump) = tape_find_pda(&signer, &name); + let (writer_pda, _writer_bump) = writer_find_pda(&tape_pda); - assert_eq!(archive_pda, ARCHIVE_ADDRESS); - assert_eq!(epoch_pda, EPOCH_ADDRESS); - assert_eq!(block_pda, BLOCK_ADDRESS); - assert_eq!(mint_pda, MINT_ADDRESS); - assert_eq!(treasury_pda, TREASURY_ADDRESS); assert_eq!(treasury_ata, TREASURY_ATA); Instruction { program_id: crate::ID, accounts: vec![ AccountMeta::new(signer, true), - AccountMeta::new(archive_pda, false), - AccountMeta::new(epoch_pda, false), - AccountMeta::new(block_pda, false), + AccountMeta::new(*archive_pda, false), + AccountMeta::new(*epoch_pda, false), + AccountMeta::new(*block_pda, false), AccountMeta::new(metadata_pda, false), - AccountMeta::new(mint_pda, false), - AccountMeta::new(treasury_pda, false), + AccountMeta::new(*mint_pda, false), + AccountMeta::new(*treasury_pda, false), AccountMeta::new(treasury_ata, false), AccountMeta::new(tape_pda, false), AccountMeta::new(writer_pda, false), @@ -67,15 +62,16 @@ pub fn build_initialize_ix( AccountMeta::new_readonly(spl_token::ID, false), AccountMeta::new_readonly(spl_associated_token_account::ID, false), AccountMeta::new_readonly(mpl_token_metadata::ID, false), - AccountMeta::new_readonly(sysvar::rent::ID, false), AccountMeta::new_readonly(sysvar::slot_hashes::ID, false), + AccountMeta::new_readonly(sysvar::rent::ID, false), + AccountMeta::new_readonly(sysvar::clock::ID, false) + ], data: Initialize {}.to_bytes(), } } pub fn build_airdrop_ix( - signer: Pubkey, beneficiary: Pubkey, amount: u64 ) -> Instruction { @@ -85,10 +81,9 @@ pub fn build_airdrop_ix( Instruction { program_id: crate::ID, accounts: vec![ - AccountMeta::new(signer, true), AccountMeta::new(beneficiary, false), - AccountMeta::new(mint_pda, false), - AccountMeta::new(treasury_pda, false), + AccountMeta::new(*mint_pda, false), + AccountMeta::new(*treasury_pda, false), AccountMeta::new_readonly(spl_token::ID, false), ], data: Airdrop { diff --git a/api/src/instruction/spool.rs b/api/src/instruction/spool.rs index 4f98b7c..f13d499 100644 --- a/api/src/instruction/spool.rs +++ b/api/src/instruction/spool.rs @@ -1,8 +1,6 @@ use steel::*; use crate::{ - consts::*, - pda::*, - types::*, + consts::*, pda::spool_find_pda, types::ProofPath, }; #[repr(u8)] @@ -29,9 +27,7 @@ pub struct Create { #[repr(C)] #[derive(Clone, Copy, Debug, Pod, Zeroable)] -pub struct Destroy { - pub number: [u8; 8], -} +pub struct Destroy {} #[repr(C)] #[derive(Clone, Copy, Debug, Pod, Zeroable)] @@ -60,7 +56,7 @@ pub fn build_create_ix( miner_address: Pubkey, number: u64, ) -> Instruction { - let (spool_address, _bump) = spool_pda(miner_address, number); + let (spool_address, _bump) = spool_find_pda(&miner_address, number); Instruction { program_id: crate::ID, @@ -69,7 +65,7 @@ pub fn build_create_ix( AccountMeta::new(miner_address, false), AccountMeta::new(spool_address, false), AccountMeta::new_readonly(solana_program::system_program::ID, false), - AccountMeta::new_readonly(sysvar::rent::ID, false), + AccountMeta::new_readonly(sysvar::clock::ID, false), ], data: Create { number: number.to_le_bytes(), @@ -82,19 +78,15 @@ pub fn build_destroy_ix( miner_address: Pubkey, number: u64, ) -> Instruction { - let (spool_address, _bump) = spool_pda(miner_address, number); + let (spool_address, _bump) = spool_find_pda(&miner_address, number); Instruction { program_id: crate::ID, accounts: vec![ AccountMeta::new(signer, true), - AccountMeta::new(miner_address, false), AccountMeta::new(spool_address, false), - AccountMeta::new_readonly(solana_program::system_program::ID, false), ], - data: Destroy { - number: number.to_le_bytes(), - }.to_bytes(), + data: Destroy {}.to_bytes(), } } diff --git a/api/src/instruction/tape.rs b/api/src/instruction/tape.rs index b2cf3f3..458fd71 100644 --- a/api/src/instruction/tape.rs +++ b/api/src/instruction/tape.rs @@ -68,8 +68,8 @@ pub fn build_create_ix( ) -> Instruction { let name = utils::to_name(name); - let (tape_address, _tape_bump) = tape_pda(signer, &name); - let (writer_address, _writer_bump) = writer_pda(tape_address); + let (tape_address, _tape_bump) = tape_find_pda(&signer, &name); + let (writer_address, _writer_bump) = writer_find_pda(&tape_address); Instruction { program_id: crate::ID, @@ -78,7 +78,7 @@ pub fn build_create_ix( AccountMeta::new(tape_address, false), AccountMeta::new(writer_address, false), AccountMeta::new_readonly(solana_program::system_program::ID, false), - AccountMeta::new_readonly(sysvar::rent::ID, false), + AccountMeta::new_readonly(sysvar::clock::ID, false), ], data: Create { name, @@ -119,6 +119,7 @@ pub fn build_write_ix( AccountMeta::new(signer, true), AccountMeta::new(tape, false), AccountMeta::new(writer, false), + AccountMeta::new_readonly(sysvar::clock::ID, false) ], data: ix_data, } @@ -142,6 +143,7 @@ pub fn build_update_ix( AccountMeta::new(signer, true), AccountMeta::new(tape, false), AccountMeta::new(writer, false), + AccountMeta::new_readonly(sysvar::clock::ID, false) ], data: Update { segment_number, @@ -165,8 +167,6 @@ pub fn build_finalize_ix( AccountMeta::new(tape, false), AccountMeta::new(writer, false), AccountMeta::new(ARCHIVE_ADDRESS, false), - AccountMeta::new_readonly(solana_program::system_program::ID, false), - AccountMeta::new_readonly(sysvar::rent::ID, false), ], data: Finalize {}.to_bytes(), } diff --git a/api/src/pda.rs b/api/src/pda.rs index adafd99..8cef999 100644 --- a/api/src/pda.rs +++ b/api/src/pda.rs @@ -1,53 +1,94 @@ +#![allow(unexpected_cfgs)] + +use std::mem::MaybeUninit; + use steel::*; use crate::consts::*; -#[cfg(debug_assertions)] -pub fn archive_pda() -> (Pubkey, u8) { - Pubkey::find_program_address(&[ARCHIVE], &crate::id()) -} -#[cfg(not(debug_assertions))] -#[inline(always)] -pub fn archive_pda() -> (Pubkey, u8) { - (ARCHIVE_ADDRESS, ARCHIVE_BUMP) -} +pub fn pda_derive_address( + seeds: &[&[u8]; N], + bump: Option, + program_id: &Pubkey, +) -> Pubkey { + /// Maximum number of seeds. + pub const MAX_SEEDS: usize = 16; -#[cfg(debug_assertions)] -pub fn epoch_pda() -> (Pubkey, u8) { - Pubkey::find_program_address(&[EPOCH], &crate::id()) -} + /// The marker used to derive [program derived addresses][pda]. + /// + /// [pda]: https://solana.com/docs/core/pda + pub const PDA_MARKER: &[u8; 21] = b"ProgramDerivedAddress"; + + const { + assert!(N < MAX_SEEDS, "number of seeds must be less than MAX_SEEDS"); + } + + const UNINIT: MaybeUninit<&[u8]> = MaybeUninit::<&[u8]>::uninit(); + let mut data = [UNINIT; MAX_SEEDS + 2]; + let mut i = 0; + + while i < N { + // SAFETY: `data` is guaranteed to have enough space for `N` seeds, + // so `i` will always be within bounds. + unsafe { + data.get_unchecked_mut(i).write(seeds.get_unchecked(i)); + } + i += 1; + } + + let bump_seed = [bump.unwrap_or_default()]; + + // SAFETY: `data` is guaranteed to have enough space for `MAX_SEEDS + 2` + // elements, and `MAX_SEEDS` is as large as `N`. + unsafe { + if bump.is_some() { + data.get_unchecked_mut(i).write(&bump_seed); + i += 1; + } + data.get_unchecked_mut(i).write(program_id.as_ref()); + data.get_unchecked_mut(i + 1).write(PDA_MARKER.as_ref()); + } + + #[cfg(target_os = "solana")] + { + let mut pda = MaybeUninit::<[u8; 32]>::uninit(); + + // SAFETY: `data` has `i + 2` elements initialized. + unsafe { + solana_sha256_hasher::sol_sha256( + data.as_ptr() as *const u8, + (i + 2) as u64, + pda.as_mut_ptr() as *mut u8, + ); + } + + // SAFETY: `pda` has been initialized by the syscall. + let pubkey = unsafe { pda.assume_init() }; + Pubkey::new_from_array(pubkey) + } -#[cfg(not(debug_assertions))] -#[inline(always)] -pub fn epoch_pda() -> (Pubkey, u8) { - (EPOCH_ADDRESS, EPOCH_BUMP) + #[cfg(not(target_os = "solana"))] + unreachable!("deriving a pda is only available on target `solana`"); } -#[cfg(debug_assertions)] -pub fn block_pda() -> (Pubkey, u8) { - Pubkey::find_program_address(&[BLOCK], &crate::id()) +pub const fn archive_pda() -> (&'static Pubkey, u8) { + (&ARCHIVE_ADDRESS, ARCHIVE_BUMP) } -#[cfg(not(debug_assertions))] -#[inline(always)] -pub fn block_pda() -> (Pubkey, u8) { - (BLOCK_ADDRESS, BLOCK_BUMP) +pub const fn epoch_pda() -> (&'static Pubkey, u8) { + (&EPOCH_ADDRESS, EPOCH_BUMP) } -#[cfg(debug_assertions)] -pub fn treasury_pda() -> (Pubkey, u8) { - Pubkey::find_program_address(&[TREASURY], &crate::id()) +pub const fn block_pda() -> (&'static Pubkey, u8) { + (&BLOCK_ADDRESS, BLOCK_BUMP) } -#[cfg(not(debug_assertions))] -#[inline(always)] -pub fn treasury_pda() -> (Pubkey, u8) { - (TREASURY_ADDRESS, TREASURY_BUMP) +pub const fn treasury_pda() -> (&'static Pubkey, u8) { + (&TREASURY_ADDRESS, TREASURY_BUMP) } -#[cfg(debug_assertions)] -pub fn treasury_ata() -> (Pubkey, u8) { - let (treasury_pda, _bump) = treasury_pda(); +pub fn treasury_find_ata() -> (Pubkey, u8) { + let (treasury_pda,_bump) = treasury_pda(); let (mint_pda, _bump) = mint_pda(); Pubkey::find_program_address( &[ @@ -59,81 +100,61 @@ pub fn treasury_ata() -> (Pubkey, u8) { ) } -#[cfg(not(debug_assertions))] -#[inline(always)] -pub fn treasury_ata() -> (Pubkey, u8) { - (TREASURY_ATA, TREASURY_ATA_BUMP) -} - -#[cfg(debug_assertions)] -pub fn mint_pda() -> (Pubkey, u8) { - Pubkey::find_program_address(&[MINT, MINT_SEED], &crate::id()) +pub const fn mint_pda() -> (&'static Pubkey, u8) { + (&MINT_ADDRESS, MINT_BUMP) } -#[cfg(not(debug_assertions))] -#[inline(always)] -pub fn mint_pda() -> (Pubkey, u8) { - (MINT_ADDRESS, MINT_BUMP) -} - -pub fn metadata_pda(mint: Pubkey) -> (Pubkey, u8) { +pub fn metadata_find_pda(mint: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address( - &[METADATA, mpl_token_metadata::ID.as_ref(), mint.as_ref()], + &[METADATA, mpl_token_metadata::ID.as_ref(), mint.as_ref() ], &mpl_token_metadata::ID, ) } -pub fn tape_pda(authority: Pubkey, name: &[u8; NAME_LEN]) -> (Pubkey, u8) { +pub fn tape_find_pda(authority: &Pubkey, name: &[u8; NAME_LEN]) -> (Pubkey, u8) { Pubkey::find_program_address(&[TAPE, authority.as_ref(), name.as_ref()], &crate::id()) } -pub fn writer_pda(tape: Pubkey) -> (Pubkey, u8) { +pub fn tape_derive_pda(authority: &Pubkey, name: &[u8; NAME_LEN], bump: u8) -> Pubkey { + pda_derive_address( + &[TAPE, authority.as_ref(), name.as_ref()], + Some(bump), + &crate::id(), + ) +} + +pub fn writer_find_pda(tape: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address(&[WRITER, tape.as_ref()], &crate::id()) } -pub fn miner_pda(authority: Pubkey, name: [u8; NAME_LEN]) -> (Pubkey, u8) { +pub fn writer_derive_pda(tape: &Pubkey, bump: u8) -> Pubkey { + pda_derive_address(&[WRITER, tape.as_ref()], Some(bump), &crate::id()) +} + +pub fn miner_find_pda(authority: &Pubkey, name: [u8; NAME_LEN]) -> (Pubkey, u8) { Pubkey::find_program_address(&[MINER, authority.as_ref(), name.as_ref()], &crate::id()) } -pub fn spool_pda(miner: Pubkey, number: u64) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[SPOOL, miner.as_ref(), number.to_le_bytes().as_ref()], +pub fn miner_derive_pda(authority: &Pubkey, name: &[u8; NAME_LEN], bump: u8) -> Pubkey { + pda_derive_address( + &[MINER, authority.as_ref(), name.as_ref()], + Some(bump), &crate::id(), ) } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_pda_against_consts() { - // These tests, as nonsensical as they seem, are to ensure that the PDAs generated by the - // consts match the ones generated by the official functions. The consts are generated by - // external deps, so if we straight up use the consts, we are trusting that the external - // deps are working as expected, which is not a good idea. Always be testing. - - let (pda, bump) = archive_pda(); - assert_eq!(bump, ARCHIVE_BUMP); - assert_eq!(pda, ARCHIVE_ADDRESS); - - let (pda, bump) = epoch_pda(); - assert_eq!(bump, EPOCH_BUMP); - assert_eq!(pda, EPOCH_ADDRESS); - - let (pda, bump) = block_pda(); - assert_eq!(bump, BLOCK_BUMP); - assert_eq!(pda, BLOCK_ADDRESS); - - let (pda, bump) = mint_pda(); - assert_eq!(bump, MINT_BUMP); - assert_eq!(pda, MINT_ADDRESS); - let (pda, bump) = treasury_pda(); - assert_eq!(bump, TREASURY_BUMP); - assert_eq!(pda, TREASURY_ADDRESS); - - let (pda, _bump) = treasury_ata(); - assert_eq!(pda, TREASURY_ATA); - } +pub fn spool_find_pda(miner: &Pubkey, number: u64) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[SPOOL, miner.as_ref(), number.to_le_bytes().as_ref()], + &crate::id(), + ) } + +pub fn spool_derive_pda(miner: &Pubkey, number: u64, bump: u8) -> Pubkey { + pda_derive_address( + &[SPOOL, miner.as_ref(), &number.to_le_bytes()], + Some(bump), + &crate::id(), + ) +} \ No newline at end of file diff --git a/api/src/state/miner.rs b/api/src/state/miner.rs index 95c7e11..d4f98a0 100644 --- a/api/src/state/miner.rs +++ b/api/src/state/miner.rs @@ -21,6 +21,8 @@ pub struct Miner { pub total_proofs: u64, pub total_rewards: u64, + + pub pda_bump: u64 } state!(AccountType, Miner); diff --git a/api/src/state/spool.rs b/api/src/state/spool.rs index 4bd7b74..7cffa33 100644 --- a/api/src/state/spool.rs +++ b/api/src/state/spool.rs @@ -16,6 +16,8 @@ pub struct Spool { pub last_proof_block: u64, pub last_proof_at: i64, + + pub pda_bump: u64, } state!(AccountType, Spool); diff --git a/api/src/state/tape.rs b/api/src/state/tape.rs index 04e648a..582bf74 100644 --- a/api/src/state/tape.rs +++ b/api/src/state/tape.rs @@ -21,6 +21,8 @@ pub struct Tape { pub last_rent_block: u64, pub total_segments: u64, + pub pda_bump: u64, + // +Phantom Vec for merkle subtree nodes (up to 4096). } diff --git a/api/src/state/writer.rs b/api/src/state/writer.rs index 15f2485..7aa00e0 100644 --- a/api/src/state/writer.rs +++ b/api/src/state/writer.rs @@ -8,6 +8,7 @@ use super::AccountType; pub struct Writer { pub tape: Pubkey, pub state: SegmentTree, + pub pda_bump: u64, } state!(AccountType, Writer); diff --git a/cli/src/commands/network.rs b/cli/src/commands/network.rs index 2632609..012b52c 100644 --- a/cli/src/commands/network.rs +++ b/cli/src/commands/network.rs @@ -110,7 +110,7 @@ pub async fn handle_register( log::print_info("Registering miner..."); - let (miner_address, _) = miner_pda(context.payer().pubkey(), to_name(&name)); + let (miner_address, _) = miner_find_pda(&context.payer().pubkey(), to_name(&name)); register_miner(context.rpc(), context.payer(), &name).await?; @@ -135,8 +135,8 @@ pub async fn get_or_create_miner( let (miner_address, name) = match (pubkey_opt, name_opt) { (Some(_), Some(_)) => bail!("Cannot provide both pubkey and name"), (Some(p), None) => (Pubkey::from_str(&p)?, None), - (None, Some(n)) => (miner_pda(payer.pubkey(), to_name(&n)).0, Some(n)), - (None, None) => (miner_pda(payer.pubkey(), to_name("default")).0, Some("default".to_string())), + (None, Some(n)) => (miner_find_pda(&payer.pubkey(), to_name(&n)).0, Some(n)), + (None, None) => (miner_find_pda(&payer.pubkey(), to_name("default")).0, Some("default".to_string())), }; let miner_account = get_miner_account(client, &miner_address).await; diff --git a/client/src/program/airdrop.rs b/client/src/program/airdrop.rs index 1a1dc62..53aa131 100644 --- a/client/src/program/airdrop.rs +++ b/client/src/program/airdrop.rs @@ -18,7 +18,7 @@ pub async fn airdrop_tokens( amount: u64, ) -> Result { let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(50_000); - let airdrop_ix = build_airdrop_ix(signer.pubkey(), beneficiary, amount); + let airdrop_ix = build_airdrop_ix(beneficiary, amount); let signature = build_send_and_confirm_tx( &[compute_budget_ix, airdrop_ix], diff --git a/client/src/tape/create.rs b/client/src/tape/create.rs index 9c7022c..3d77679 100644 --- a/client/src/tape/create.rs +++ b/client/src/tape/create.rs @@ -17,8 +17,8 @@ pub async fn create_tape( name: &str, ) -> Result<(Pubkey, Pubkey, Signature)> { - let (tape_address, _tape_bump) = tape_pda(signer.pubkey(), &to_name(name)); - let (writer_address, _writer_bump) = writer_pda(tape_address); + let (tape_address, _tape_bump) = tape_find_pda(&signer.pubkey(), &to_name(name)); + let (writer_address, _writer_bump) = writer_find_pda(&tape_address); let create_ix = build_create_ix( signer.pubkey(), diff --git a/client/src/utils/account.rs b/client/src/utils/account.rs index 2cfd88a..30003fc 100644 --- a/client/src/utils/account.rs +++ b/client/src/utils/account.rs @@ -74,7 +74,7 @@ pub async fn get_epoch_account(client: &Arc) -> Result<(Epoch, Pubkey let account = Epoch::unpack(&account.data) .map_err(|e| anyhow!("Failed to unpack epoch account: {}", e)) .copied()?; - Ok((account, epoch_address)) + Ok((account, *epoch_address)) } pub async fn get_block_account(client: &Arc) -> Result<(Block, Pubkey)> { @@ -84,7 +84,7 @@ pub async fn get_block_account(client: &Arc) -> Result<(Block, Pubkey let account = Block::unpack(&account.data) .map_err(|e| anyhow!("Failed to unpack block account: {}", e)) .copied()?; - Ok((account, block_address)) + Ok((account, *block_address)) } pub async fn get_archive_account(client: &Arc) -> Result<(Archive, Pubkey)> { @@ -94,5 +94,5 @@ pub async fn get_archive_account(client: &Arc) -> Result<(Archive, Pu let account = Archive::unpack(&account.data) .map_err(|e| anyhow!("Failed to unpack archive account: {}", e)) .copied()?; - Ok((account, archive_address)) + Ok((account, *archive_address)) } diff --git a/example/Cargo.toml b/example/Cargo.toml deleted file mode 100644 index 26c79f4..0000000 --- a/example/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "tape-example" -publish = false -description.workspace = true -version.workspace = true -edition.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true -readme.workspace = true -keywords.workspace = true - -[lib] -crate-type = ["cdylib", "lib"] -name = "example" - -[dependencies] -tape-api.workspace = true -tape-program.workspace = true -mpl-token-metadata.workspace = true -solana-program.workspace = true -spl-token.workspace = true -spl-associated-token-account.workspace = true -steel.workspace = true -brine-tree.workspace = true - -# Fix for issues with `build-sbf` rustc being on `1.79-dev` -bytemuck_derive=">=1.8.1, <1.9.0" -half = "=2.4.1" - -[dev-dependencies] -bincode = "1.3" -solana-sdk = "2.1.0" -litesvm = "0.5.0" -litesvm-token = "0.5.0" -pretty-hex = "0.4.1" -rand = "0.8.5" -solana-compute-budget = "2.1.16" diff --git a/example/src/lib.rs b/example/src/lib.rs deleted file mode 100644 index 3f77839..0000000 --- a/example/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -#![allow(unexpected_cfgs)] -#![allow(unused)] - -use tape_api::instruction::tape::*; -use steel::*; - -declare_id!("Gzuu6orA9tz2ifE7zyupNiuhogYkRBmbuQpWJme5dGhJ"); - -pub fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - _data: &[u8], -) -> ProgramResult { - let [ - signer_info, - tape_info, - writer_info, - tape_program_info, - ] = accounts else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - - solana_program::msg!(""); - - let your_data = vec![42; 1024]; // (you can be creative here) - let ix = &build_write_ix( - *signer_info.key, - *tape_info.key, - *writer_info.key, - &your_data - ); - - solana_program::program::invoke(ix, accounts); - - Ok(()) -} - -entrypoint!(process_instruction); diff --git a/example/tests/elfs/metadata.so b/example/tests/elfs/metadata.so deleted file mode 100644 index e3932e2..0000000 Binary files a/example/tests/elfs/metadata.so and /dev/null differ diff --git a/example/tests/example.rs b/example/tests/example.rs deleted file mode 100644 index 159bfa3..0000000 --- a/example/tests/example.rs +++ /dev/null @@ -1,69 +0,0 @@ -#![cfg(test)] -#![allow(unused)] - -pub mod utils; -use utils::*; -use steel::*; -use tape_api::prelude::*; - -use solana_sdk::{ - signature::Keypair, - signer::Signer, - transaction::Transaction, - clock::Clock, - pubkey::Pubkey -}; - -use litesvm::{types::{TransactionMetadata, TransactionResult}, LiteSVM}; - -#[test] -fn run_integration() { - let mut svm = setup_svm(); - - let payer = create_payer(&mut svm); - let payer_pk = payer.pubkey(); - - // Create a tape that we can write to - let (tape_address, writer_address) = create_tape(&mut svm, &payer, "tape-name"); - - // Call our example program - let ix = Instruction { - program_id: example::ID, - accounts: vec![ - AccountMeta::new(payer_pk, true), - AccountMeta::new(tape_address, false), - AccountMeta::new(writer_address, false), - AccountMeta::new_readonly(tape_api::ID, false), - ], - data: vec![], - }; - - let blockhash = svm.latest_blockhash(); - let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); - let res = send_tx(&mut svm, tx); - assert!(res.is_ok()); - - // Verify the on-chain state matches the local expectations - - let account = svm.get_account(&tape_address).unwrap(); - let tape = Tape::unpack(&account.data).unwrap(); - - let mut local_tree = SegmentTree::new(&[tape_address.as_ref()]); - - let data = vec![42; 1024]; - - let segments = data.chunks(SEGMENT_SIZE); - for (segment_number, segment) in segments.enumerate() { - let canonical_segment = padded_array::(segment); - - assert!(write_segment( - &mut local_tree, - segment_number as u64, - &canonical_segment, - ).is_ok()); - } - - assert_eq!(tape.total_segments, 1024 / SEGMENT_SIZE as u64); - assert_eq!(tape.merkle_root, local_tree.get_root().as_ref()); -} - diff --git a/example/tests/utils/mod.rs b/example/tests/utils/mod.rs deleted file mode 100644 index 4f9cfe0..0000000 --- a/example/tests/utils/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod svm; -pub mod token; -pub mod tape; - -pub use svm::*; -pub use token::*; -pub use tape::*; diff --git a/example/tests/utils/svm.rs b/example/tests/utils/svm.rs deleted file mode 100644 index 4745d33..0000000 --- a/example/tests/utils/svm.rs +++ /dev/null @@ -1,110 +0,0 @@ -use std::{env, path::PathBuf}; -use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; -use solana_compute_budget::compute_budget::ComputeBudget; -use litesvm::{types::{TransactionMetadata, TransactionResult}, LiteSVM}; -use super::init_tape_program; -use pretty_hex::*; -use bincode; - -pub fn program_bytes() -> Vec { - // Fetch the example program bytes from target - let mut so_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - so_path.push("../target/deploy/example.so"); - std::fs::read(so_path).unwrap() -} - -pub fn tape_bytes() -> Vec { - // Fetch the tape program bytes from target - let mut so_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - so_path.push("../target/deploy/tape.so"); - std::fs::read(so_path).unwrap() -} - -pub fn metadata_bytes() -> Vec { - // Fetch the metadata program bytes from elfs/ dir before running the test - let mut so_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - so_path.push("tests/elfs/metadata.so"); - std::fs::read(so_path).unwrap() -} - -pub fn setup_svm() -> LiteSVM { - let mut svm = LiteSVM::new().with_compute_budget( - ComputeBudget { - compute_unit_limit: 1_000_000, - ..Default::default() - } - ); - svm.add_program(mpl_token_metadata::ID, &metadata_bytes()); - svm.add_program(tape_api::ID, &tape_bytes()); - svm.add_program(example::ID, &program_bytes()); - - let payer = create_payer(&mut svm); - let payer_pk = payer.pubkey(); - - // Initialize the tape program (not usually required, this would already be done on-chain) - init_tape_program(&mut svm, &payer); - - svm -} - -pub fn send_tx(svm: &mut LiteSVM, tx: Transaction) -> TransactionResult { - let res = svm.send_transaction(tx.clone()); - - let meta = match res.as_ref() { - Ok(v) => v.clone(), - Err(v) => v.meta.clone() - }; - - print_tx(meta, tx); - - if res.is_err() { - println!("error:\t{:?}", res.as_ref().err().unwrap().err); - } - - res.clone() -} - -pub fn create_payer(svm: &mut LiteSVM) -> Keypair { - let payer_kp = Keypair::new(); - let payer_pk = payer_kp.pubkey(); - svm.airdrop(&payer_pk, 1_000_000_000).unwrap(); - payer_kp -} - -pub fn create_keypair() -> Keypair { - Keypair::new() -} - -pub fn get_tx_size(tx: &Transaction) -> usize { - bincode::serialize(tx).unwrap().len() -} - -pub fn print_tx(meta: TransactionMetadata, tx: Transaction) { - let msg = tx.message().serialize(); - - println!("\n"); - println!("--------------------------------------------------------------------------------"); - println!("sig:\t{:?}", meta.signature); - println!("len:\t{:?}", msg.len()); - - for i in 0..tx.message.instructions.len() { - let ix = &tx.message.instructions[i]; - println!("accounts:"); - - for key in &ix.accounts { - println!("\t{}: {:?}", key, tx.message.account_keys[*key as usize]); - } - - println!("\ndata:\n\t{:?}", ix.data); - println!("\n\n{}\n", pretty_hex(&ix.data)) - } - - println!(); - println!("size:\t{:?}", get_tx_size(&tx)); - println!("cu:\t{:?}", meta.compute_units_consumed); - println!("logs:"); - for log in &meta.logs { - println!("\t{log:?}"); - } - println!(); -} diff --git a/example/tests/utils/tape.rs b/example/tests/utils/tape.rs deleted file mode 100644 index 3a12d05..0000000 --- a/example/tests/utils/tape.rs +++ /dev/null @@ -1,42 +0,0 @@ -use solana_sdk::{ - signature::Keypair, - signer::Signer, - transaction::Transaction, - pubkey::Pubkey -}; - -use super::send_tx; -use litesvm::LiteSVM; - -use tape_api::prelude::*; -use tape_api::instruction::program::{ - build_initialize_ix, -}; -use tape_api::instruction::tape::{ - build_create_ix, -}; - -pub fn init_tape_program(svm: &mut LiteSVM, payer: &Keypair) { - let payer_pk = payer.pubkey(); - - let ix = build_initialize_ix(payer_pk); - let blockhash = svm.latest_blockhash(); - let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); - let res = send_tx(svm, tx); - - assert!(res.is_ok()); -} - -pub fn create_tape(svm: &mut LiteSVM, payer: &Keypair, tape_name: &str) -> (Pubkey, Pubkey) { - let payer_pk = payer.pubkey(); - let (tape_address, _) = tape_pda(payer_pk, &to_name(tape_name)); - let (writer_address, _) = writer_pda(tape_address); - - let blockhash = svm.latest_blockhash(); - let ix = build_create_ix(payer_pk, tape_name); - let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); - let res = send_tx(svm, tx); - assert!(res.is_ok()); - - (tape_address, writer_address) -} diff --git a/example/tests/utils/token.rs b/example/tests/utils/token.rs deleted file mode 100644 index 7a290ea..0000000 --- a/example/tests/utils/token.rs +++ /dev/null @@ -1,45 +0,0 @@ -use solana_sdk::{pubkey::Pubkey, signature::Keypair}; -use litesvm::{types::FailedTransactionMetadata, LiteSVM}; -use litesvm_token::{ - CreateAssociatedTokenAccount, - CreateMint, - MintTo, - spl_token::state::{Account, Mint}, - get_spl_account -}; - -pub fn create_mint(svm: &mut LiteSVM, payer_kp: &Keypair, owner_pk: &Pubkey, decimals: u8) -> Pubkey { - CreateMint::new(svm, payer_kp) - .authority(owner_pk) - .decimals(decimals) - .send() - .unwrap() -} - -pub fn create_ata(svm: &mut LiteSVM, payer_kp: &Keypair, mint_pk: &Pubkey, owner_pk: &Pubkey) -> Pubkey { - CreateAssociatedTokenAccount::new(svm, payer_kp, mint_pk) - .owner(owner_pk) - .send() - .unwrap() -} - -pub fn get_ata_balance(svm: &LiteSVM, ata: &Pubkey) -> u64 { - let info : Account = get_spl_account(svm, ata).unwrap(); - info.amount -} - -pub fn get_mint(svm: &LiteSVM, mint: &Pubkey) -> Mint { - get_spl_account(svm, mint).unwrap() -} - -pub fn mint_to(svm: &mut LiteSVM, - payer: &Keypair, - mint: &Pubkey, - mint_owner: &Keypair, - destination: &Pubkey, - amount: u64, -) -> Result<(), FailedTransactionMetadata> { - MintTo::new(svm, payer, mint, destination, amount) - .owner(mint_owner) - .send() -} diff --git a/program/Cargo.toml b/program/Cargo.toml index eac4492..65ce199 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -31,6 +31,7 @@ spl-associated-token-account.workspace = true steel.workspace = true brine-tree.workspace = true + # Fix for issues with `build-sbf` rustc being on `1.79-dev` bytemuck_derive=">=1.8.1, <1.9.0" half = "=2.4.1" @@ -45,3 +46,7 @@ rand = "0.8.5" solana-compute-budget = "2.1.16" solana-client = "=2.1" tokio = { version = "1.37", features = ["full"] } +chrono.workspace = true +sha2 = "0.10.9" +serde_json = "1.0.142" +serde.workspace = true diff --git a/program/cu_logs.json b/program/cu_logs.json new file mode 100644 index 0000000..d02a43b --- /dev/null +++ b/program/cu_logs.json @@ -0,0 +1,292 @@ +[ + { + "timestamp": "2025-09-03 14:11:25", + "entries": { + "TapeFinalize": { + "value": 14908, + "diff": 0 + }, + "MinerRegister": { + "value": 7764, + "diff": 0 + }, + "SpoolCreate": { + "value": 9629, + "diff": 0 + }, + "MinerClaim": { + "value": 11096, + "diff": 0 + }, + "MinerMine": { + "value": 5435278, + "diff": -1247 + }, + "TapeWrite": { + "value": 160452, + "diff": -72 + }, + "SpoolPack": { + "value": 23116, + "diff": 36 + }, + "SpoolUnpack": { + "value": 44176, + "diff": 16 + }, + "TapeUpdate": { + "value": 51624, + "diff": -76 + }, + "TapeSubsidize": { + "value": 38112, + "diff": 0 + }, + "ProgramInitialize": { + "value": 191839, + "diff": 12 + }, + "TapeCreate": { + "value": 96908, + "diff": 13500 + }, + "SpoolCommit": { + "value": 60686, + "diff": -8 + } + }, + "checksum": "ec7ee8f3ca5af10768038f2d1d4c29d2ae1470c7c7443c3f375a095805e72d38" + }, + { + "timestamp": "2025-09-03 14:07:19", + "entries": { + "SpoolUnpack": { + "value": 44160, + "diff": -20 + }, + "MinerRegister": { + "value": 7764, + "diff": 0 + }, + "MinerMine": { + "value": 5436525, + "diff": 382 + }, + "MinerClaim": { + "value": 11096, + "diff": 0 + }, + "ProgramInitialize": { + "value": 191827, + "diff": 17988 + }, + "TapeCreate": { + "value": 83408, + "diff": 15000 + }, + "TapeWrite": { + "value": 160524, + "diff": -28 + }, + "TapeFinalize": { + "value": 14908, + "diff": 0 + }, + "SpoolCommit": { + "value": 60694, + "diff": -4 + }, + "SpoolCreate": { + "value": 9629, + "diff": 0 + }, + "TapeSubsidize": { + "value": 38112, + "diff": 0 + }, + "SpoolPack": { + "value": 23080, + "diff": -36 + }, + "TapeUpdate": { + "value": 51700, + "diff": -12 + } + }, + "checksum": "e34bca3b56f895de30f54483627b95845388f38cfe0f4f907652ac18e7c38c7d" + }, + { + "timestamp": "2025-09-03 14:05:47", + "entries": { + "SpoolPack": { + "value": 23116, + "diff": -20 + }, + "TapeCreate": { + "value": 68408, + "diff": -9000 + }, + "TapeFinalize": { + "value": 14908, + "diff": 0 + }, + "TapeSubsidize": { + "value": 38112, + "diff": 0 + }, + "SpoolCreate": { + "value": 9629, + "diff": -6000 + }, + "ProgramInitialize": { + "value": 173839, + "diff": -8992 + }, + "SpoolUnpack": { + "value": 44180, + "diff": -112 + }, + "MinerClaim": { + "value": 11096, + "diff": 0 + }, + "TapeUpdate": { + "value": 51712, + "diff": -4 + }, + "SpoolCommit": { + "value": 60698, + "diff": 108 + }, + "TapeWrite": { + "value": 160552, + "diff": 120 + }, + "MinerRegister": { + "value": 7764, + "diff": 0 + }, + "MinerMine": { + "value": 5436143, + "diff": 1975 + } + }, + "checksum": "376dca5b224dd795f8b714d0c2982c68c41e8e87c2dca4c48042c1aba50acf87" + }, + { + "timestamp": "2025-09-03 11:25:32", + "entries": { + "SpoolPack": { + "value": 23136, + "diff": 72 + }, + "TapeSubsidize": { + "value": 38112, + "diff": 0 + }, + "SpoolCommit": { + "value": 60590, + "diff": -92 + }, + "SpoolCreate": { + "value": 15629, + "diff": 6000 + }, + "MinerClaim": { + "value": 11096, + "diff": 0 + }, + "TapeWrite": { + "value": 160432, + "diff": -180 + }, + "MinerRegister": { + "value": 7764, + "diff": 0 + }, + "SpoolUnpack": { + "value": 44292, + "diff": 160 + }, + "MinerMine": { + "value": 5434168, + "diff": -3340 + }, + "TapeCreate": { + "value": 77408, + "diff": -4500 + }, + "ProgramInitialize": { + "value": 182831, + "diff": -4504 + }, + "TapeFinalize": { + "value": 14908, + "diff": 0 + }, + "TapeUpdate": { + "value": 51716, + "diff": 8 + } + }, + "checksum": "1cf96ca28a82a38cdec575584dda2dfd9b9c7a4b9c9ea9afd905fee3fedd6d10" + }, + { + "timestamp": "2025-09-03 11:24:10", + "entries": { + "MinerMine": { + "value": 5437508, + "diff": 7895 + }, + "TapeSubsidize": { + "value": 38112, + "diff": 0 + }, + "TapeWrite": { + "value": 160612, + "diff": 44 + }, + "MinerRegister": { + "value": 7764, + "diff": 0 + }, + "TapeUpdate": { + "value": 51708, + "diff": 0 + }, + "SpoolCommit": { + "value": 60682, + "diff": 24 + }, + "TapeCreate": { + "value": 81908, + "diff": 9000 + }, + "TapeFinalize": { + "value": 14908, + "diff": 0 + }, + "SpoolCreate": { + "value": 9629, + "diff": 0 + }, + "SpoolUnpack": { + "value": 44132, + "diff": -32 + }, + "MinerClaim": { + "value": 11096, + "diff": 0 + }, + "SpoolPack": { + "value": 23064, + "diff": -32 + }, + "ProgramInitialize": { + "value": 187335, + "diff": 8 + } + }, + "checksum": "65bfedfd27dd0e74255f3e07933c1bd7562cabc9b2b5445938bf5b255deb63be" + } +] \ No newline at end of file diff --git a/program/show_cu_logs.py b/program/show_cu_logs.py new file mode 100644 index 0000000..355ca09 --- /dev/null +++ b/program/show_cu_logs.py @@ -0,0 +1,42 @@ +import json +from tabulate import tabulate +from pathlib import Path + +def fmt(num): + return f"{num:,}" + +def fmt_diff(num): + sign = "+" if num > 0 else "" + return f"{sign}{num:,}" + +# Path to JSON +base_dir = Path(__file__).parent +json_path = base_dir / "cu_logs.json" + +try: + with open("cu_logs.json") as f: + data = json.load(f) +except FileNotFoundError: + print("File not found. No logs to display.") + data = [] + +data = data[:3] + +for d in data: + entries = d["entries"] + + # Convert each row: (Instruction, Value, Diff) + rows = [ + ( + k, + fmt(v["value"]), + fmt_diff(v["diff"]), + ) + for k, v in sorted(entries.items()) + ] + + print() + print("## Run at", d["timestamp"]) + print() + print(tabulate(rows, headers=["Instruction", "Compute Units", "Diff"], tablefmt="github")) + print() diff --git a/program/src/miner/claim.rs b/program/src/miner/claim.rs index 91848f7..feacb06 100644 --- a/program/src/miner/claim.rs +++ b/program/src/miner/claim.rs @@ -7,7 +7,7 @@ pub fn process_claim(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult let [ signer_info, beneficiary_info, - proof_info, + miner_info, treasury_info, treasury_ata_info, token_program_info, @@ -17,12 +17,7 @@ pub fn process_claim(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult signer_info.is_signer()?; - beneficiary_info - .is_writable()? - .as_token_account()? - .assert(|t| t.mint() == MINT_ADDRESS)?; - - let miner = proof_info + let miner = miner_info .as_account_mut::(&tape_api::ID)? .assert_mut_err( |p| p.authority == *signer_info.key, @@ -33,12 +28,8 @@ pub fn process_claim(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult .is_treasury()?; treasury_ata_info - .is_writable()? .is_treasury_ata()?; - token_program_info - .is_program(&spl_token::ID)?; - let mut amount = u64::from_le_bytes(args.amount); // If amount is zero, we claim the entire unclaimed rewards. @@ -53,13 +44,14 @@ pub fn process_claim(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult .ok_or(TapeError::ClaimTooLarge)?; // Transfer tokens from treasury to beneficiary. - transfer_signed( + transfer_signed_with_bump( treasury_info, treasury_ata_info, beneficiary_info, token_program_info, amount, &[TREASURY], + treasury_pda().1 )?; Ok(()) diff --git a/program/src/miner/mine.rs b/program/src/miner/mine.rs index 7405faa..af6a957 100644 --- a/program/src/miner/mine.rs +++ b/program/src/miner/mine.rs @@ -8,7 +8,6 @@ const EPOCHS_PER_YEAR: u64 = 365 * 24 * 60 / EPOCH_BLOCKS; pub fn process_mine(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { msg!("Starting mine instruction processing"); - let current_time = Clock::get()?.unix_timestamp; msg!("data size: {}", data.len()); let args = Mine::try_from_bytes(data)?; @@ -20,11 +19,15 @@ pub fn process_mine(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { tape_info, archive_info, slot_hashes_info, + clock_info ] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?; + + let current_time = Clock::from_account_info(clock_info)?.unix_timestamp; + msg!("Verified signer"); let archive = archive_info @@ -50,7 +53,7 @@ pub fn process_mine(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { .as_account_mut::(&tape_api::ID)?; msg!("Loaded miner account"); - let (miner_address, _miner_bump) = miner_pda(miner.authority, miner.name); + let miner_address = miner_derive_pda(&miner.authority, &miner.name, miner.pda_bump as u8); msg!("Computed miner PDA: {}", miner_address); check_condition( diff --git a/program/src/miner/register.rs b/program/src/miner/register.rs index 007f786..b705278 100644 --- a/program/src/miner/register.rs +++ b/program/src/miner/register.rs @@ -3,38 +3,37 @@ use tape_api::instruction::miner::Register; use steel::*; pub fn process_register(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { - let current_time = Clock::get()?.unix_timestamp; let args = Register::try_from_bytes(data)?; let [ signer_info, miner_info, system_program_info, - rent_info, slot_hashes_info, + clock_info ] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?; - let (miner_pda, _bump) = miner_pda(*signer_info.key, args.name); + let current_time = Clock::from_account_info(clock_info)?.unix_timestamp; + + let (miner_pda, miner_bump) = miner_find_pda(signer_info.key, args.name); miner_info .is_empty()? - .is_writable()? .has_address(&miner_pda)?; - system_program_info.is_program(&system_program::ID)?; - rent_info.is_sysvar(&sysvar::rent::ID)?; slot_hashes_info.is_sysvar(&sysvar::slot_hashes::ID)?; // Register miner. - create_program_account::( + create_program_account_with_bump::( miner_info, system_program_info, signer_info, &tape_api::ID, &[MINER, signer_info.key.as_ref(), args.name.as_ref()], + miner_bump )?; let miner = miner_info.as_account_mut::(&tape_api::ID)?; @@ -47,6 +46,7 @@ pub fn process_register(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramRes miner.total_proofs = 0; miner.total_rewards = 0; miner.unclaimed_rewards = 0; + miner.pda_bump = miner_bump as u64; let next_challenge = compute_next_challenge( &miner_info.key.to_bytes(), diff --git a/program/src/miner/unregister.rs b/program/src/miner/unregister.rs index 16ba1a7..fa1b495 100644 --- a/program/src/miner/unregister.rs +++ b/program/src/miner/unregister.rs @@ -5,7 +5,6 @@ pub fn process_unregister(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program let [ signer_info, miner_info, - system_program_info, ] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; @@ -13,7 +12,6 @@ pub fn process_unregister(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program signer_info.is_signer()?; miner_info - .is_writable()? .as_account::(&tape_api::ID)? .assert_err( |p| p.authority == *signer_info.key, @@ -21,9 +19,6 @@ pub fn process_unregister(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program )? .assert(|p| p.unclaimed_rewards == 0)?; - system_program_info - .is_program(&system_program::ID)?; - // Return rent to signer. miner_info.close(signer_info)?; diff --git a/program/src/program/airdrop.rs b/program/src/program/airdrop.rs index fe3c449..e6a5f39 100644 --- a/program/src/program/airdrop.rs +++ b/program/src/program/airdrop.rs @@ -5,8 +5,7 @@ use tape_api::instruction::program::Airdrop; pub fn process_airdrop(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { let args = Airdrop::try_from_bytes(data)?; let [ - signer_info, - beneficiary_info, + beneficiary_ata_info, mint_info, treasury_info, token_program_info, @@ -14,39 +13,24 @@ pub fn process_airdrop(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResu return Err(ProgramError::NotEnoughAccountKeys); }; - // Verify signer - signer_info.is_signer()?; - // Verify accounts let (mint_address, _mint_bump) = mint_pda(); - let (treasury_address, _treasury_bump) = treasury_pda(); mint_info - .is_writable()? .has_address(&mint_address)?; - treasury_info - .is_treasury()? - .has_address(&treasury_address)?; - token_program_info - .is_program(&spl_token::ID)?; - - // Verify beneficiary is a valid ATA - beneficiary_info - .is_writable()? - .as_token_account()? - .assert(|t| t.mint() == MINT_ADDRESS)?; // Parse amount let amount = u64::from_le_bytes(args.amount); // Mint tokens to beneficiary's ATA - mint_to_signed( + mint_to_signed_with_bump( mint_info, - beneficiary_info, + beneficiary_ata_info, treasury_info, token_program_info, amount, &[TREASURY], + treasury_pda().1, )?; Ok(()) diff --git a/program/src/program/initialize.rs b/program/src/program/initialize.rs index 5ded9d6..b787e11 100644 --- a/program/src/program/initialize.rs +++ b/program/src/program/initialize.rs @@ -23,81 +23,60 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program treasury_ata_info, tape_info, writer_info, - tape_program_info, + _tape_program_info, system_program_info, token_program_info, associated_token_program_info, metadata_program_info, - rent_sysvar_info, slot_hashes_info, + rent_sysvar_info, + clock_info ] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; + let (archive_address, archive_bump) = archive_pda(); + let (epcoh_address, epoch_bump) = epoch_pda(); + let (block_address, block_bump) = block_pda(); + let (mint_address, mint_bump) = mint_pda(); + let (treasury_address, treasury_bump) = treasury_pda(); + let (metadata_address, _metadata_bump) = metadata_find_pda(mint_address); + archive_info .is_empty()? - .is_writable()? - .has_seeds(&[ARCHIVE], &tape_api::ID)?; + .has_address(archive_address)?; epoch_info .is_empty()? - .is_writable()? - .has_seeds(&[EPOCH], &tape_api::ID)?; + .has_address(epcoh_address)?; block_info .is_empty()? - .is_writable()? - .has_seeds(&[BLOCK], &tape_api::ID)?; - - // Check mint, metadata, treasury - let (mint_address, mint_bump) = mint_pda(); - let (treasury_address, treasury_bump) = treasury_pda(); - let (metadata_address, _metadata_bump) = metadata_pda(mint_address); - - assert_eq!(mint_bump, MINT_BUMP); - assert_eq!(treasury_bump, TREASURY_BUMP); + .has_address(block_address)?; mint_info .is_empty()? - .is_writable()? - .has_address(&mint_address)?; + .has_address(mint_address)?; metadata_info .is_empty()? - .is_writable()? .has_address(&metadata_address)?; treasury_info .is_empty()? - .is_writable()? - .has_address(&treasury_address)?; + .has_address(treasury_address)?; treasury_ata_info - .is_empty()? - .is_writable()?; - - // Check programs and sysvars. - tape_program_info - .is_program(&tape_api::ID)?; - system_program_info - .is_program(&system_program::ID)?; - token_program_info - .is_program(&spl_token::ID)?; - associated_token_program_info - .is_program(&spl_associated_token_account::ID)?; - - metadata_program_info - .is_program(&mpl_token_metadata::ID)?; - rent_sysvar_info - .is_sysvar(&sysvar::rent::ID)?; + .is_empty()?; // Initialize epoch. - create_program_account::( + create_program_account_with_bump::( epoch_info, system_program_info, signer_info, &tape_api::ID, &[EPOCH], + epoch_bump )?; let epoch = epoch_info.as_account_mut::(&tape_api::ID)?; @@ -112,12 +91,13 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program epoch.last_epoch_at = 0; // Initialize block. - create_program_account::( + create_program_account_with_bump::( block_info, system_program_info, signer_info, &tape_api::ID, &[BLOCK], + block_bump )?; let block = block_info.as_account_mut::(&tape_api::ID)?; @@ -128,7 +108,7 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program block.last_block_at = 0; let next_challenge = compute_next_challenge( - &BLOCK_ADDRESS.to_bytes(), + &block_address.to_bytes(), slot_hashes_info ); @@ -136,12 +116,13 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program block.challenge_set = 1; // Initialize archive. - create_program_account::( + create_program_account_with_bump::( archive_info, system_program_info, signer_info, &tape_api::ID, &[ARCHIVE], + archive_bump )?; let archive = archive_info.as_account_mut::(&tape_api::ID)?; @@ -150,12 +131,13 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program archive.segments_stored = 0; // Initialize treasury. - create_program_account::( + create_program_account_with_bump::( treasury_info, system_program_info, signer_info, &tape_api::ID, &[TREASURY], + treasury_bump )?; // Initialize mint. @@ -166,8 +148,9 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program Mint::LEN, &spl_token::ID, &[MINT, MINT_SEED], - MINT_BUMP, + mint_bump )?; + initialize_mint_signed_with_bump( mint_info, treasury_info, @@ -176,7 +159,7 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program rent_sysvar_info, TOKEN_DECIMALS, &[MINT, MINT_SEED], - MINT_BUMP, + mint_bump )?; // Initialize mint metadata. @@ -217,20 +200,21 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program )?; // Fund the treasury token account. - mint_to_signed( + mint_to_signed_with_bump( mint_info, treasury_ata_info, treasury_info, token_program_info, MAX_SUPPLY, &[TREASURY], + treasury_bump )?; // Create the genesis tape let name = "genesis"; - let (tape_address, _tape_bump) = tape_pda(*signer_info.key, &to_name(name)); - let (writer_address, _writer_bump) = writer_pda(tape_address); + let (tape_address, _tape_bump) = tape_find_pda(signer_info.key, &to_name(name)); + let (writer_address, _writer_bump) = writer_find_pda(&tape_address); // Create the tape invoke( @@ -243,8 +227,7 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program tape_info.clone(), writer_info.clone(), system_program_info.clone(), - rent_sysvar_info.clone(), - slot_hashes_info.clone(), + clock_info.clone(), ], )?; @@ -260,6 +243,7 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program signer_info.clone(), tape_info.clone(), writer_info.clone(), + clock_info.clone() ], )?; @@ -273,8 +257,8 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program ), &[ treasury_info.clone(), - treasury_ata_info.clone(), tape_info.clone(), + treasury_ata_info.clone(), ], &[&[TREASURY, &[TREASURY_BUMP]]] )?; @@ -291,8 +275,6 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program tape_info.clone(), writer_info.clone(), archive_info.clone(), - system_program_info.clone(), - rent_sysvar_info.clone(), ], )?; diff --git a/program/src/spool/commit.rs b/program/src/spool/commit.rs index f215332..3cf13f0 100644 --- a/program/src/spool/commit.rs +++ b/program/src/spool/commit.rs @@ -31,14 +31,9 @@ pub fn process_spool_commit(accounts: &[AccountInfo<'_>], data: &[u8]) -> Progra let merkle_root = &spool.contains; let merkle_proof = args.proof.as_ref(); + assert!(merkle_proof.len() == SEGMENT_PROOF_LEN); - // let segment_id = args.index; - // let leaf = Leaf::new(&[ - // segment_id.as_ref(), // u64 (8 bytes) - // &args.value, - // ]); - let leaf = Leaf::from(args.value); check_condition( diff --git a/program/src/spool/create.rs b/program/src/spool/create.rs index 1e12612..a3a6d27 100644 --- a/program/src/spool/create.rs +++ b/program/src/spool/create.rs @@ -3,29 +3,26 @@ use tape_api::instruction::spool::Create; use steel::*; pub fn process_spool_create(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { - let current_time = Clock::get()?.unix_timestamp; let args = Create::try_from_bytes(data)?; let [ signer_info, miner_info, spool_info, - system_program_info, - rent_info, + system_program_info, + clock_info ] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?; - system_program_info.is_program(&system_program::ID)?; - rent_info.is_sysvar(&sysvar::rent::ID)?; + let current_time = Clock::from_account_info(clock_info)?.unix_timestamp; let spool_number = u64::from_le_bytes(args.number); - let (spool_pda, _bump) = spool_pda(*miner_info.key, spool_number); + let (spool_pda, spool_bump) = spool_find_pda(miner_info.key, spool_number); spool_info .is_empty()? - .is_writable()? .has_address(&spool_pda)?; miner_info @@ -36,12 +33,13 @@ pub fn process_spool_create(accounts: &[AccountInfo<'_>], data: &[u8]) -> Progra )?; // Create spool account. - create_program_account::( + create_program_account_with_bump::( spool_info, system_program_info, signer_info, &tape_api::ID, &[SPOOL, miner_info.key.as_ref(), &args.number], + spool_bump )?; let spool = spool_info.as_account_mut::(&tape_api::ID)?; @@ -53,6 +51,7 @@ pub fn process_spool_create(accounts: &[AccountInfo<'_>], data: &[u8]) -> Progra spool.state = TapeTree::new(&[spool_info.key.as_ref()]); spool.contains = [0; 32]; spool.total_tapes = 0; + spool.pda_bump = spool_bump as u64; Ok(()) } diff --git a/program/src/spool/destroy.rs b/program/src/spool/destroy.rs index 5a003e5..9570de5 100644 --- a/program/src/spool/destroy.rs +++ b/program/src/spool/destroy.rs @@ -5,18 +5,13 @@ pub fn process_spool_destroy(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Prog let [ signer_info, spool_info, - system_program_info, ] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?; - system_program_info - .is_program(&system_program::ID)?; - spool_info - .is_writable()? .as_account::(&tape_api::ID)? .assert_err( |p| p.authority == *signer_info.key, diff --git a/program/src/spool/unpack.rs b/program/src/spool/unpack.rs index be359c0..11695e7 100644 --- a/program/src/spool/unpack.rs +++ b/program/src/spool/unpack.rs @@ -30,8 +30,6 @@ pub fn process_spool_unpack(accounts: &[AccountInfo<'_>], data: &[u8]) -> Progra &args.value, ]); - // let leaf = Leaf::from(args.value); - check_condition( spool.state.contains_leaf(&merkle_proof, leaf), TapeError::SpoolUnpackFailed, diff --git a/program/src/tape/create.rs b/program/src/tape/create.rs index 88f7678..3dd128c 100644 --- a/program/src/tape/create.rs +++ b/program/src/tape/create.rs @@ -3,53 +3,48 @@ use tape_api::instruction::tape::Create; use steel::*; pub fn process_tape_create(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { - let current_slot = Clock::get()?.slot; let args = Create::try_from_bytes(data)?; let [ signer_info, tape_info, writer_info, system_program_info, - rent_sysvar_info, + clock_info ] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?; - let (tape_address, _tape_bump) = tape_pda(*signer_info.key, &args.name); - let (writer_address, _writer_bump) = writer_pda(tape_address); + let current_slot = Clock::from_account_info(clock_info)?.slot; + + let (tape_address, tape_bump) = tape_find_pda(signer_info.key, &args.name); + let (writer_address, writer_bump) = writer_find_pda(&tape_address); tape_info .is_empty()? - .is_writable()? .has_address(&tape_address)?; writer_info .is_empty()? - .is_writable()? .has_address(&writer_address)?; - system_program_info - .is_program(&system_program::ID)?; - - rent_sysvar_info - .is_sysvar(&sysvar::rent::ID)?; - - create_program_account::( + create_program_account_with_bump::( tape_info, system_program_info, signer_info, &tape_api::ID, &[TAPE, signer_info.key.as_ref(), &args.name], + tape_bump )?; - create_program_account::( + create_program_account_with_bump::( writer_info, system_program_info, signer_info, &tape_api::ID, &[WRITER, tape_info.key.as_ref()], + writer_bump )?; let tape = tape_info.as_account_mut::(&tape_api::ID)?; @@ -64,9 +59,11 @@ pub fn process_tape_create(accounts: &[AccountInfo<'_>], data: &[u8]) -> Program tape.header = [0; HEADER_SIZE]; tape.first_slot = current_slot; tape.tail_slot = current_slot; + tape.pda_bump = tape_bump as u64; writer.tape = *tape_info.key; writer.state = SegmentTree::new(&[tape_info.key.as_ref()]); + writer.pda_bump = writer_bump as u64; Ok(()) } diff --git a/program/src/tape/finalize.rs b/program/src/tape/finalize.rs index 9a1d5e9..f67d414 100644 --- a/program/src/tape/finalize.rs +++ b/program/src/tape/finalize.rs @@ -9,8 +9,6 @@ pub fn process_tape_finalize(accounts: &[AccountInfo<'_>], data: &[u8]) -> Progr tape_info, writer_info, archive_info, - system_program_info, - rent_sysvar_info, ] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; @@ -35,18 +33,12 @@ pub fn process_tape_finalize(accounts: &[AccountInfo<'_>], data: &[u8]) -> Progr .is_archive()? .as_account_mut::(&tape_api::ID)?; - let (tape_address, _tape_bump) = tape_pda(*signer_info.key, &tape.name); - let (writer_address, _writer_bump) = writer_pda(tape_address); + let tape_address = tape_derive_pda(signer_info.key, &tape.name, tape.pda_bump as u8); + let writer_address = writer_derive_pda(&tape_address, writer.pda_bump as u8); tape_info.has_address(&tape_address)?; writer_info.has_address(&writer_address)?; - system_program_info - .is_program(&system_program::ID)?; - - rent_sysvar_info - .is_sysvar(&sysvar::rent::ID)?; - // Can't finalize if the tape with no data on it. check_condition( tape.state.eq(&u64::from(TapeState::Writing)), diff --git a/program/src/tape/set_header.rs b/program/src/tape/set_header.rs index dca8e11..b7c000f 100644 --- a/program/src/tape/set_header.rs +++ b/program/src/tape/set_header.rs @@ -20,7 +20,7 @@ pub fn process_tape_set_header(accounts: &[AccountInfo<'_>], data: &[u8]) -> Pro ProgramError::MissingRequiredSignature, )?; - let (tape_address, _tape_bump) = tape_pda(*signer_info.key, &tape.name); + let tape_address= tape_derive_pda(signer_info.key, &tape.name, tape.pda_bump as u8); tape_info.has_address(&tape_address)?; diff --git a/program/src/tape/subsidize.rs b/program/src/tape/subsidize.rs index c2f9ad6..893b880 100644 --- a/program/src/tape/subsidize.rs +++ b/program/src/tape/subsidize.rs @@ -14,18 +14,13 @@ pub fn process_tape_subsidize_rent(accounts: &[AccountInfo<'_>], data: &[u8]) -> return Err(ProgramError::NotEnoughAccountKeys); }; - signer_info.is_signer()?; - // We don't require the owner of the tape to be the // signer; anyone can subsidize any tape. let tape = tape_info .as_account_mut::(&tape_api::ID)?; treasury_ata_info - .is_writable()?; - - token_program_info - .is_program(&spl_token::ID)?; + .is_treasury_ata()?; let amount = u64::from_le_bytes(args.amount); diff --git a/program/src/tape/update.rs b/program/src/tape/update.rs index 9440486..53dd1d8 100644 --- a/program/src/tape/update.rs +++ b/program/src/tape/update.rs @@ -4,17 +4,20 @@ use tape_api::instruction::tape::Update; use steel::*; pub fn process_tape_update(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { - let current_slot = Clock::get()?.slot; + let args = Update::try_from_bytes(data)?; let [ signer_info, tape_info, - writer_info, + writer_info, + clock_info ] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; + let current_slot = Clock::from_account_info(clock_info)?.slot; + signer_info.is_signer()?; let tape = tape_info @@ -31,8 +34,8 @@ pub fn process_tape_update(accounts: &[AccountInfo<'_>], data: &[u8]) -> Program ProgramError::InvalidAccountData, )?; - let (tape_address, _tape_bump) = tape_pda(*signer_info.key, &tape.name); - let (writer_address, _writer_bump) = writer_pda(tape_address); + let tape_address = tape_derive_pda(signer_info.key, &tape.name,tape.pda_bump as u8); + let writer_address = writer_derive_pda(&tape_address, writer.pda_bump as u8); tape_info.has_address(&tape_address)?; writer_info.has_address(&writer_address)?; diff --git a/program/src/tape/write.rs b/program/src/tape/write.rs index c9589fa..bbdbfa0 100644 --- a/program/src/tape/write.rs +++ b/program/src/tape/write.rs @@ -2,17 +2,19 @@ use tape_api::prelude::*; use steel::*; pub fn process_tape_write(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { - let current_slot = Clock::get()?.slot; let [ signer_info, tape_info, writer_info, + clock_info ] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?; + let current_slot = Clock::from_account_info(clock_info)?.slot; + let tape = tape_info .as_account_mut::(&tape_api::ID)? .assert_mut_err( @@ -27,8 +29,8 @@ pub fn process_tape_write(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramR ProgramError::InvalidAccountData, )?; - let (tape_address, _tape_bump) = tape_pda(*signer_info.key, &tape.name); - let (writer_address, _writer_bump) = writer_pda(tape_address); + let tape_address = tape_derive_pda(signer_info.key, &tape.name, tape.pda_bump as u8); + let writer_address = writer_derive_pda(&tape_address, writer.pda_bump as u8); tape_info.has_address(&tape_address)?; writer_info.has_address(&writer_address)?; diff --git a/program/tests/integration.rs b/program/tests/integration.rs index edf8e4b..3c18bd3 100644 --- a/program/tests/integration.rs +++ b/program/tests/integration.rs @@ -50,26 +50,27 @@ struct PackedTape { #[test] fn run_integration() { - // Setup environment - let (mut svm, payer) = setup_environment(); + + let mut svm = SvmWithCUTracker::new(); // Initialize program - initialize_program(&mut svm, &payer); + initialize_program(&mut svm); + let owner_pk = svm.payer.pubkey(); // Register miner let miner_name = "miner-name"; - let miner_address = register_miner(&mut svm, &payer, miner_name); - let ata = create_ata(&mut svm, &payer, &MINT_ADDRESS, &payer.pubkey()); + let miner_address = register_miner(&mut svm, miner_name); + let ata = create_ata(&mut svm, &MINT_ADDRESS, &owner_pk); // Create a miner spool let spool_number = 1; - let mut stored_spool = create_spool(&mut svm, &payer, miner_address, spool_number); + let mut stored_spool = create_spool(&mut svm, miner_address, spool_number); // Fetch and store genesis tape - let genesis_tape = get_genesis_tape(&mut svm, &payer); + let genesis_tape = get_genesis_tape(&mut svm); // Pack the tape into a miner specific representation - pack_tape(&mut svm, &payer, &genesis_tape, &mut stored_spool); + pack_tape(&mut svm, &genesis_tape, &mut stored_spool); // Verify initial accounts verify_archive_account(&svm, 1); @@ -81,8 +82,8 @@ fn run_integration() { verify_treasury_ata(&svm); // Mine the genesis tape (to earn some tokens) - do_mining_run(&mut svm, &payer, &stored_spool, 5); - claim_rewards(&mut svm, &payer, miner_address, ata); + do_mining_run(&mut svm, &stored_spool, 5); + claim_rewards(&mut svm, miner_address, ata); let ata_balance = get_ata_balance(&svm, &ata); assert!(ata_balance > 0); @@ -90,22 +91,24 @@ fn run_integration() { println!("ATA balance after claiming rewards: {ata_balance}"); // Advance clock - let mut initial_clock = svm.get_sysvar::(); + let mut initial_clock = svm.svm.get_sysvar::(); initial_clock.slot = 10; - svm.set_sysvar::(&initial_clock); + svm.svm.set_sysvar::(&initial_clock); // Create tapes let tape_count = 5; for tape_index in 1..tape_count { - let stored_tape = create_and_verify_tape(&mut svm, &payer, ata, tape_index); - pack_tape(&mut svm, &payer, &stored_tape, &mut stored_spool); + let stored_tape = create_and_verify_tape(&mut svm, ata, tape_index); + pack_tape(&mut svm, &stored_tape, &mut stored_spool); } // Verify archive account after tape creation verify_archive_account(&svm, tape_count); // Mine again with more tapes this time - do_mining_run(&mut svm, &payer, &stored_spool, 5); + do_mining_run(&mut svm, &stored_spool, 5); + + svm.commit_cus_change_log(); } fn setup_environment() -> (LiteSVM, Keypair) { @@ -117,6 +120,7 @@ fn setup_environment() -> (LiteSVM, Keypair) { fn subsidize_tape( svm: &mut LiteSVM, payer: &Keypair, + cu_tracker: &mut ComputeUnitsTracker, ata: Pubkey, tape_address: Pubkey, amount: u64, @@ -134,6 +138,7 @@ fn subsidize_tape( let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); let res = send_tx(svm, tx); assert!(res.is_ok()); + cu_tracker.track_cus(ProgramIx::TapeSubsidize,res.unwrap().compute_units_consumed); let account = svm.get_account(&tape_address).unwrap(); let tape = Tape::unpack(&account.data).unwrap(); @@ -141,11 +146,11 @@ fn subsidize_tape( } fn claim_rewards( - svm: &mut LiteSVM, - payer: &Keypair, + svm: &mut SvmWithCUTracker, miner_address: Pubkey, miner_ata: Pubkey, ) { + let SvmWithCUTracker { svm, cu_tracker, payer } = svm; let payer_pk = payer.pubkey(); let blockhash = svm.latest_blockhash(); @@ -159,6 +164,7 @@ fn claim_rewards( let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); let res = send_tx(svm, tx); assert!(res.is_ok()); + cu_tracker.track_cus(ProgramIx::MinerClaim, res.unwrap().compute_units_consumed); // Verify miner account after claiming rewards let account = svm.get_account(&miner_address).unwrap(); @@ -168,11 +174,11 @@ fn claim_rewards( } fn do_mining_run( - svm: &mut LiteSVM, - payer: &Keypair, + svm: &mut SvmWithCUTracker, stored_spool: &StoredSpool, num_iterations: u64, ) { + let SvmWithCUTracker { svm, cu_tracker, payer } = svm; for _ in 0..num_iterations { // We need to expire the blockhash because we're not checking if the mining commitment // needs to change (when it doesn't, we get a AlreadyProcessed error). Todo, check before @@ -275,6 +281,7 @@ fn do_mining_run( commit_for_mining( svm, payer, + cu_tracker, stored_spool, tape_index, segment_number @@ -284,6 +291,7 @@ fn do_mining_run( perform_mining( svm, payer, + cu_tracker, stored_spool.miner, packed_tape.address, pow, @@ -304,6 +312,7 @@ fn do_mining_run( perform_mining( svm, payer, + cu_tracker, stored_spool.miner, packed_tape.address, pow, @@ -313,10 +322,11 @@ fn do_mining_run( } } -fn get_genesis_tape(svm: &mut LiteSVM, payer: &Keypair) -> StoredTape { +fn get_genesis_tape(svm: &mut SvmWithCUTracker) -> StoredTape { + let SvmWithCUTracker { svm, cu_tracker:_, payer } = svm; let genesis_name = "genesis".to_string(); let genesis_name_bytes = to_name(&genesis_name); - let (genesis_pubkey, _) = tape_pda(payer.pubkey(), &genesis_name_bytes); + let (genesis_pubkey, _) = tape_find_pda(&payer.pubkey(), &genesis_name_bytes); let account = svm.get_account(&genesis_pubkey).expect("Genesis tape should exist"); let tape = Tape::unpack(&account.data).expect("Failed to unpack genesis tape"); @@ -338,16 +348,20 @@ fn get_genesis_tape(svm: &mut LiteSVM, payer: &Keypair) -> StoredTape { } -fn initialize_program(svm: &mut LiteSVM, payer: &Keypair) { +fn initialize_program(svm: &mut SvmWithCUTracker) { + let SvmWithCUTracker { svm, cu_tracker, payer } = svm; let payer_pk = payer.pubkey(); let ix = instruction::program::build_initialize_ix(payer_pk); let blockhash = svm.latest_blockhash(); let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); let res = send_tx(svm, tx); assert!(res.is_ok()); + cu_tracker.track_cus(ProgramIx::ProgramInitialize, res.unwrap().compute_units_consumed); } -fn verify_archive_account(svm: &LiteSVM, expected_tapes_stored: u64) { +fn verify_archive_account(svm: &SvmWithCUTracker, expected_tapes_stored: u64) { + let SvmWithCUTracker {svm,..} = svm; + let (archive_address, _archive_bump) = archive_pda(); let account = svm .get_account(&archive_address) @@ -356,7 +370,9 @@ fn verify_archive_account(svm: &LiteSVM, expected_tapes_stored: u64) { assert_eq!(archive.tapes_stored, expected_tapes_stored); } -fn verify_epoch_account(svm: &LiteSVM) { +fn verify_epoch_account(svm: &SvmWithCUTracker) { + let SvmWithCUTracker {svm,..} = svm; + let (epoch_address, _epoch_bump) = epoch_pda(); let account = svm .get_account(&epoch_address) @@ -372,7 +388,9 @@ fn verify_epoch_account(svm: &LiteSVM) { assert_eq!(epoch.last_epoch_at, 0); } -fn verify_block_account(svm: &LiteSVM) { +fn verify_block_account(svm: &SvmWithCUTracker) { + let SvmWithCUTracker {svm,..} = svm; + let (block_address, _block_bump) = block_pda(); let account = svm .get_account(&block_address) @@ -386,53 +404,63 @@ fn verify_block_account(svm: &LiteSVM) { assert!(block.challenge.ne(&[0u8; 32])); } -fn verify_treasury_account(svm: &LiteSVM) { +fn verify_treasury_account(svm: &SvmWithCUTracker) { + let SvmWithCUTracker {svm,..} = svm; + let (treasury_address, _treasury_bump) = treasury_pda(); let _treasury_account = svm .get_account(&treasury_address) .expect("Treasury account should exist"); } -fn verify_mint_account(svm: &LiteSVM) { +fn verify_mint_account(svm: &SvmWithCUTracker) { + let SvmWithCUTracker {svm,..} = svm; + let (mint_address, _mint_bump) = mint_pda(); let mint = get_mint(svm, &mint_address); assert_eq!(mint.supply, MAX_SUPPLY); assert_eq!(mint.decimals, TOKEN_DECIMALS); } -fn verify_metadata_account(svm: &LiteSVM) { +fn verify_metadata_account(svm: &SvmWithCUTracker) { + let SvmWithCUTracker {svm,..} = svm; + let (mint_address, _mint_bump) = mint_pda(); - let (metadata_address, _metadata_bump) = metadata_pda(mint_address); + let (metadata_address, _metadata_bump) = metadata_find_pda(mint_address); let account = svm .get_account(&metadata_address) .expect("Metadata account should exist"); assert!(!account.data.is_empty()); } -fn verify_treasury_ata(svm: &LiteSVM) { - let (treasury_ata_address, _ata_bump) = treasury_ata(); +fn verify_treasury_ata(svm: &SvmWithCUTracker) { + let SvmWithCUTracker {svm,..} = svm; + + let (treasury_ata_address, _ata_bump) = treasury_find_ata(); let account = svm .get_account(&treasury_ata_address) .expect("Treasury ATA should exist"); assert!(!account.data.is_empty()); } + fn create_and_verify_tape( - svm: &mut LiteSVM, - payer: &Keypair, + svm: &mut SvmWithCUTracker, ata: Pubkey, tape_index: u64, ) -> StoredTape { + let SvmWithCUTracker { svm, cu_tracker, payer } = svm; let payer_pk = payer.pubkey(); let tape_name = format!("tape-name-{tape_index}"); - let (tape_address, _tape_bump) = tape_pda(payer_pk, &to_name(&tape_name)); - let (writer_address, _writer_bump) = writer_pda(tape_address); + let (tape_address, _tape_bump) = tape_find_pda(&payer_pk, &to_name(&tape_name)); + let (writer_address, _writer_bump) = writer_find_pda(&tape_address); // Create tape and verify initial state let mut stored_tape = create_tape( svm, payer, + cu_tracker, &tape_name, tape_address, writer_address @@ -444,6 +472,7 @@ fn create_and_verify_tape( write_tape( svm, payer, + cu_tracker, tape_address, writer_address, &mut stored_tape, @@ -453,6 +482,7 @@ fn create_and_verify_tape( update_tape( svm, payer, + cu_tracker, tape_address, writer_address, &mut stored_tape, @@ -466,6 +496,7 @@ fn create_and_verify_tape( subsidize_tape( svm, payer, + cu_tracker, ata, tape_address, min_rent, @@ -474,6 +505,7 @@ fn create_and_verify_tape( finalize_tape( svm, payer, + cu_tracker, tape_address, writer_address, &mut stored_tape, @@ -486,6 +518,7 @@ fn create_and_verify_tape( fn create_tape( svm: &mut LiteSVM, payer: &Keypair, + cu_tracker: &mut ComputeUnitsTracker, tape_name: &str, tape_address: Pubkey, writer_address: Pubkey, @@ -498,6 +531,7 @@ fn create_tape( let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); let res = send_tx(svm, tx); assert!(res.is_ok()); + cu_tracker.track_cus(ProgramIx::TapeCreate,res.unwrap().compute_units_consumed); // Verify tape account let account = svm.get_account(&tape_address).unwrap(); @@ -528,6 +562,7 @@ fn create_tape( fn write_tape( svm: &mut LiteSVM, payer: &Keypair, + cu_tracker: &mut ComputeUnitsTracker, tape_address: Pubkey, writer_address: Pubkey, stored_tape: &mut StoredTape, @@ -543,6 +578,7 @@ fn write_tape( let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); let res = send_tx(svm, tx); assert!(res.is_ok()); + cu_tracker.track_cus(ProgramIx::TapeWrite,res.unwrap().compute_units_consumed); // Update local state let segments = data.chunks(SEGMENT_SIZE); @@ -580,6 +616,7 @@ fn write_tape( fn update_tape( svm: &mut LiteSVM, payer: &Keypair, + cu_tracker: &mut ComputeUnitsTracker, tape_address: Pubkey, writer_address: Pubkey, stored_tape: &mut StoredTape, @@ -631,6 +668,7 @@ fn update_tape( let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); let res = send_tx(svm, tx); assert!(res.is_ok()); + cu_tracker.track_cus(ProgramIx::TapeUpdate,res.unwrap().compute_units_consumed); // Update local tree assert!(update_segment( @@ -665,6 +703,7 @@ fn update_tape( fn finalize_tape( svm: &mut LiteSVM, payer: &Keypair, + cu_tracker: &mut ComputeUnitsTracker, tape_address: Pubkey, writer_address: Pubkey, stored_tape: &mut StoredTape, @@ -678,7 +717,7 @@ fn finalize_tape( let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); let res = send_tx(svm, tx); assert!(res.is_ok()); - + cu_tracker.track_cus(ProgramIx::TapeFinalize,res.unwrap().compute_units_consumed); // Verify update fails after finalization let target_segment: u64 = 0; @@ -721,15 +760,17 @@ fn finalize_tape( stored_tape.number = tape_index + 1; } -fn register_miner(svm: &mut LiteSVM, payer: &Keypair, miner_name: &str) -> Pubkey { +fn register_miner(svm: &mut SvmWithCUTracker, miner_name: &str) -> Pubkey { + let SvmWithCUTracker { svm, cu_tracker, payer } = svm; let payer_pk = payer.pubkey(); - let (miner_address, _miner_bump) = miner_pda(payer_pk, to_name(miner_name)); + let (miner_address, _miner_bump) = miner_find_pda(&payer_pk, to_name(miner_name)); let blockhash = svm.latest_blockhash(); let ix = instruction::miner::build_register_ix(payer_pk, miner_name); let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); let res = send_tx(svm, tx); assert!(res.is_ok()); + cu_tracker.track_cus(ProgramIx::MinerRegister, res.unwrap().compute_units_consumed); let account = svm.get_account(&miner_address).unwrap(); let miner = Miner::unpack(&account.data).unwrap(); @@ -746,15 +787,18 @@ fn register_miner(svm: &mut LiteSVM, payer: &Keypair, miner_name: &str) -> Pubke miner_address } -fn create_spool(svm: &mut LiteSVM, payer: &Keypair, miner_address: Pubkey, number: u64) -> StoredSpool { +fn create_spool(svm: &mut SvmWithCUTracker, miner_address: Pubkey, number: u64) -> StoredSpool { + let SvmWithCUTracker { svm, cu_tracker, payer } = svm; + let payer_pk = payer.pubkey(); - let (spool_address, _bump) = spool_pda(miner_address, number); + let (spool_address, _bump) = spool_find_pda(&miner_address, number); let blockhash = svm.latest_blockhash(); let ix = instruction::spool::build_create_ix(payer_pk, miner_address, number); let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); let res = send_tx(svm, tx); assert!(res.is_ok()); + cu_tracker.track_cus(ProgramIx::SpoolCreate, res.unwrap().compute_units_consumed); let account = svm.get_account(&spool_address).unwrap(); let spool = Spool::unpack(&account.data).unwrap(); @@ -830,6 +874,7 @@ fn get_packed_tape( fn commit_for_mining( svm: &mut LiteSVM, payer: &Keypair, + cu_tracker: &mut ComputeUnitsTracker, stored_spool: &StoredSpool, tape_index: u64, segment_index: u64, @@ -837,7 +882,7 @@ fn commit_for_mining( let payer_pk = payer.pubkey(); let blockhash = svm.latest_blockhash(); - let ix = [ + let [unpack_ix,commit_ix] = [ unpack_tape_ix( payer, stored_spool, @@ -851,10 +896,15 @@ fn commit_for_mining( ), ]; - let tx = Transaction::new_signed_with_payer(&ix, Some(&payer_pk), &[&payer], blockhash); + let tx = Transaction::new_signed_with_payer(&[unpack_ix], Some(&payer_pk), &[&payer], blockhash); let res = send_tx(svm, tx); + assert!(res.is_ok()); + cu_tracker.track_cus(ProgramIx::SpoolUnpack, res.unwrap().compute_units_consumed); + let tx = Transaction::new_signed_with_payer(&[commit_ix], Some(&payer_pk), &[&payer], blockhash); + let res = send_tx(svm, tx); assert!(res.is_ok()); + cu_tracker.track_cus(ProgramIx::SpoolCommit, res.unwrap().compute_units_consumed); // Verify that the mining account has the leaf we need let account = svm.get_account(&stored_spool.miner) @@ -956,11 +1006,11 @@ fn unpack_tape_ix( } fn pack_tape( - svm: &mut LiteSVM, - payer: &Keypair, + svm: &mut SvmWithCUTracker, stored_tape: &StoredTape, stored_spool: &mut StoredSpool, ) { + let SvmWithCUTracker { svm, cu_tracker, payer } = svm; // Get the required difficulty for packing let (epoch_address, _epoch_bump) = epoch_pda(); let epoch_account = svm.get_account(&epoch_address).unwrap(); @@ -982,6 +1032,7 @@ fn pack_tape( let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); let res = send_tx(svm, tx); assert!(res.is_ok()); + cu_tracker.track_cus(ProgramIx::SpoolPack, res.unwrap().compute_units_consumed); stored_spool.tree.try_add_leaf( Leaf::new(&[ @@ -997,6 +1048,7 @@ fn pack_tape( fn perform_mining( svm: &mut LiteSVM, payer: &Keypair, + cu_tracker: &mut ComputeUnitsTracker, miner_address: Pubkey, tape_address: Pubkey, pow: PoW, @@ -1016,6 +1068,7 @@ fn perform_mining( let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer_pk), &[&payer], blockhash); let res = send_tx(svm, tx); assert!(res.is_ok()); + cu_tracker.track_cus(ProgramIx::MinerMine, res.unwrap().compute_units_consumed); let account = svm.get_account(&miner_address).unwrap(); let miner = Miner::unpack(&account.data).unwrap(); diff --git a/program/tests/utils/svm.rs b/program/tests/utils/svm.rs index 82354d4..e98752d 100644 --- a/program/tests/utils/svm.rs +++ b/program/tests/utils/svm.rs @@ -1,3 +1,5 @@ +use chrono::Local; +use serde::{Deserialize, Serialize}; use steel::*; use tape_api::instruction::{ tape as tape_ix, @@ -5,13 +7,215 @@ use tape_api::instruction::{ spool as spool_ix, program as program_ix, }; +use std::collections::HashMap; use std::path::PathBuf; +use std::fs::{read_to_string, write}; +use std::path::Path; +use sha2::{Sha256, Digest}; use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; use solana_compute_budget::compute_budget::ComputeBudget; use litesvm::{types::{TransactionMetadata, TransactionResult}, LiteSVM}; use pretty_hex::*; use bincode; +use crate::setup_environment; + +#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum ProgramIx { + ProgramInitialize, + #[cfg(feature = "airdrop")] + Airdrop, + // Tape instructions + TapeCreate, + TapeWrite, + TapeUpdate, + TapeFinalize, + TapeSetHeader, + TapeSubsidize, + // Miner instructions + MinerRegister, + MinerUnregister, + MinerMine, + MinerClaim, + // Spool instructions + SpoolCreate, + SpoolDestroy, + SpoolPack, + SpoolUnpack, + SpoolCommit, +} + +impl ToString for ProgramIx { + fn to_string(&self) -> String { + match self { + ProgramIx::ProgramInitialize => "ProgramInitialize", + #[cfg(feature = "airdrop")] + ProgramIx::Airdrop => "Airdrop", + + // Tape instructions + ProgramIx::TapeCreate => "TapeCreate", + ProgramIx::TapeWrite => "TapeWrite", + ProgramIx::TapeUpdate => "TapeUpdate", + ProgramIx::TapeFinalize => "TapeFinalize", + ProgramIx::TapeSetHeader => "TapeSetHeader", + ProgramIx::TapeSubsidize => "TapeSubsidize", + + // Miner instructions + ProgramIx::MinerRegister => "MinerRegister", + ProgramIx::MinerUnregister => "MinerUnregister", + ProgramIx::MinerMine => "MinerMine", + ProgramIx::MinerClaim => "MinerClaim", + + // Spool instructions + ProgramIx::SpoolCreate => "SpoolCreate", + ProgramIx::SpoolDestroy => "SpoolDestroy", + ProgramIx::SpoolPack => "SpoolPack", + ProgramIx::SpoolUnpack => "SpoolUnpack", + ProgramIx::SpoolCommit => "SpoolCommit", + }.to_string() + } +} + +pub struct ComputeUnitsTracker(HashMap); + +#[derive(Serialize, Deserialize)] +struct CuLog { + timestamp: String, + entries: HashMap, + checksum: String, +} + +#[derive(Serialize, Deserialize, Clone, Copy)] +struct CuEntry { + value: u64, + diff: i64 +} + +impl Default for CuEntry{ + fn default() -> Self { + CuEntry { value: 0, diff: 0 } + } +} + + +impl ComputeUnitsTracker { + pub fn new() -> Self { + ComputeUnitsTracker(HashMap::new()) + } + + pub fn track_cus(&mut self, ix: ProgramIx, cus: u64){ + let ix = self.0.entry(ix).or_default(); + *ix += cus; + } + + fn hash_cu_log_entries(logs: &HashMap) -> String { + let mut entries: Vec<_> = logs.iter().collect(); + entries.sort_by_key(|(ix, _)| ix.to_string()); // deterministic order + + let mut hasher = Sha256::new(); + for (ix, cu) in entries { + hasher.update(format!("{}-{}", ix.to_string(), cu)); + } + format!("{:x}", hasher.finalize()) + } + + + pub fn commit_to_change_log(&self) { + let path = Path::new("cu_logs.json"); + let logs_hash = Self::hash_cu_log_entries(&self.0); + let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + + let mut previous_entries: Option> = None; + let mut all_logs: Vec = Vec::new(); + + // Load existing JSON array + if path.exists() { + if let Ok(data) = read_to_string(path) { + if let Ok(logs) = serde_json::from_str::>(&data) { + if let Some(last) = logs.first() { + let prev_map = last.entries.iter() + .map(|(k, v)| (k.clone(), *v)) + .collect(); + previous_entries = Some(prev_map); + } + all_logs = logs; + } + } + } + + // Skip if checksum already exists + if all_logs.iter().any(|entry| entry.checksum == logs_hash) { + return; + } + + // Calculate diffs + let mut entries = HashMap::new(); + for (ix, cu) in &self.0 { + let name = ix.to_string(); + let old = previous_entries + .as_ref() + .and_then(|prev| prev.get(&name)) + .cloned() + .unwrap_or(CuEntry::default()); + let diff = if old.value > 0 { + *cu as i64 - old.value as i64 + } else { + 0 + }; + + entries.insert(name, CuEntry { + value: *cu, + diff, + }); + } + + // Prepend new log entry + let new_log = CuLog { + timestamp, + checksum: logs_hash, + entries, + }; + all_logs.insert(0, new_log); + + // Truncate to max 5 entries + if all_logs.len() > 5 { + all_logs.truncate(5); + } + + // Write full array back to file + let output = serde_json::to_string_pretty(&all_logs).expect("serialization failed"); + write(path, output).expect("write failed"); + } + +} + +pub struct SvmWithCUTracker{ + pub svm: LiteSVM, + pub cu_tracker: ComputeUnitsTracker, + pub payer: Keypair +} + +impl SvmWithCUTracker{ + pub fn new() -> Self { + let (svm, payer) = setup_environment(); + Self { svm, cu_tracker: ComputeUnitsTracker::new(), payer } + } + + pub fn payer(&self) -> &Keypair { + &self.payer + } + + pub fn track_cus_consumed(&mut self, ix: ProgramIx, cus: u64){ + self.cu_tracker.track_cus(ix, cus); + } + + pub fn commit_cus_change_log(&self) { + self.cu_tracker.commit_to_change_log(); + } + +} + + pub fn program_bytes() -> Vec { // Fetch the tape program bytes from target let mut so_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); diff --git a/program/tests/utils/token.rs b/program/tests/utils/token.rs index dfb859c..12afe74 100644 --- a/program/tests/utils/token.rs +++ b/program/tests/utils/token.rs @@ -9,8 +9,11 @@ use litesvm_token::{ }; use spl_associated_token_account::get_associated_token_address; +use crate::utils::SvmWithCUTracker; -pub fn create_mint(svm: &mut LiteSVM, payer_kp: &Keypair, owner_pk: &Pubkey, decimals: u8) -> Pubkey { + +pub fn create_mint(svm: &mut SvmWithCUTracker, payer_kp: &Keypair, owner_pk: &Pubkey, decimals: u8) -> Pubkey { + let SvmWithCUTracker { svm, cu_tracker:_, payer:_ } = svm; CreateMint::new(svm, payer_kp) .authority(owner_pk) .decimals(decimals) @@ -18,8 +21,9 @@ pub fn create_mint(svm: &mut LiteSVM, payer_kp: &Keypair, owner_pk: &Pubkey, dec .unwrap() } -pub fn create_ata(svm: &mut LiteSVM, payer_kp: &Keypair, mint_pk: &Pubkey, owner_pk: &Pubkey) -> Pubkey { - CreateAssociatedTokenAccount::new(svm, payer_kp, mint_pk) +pub fn create_ata(svm: &mut SvmWithCUTracker, mint_pk: &Pubkey, owner_pk: &Pubkey) -> Pubkey { + let SvmWithCUTracker { svm, cu_tracker:_, payer } = svm; + CreateAssociatedTokenAccount::new(svm, payer, mint_pk) .owner(owner_pk) .send() .unwrap() @@ -29,8 +33,8 @@ pub fn get_ata_address(mint_pk: &Pubkey, owner_pk: &Pubkey) -> Pubkey { get_associated_token_address(owner_pk, mint_pk) } -pub fn get_ata_balance(svm: &LiteSVM, ata: &Pubkey) -> u64 { - let info : Account = get_spl_account(svm, ata).unwrap(); +pub fn get_ata_balance(svm: &SvmWithCUTracker, ata: &Pubkey) -> u64 { + let info : Account = get_spl_account(&svm.svm, ata).unwrap(); info.amount }