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
27 changes: 19 additions & 8 deletions docs/design/mimd-0024-fraud-proofs.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ pub struct SelectedVerifier {
/// PDA: `["state-buffer", account, commit_seq, authority]`
/// Created by: first `WriteStateBuffer`.
/// Frozen by: final `WriteStateBuffer` chunk.
/// Closed by: `CloseTerminalAccounts`.
/// Closed by: `CloseStateBuffer` when no live commitment needs it.
///
/// Header and extendable bytes for an opened state payload.
pub struct StateBuffer {
Expand All @@ -466,6 +466,9 @@ pub struct StateBuffer {
/// Challenger authority is used for the challenged state.
pub authority: Pubkey,

/// Initial payer that receives the buffer's reclaimed rent.
pub rent_payer: Pubkey,

/// Account whose state payload is being opened.
pub account_pubkey: Pubkey,

Expand Down Expand Up @@ -597,6 +600,7 @@ Commitment, approval, challenge, resolution, and finalization instructions.
| Instruction | Expected invoker | Description |
| --- | --- | --- |
| `WriteStateBuffer`<ul><li>ix-data: <code>commit_seq, total_len, offset, chunk</code></li><li>accounts: <strong>payer signer, authority signer, StateBuffer, delegated account, system program</strong></li></ul> | Authority for a known state-buffer flow | System-level instruction. Creates, grows, writes, and finalizes a DLP-owned `StateBuffer` PDA using the fixed `["state-buffer", account, commit_seq, authority]` seed pattern. The instruction remains cluster-neutral; consuming instructions validate the payload, authority, and cluster relationship. |
| `CloseStateBuffer`<ul><li>ix-data: <code>commit_seq, state_buffer_bump, pending_commitment_bump</code></li><li>accounts: <strong>authority signer, StateBuffer, delegated account, stored rent payer, PendingCommitment</strong></li></ul> | Buffer authority | Closes a consumed or abandoned buffer and returns its rent to the payer recorded by the first write. Cleanup is idempotent and rejects buffers still needed by a live commitment. |
| `ProposeCommitment`<ul><li>ix-data: <code>commit_seq, lamports, owner, da_pointer_hash, er_slot</code></li><li>accounts: <strong>current validator signer, OperatorBond, PendingCommitment, finalized operator StateBuffer, delegated account, DelegationRecord, DelegationMetadata, DelegationCluster, VerifierRegistry, system program</strong></li></ul> | Current validator | Requires `Challenge` mode, verifies the delegation's cluster and current validator, consumes only cluster-scoped bond and registry accounts, selects verifiers, and starts the challenge window. `DelegationRecord` and `DelegationMetadata` are temporary v1-layout dependencies until their v2 replacement exists. |
| `ApproveCommitment`<ul><li>ix-data: <code>empty</code></li><li>accounts: <strong>verifier signer, VerifierBond, PendingCommitment</strong></li></ul> | Selected verifier | Records approval from the selected verifier. v2 MVP requires exactly one selected verifier, so no verifier index is needed. Duplicate approvals do not increase the count. |
| `RaiseChallenge`<ul><li>ix-data: <code>state_commitment_hash, challenge_hash, stake_lamports</code></li><li>accounts: <strong>challenger signer, Challenge, PendingCommitment, DelegationCluster, system program</strong></li></ul> | Challenger | Requires the cluster stored by the pending commitment, locks challenger stake, stores the salted `challenge_hash`, and blocks normal finalization until reveal, timeout, or resolution. It does not consume challenger state bytes. |
Expand All @@ -616,7 +620,7 @@ These instructions can be skipped while implementing the first usable v2 flow.
| `ExtendChallengeWindow`<ul><li>ix-data: <code>empty</code></li><li>accounts: <strong>cranker, PendingCommitment, DelegationCluster</strong></li></ul> | Cranker | Low priority. Extends an under-approved commitment according to the commitment's cluster policy, or expires it after the maximum extensions. Initial v2 can expire under-approved commitments instead. |
| `MarkChallengerRevealTimeout`<ul><li>ix-data: <code>empty</code></li><li>accounts: <strong>cranker, PendingCommitment, Challenge, fee vault</strong></li></ul> | Cranker | Deferred. Closes an unrevealed challenge after the reveal timeout, applies the configured challenger penalty, and unblocks or expires the pending commitment according to final policy. |
| `ClaimPayout`<ul><li>ix-data: <code>empty</code></li><li>accounts: <strong>beneficiary signer, PayoutTimelock</strong></li></ul> | Beneficiary | Low priority. Needed only if dispute payouts are delayed through `PayoutTimelock`; otherwise `ResolveDispute` can pay immediately. |
| `CloseTerminalAccounts`<ul><li>ix-data: <code>close_kind</code></li><li>accounts: <strong>recipient, account to close, terminal parent account</strong></li></ul> | Cranker or recipient | Low priority. Closes terminal records and buffers after their parent commitment or challenge can no longer change. |
| `CloseTerminalAccounts`<ul><li>ix-data: <code>close_kind</code></li><li>accounts: <strong>recipient, account to close, terminal parent account</strong></li></ul> | Cranker or recipient | Low priority. Closes the remaining terminal commitment and challenge accounts after their parent state can no longer change. `StateBuffer` cleanup uses `CloseStateBuffer`. |

### Obsolete Or Remove-Candidate Instructions

Expand All @@ -633,12 +637,12 @@ These instructions are not part of the current v2 MVP flow.
DLP owns the buffer account because the buffer is part of the fraud-proof state
machine, not only temporary transport.

The first write creates the `StateBuffer` PDA, writes the fixed fields, and
allocates up to the maximum account growth allowed in one instruction. Later
writes grow the same PDA by at most that limit and append more bytes. Each new
write must use `offset == payload.len()`; this keeps the MVP simple and avoids a
separate chunks bitmap account. An exact old chunk may be repeated as an
idempotent retry.
The first write creates the `StateBuffer` PDA, records its rent payer, writes the
fixed fields, and allocates up to the maximum account growth allowed in one
instruction. Later writes grow the same PDA by at most that limit and append
more bytes. Each new write must use `offset == payload.len()`; this keeps the MVP
simple and avoids a separate chunks bitmap account. An exact old chunk may be
repeated as an idempotent retry.

The account layout is:

Expand Down Expand Up @@ -674,6 +678,13 @@ When `payload.len() == total_len`, `WriteStateBuffer` hashes the payload and set
`finalized = true`. After that, the buffer cannot accept new bytes; exact
duplicate retries remain valid.

`CloseStateBuffer` requires the buffer authority to sign and always returns rent
to the payer recorded by the first write. It can close partial abandoned uploads,
finalized abandoned uploads, and buffers consumed by Direct settlement. If the
canonical `PendingCommitment` exists, cleanup is allowed only after that
commitment is finalized, expired, or cancelled. Repeating cleanup after the
buffer is already closed succeeds without moving lamports again.

Consuming instructions must check:

- buffer PDA seeds match `account_pubkey`, `commit_seq`, and `authority`;
Expand Down
14 changes: 14 additions & 0 deletions interface/src/args/close_state_buffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use wheels::variable_offset_layout;

#[derive(Clone, Debug, PartialEq, Eq)]
#[variable_offset_layout(buffer_offset = 1)]
pub struct CloseStateBufferArgs {
/// Commit sequence number that identifies the buffer and its possible commitment.
pub commit_seq: u32,

/// Canonical StateBuffer PDA bump. Set by the instruction builder.
pub state_buffer_bump: u8,

/// Canonical PendingCommitment PDA bump. Set by the instruction builder.
pub pending_commitment_bump: u8,
}
2 changes: 2 additions & 0 deletions interface/src/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// instruction tag, so v2 instruction args use `buffer_offset = 1`.

mod challenger_reveal;
mod close_state_buffer;
mod commit_finalize;
mod commit_finalize_from_buffer;
mod delegate;
Expand All @@ -17,6 +18,7 @@ mod update_verifier_registry;
mod write_state_buffer;

pub use challenger_reveal::*;
pub use close_state_buffer::*;
pub use commit_finalize::*;
pub use commit_finalize_from_buffer::*;
pub use delegate::*;
Expand Down
6 changes: 6 additions & 0 deletions interface/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ pub enum DlpError {
#[error("Account data is too large for one-step undelegation")]
AccountDataTooLargeForUndelegation = 56,

#[error("Invalid StateBuffer rent payer")]
InvalidStateBufferRentPayer = 57,

#[error("StateBuffer is still required by a live commitment")]
StateBufferInUse = 58,

#[error("An infallible error is encountered possibly due to logic error")]
InfallibleError = 100,
}
Expand Down
2 changes: 2 additions & 0 deletions interface/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ pub enum DlpV2Instruction {
PrepareCommitFinalize = 115,
/// Returns a delegated account to its original owner.
Undelegate = 116,
/// Closes a state buffer and returns its rent to the stored payer.
CloseStateBuffer = 117,
}

impl DlpV2Instruction {
Expand Down
47 changes: 47 additions & 0 deletions interface/src/instruction_builder/close_state_buffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use wheels::{layout::Encodable, DataLayoutError};

use crate::{
compat::{Compatize, Modernize},
pda::{pending_commitment_pda_with_bump, state_buffer_pda_with_bump},
CloseStateBufferArgs, DlpV2Instruction,
};

/// Builds an instruction that closes one StateBuffer.
pub fn close_state_buffer(
authority: Pubkey,
account: Pubkey,
commit_seq: u32,
rent_payer: Pubkey,
) -> Result<Instruction, DataLayoutError> {
let account = account.compatize();
let authority = authority.compatize();
let (state_buffer, state_buffer_bump) =
state_buffer_pda_with_bump(&account, commit_seq, &authority);
let (pending_commitment, pending_commitment_bump) =
pending_commitment_pda_with_bump(&account, commit_seq);

Ok(Instruction {
program_id: crate::id().modernize(),
accounts: vec![
AccountMeta::new_readonly(authority.modernize(), true),
AccountMeta::new(state_buffer.modernize(), false),
AccountMeta::new_readonly(account.modernize(), false),
AccountMeta::new(rent_payer, false),
AccountMeta::new_readonly(pending_commitment.modernize(), false),
],
data: [
DlpV2Instruction::CloseStateBuffer.to_vec(),
CloseStateBufferArgs {
commit_seq,
state_buffer_bump,
pending_commitment_bump,
}
.encode()?,
]
.concat(),
})
}
2 changes: 2 additions & 0 deletions interface/src/instruction_builder/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod approve_commitment;
mod challenger_reveal;
mod close_state_buffer;
mod commit_finalize;
mod commit_finalize_from_buffer;
mod delegate;
Expand All @@ -17,6 +18,7 @@ mod write_state_buffer;

pub use approve_commitment::*;
pub use challenger_reveal::*;
pub use close_state_buffer::*;
pub use commit_finalize::*;
pub use commit_finalize_from_buffer::*;
pub use delegate::*;
Expand Down
17 changes: 15 additions & 2 deletions interface/src/pda.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ pub fn state_buffer_pda(
commit_seq: u32,
authority: &Pubkey,
) -> Pubkey {
state_buffer_pda_with_bump(account, commit_seq, authority).0
}

pub fn state_buffer_pda_with_bump(
account: &Pubkey,
commit_seq: u32,
authority: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
STATE_BUFFER_SEED,
Expand All @@ -73,10 +81,16 @@ pub fn state_buffer_pda(
],
&crate::id(),
)
.0
}

pub fn pending_commitment_pda(account: &Pubkey, commit_seq: u32) -> Pubkey {
pending_commitment_pda_with_bump(account, commit_seq).0
}

pub fn pending_commitment_pda_with_bump(
account: &Pubkey,
commit_seq: u32,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
PENDING_COMMITMENT_SEED,
Expand All @@ -85,7 +99,6 @@ pub fn pending_commitment_pda(account: &Pubkey, commit_seq: u32) -> Pubkey {
],
&crate::id(),
)
.0
}

pub fn challenge_pda(
Expand Down
5 changes: 4 additions & 1 deletion interface/src/state/state_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{compat::Pubkey, error::DlpError};

/// PDA: `["state-buffer", account, commit_seq, authority]`.
/// Created by `WriteStateBuffer`.
/// Closed by `CloseTerminalAccounts` after finalize, cancel, or expiry.
/// Closed by `CloseStateBuffer` when no live commitment needs it.
#[derive(Clone, Debug, PartialEq, Eq)]
#[fixed_offset_layout(buffer_offset = 0)]
pub struct StateBuffer {
Expand All @@ -20,6 +20,9 @@ pub struct StateBuffer {
/// challenger identity for a challenger dispute buffer.
pub authority: Pubkey,

/// Account that funded this buffer and receives its reclaimed rent.
pub rent_payer: Pubkey,

/// Delegated account whose payload is stored after this header.
pub account_pubkey: Pubkey,

Expand Down
147 changes: 147 additions & 0 deletions program/src/processor/close_state_buffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use delegation_program_interface as dpi;
use dpi::{
error::DlpError,
pda::{PENDING_COMMITMENT_SEED, STATE_BUFFER_SEED},
CloseStateBufferArgs, PendingCommitment, StateBuffer,
PENDING_COMMITMENT_STATUS_CANCELLED, PENDING_COMMITMENT_STATUS_EXPIRED,
PENDING_COMMITMENT_STATUS_FINALIZED,
};
use pinocchio::{
address::address_eq, error::ProgramError, AccountView, ProgramResult,
};
use wheels::{
layout::Decodable, require, require_eq, require_eq_keys,
require_n_accounts, require_owned_by, require_signer,
};

use crate::requires::is_uninitialized_account;

/// Close a StateBuffer and return its rent to the payer that created it.
///
/// Accounts:
/// 0: `[signer]` buffer authority
/// 1: `[writable]` StateBuffer PDA
/// 2: `[]` delegated account identified by the buffer
/// 3: `[writable]` stored StateBuffer rent payer
/// 4: `[]` PendingCommitment PDA, initialized or not
#[inline(never)]
pub fn process_close_state_buffer(
accounts: &[AccountView],
data: &[u8],
) -> ProgramResult {
let [authority, state_buffer, delegated_account, rent_payer, pending_commitment] =
require_n_accounts!(accounts, 5);

require_signer!(authority);
require!(rent_payer.is_writable(), ProgramError::Immutable);

let args = CloseStateBufferArgs::decode(data)?;
let commit_seq = args.commit_seq().to_le_bytes();
let state_buffer_bump = [args.state_buffer_bump()];
dpi::require_pda!(
state_buffer,
&[
STATE_BUFFER_SEED,
delegated_account.address().as_ref(),
&commit_seq,
authority.address().as_ref(),
&state_buffer_bump,
],
&crate::fast::ID,
true
);

// The close operation is idempotent so cleanup can be retried safely.
if is_uninitialized_account(state_buffer) {
return Ok(());
}

require_owned_by!(state_buffer, &crate::fast::ID);
let state_buffer_data = state_buffer.try_borrow()?;
let state = StateBuffer::decode(state_buffer_data.as_ref())?;
require_eq!(
&state.discriminator(),
&StateBuffer::DISCRIMINATOR,
ProgramError::InvalidAccountData
);
require_eq!(
state.bump(),
args.state_buffer_bump(),
ProgramError::InvalidAccountData
);
require_eq_keys!(
state.authority(),
authority.address(),
DlpError::InvalidAuthority
);
require_eq_keys!(
state.account_pubkey(),
delegated_account.address(),
ProgramError::InvalidAccountData
);
require_eq!(
state.commit_seq(),
args.commit_seq(),
ProgramError::InvalidInstructionData
);
require_eq_keys!(
state.rent_payer(),
rent_payer.address(),
DlpError::InvalidStateBufferRentPayer
);

let pending_commitment_bump = [args.pending_commitment_bump()];
dpi::require_pda!(
pending_commitment,
&[
PENDING_COMMITMENT_SEED,
delegated_account.address().as_ref(),
&commit_seq,
&pending_commitment_bump,
],
&crate::fast::ID,
false
);
if !is_uninitialized_account(pending_commitment) {
require_owned_by!(pending_commitment, &crate::fast::ID);
let pending_data = pending_commitment.try_borrow()?;
let pending = PendingCommitment::decode(pending_data.as_ref())?;
require_eq!(
&pending.discriminator(),
&PendingCommitment::DISCRIMINATOR,
ProgramError::InvalidAccountData
);
require_eq_keys!(
pending.account_pubkey(),
delegated_account.address(),
ProgramError::InvalidAccountData
);
require_eq!(
pending.commit_seq(),
args.commit_seq(),
ProgramError::InvalidAccountData
);
require!(
matches!(
pending.status(),
PENDING_COMMITMENT_STATUS_FINALIZED
| PENDING_COMMITMENT_STATUS_EXPIRED
| PENDING_COMMITMENT_STATUS_CANCELLED
),
DlpError::StateBufferInUse
);
}

drop(state_buffer_data);
rent_payer.set_lamports(
rent_payer
.lamports()
.checked_add(state_buffer.lamports())
.ok_or(DlpError::Overflow)?,
);
state_buffer.set_lamports(0);
unsafe {
state_buffer.assign(&pinocchio_system::ID);
}
state_buffer.resize(0)
}
Loading
Loading