Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion api/src/state/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ pub struct Queue {
/// can be drained and closed. 0 = active. Reuses former padding, so
/// existing (zeroed) accounts read as active.
pub paused: u8,
pub _padding: [u8; 2],
/// 1 if delegated to an ephemeral rollup. `DelegateOracleQueue` sets this
/// before delegation, and the rollup clone preserves it. Such rollups cannot
/// transfer lamports from accounts they do not hold, including the requester
/// wallet and confined queue, so queue requests and payouts incur no fee.
/// Reuses former padding; existing zero-initialized accounts default to 0.
pub ephemeral: u8,
pub _padding: [u8; 1],
}

/// Single queue entry. This is written into the variable region and
Expand Down
5 changes: 4 additions & 1 deletion program/src/delegate_oracle_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,15 @@ pub fn process_delegate_oracle_queue(accounts: &[AccountInfo<'_>], data: &[u8])
// be pulled into the rollup. A drained queue is not "unused": removed items
// leave non-zero bytes behind, so the variable region is checked too.
{
let data = oracle_queue_info.try_borrow_data()?;
let mut data = oracle_queue_info.try_borrow_mut_data()?;
let queue = Queue::try_from_bytes(&data)?;
let variable_region_start = 8 + core::mem::size_of::<Queue>();
if queue.item_count != 0 || data[variable_region_start..].iter().any(|&b| b != 0) {
return Err(SolanaVrfError::QueueAlreadyInUse.into());
}
// Mark the queue before delegating, so the rollup's clone carries the
// mark and its requests move no fee.
QueueAccount::load(&mut data)?.header.ephemeral = 1;
}

// Delegate
Expand Down
13 changes: 8 additions & 5 deletions program/src/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ use solana_program::pubkey::Pubkey;
use solana_vrf_api::consts::DEFAULT_EPHEMERAL_QUEUE;
#[cfg(feature = "ephemeral-test-queue")]
use solana_vrf_api::consts::DEFAULT_EPHEMERAL_TEST_QUEUE;
use solana_vrf_api::state::Queue;

/// Whether `queue` is exempt from the per-request fee (and the matching oracle payout).
/// `DEFAULT_EPHEMERAL_QUEUE` is always exempt; the local test queue only with the
/// `ephemeral-test-queue` feature, so production builds never exempt it.
pub fn is_fee_exempt_ephemeral_queue(queue: &Pubkey) -> bool {
if queue == &DEFAULT_EPHEMERAL_QUEUE {
/// Whether the queue is exempt from request fees and matching oracle payouts.
/// Queues delegated to ephemeral rollups are exempt because the rollup cannot
/// transfer lamports from the requester or the confined queue.
/// `DEFAULT_EPHEMERAL_QUEUE` is always exempt. The local test queue is exempt
/// only when the `ephemeral-test-queue` feature is enabled.
pub fn is_fee_exempt_ephemeral_queue(queue: &Pubkey, header: &Queue) -> bool {
if header.ephemeral != 0 || queue == &DEFAULT_EPHEMERAL_QUEUE {
return true;
}
#[cfg(feature = "ephemeral-test-queue")]
Expand Down
8 changes: 5 additions & 3 deletions program/src/provide_randomness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,12 @@ pub fn process_provide_randomness(accounts: &[AccountInfo<'_>], data: &[u8]) ->
let metas = removed_item.account_metas(queue_acc.acc).to_vec();
let disc = removed_item.callback_discriminator(queue_acc.acc).to_vec();
let args_bytes = removed_item.callback_args(queue_acc.acc).to_vec();
(removed_item, metas, disc, args_bytes)
let fee_exempt =
crate::fees::is_fee_exempt_ephemeral_queue(oracle_queue_info.key, queue_acc.header);
(removed_item, metas, disc, args_bytes, fee_exempt)
};

let (removed_item, metas_vec, disc_vec, args_vec) = removed_item_and_buf;
let (removed_item, metas_vec, disc_vec, args_vec, fee_exempt) = removed_item_and_buf;

// Invoke the callback with randomness
callback_program_info.has_address(&Pubkey::new_from_array(removed_item.callback_program_id))?;
Expand Down Expand Up @@ -212,7 +214,7 @@ pub fn process_provide_randomness(accounts: &[AccountInfo<'_>], data: &[u8]) ->
}

// Collect the fees (unless this is a fee-exempt ephemeral queue)
if !crate::fees::is_fee_exempt_ephemeral_queue(oracle_queue_info.key) {
if !fee_exempt {
let cost = if removed_item.priority_request == 1 {
VRF_HIGH_PRIORITY_LAMPORTS_COST
} else {
Expand Down
4 changes: 3 additions & 1 deletion program/src/purge_expired_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ pub fn process_purge_expired_requests(accounts: &[AccountInfo<'_>], data: &[u8])
// The oracle also accrues fees on malformed/expired requests to
// 1) incentivize queue cleaning and
// 2) disincentivize creation of malformed requests
if total_cost > 0 && !crate::fees::is_fee_exempt_ephemeral_queue(oracle_queue_info.key) {
if total_cost > 0
&& !crate::fees::is_fee_exempt_ephemeral_queue(oracle_queue_info.key, queue_acc.header)
{
crate::fees::transfer_fee(oracle_queue_info, oracle_info, total_cost)?;
}

Expand Down
7 changes: 4 additions & 3 deletions program/src/request_randomness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ pub fn process_request_randomness(
let slot = Clock::get()?.slot;
let time = Clock::get()?.unix_timestamp;

{
let fee_exempt = {
// Borrow queue account data and load QueueAccount view
let mut data = oracle_queue_info.try_borrow_mut_data()?;
Queue::try_from_bytes(&data)?;
Expand Down Expand Up @@ -145,10 +145,11 @@ pub fn process_request_randomness(
.to_bytes()
},
)?;
}
crate::fees::is_fee_exempt_ephemeral_queue(oracle_queue_info.key, queue_acc.header)
};

// Transfer request cost to the queue PDA (unless this is a fee-exempt ephemeral queue)
if !crate::fees::is_fee_exempt_ephemeral_queue(oracle_queue_info.key) {
if !fee_exempt {
let cost = if high_priority {
VRF_HIGH_PRIORITY_LAMPORTS_COST
} else {
Expand Down
97 changes: 96 additions & 1 deletion program/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use solana_curve25519::ristretto::PodRistrettoPoint;
use solana_curve25519::scalar::PodScalar;
use solana_program::rent::Rent;
use solana_program::sysvar::slot_hashes;
use solana_program_test::{processor, read_file, ProgramTest, ProgramTestContext};
use solana_program_test::{processor, read_file, BanksClient, ProgramTest, ProgramTestContext};
use solana_sdk::account::Account;
use solana_sdk::{pubkey, signature::Keypair, signer::Signer, transaction::Transaction};
use solana_vrf::vrf::{compute_vrf, generate_vrf_keypair, verify_vrf};
Expand Down Expand Up @@ -514,6 +514,101 @@ async fn run_test() {
);
}

// A queue delegated to an ephemeral rollup carries the ephemeral mark, and the
// rollup's clone of it takes a request and fulfils it with no fee moving: a
// rollup refuses a transaction that changes the lamports of an account it does
// not hold, the requester's wallet or the confined queue.
#[tokio::test]
async fn delegated_queue_requests_move_no_fee() {
let mut context = setup().await;
let banks = context.banks_client.clone();
let authority_keypair = Keypair::try_from(&TEST_AUTHORITY[..]).unwrap();
let oracle_keypair = Keypair::try_from(&TEST_ORACLE[..]).unwrap();
let (oracle_vrf_sk, oracle_vrf_pk) = generate_vrf_keypair(&oracle_keypair);

let payer = context.payer.insecure_clone();
send(&banks, &[initialize(payer.pubkey())], &[&payer]).await;
let ix = add_oracle(
authority_keypair.pubkey(),
oracle_keypair.pubkey(),
oracle_vrf_pk.compress().to_bytes(),
);
send(&banks, &[ix], &[&authority_keypair]).await;
// Queue 0 is DEFAULT_TEST_QUEUE, the one the callback program accepts.
let ixs = initialize_oracle_queue(payer.pubkey(), oracle_keypair.pubkey(), 0, Some(10_000));
send(&banks, &ixs, &[&payer, &oracle_keypair]).await;
let queue = oracle_queue_pda(&oracle_keypair.pubkey(), 0).0;
let ix = delegate_oracle_queue(oracle_keypair.pubkey(), queue, 0);
send(&banks, &[ix], &[&oracle_keypair]).await;

let delegated = banks.get_account(queue).await.unwrap().unwrap();
assert_eq!(delegated.owner, DELEGATION_PROGRAM_ID);
let mut data = delegated.data.clone();
assert_eq!(
QueueAccount::load(&mut data[..]).unwrap().header.ephemeral,
1
);

// The rollup's clone: the same bytes and lamports, owned by this program.
context.set_account(
&queue,
&Account {
owner: solana_vrf_api::ID,
..delegated.clone()
}
.into(),
);

send(
&banks,
&[request_randomness_to_queue(payer.pubkey(), 7, queue)],
&[&payer],
)
.await;
let requested = banks.get_account(queue).await.unwrap().unwrap();
let mut data = requested.data.clone();
let item = QueueAccount::load(&mut data[..])
.unwrap()
.get_item_by_index(0)
.unwrap()
.id;
assert_eq!(
requested.lamports, delegated.lamports,
"the request moved a fee"
);

let slot = banks.get_sysvar::<Clock>().await.unwrap().slot;
context.warp_to_slot(slot + 1).unwrap();
let (output, (commitment_base, commitment_hash, s)) = compute_vrf(oracle_vrf_sk, &item);
let provide = provide_randomness(
oracle_keypair.pubkey(),
queue,
TEST_CALLBACK_PROGRAM,
item,
PodRistrettoPoint(output.to_bytes()),
PodRistrettoPoint(commitment_base.to_bytes()),
PodRistrettoPoint(commitment_hash.to_bytes()),
PodScalar(s.to_bytes()),
);
let compute = ComputeBudgetInstruction::set_compute_unit_limit(2_000_000);
send(&banks, &[compute, provide], &[&oracle_keypair]).await;
let provided = banks.get_account(queue).await.unwrap().unwrap();
let mut data = provided.data.clone();
assert_eq!(QueueAccount::load(&mut data[..]).unwrap().len(), 0);
assert_eq!(
provided.lamports, delegated.lamports,
"the fulfilment paid a fee out"
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

/// send signs with every signer, the first paying, and requires success.
async fn send(banks: &BanksClient, ixs: &[Instruction], signers: &[&Keypair]) {
let blockhash = banks.get_latest_blockhash().await.unwrap();
let tx =
Transaction::new_signed_with_payer(ixs, Some(&signers[0].pubkey()), signers, blockhash);
banks.process_transaction(tx).await.unwrap();
}

pub fn request_randomness(signer: Pubkey, client_seed: u8) -> Instruction {
// Forward to the generic helper, using the default oracle queue used previously
let oracle_queue = pubkey!("GKE6d7iv8kCBrsxr78W3xVdjGLLLJnxsGiuzrsZCGEvb");
Expand Down