mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-04-09 04:33:11 +00:00
add clock usage example programs
This commit is contained in:
parent
34de497d4d
commit
015999b3a5
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -7843,6 +7843,7 @@ dependencies = [
|
||||
name = "test_programs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clock_core",
|
||||
"nssa_core",
|
||||
"risc0-zkvm",
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/pinata_cooldown.bin
Normal file
BIN
artifacts/test_program_methods/pinata_cooldown.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/time_locked_transfer.bin
Normal file
BIN
artifacts/test_program_methods/time_locked_transfer.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -319,6 +319,18 @@ mod tests {
|
||||
use test_program_methods::VALIDITY_WINDOW_CHAIN_CALLER_ELF;
|
||||
Self::new(VALIDITY_WINDOW_CHAIN_CALLER_ELF.to_vec()).unwrap()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn time_locked_transfer() -> Self {
|
||||
use test_program_methods::TIME_LOCKED_TRANSFER_ELF;
|
||||
Self::new(TIME_LOCKED_TRANSFER_ELF.to_vec()).unwrap()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn pinata_cooldown() -> Self {
|
||||
use test_program_methods::PINATA_COOLDOWN_ELF;
|
||||
Self::new(PINATA_COOLDOWN_ELF.to_vec()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -400,6 +400,8 @@ pub mod tests {
|
||||
self.insert_program(Program::claimer());
|
||||
self.insert_program(Program::changer_claimer());
|
||||
self.insert_program(Program::validity_window());
|
||||
self.insert_program(Program::time_locked_transfer());
|
||||
self.insert_program(Program::pinata_cooldown());
|
||||
self
|
||||
}
|
||||
|
||||
@ -3644,6 +3646,227 @@ pub mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn time_locked_transfer_transaction(
|
||||
from: AccountId,
|
||||
from_key: &PrivateKey,
|
||||
from_nonce: u128,
|
||||
to: AccountId,
|
||||
clock_account_id: AccountId,
|
||||
amount: u128,
|
||||
deadline: u64,
|
||||
) -> PublicTransaction {
|
||||
let program_id = Program::time_locked_transfer().id();
|
||||
let message = public_transaction::Message::try_new(
|
||||
program_id,
|
||||
vec![from, to, clock_account_id],
|
||||
vec![Nonce(from_nonce)],
|
||||
(amount, deadline),
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[from_key]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_locked_transfer_succeeds_when_deadline_has_passed() {
|
||||
let recipient_id = AccountId::new([42; 32]);
|
||||
let genesis_timestamp = 500_u64;
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&[(recipient_id, 0)], &[], genesis_timestamp)
|
||||
.with_test_programs();
|
||||
let key1 = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1));
|
||||
state.force_insert_account(
|
||||
sender_id,
|
||||
Account {
|
||||
program_owner: Program::time_locked_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let amount = 100_u128;
|
||||
// Deadline in the past: transfer should succeed.
|
||||
let deadline = 0_u64;
|
||||
|
||||
let tx = time_locked_transfer_transaction(
|
||||
sender_id,
|
||||
&key1,
|
||||
0,
|
||||
recipient_id,
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
amount,
|
||||
deadline,
|
||||
);
|
||||
|
||||
let block_id = 1;
|
||||
let timestamp = genesis_timestamp + 100;
|
||||
state
|
||||
.transition_from_public_transaction(&tx, block_id, timestamp)
|
||||
.unwrap();
|
||||
|
||||
// Balances changed.
|
||||
assert_eq!(state.get_account_by_id(sender_id).balance, 0);
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_locked_transfer_fails_when_deadline_is_in_the_future() {
|
||||
let recipient_id = AccountId::new([42; 32]);
|
||||
let genesis_timestamp = 500_u64;
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&[(recipient_id, 0)], &[], genesis_timestamp)
|
||||
.with_test_programs();
|
||||
let key1 = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1));
|
||||
state.force_insert_account(
|
||||
sender_id,
|
||||
Account {
|
||||
program_owner: Program::time_locked_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let amount = 100_u128;
|
||||
// Far-future deadline: program should panic.
|
||||
let deadline = u64::MAX;
|
||||
|
||||
let tx = time_locked_transfer_transaction(
|
||||
sender_id,
|
||||
&key1,
|
||||
0,
|
||||
recipient_id,
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
amount,
|
||||
deadline,
|
||||
);
|
||||
|
||||
let block_id = 1;
|
||||
let timestamp = genesis_timestamp + 100;
|
||||
let result = state.transition_from_public_transaction(&tx, block_id, timestamp);
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Transfer should fail when deadline is in the future"
|
||||
);
|
||||
// Balances unchanged.
|
||||
assert_eq!(state.get_account_by_id(sender_id).balance, 100);
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, 0);
|
||||
}
|
||||
|
||||
fn pinata_cooldown_data(prize: u128, cooldown_ms: u64, last_claim_timestamp: u64) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(32);
|
||||
buf.extend_from_slice(&prize.to_le_bytes());
|
||||
buf.extend_from_slice(&cooldown_ms.to_le_bytes());
|
||||
buf.extend_from_slice(&last_claim_timestamp.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
fn pinata_cooldown_transaction(
|
||||
pinata_id: AccountId,
|
||||
winner_id: AccountId,
|
||||
clock_account_id: AccountId,
|
||||
) -> PublicTransaction {
|
||||
let program_id = Program::pinata_cooldown().id();
|
||||
let message = public_transaction::Message::try_new(
|
||||
program_id,
|
||||
vec![pinata_id, winner_id, clock_account_id],
|
||||
vec![],
|
||||
(),
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pinata_cooldown_claim_succeeds_after_cooldown() {
|
||||
let winner_id = AccountId::new([11; 32]);
|
||||
let pinata_id = AccountId::new([99; 32]);
|
||||
|
||||
let genesis_timestamp = 1000_u64;
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&[(winner_id, 0)], &[], genesis_timestamp)
|
||||
.with_test_programs();
|
||||
|
||||
let prize = 50_u128;
|
||||
let cooldown_ms = 500_u64;
|
||||
// Last claim was at genesis, so any timestamp >= genesis + cooldown should work.
|
||||
let last_claim_timestamp = genesis_timestamp;
|
||||
|
||||
state.force_insert_account(
|
||||
pinata_id,
|
||||
Account {
|
||||
program_owner: Program::pinata_cooldown().id(),
|
||||
balance: 1000,
|
||||
data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let tx = pinata_cooldown_transaction(pinata_id, winner_id, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
|
||||
let block_id = 1;
|
||||
let block_timestamp = genesis_timestamp + cooldown_ms;
|
||||
// Advance clock so the cooldown check reads an updated timestamp.
|
||||
let clock_tx = clock_transaction(block_timestamp);
|
||||
state
|
||||
.transition_from_public_transaction(&clock_tx, block_id, block_timestamp)
|
||||
.unwrap();
|
||||
|
||||
state
|
||||
.transition_from_public_transaction(&tx, block_id, block_timestamp)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.get_account_by_id(pinata_id).balance, 1000 - prize);
|
||||
assert_eq!(state.get_account_by_id(winner_id).balance, prize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pinata_cooldown_claim_fails_during_cooldown() {
|
||||
let winner_id = AccountId::new([11; 32]);
|
||||
let pinata_id = AccountId::new([99; 32]);
|
||||
|
||||
let genesis_timestamp = 1000_u64;
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&[(winner_id, 0)], &[], genesis_timestamp)
|
||||
.with_test_programs();
|
||||
|
||||
let prize = 50_u128;
|
||||
let cooldown_ms = 500_u64;
|
||||
let last_claim_timestamp = genesis_timestamp;
|
||||
|
||||
state.force_insert_account(
|
||||
pinata_id,
|
||||
Account {
|
||||
balance: 1000,
|
||||
data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let tx = pinata_cooldown_transaction(pinata_id, winner_id, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
|
||||
let block_id = 1;
|
||||
// Timestamp is only 100ms after last claim, well within the 500ms cooldown.
|
||||
let block_timestamp = genesis_timestamp + 100;
|
||||
let clock_tx = clock_transaction(block_timestamp);
|
||||
state
|
||||
.transition_from_public_transaction(&clock_tx, block_id, block_timestamp)
|
||||
.unwrap();
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, block_id, block_timestamp);
|
||||
|
||||
assert!(result.is_err(), "Claim should fail during cooldown period");
|
||||
assert_eq!(state.get_account_by_id(pinata_id).balance, 1000);
|
||||
assert_eq!(state.get_account_by_id(winner_id).balance, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_serialization_roundtrip() {
|
||||
let account_id_1 = AccountId::new([1; 32]);
|
||||
|
||||
@ -9,5 +9,6 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa_core.workspace = true
|
||||
clock_core.workspace = true
|
||||
|
||||
risc0-zkvm.workspace = true
|
||||
|
||||
114
test_program_methods/guest/src/bin/pinata_cooldown.rs
Normal file
114
test_program_methods/guest/src/bin/pinata_cooldown.rs
Normal file
@ -0,0 +1,114 @@
|
||||
//! Cooldown-based pinata program.
|
||||
//!
|
||||
//! A Piñata program that uses the on-chain clock to prevent abuse.
|
||||
//! After each prize claim the program records the current timestamp; the next claim is only
|
||||
//! allowed once a configurable cooldown period has elapsed.
|
||||
//!
|
||||
//! Expected pre-states (in order):
|
||||
//! 0 - pinata account (authorized, owned by this program)
|
||||
//! 1 - winner account
|
||||
//! 2 - clock account (read-only, e.g. `CLOCK_01`).
|
||||
//!
|
||||
//! Pinata account data layout (24 bytes):
|
||||
//! [prize: u64 LE | `cooldown_ms`: u64 LE | `last_claim_timestamp`: u64 LE].
|
||||
|
||||
use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData};
|
||||
use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs};
|
||||
|
||||
type Instruction = ();
|
||||
|
||||
struct PinataState {
|
||||
prize: u128,
|
||||
cooldown_ms: u64,
|
||||
last_claim_timestamp: u64,
|
||||
}
|
||||
|
||||
impl PinataState {
|
||||
fn from_bytes(bytes: &[u8]) -> Self {
|
||||
assert!(bytes.len() >= 32, "Pinata account data too short");
|
||||
let prize = u128::from_le_bytes(bytes[..16].try_into().unwrap());
|
||||
let cooldown_ms = u64::from_le_bytes(bytes[16..24].try_into().unwrap());
|
||||
let last_claim_timestamp = u64::from_le_bytes(bytes[24..32].try_into().unwrap());
|
||||
Self {
|
||||
prize,
|
||||
cooldown_ms,
|
||||
last_claim_timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(32);
|
||||
buf.extend_from_slice(&self.prize.to_le_bytes());
|
||||
buf.extend_from_slice(&self.cooldown_ms.to_le_bytes());
|
||||
buf.extend_from_slice(&self.last_claim_timestamp.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
pre_states,
|
||||
instruction: (),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([pinata, winner, clock_pre]) = <[_; 3]>::try_from(pre_states) else {
|
||||
panic!("Expected exactly 3 input accounts: pinata, winner, clock");
|
||||
};
|
||||
|
||||
// Check the clock account is the system clock account
|
||||
assert_eq!(clock_pre.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
|
||||
let clock_data = ClockAccountData::from_bytes(&clock_pre.account.data.clone().into_inner());
|
||||
let current_timestamp = clock_data.timestamp;
|
||||
|
||||
let pinata_state = PinataState::from_bytes(&pinata.account.data.clone().into_inner());
|
||||
|
||||
// Enforce cooldown: the elapsed time since the last claim must exceed the cooldown period.
|
||||
let elapsed = current_timestamp.saturating_sub(pinata_state.last_claim_timestamp);
|
||||
assert!(
|
||||
elapsed >= pinata_state.cooldown_ms,
|
||||
"Cooldown not elapsed: {elapsed}ms since last claim, need {}ms",
|
||||
pinata_state.cooldown_ms,
|
||||
);
|
||||
|
||||
let mut pinata_post = pinata.account.clone();
|
||||
let mut winner_post = winner.account.clone();
|
||||
|
||||
pinata_post.balance = pinata_post
|
||||
.balance
|
||||
.checked_sub(pinata_state.prize)
|
||||
.expect("Not enough balance in the pinata");
|
||||
winner_post.balance = winner_post
|
||||
.balance
|
||||
.checked_add(pinata_state.prize)
|
||||
.expect("Overflow when adding prize to winner");
|
||||
|
||||
// Update the last claim timestamp.
|
||||
let updated_state = PinataState {
|
||||
last_claim_timestamp: current_timestamp,
|
||||
..pinata_state
|
||||
};
|
||||
pinata_post.data = updated_state
|
||||
.to_bytes()
|
||||
.try_into()
|
||||
.expect("Pinata state should fit in account data");
|
||||
|
||||
// Clock account is read-only.
|
||||
let clock_post = clock_pre.account.clone();
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
instruction_words,
|
||||
vec![pinata, winner, clock_pre],
|
||||
vec![
|
||||
AccountPostState::new_claimed_if_default(pinata_post, Claim::Authorized),
|
||||
AccountPostState::new(winner_post),
|
||||
AccountPostState::new(clock_post),
|
||||
],
|
||||
)
|
||||
.write();
|
||||
}
|
||||
70
test_program_methods/guest/src/bin/time_locked_transfer.rs
Normal file
70
test_program_methods/guest/src/bin/time_locked_transfer.rs
Normal file
@ -0,0 +1,70 @@
|
||||
//! Time-locked transfer program.
|
||||
//!
|
||||
//! Demonstrates how a program can include a clock account among its inputs and use the on-chain
|
||||
//! timestamp in its logic. The transfer only executes when the clock timestamp is at or past a
|
||||
//! caller-supplied deadline; otherwise the program panics.
|
||||
//!
|
||||
//! Expected pre-states (in order):
|
||||
//! 0 - sender account (authorized)
|
||||
//! 1 - receiver account
|
||||
//! 2 - clock account (read-only, e.g. `CLOCK_01`).
|
||||
|
||||
use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData};
|
||||
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
|
||||
|
||||
/// (`amount`, `deadline_timestamp`).
|
||||
type Instruction = (u128, u64);
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
pre_states,
|
||||
instruction: (amount, deadline),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([sender_pre, receiver_pre, clock_pre]) = <[_; 3]>::try_from(pre_states) else {
|
||||
panic!("Expected exactly 3 input accounts: sender, receiver, clock");
|
||||
};
|
||||
|
||||
// Check the clock account is the system clock account
|
||||
assert_eq!(clock_pre.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
|
||||
// Read the current timestamp from the clock account.
|
||||
let clock_data = ClockAccountData::from_bytes(&clock_pre.account.data.clone().into_inner());
|
||||
|
||||
assert!(
|
||||
clock_data.timestamp >= deadline,
|
||||
"Transfer is time-locked until timestamp {deadline}, current is {}",
|
||||
clock_data.timestamp,
|
||||
);
|
||||
|
||||
let mut sender_post = sender_pre.account.clone();
|
||||
let mut receiver_post = receiver_pre.account.clone();
|
||||
|
||||
sender_post.balance = sender_post
|
||||
.balance
|
||||
.checked_sub(amount)
|
||||
.expect("Insufficient balance");
|
||||
receiver_post.balance = receiver_post
|
||||
.balance
|
||||
.checked_add(amount)
|
||||
.expect("Balance overflow");
|
||||
|
||||
// Clock account is read-only: post state equals pre state.
|
||||
let clock_post = clock_pre.account.clone();
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
instruction_words,
|
||||
vec![sender_pre, receiver_pre, clock_pre],
|
||||
vec![
|
||||
AccountPostState::new(sender_post),
|
||||
AccountPostState::new(receiver_post),
|
||||
AccountPostState::new(clock_post),
|
||||
],
|
||||
)
|
||||
.write();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user