Skip to content

Commit 3979a4b

Browse files
builder-stake: move the instruction builders into the crate
The harness said to do this when something outside the tests needed them, and something does. A caller in another repository has to post a bond to test what it does with the account, and hand-encoding the accounts there would put the processor's account order in two places, in two repositories, with nothing to keep them in step. A move, not a rewrite. The harness re-exports them, so every test that used them reads unchanged, and all 21 still pass.
1 parent c138947 commit 3979a4b

4 files changed

Lines changed: 174 additions & 157 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file.
2323
- `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)
2424
- 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.
2525
- Solana programs (`solana/`)
26+
- `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.
2627
- `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.
2728
- `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.
2829
- 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.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
//! Instruction builders for `builder-stake`.
2+
//!
3+
//! Every builder here fixes the account order the processor expects, so a caller cannot get it
4+
//! wrong and a change to the processor has one place to update rather than one per caller.
5+
6+
use doublezero_program_tools::get_program_data_address;
7+
use solana_instruction::{AccountMeta, Instruction};
8+
use solana_pubkey::Pubkey;
9+
use solana_system_interface::program as system_program;
10+
11+
use crate::{
12+
instruction::BuilderStakeInstructionData,
13+
state::{self, BuilderStake, ProgramConfig},
14+
DOUBLEZERO_MINT_KEY, ID,
15+
};
16+
17+
fn encode(data: &BuilderStakeInstructionData) -> Vec<u8> {
18+
borsh::to_vec(data).unwrap()
19+
}
20+
21+
pub fn initialize_program(payer: &Pubkey) -> Instruction {
22+
Instruction {
23+
program_id: ID,
24+
accounts: vec![
25+
AccountMeta::new(*payer, true),
26+
AccountMeta::new(ProgramConfig::find_address().0, false),
27+
AccountMeta::new_readonly(system_program::ID, false),
28+
],
29+
data: encode(&BuilderStakeInstructionData::InitializeProgram),
30+
}
31+
}
32+
33+
pub fn set_admin(admin: &Pubkey) -> Instruction {
34+
Instruction {
35+
program_id: ID,
36+
accounts: vec![
37+
AccountMeta::new_readonly(get_program_data_address(&ID).0, false),
38+
AccountMeta::new_readonly(*admin, true),
39+
AccountMeta::new(ProgramConfig::find_address().0, false),
40+
],
41+
data: encode(&BuilderStakeInstructionData::SetAdmin(*admin)),
42+
}
43+
}
44+
45+
pub fn set_paused(admin: &Pubkey, paused: bool) -> Instruction {
46+
use crate::instruction::{ProgramConfiguration, ProgramFlagConfiguration};
47+
48+
Instruction {
49+
program_id: ID,
50+
accounts: vec![
51+
AccountMeta::new_readonly(*admin, true),
52+
AccountMeta::new(ProgramConfig::find_address().0, false),
53+
],
54+
data: encode(&BuilderStakeInstructionData::ConfigureProgram(
55+
ProgramConfiguration::Flag(ProgramFlagConfiguration::IsPaused(paused)),
56+
)),
57+
}
58+
}
59+
60+
pub fn set_tier_parameters(
61+
admin: &Pubkey,
62+
up_to_1gbps_2z_amount: u64,
63+
up_to_5gbps_2z_amount: u64,
64+
unmetered_2z_amount: u64,
65+
) -> Instruction {
66+
use crate::instruction::ProgramConfiguration;
67+
68+
Instruction {
69+
program_id: ID,
70+
accounts: vec![
71+
AccountMeta::new_readonly(*admin, true),
72+
AccountMeta::new(ProgramConfig::find_address().0, false),
73+
],
74+
data: encode(&BuilderStakeInstructionData::ConfigureProgram(
75+
ProgramConfiguration::TierParameters {
76+
up_to_1gbps_2z_amount,
77+
up_to_5gbps_2z_amount,
78+
unmetered_2z_amount,
79+
},
80+
)),
81+
}
82+
}
83+
84+
pub fn initialize_builder_stake(
85+
builder: &Pubkey,
86+
stake_index: u64,
87+
committed_rate_bits_per_sec: u64,
88+
) -> Instruction {
89+
let stake_key = BuilderStake::find_address(builder, stake_index).0;
90+
91+
Instruction {
92+
program_id: ID,
93+
accounts: vec![
94+
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
95+
AccountMeta::new(*builder, true),
96+
AccountMeta::new(stake_key, false),
97+
AccountMeta::new(state::find_2z_token_pda_address(&stake_key).0, false),
98+
AccountMeta::new_readonly(DOUBLEZERO_MINT_KEY, false),
99+
AccountMeta::new_readonly(spl_token_interface::ID, false),
100+
AccountMeta::new_readonly(system_program::ID, false),
101+
],
102+
data: encode(&BuilderStakeInstructionData::InitializeBuilderStake {
103+
stake_index,
104+
committed_rate_bits_per_sec,
105+
}),
106+
}
107+
}
108+
109+
pub fn post_bond(
110+
builder: &Pubkey,
111+
stake_index: u64,
112+
source_token_account: &Pubkey,
113+
amount: u64,
114+
) -> Instruction {
115+
let stake_key = BuilderStake::find_address(builder, stake_index).0;
116+
117+
Instruction {
118+
program_id: ID,
119+
accounts: vec![
120+
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
121+
AccountMeta::new_readonly(*builder, true),
122+
AccountMeta::new(stake_key, false),
123+
AccountMeta::new(state::find_2z_token_pda_address(&stake_key).0, false),
124+
AccountMeta::new(*source_token_account, false),
125+
AccountMeta::new_readonly(spl_token_interface::ID, false),
126+
],
127+
data: encode(&BuilderStakeInstructionData::PostBond { amount }),
128+
}
129+
}
130+
131+
pub fn withdraw(
132+
builder: &Pubkey,
133+
stake_index: u64,
134+
destination_token_account: &Pubkey,
135+
amount: u64,
136+
) -> Instruction {
137+
let stake_key = BuilderStake::find_address(builder, stake_index).0;
138+
139+
Instruction {
140+
program_id: ID,
141+
accounts: vec![
142+
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
143+
AccountMeta::new_readonly(*builder, true),
144+
AccountMeta::new(stake_key, false),
145+
AccountMeta::new(state::find_2z_token_pda_address(&stake_key).0, false),
146+
AccountMeta::new(*destination_token_account, false),
147+
AccountMeta::new_readonly(spl_token_interface::ID, false),
148+
],
149+
data: encode(&BuilderStakeInstructionData::Withdraw { amount }),
150+
}
151+
}
152+
153+
/// Move a stake's hold expiry. Development builds only; the test binary is one.
154+
pub fn set_hold_expiry(admin: &Pubkey, builder: &Pubkey, stake_index: u64, at: i64) -> Instruction {
155+
Instruction {
156+
program_id: ID,
157+
accounts: vec![
158+
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
159+
AccountMeta::new_readonly(*admin, true),
160+
AccountMeta::new(BuilderStake::find_address(builder, stake_index).0, false),
161+
],
162+
data: encode(&BuilderStakeInstructionData::SetHoldExpiry {
163+
hold_expires_at: at,
164+
}),
165+
}
166+
}

