Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file.
- `sdk/shreds/go` carries the feed subscription program's `FeedDistribution` account: how much USDC one feed collected for one calendar month. The program is a second program alongside shred subscription and had nothing in this SDK, so each consumer decoded the account at fixed byte offsets itself, lake included. The account is a bytemuck Pod read here field by field, which agrees with the Pod bytes only because the field order leaves no interior padding; `TestStructSizes` pins the 120-byte total and a new test pins every field against a real mainnet account. `Client` is built around one program ID and so gains no fetch method, and `DeserializeFeedDistribution` is exported for a caller that makes its own `getProgramAccounts` call. `make sdk-test` never ran `./sdk/shreds/go/...`, so this package's layout pins have never run in CI; it runs them now. (#4216)
- The TypeScript and Python `Feed` deserializers read the RFC-28 tail and synthesize `Active` for an account that carries no status byte, matching the Rust program. New `feed_legacy` fixture covers that path alongside the updated `feed` fixture.
- Solana programs (`solana/`)
- `builder-stake` carries its own instruction builders in `instruction::builders`, moved out of the test harness now that a caller outside the crate needs them. Each one fixes the account order the processor expects, so a change there has one place to update rather than one per caller.
- `builder-stake` exposes its `processor` module and `try_process_instruction` under the existing `entrypoint` feature, so a test in another crate can load the program natively through `processor!` rather than building it to BPF first. `doublezero-serviceability` already exposes its own for the same reason. The crate's own tests still run against the `.so`, and nothing is exposed to a consumer that does not ask for the feature.
- `builder-stake` holds a bond for six months and returns the excess after. The first bond starts the hold and a later one does not restart it, so a repricing that forces a top-up cannot push a builder's withdrawal date out. `Withdraw` returns anything above the stake's requirement to a token account the builder names, and refuses both before the hold elapses and below the requirement, which is what stops a builder walking its bond out from under a live feed. The hold is 180 days rather than calendar months, because a month has no fixed length and the alternative is calendar arithmetic onchain to move a one-off boundary by at most three days. A stake that has never been bonded has no hold, and a zero expiry means the hold has not started rather than that it ended in 1970. Every instruction taking a stake checks the account is at the address its own fields derive, which the zero-copy reader does not do. A `SetHoldExpiry` instruction lets a devnet demo show a withdrawal without waiting: it exists in every build and refuses outside a `development` one, rather than sitting behind a `#[cfg]` that would give the two binaries different instruction encodings for the same bytes.
- New `builder-stake` program at `dzbschFChpPoWihZFdnYjyzHJicZwPHb6QTntHjhLki`, holding the 2Z bond a builder posts before deploying a feed under RFC-28. A `BuilderStake` PDA, a 2Z token account owned by it, and `InitializeProgram`, `SetAdmin`, `ConfigureProgram`, `InitializeBuilderStake` and `PostBond`. A bond rather than a deposit: it is returnable after the hold and forfeitable by slashing, and `deposit` carries neither. The address is keyed on `(builder, stake_index)` rather than the builder alone, because RFC-28 collateralizes each feed on its own bond and a builder-only address would cap a builder at one stake for life. The program starts paused, so a deployment with no admin and no tier table holds nothing. No slash instruction yet: the burn authority is what makes this its own deployable, and writing it before the verdict signer is settled means writing it twice. Bond sizing and the six-month hold are not here either.
Expand Down
172 changes: 172 additions & 0 deletions solana/programs/builder-stake/src/instruction/builders.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//! Instruction builders for `builder-stake`.
//!
//! Every builder here fixes the account order the processor expects, so a caller cannot get it
//! wrong and a change to the processor has one place to update rather than one per caller.

use doublezero_program_tools::get_program_data_address;
use solana_instruction::{AccountMeta, Instruction};
use solana_pubkey::Pubkey;
use solana_system_interface::program as system_program;

use crate::{
instruction::BuilderStakeInstructionData,
state::{self, BuilderStake, ProgramConfig},
DOUBLEZERO_MINT_KEY, ID,
};

fn encode(data: &BuilderStakeInstructionData) -> Vec<u8> {
borsh::to_vec(data).unwrap()
}

pub fn initialize_program(payer: &Pubkey) -> Instruction {
Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new(*payer, true),
AccountMeta::new(ProgramConfig::find_address().0, false),
AccountMeta::new_readonly(system_program::ID, false),
],
data: encode(&BuilderStakeInstructionData::InitializeProgram),
}
}

/// Set the program admin.
///
/// Two keys, because the processor reads two: `upgrade_authority` signs and is checked against the
/// program data account, and `admin_key` is the value written to the config. They are the same key
/// in these tests and need not be in production, so a single parameter would make the ordinary
/// case inexpressible.
pub fn set_admin(upgrade_authority: &Pubkey, admin_key: &Pubkey) -> Instruction {
Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(get_program_data_address(&ID).0, false),
AccountMeta::new_readonly(*upgrade_authority, true),
AccountMeta::new(ProgramConfig::find_address().0, false),
],
data: encode(&BuilderStakeInstructionData::SetAdmin(*admin_key)),
}
}

pub fn set_paused(admin: &Pubkey, paused: bool) -> Instruction {
use crate::instruction::{ProgramConfiguration, ProgramFlagConfiguration};

Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(*admin, true),
AccountMeta::new(ProgramConfig::find_address().0, false),
],
data: encode(&BuilderStakeInstructionData::ConfigureProgram(
ProgramConfiguration::Flag(ProgramFlagConfiguration::IsPaused(paused)),
)),
}
}

pub fn set_tier_parameters(
admin: &Pubkey,
up_to_1gbps_2z_amount: u64,
up_to_5gbps_2z_amount: u64,
unmetered_2z_amount: u64,
) -> Instruction {
use crate::instruction::ProgramConfiguration;

Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(*admin, true),
AccountMeta::new(ProgramConfig::find_address().0, false),
],
data: encode(&BuilderStakeInstructionData::ConfigureProgram(
ProgramConfiguration::TierParameters {
up_to_1gbps_2z_amount,
up_to_5gbps_2z_amount,
unmetered_2z_amount,
},
)),
}
}

pub fn initialize_builder_stake(
builder: &Pubkey,
stake_index: u64,
committed_rate_bits_per_sec: u64,
) -> Instruction {
let stake_key = BuilderStake::find_address(builder, stake_index).0;

Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
AccountMeta::new(*builder, true),
AccountMeta::new(stake_key, false),
AccountMeta::new(state::find_2z_token_pda_address(&stake_key).0, false),
AccountMeta::new_readonly(DOUBLEZERO_MINT_KEY, false),
AccountMeta::new_readonly(spl_token_interface::ID, false),
AccountMeta::new_readonly(system_program::ID, false),
],
data: encode(&BuilderStakeInstructionData::InitializeBuilderStake {
stake_index,
committed_rate_bits_per_sec,
}),
}
}

pub fn post_bond(
builder: &Pubkey,
stake_index: u64,
source_token_account: &Pubkey,
amount: u64,
) -> Instruction {
let stake_key = BuilderStake::find_address(builder, stake_index).0;

Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
AccountMeta::new_readonly(*builder, true),
AccountMeta::new(stake_key, false),
AccountMeta::new(state::find_2z_token_pda_address(&stake_key).0, false),
AccountMeta::new(*source_token_account, false),
AccountMeta::new_readonly(spl_token_interface::ID, false),
],
data: encode(&BuilderStakeInstructionData::PostBond { amount }),
}
}

pub fn withdraw(
builder: &Pubkey,
stake_index: u64,
destination_token_account: &Pubkey,
amount: u64,
) -> Instruction {
let stake_key = BuilderStake::find_address(builder, stake_index).0;

Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
AccountMeta::new_readonly(*builder, true),
AccountMeta::new(stake_key, false),
AccountMeta::new(state::find_2z_token_pda_address(&stake_key).0, false),
AccountMeta::new(*destination_token_account, false),
AccountMeta::new_readonly(spl_token_interface::ID, false),
],
data: encode(&BuilderStakeInstructionData::Withdraw { amount }),
}
}

/// Move a stake's hold expiry. Development builds only; the test binary is one.
pub fn set_hold_expiry(admin: &Pubkey, builder: &Pubkey, stake_index: u64, at: i64) -> Instruction {
Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
AccountMeta::new_readonly(*admin, true),
AccountMeta::new(BuilderStake::find_address(builder, stake_index).0, false),
],
data: encode(&BuilderStakeInstructionData::SetHoldExpiry {
hold_expires_at: at,
}),
}
}
2 changes: 2 additions & 0 deletions solana/programs/builder-stake/src/instruction/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod builders;

use borsh::{BorshDeserialize, BorshSerialize};
use solana_pubkey::Pubkey;

Expand Down
171 changes: 11 additions & 160 deletions solana/programs/builder-stake/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
#![allow(dead_code)]

use doublezero_builder_stake::{
instruction::BuilderStakeInstructionData,
state::{self, BuilderStake, ProgramConfig},
state::{BuilderStake, ProgramConfig},
DOUBLEZERO_MINT_KEY, ID,
};
use solana_loader_v3_interface::{get_program_data_address, state::UpgradeableLoaderState};
use solana_program_test::{ProgramTest, ProgramTestContext};
use solana_sdk::{
account::Account,
instruction::{AccountMeta, Instruction, InstructionError},
instruction::{Instruction, InstructionError},
program_pack::Pack,
pubkey::Pubkey,
signature::{Keypair, Signer},
Expand Down Expand Up @@ -231,9 +230,12 @@ impl TestSetup {

let admin = self.upgrade_authority.pubkey();
let upgrade_authority = self.upgrade_authority.insecure_clone();
self.send(set_admin(&admin), &[&upgrade_authority])
.await
.unwrap();
self.send(
set_admin(&upgrade_authority.pubkey(), &admin),
&[&upgrade_authority],
)
.await
.unwrap();
}

pub async fn read_builder_stake(&mut self, key: &Pubkey) -> BuilderStake {
Expand Down Expand Up @@ -281,157 +283,6 @@ impl TestSetup {
}
}

//
// Instruction builders.
//

fn encode(data: &BuilderStakeInstructionData) -> Vec<u8> {
borsh::to_vec(data).unwrap()
}

pub fn initialize_program(payer: &Pubkey) -> Instruction {
Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new(*payer, true),
AccountMeta::new(ProgramConfig::find_address().0, false),
AccountMeta::new_readonly(system_program::ID, false),
],
data: encode(&BuilderStakeInstructionData::InitializeProgram),
}
}