solana/programs/builder-stake/src/instruction/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
pub mod builders;
2+
13
use borsh::{BorshDeserialize, BorshSerialize};
24
use solana_pubkey::Pubkey;
35

solana/programs/builder-stake/tests/common/mod.rs

Lines changed: 5 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66
#![allow(dead_code)]
77

88
use doublezero_builder_stake::{
9-
instruction::BuilderStakeInstructionData,
10-
state::{self, BuilderStake, ProgramConfig},
9+
state::{BuilderStake, ProgramConfig},
1110
DOUBLEZERO_MINT_KEY, ID,
1211
};
1312
use solana_loader_v3_interface::{get_program_data_address, state::UpgradeableLoaderState};
1413
use solana_program_test::{ProgramTest, ProgramTestContext};
1514
use solana_sdk::{
1615
account::Account,
17-
instruction::{AccountMeta, Instruction, InstructionError},
16+
instruction::{Instruction, InstructionError},
1817
program_pack::Pack,
1918
pubkey::Pubkey,
2019
signature::{Keypair, Signer},
@@ -281,157 +280,6 @@ impl TestSetup {
281280
}
282281
}
283282

284-
//
285-
// Instruction builders.
286-
//
287-
288-
fn encode(data: &BuilderStakeInstructionData) -> Vec<u8> {
289-
borsh::to_vec(data).unwrap()
290-
}
291-
292-
pub fn initialize_program(payer: &Pubkey) -> Instruction {
293-
Instruction {
294-
program_id: ID,
295-
accounts: vec![
296-
AccountMeta::new(*payer, true),
297-
AccountMeta::new(ProgramConfig::find_address().0, false),
298-
AccountMeta::new_readonly(system_program::ID, false),
299-
],
300-
data: encode(&BuilderStakeInstructionData::InitializeProgram),
301-
}
302-
}
303-
304-
pub fn set_admin(admin: &Pubkey) -> Instruction {
305-
Instruction {
306-
program_id: ID,
307-
accounts: vec![
308-
AccountMeta::new_readonly(get_program_data_address(&ID), false),
309-
AccountMeta::new_readonly(*admin, true),
310-
AccountMeta::new(ProgramConfig::find_address().0, false),
311-
],
312-
data: encode(&BuilderStakeInstructionData::SetAdmin(*admin)),
313-
}
314-
}
315-
316-
pub fn set_paused(admin: &Pubkey, paused: bool) -> Instruction {
317-
use doublezero_builder_stake::instruction::{ProgramConfiguration, ProgramFlagConfiguration};
318-
319-
Instruction {
320-
program_id: ID,
321-
accounts: vec![
322-
AccountMeta::new_readonly(*admin, true),
323-
AccountMeta::new(ProgramConfig::find_address().0, false),
324-
],
325-
data: encode(&BuilderStakeInstructionData::ConfigureProgram(
326-
ProgramConfiguration::Flag(ProgramFlagConfiguration::IsPaused(paused)),
327-
)),
328-
}
329-
}
330-
331-
pub fn set_tier_parameters(
332-
admin: &Pubkey,
333-
up_to_1gbps_2z_amount: u64,
334-
up_to_5gbps_2z_amount: u64,
335-
unmetered_2z_amount: u64,
336-
) -> Instruction {
337-
use doublezero_builder_stake::instruction::ProgramConfiguration;
338-
339-
Instruction {
340-
program_id: ID,
341-
accounts: vec![
342-
AccountMeta::new_readonly(*admin, true),
343-
AccountMeta::new(ProgramConfig::find_address().0, false),
344-
],
345-
data: encode(&BuilderStakeInstructionData::ConfigureProgram(
346-
ProgramConfiguration::TierParameters {
347-
up_to_1gbps_2z_amount,
348-
up_to_5gbps_2z_amount,
349-
unmetered_2z_amount,
350-
},
351-
)),
352-
}
353-
}
354-
355-
pub fn initialize_builder_stake(
356-
builder: &Pubkey,
357-
stake_index: u64,
358-
committed_rate_bits_per_sec: u64,
359-
) -> Instruction {
360-
let stake_key = BuilderStake::find_address(builder, stake_index).0;
361-
362-
Instruction {
363-
program_id: ID,
364-
accounts: vec![
365-
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
366-
AccountMeta::new(*builder, true),
367-
AccountMeta::new(stake_key, false),
368-
AccountMeta::new(state::find_2z_token_pda_address(&stake_key).0, false),
369-
AccountMeta::new_readonly(DOUBLEZERO_MINT_KEY, false),
370-
AccountMeta::new_readonly(spl_token_interface::ID, false),
371-
AccountMeta::new_readonly(system_program::ID, false),
372-
],
373-
data: encode(&BuilderStakeInstructionData::InitializeBuilderStake {
374-
stake_index,
375-
committed_rate_bits_per_sec,
376-
}),
377-
}
378-
}
379-
380-
pub fn post_bond(
381-
builder: &Pubkey,
382-
stake_index: u64,
383-
source_token_account: &Pubkey,
384-
amount: u64,
385-
) -> Instruction {
386-
let stake_key = BuilderStake::find_address(builder, stake_index).0;
387-
388-
Instruction {
389-
program_id: ID,
390-
accounts: vec![
391-
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
392-
AccountMeta::new_readonly(*builder, true),
393-
AccountMeta::new(stake_key, false),
394-
AccountMeta::new(state::find_2z_token_pda_address(&stake_key).0, false),
395-
AccountMeta::new(*source_token_account, false),
396-
AccountMeta::new_readonly(spl_token_interface::ID, false),
397-
],
398-
data: encode(&BuilderStakeInstructionData::PostBond { amount }),
399-
}
400-
}
401-
402-
pub fn withdraw(
403-
builder: &Pubkey,
404-
stake_index: u64,
405-
destination_token_account: &Pubkey,
406-
amount: u64,
407-
) -> Instruction {
408-
let stake_key = BuilderStake::find_address(builder, stake_index).0;
409-
410-
Instruction {
411-
program_id: ID,
412-
accounts: vec![
413-
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
414-
AccountMeta::new_readonly(*builder, true),
415-
AccountMeta::new(stake_key, false),
416-
AccountMeta::new(state::find_2z_token_pda_address(&stake_key).0, false),
417-
AccountMeta::new(*destination_token_account, false),
418-
AccountMeta::new_readonly(spl_token_interface::ID, false),
419-
],
420-
data: encode(&BuilderStakeInstructionData::Withdraw { amount }),
421-
}
422-
}
423-
424-
/// Move a stake's hold expiry. Development builds only; the test binary is one.
425-
pub fn set_hold_expiry(admin: &Pubkey, builder: &Pubkey, stake_index: u64, at: i64) -> Instruction {
426-
Instruction {
427-
program_id: ID,
428-
accounts: vec![
429-
AccountMeta::new_readonly(ProgramConfig::find_address().0, false),
430-
AccountMeta::new_readonly(*admin, true),
431-
AccountMeta::new(BuilderStake::find_address(builder, stake_index).0, false),
432-
],
433-
data: encode(&BuilderStakeInstructionData::SetHoldExpiry {
434-
hold_expires_at: at,
435-
}),
436-
}
437-
}
283+
// The instruction builders moved into the crate, where a caller outside these tests can reach
284+
// them. Re-exported so the test files that use them read unchanged.
285+
pub use doublezero_builder_stake::instruction::builders::*;

0 commit comments

Comments
 (0)