pub fn set_admin(admin: &Pubkey) -> Instruction {
Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(get_program_data_address(&ID), false),
AccountMeta::new_readonly(*admin, true),
AccountMeta::new(ProgramConfig::find_address().0, false),
],
data: encode(&BuilderStakeInstructionData::SetAdmin(*admin)),
}
}

pub fn set_paused(admin: &Pubkey, paused: bool) -> Instruction {
use doublezero_builder_stake::instruction::{ProgramConfiguration, ProgramFlagConfiguration};

Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(*admin, true),
AccountMeta::new(ProgramConfig::find_address().0, false),
],
data: encode(&BuilderStakeInstructionData::ConfigureProgram(
ProgramConfiguration::Flag(ProgramFlagConfiguration::IsPaused(paused)),
)),
}
}

pub fn set_tier_parameters(
admin: &Pubkey,
up_to_1gbps_2z_amount: u64,
up_to_5gbps_2z_amount: u64,
unmetered_2z_amount: u64,
) -> Instruction {
use doublezero_builder_stake::instruction::ProgramConfiguration;

Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(*admin, true),
AccountMeta::new(ProgramConfig::find_address().0, false),
],
data: encode(&BuilderStakeInstructionData::ConfigureProgram(
ProgramConfiguration::TierParameters {
up_to_1gbps_2z_amount,
up_to_5gbps_2z_amount,
unmetered_2z_amount,
},
)),
}
}

pub fn initialize_builder_stake(
builder: &Pubkey,
stake_index: u64,
committed_rate_bits_per_sec: u64,
) -> Instruction {
let stake_key = BuilderStake::find_address(builder, stake_index).0;

Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
AccountMeta::new(*builder, true),
AccountMeta::new(stake_key, false),
AccountMeta::new(state::find_2z_token_pda_address(&stake_key).0, false),
AccountMeta::new_readonly(DOUBLEZERO_MINT_KEY, false),
AccountMeta::new_readonly(spl_token_interface::ID, false),
AccountMeta::new_readonly(system_program::ID, false),
],
data: encode(&BuilderStakeInstructionData::InitializeBuilderStake {
stake_index,
committed_rate_bits_per_sec,
}),
}
}

pub fn post_bond(
builder: &Pubkey,
stake_index: u64,
source_token_account: &Pubkey,
amount: u64,
) -> Instruction {
let stake_key = BuilderStake::find_address(builder, stake_index).0;

Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
AccountMeta::new_readonly(*builder, true),
AccountMeta::new(stake_key, false),
AccountMeta::new(state::find_2z_token_pda_address(&stake_key).0, false),
AccountMeta::new(*source_token_account, false),
AccountMeta::new_readonly(spl_token_interface::ID, false),
],
data: encode(&BuilderStakeInstructionData::PostBond { amount }),
}
}

pub fn withdraw(
builder: &Pubkey,
stake_index: u64,
destination_token_account: &Pubkey,
amount: u64,
) -> Instruction {
let stake_key = BuilderStake::find_address(builder, stake_index).0;

Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
AccountMeta::new_readonly(*builder, true),
AccountMeta::new(stake_key, false),
AccountMeta::new(state::find_2z_token_pda_address(&stake_key).0, false),
AccountMeta::new(*destination_token_account, false),
AccountMeta::new_readonly(spl_token_interface::ID, false),
],
data: encode(&BuilderStakeInstructionData::Withdraw { amount }),
}
}

/// Move a stake's hold expiry. Development builds only; the test binary is one.
pub fn set_hold_expiry(admin: &Pubkey, builder: &Pubkey, stake_index: u64, at: i64) -> Instruction {
Instruction {
program_id: ID,
accounts: vec![
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
AccountMeta::new_readonly(*admin, true),
AccountMeta::new(BuilderStake::find_address(builder, stake_index).0, false),
],
data: encode(&BuilderStakeInstructionData::SetHoldExpiry {
hold_expires_at: at,
}),
}
}
// The instruction builders moved into the crate, where a caller outside these tests can reach
// them. Re-exported so the test files that use them read unchanged.
pub use doublezero_builder_stake::instruction::builders::*;
Loading
Loading