refactor!(indexer): bundle protocol/FFI privacy message into actions

BREAKING!

Before: The message format for private transactions included separate
fields for private transaction state identifiers such as commitments
and nullifiers.

After: The commitments/nullifiers/ciphertexts are bound in an Action
struct, likewise for public state-bearing messages.

Mitigation: assume incoming messages are in a newly-specified format.
This commit is contained in:
Artem Gureev 2026-07-16 13:36:11 +00:00 committed by agureev
parent e409b3ac16
commit beccc9cbaf
8 changed files with 213 additions and 233 deletions

View File

@ -68,15 +68,18 @@ pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl Into
witness_set,
} = tx;
let PrivacyPreservingMessage {
public_account_ids,
public_actions,
nonces,
public_post_states: _,
encrypted_private_post_states,
new_commitments,
new_nullifiers,
private_actions,
block_validity_window,
timestamp_validity_window,
} = message;
let private_action_count = private_actions.len();
let public_account_ids: Vec<_> = public_actions
.into_iter()
.map(|action| action.account_id)
.collect();
let public_account_count = public_account_ids.len();
let WitnessSet {
signatures_and_public_keys: _,
proof,
@ -90,22 +93,12 @@ pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl Into
<div class="info-row">
<span class="info-label">"Public Accounts:"</span>
<span class="info-value">
{public_account_ids.len().to_string()}
{public_account_count.to_string()}
</span>
</div>
<div class="info-row">
<span class="info-label">"New Commitments:"</span>
<span class="info-value">{new_commitments.len().to_string()}</span>
</div>
<div class="info-row">
<span class="info-label">"Nullifiers:"</span>
<span class="info-value">{new_nullifiers.len().to_string()}</span>
</div>
<div class="info-row">
<span class="info-label">"Encrypted States:"</span>
<span class="info-value">
{encrypted_private_post_states.len().to_string()}
</span>
<span class="info-label">"Private Actions:"</span>
<span class="info-value">{private_action_count.to_string()}</span>
</div>
<div class="info-row">
<span class="info-label">"Proof Size:"</span>

View File

@ -40,8 +40,8 @@ pub fn TransactionPreview(transaction: Transaction) -> impl IntoView {
} = tx;
format!(
"{} public accounts, {} commitments",
message.public_account_ids.len(),
message.new_commitments.len()
message.public_actions.len(),
message.private_actions.len()
)
}
Transaction::ProgramDeployment(tx) => {

View File

@ -225,13 +225,18 @@ typedef struct FfiAccount {
struct FfiU128 nonce;
} FfiAccount;
typedef struct FfiVec_FfiAccount {
struct FfiAccount *entries;
typedef struct FfiPublicAction {
FfiAccountId account_id;
struct FfiAccount post_state;
} FfiPublicAction;
typedef struct FfiVec_FfiPublicAction {
struct FfiPublicAction *entries;
uintptr_t len;
uintptr_t capacity;
} FfiVec_FfiAccount;
} FfiVec_FfiPublicAction;
typedef struct FfiVec_FfiAccount FfiAccountList;
typedef struct FfiVec_FfiPublicAction FfiPublicActionList;
typedef struct FfiVec_u8 {
uint8_t *entries;
@ -247,42 +252,25 @@ typedef struct FfiEncryptedAccountData {
uint8_t view_tag;
} FfiEncryptedAccountData;
typedef struct FfiVec_FfiEncryptedAccountData {
struct FfiEncryptedAccountData *entries;
uintptr_t len;
uintptr_t capacity;
} FfiVec_FfiEncryptedAccountData;
typedef struct FfiVec_FfiEncryptedAccountData FfiEncryptedAccountDataList;
typedef struct FfiVec_FfiBytes32 {
struct FfiBytes32 *entries;
uintptr_t len;
uintptr_t capacity;
} FfiVec_FfiBytes32;
typedef struct FfiVec_FfiBytes32 FfiVecBytes32;
typedef struct FfiNullifierCommitmentSet {
typedef struct FfiPrivateAction {
struct FfiBytes32 nullifier;
struct FfiBytes32 commitment_set_digest;
} FfiNullifierCommitmentSet;
struct FfiBytes32 root;
struct FfiBytes32 commitment;
struct FfiEncryptedAccountData encrypted_post_state;
} FfiPrivateAction;
typedef struct FfiVec_FfiNullifierCommitmentSet {
struct FfiNullifierCommitmentSet *entries;
typedef struct FfiVec_FfiPrivateAction {
struct FfiPrivateAction *entries;
uintptr_t len;
uintptr_t capacity;
} FfiVec_FfiNullifierCommitmentSet;
} FfiVec_FfiPrivateAction;
typedef struct FfiVec_FfiNullifierCommitmentSet FfiNullifierCommitmentSetList;
typedef struct FfiVec_FfiPrivateAction FfiPrivateActionList;
typedef struct FfiPrivacyPreservingMessage {
FfiAccountIdList public_account_ids;
FfiPublicActionList public_actions;
FfiNonceList nonces;
FfiAccountList public_post_states;
FfiEncryptedAccountDataList encrypted_private_post_states;
FfiVecBytes32 new_commitments;
FfiNullifierCommitmentSetList new_nullifiers;
FfiPrivateActionList private_actions;
uint64_t block_validity_window[2];
uint64_t timestamp_validity_window[2];
} FfiPrivacyPreservingMessage;

View File

@ -1,17 +1,19 @@
use indexer_service_protocol::{
AccountId, Ciphertext, Commitment, CommitmentSetDigest, EncryptedAccountData,
EphemeralPublicKey, HashType, Nullifier, PrivacyPreservingMessage,
PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction,
ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, Signature, Transaction,
ValidityWindow, WitnessSet,
PrivacyPreservingTransaction, PrivateAction, ProgramDeploymentMessage,
ProgramDeploymentTransaction, ProgramId, Proof, PublicActionWithID, PublicKey, PublicMessage,
PublicTransaction, Signature, Transaction, ValidityWindow, WitnessSet,
};
use crate::api::types::{
FfiBytes32, FfiHashType, FfiOption, FfiProgramId, FfiPublicKey, FfiSignature, FfiVec,
FfiAccountId, FfiBytes32, FfiHashType, FfiOption, FfiProgramId, FfiPublicKey, FfiSignature,
FfiVec,
account::FfiAccount,
vectors::{
FfiAccountIdList, FfiAccountList, FfiEncryptedAccountDataList, FfiInstructionDataList,
FfiNonceList, FfiNullifierCommitmentSetList, FfiProgramDeploymentMessage, FfiProof,
FfiSignaturePubKeyList, FfiVecBytes32, FfiVecU8,
FfiAccountIdList, FfiInstructionDataList, FfiNonceList, FfiPrivateActionList,
FfiProgramDeploymentMessage, FfiProof, FfiPublicActionList, FfiSignaturePubKeyList,
FfiVecU8,
},
};
@ -156,12 +158,15 @@ impl From<Box<FfiPrivateTransactionBody>> for PrivacyPreservingTransaction {
Self {
hash: HashType(value.hash.data),
message: PrivacyPreservingMessage {
public_account_ids: {
let std_vec: Vec<_> = value.message.public_account_ids.into();
public_actions: {
let std_vec: Vec<_> = value.message.public_actions.into();
std_vec
.into_iter()
.map(|ffi_val| AccountId {
value: ffi_val.data,
.map(|ffi_val| PublicActionWithID {
account_id: AccountId {
value: ffi_val.account_id.data,
},
post_state: ffi_val.post_state.into(),
})
.collect()
},
@ -169,37 +174,21 @@ impl From<Box<FfiPrivateTransactionBody>> for PrivacyPreservingTransaction {
let std_vec: Vec<_> = value.message.nonces.into();
std_vec.into_iter().map(Into::into).collect()
},
public_post_states: {
let std_vec: Vec<_> = value.message.public_post_states.into();
std_vec.into_iter().map(Into::into).collect()
},
encrypted_private_post_states: {
let std_vec: Vec<_> = value.message.encrypted_private_post_states.into();
private_actions: {
let std_vec: Vec<_> = value.message.private_actions.into();
std_vec
.into_iter()
.map(|ffi_val| EncryptedAccountData {
ciphertext: Ciphertext(ffi_val.ciphertext.into()),
epk: EphemeralPublicKey(ffi_val.epk.into()),
view_tag: ffi_val.view_tag,
})
.collect()
},
new_commitments: {
let std_vec: Vec<_> = value.message.new_commitments.into();
std_vec
.into_iter()
.map(|ffi_val| Commitment(ffi_val.data))
.collect()
},
new_nullifiers: {
let std_vec: Vec<_> = value.message.new_nullifiers.into();
std_vec
.into_iter()
.map(|ffi_val| {
(
Nullifier(ffi_val.nullifier.data),
CommitmentSetDigest(ffi_val.commitment_set_digest.data),
)
.map(|ffi_val| PrivateAction {
nullifier: Nullifier(ffi_val.nullifier.data),
root: CommitmentSetDigest(ffi_val.root.data),
commitment: Commitment(ffi_val.commitment.data),
encrypted_post_state: EncryptedAccountData {
ciphertext: Ciphertext(
ffi_val.encrypted_post_state.ciphertext.into(),
),
epk: EphemeralPublicKey(ffi_val.encrypted_post_state.epk.into()),
view_tag: ffi_val.encrypted_post_state.view_tag,
},
})
.collect()
},
@ -229,14 +218,53 @@ impl From<Box<FfiPrivateTransactionBody>> for PrivacyPreservingTransaction {
}
}
#[repr(C)]
pub struct FfiPublicAction {
pub account_id: FfiAccountId,
pub post_state: FfiAccount,
}
impl From<PublicActionWithID> for FfiPublicAction {
fn from(value: PublicActionWithID) -> Self {
let post_state: lee::Account = value
.post_state
.try_into()
.expect("Source is in blocks, must fit");
Self {
account_id: value.account_id.into(),
post_state: post_state.into(),
}
}
}
#[repr(C)]
pub struct FfiPrivateAction {
pub nullifier: FfiBytes32,
pub root: FfiBytes32,
pub commitment: FfiBytes32,
pub encrypted_post_state: FfiEncryptedAccountData,
}
impl From<PrivateAction> for FfiPrivateAction {
fn from(value: PrivateAction) -> Self {
Self {
nullifier: FfiBytes32 {
data: value.nullifier.0,
},
root: FfiBytes32 { data: value.root.0 },
commitment: FfiBytes32 {
data: value.commitment.0,
},
encrypted_post_state: value.encrypted_post_state.into(),
}
}
}
#[repr(C)]
pub struct FfiPrivacyPreservingMessage {
pub public_account_ids: FfiAccountIdList,
pub public_actions: FfiPublicActionList,
pub nonces: FfiNonceList,
pub public_post_states: FfiAccountList,
pub encrypted_private_post_states: FfiEncryptedAccountDataList,
pub new_commitments: FfiVecBytes32,
pub new_nullifiers: FfiNullifierCommitmentSetList,
pub private_actions: FfiPrivateActionList,
pub block_validity_window: [u64; 2],
pub timestamp_validity_window: [u64; 2],
}
@ -244,18 +272,15 @@ pub struct FfiPrivacyPreservingMessage {
impl From<PrivacyPreservingMessage> for FfiPrivacyPreservingMessage {
fn from(value: PrivacyPreservingMessage) -> Self {
let PrivacyPreservingMessage {
public_account_ids,
public_actions,
nonces,
public_post_states,
encrypted_private_post_states,
new_commitments,
new_nullifiers,
private_actions,
block_validity_window,
timestamp_validity_window,
} = value;
Self {
public_account_ids: public_account_ids
public_actions: public_actions
.into_iter()
.map(Into::into)
.collect::<Vec<_>>()
@ -265,25 +290,7 @@ impl From<PrivacyPreservingMessage> for FfiPrivacyPreservingMessage {
.map(Into::into)
.collect::<Vec<_>>()
.into(),
public_post_states: public_post_states
.into_iter()
.map(|acc_ind| -> lee::Account {
acc_ind.try_into().expect("Source is in blocks, must fit")
})
.map(Into::into)
.collect::<Vec<_>>()
.into(),
encrypted_private_post_states: encrypted_private_post_states
.into_iter()
.map(Into::into)
.collect::<Vec<_>>()
.into(),
new_commitments: new_commitments
.into_iter()
.map(|comm| FfiBytes32 { data: comm.0 })
.collect::<Vec<_>>()
.into(),
new_nullifiers: new_nullifiers
private_actions: private_actions
.into_iter()
.map(Into::into)
.collect::<Vec<_>>()
@ -294,21 +301,6 @@ impl From<PrivacyPreservingMessage> for FfiPrivacyPreservingMessage {
}
}
#[repr(C)]
pub struct FfiNullifierCommitmentSet {
pub nullifier: FfiBytes32,
pub commitment_set_digest: FfiBytes32,
}
impl From<(Nullifier, CommitmentSetDigest)> for FfiNullifierCommitmentSet {
fn from(value: (Nullifier, CommitmentSetDigest)) -> Self {
Self {
nullifier: FfiBytes32 { data: value.0.0 },
commitment_set_digest: FfiBytes32 { data: value.1.0 },
}
}
}
#[repr(C)]
pub struct FfiEncryptedAccountData {
pub ciphertext: FfiVecU8,

View File

@ -1,19 +1,12 @@
use crate::api::types::{
FfiAccountId, FfiBytes32, FfiNonce, FfiVec,
account::FfiAccount,
transaction::{
FfiEncryptedAccountData, FfiNullifierCommitmentSet, FfiSignaturePubKeyEntry, FfiTransaction,
},
FfiAccountId, FfiNonce, FfiVec,
transaction::{FfiPrivateAction, FfiPublicAction, FfiSignaturePubKeyEntry, FfiTransaction},
};
pub type FfiVecU8 = FfiVec<u8>;
pub type FfiAccountList = FfiVec<FfiAccount>;
pub type FfiAccountIdList = FfiVec<FfiAccountId>;
pub type FfiVecBytes32 = FfiVec<FfiBytes32>;
pub type FfiBlockBody = FfiVec<FfiTransaction>;
pub type FfiNonceList = FfiVec<FfiNonce>;
@ -26,6 +19,6 @@ pub type FfiProof = FfiVecU8;
pub type FfiProgramDeploymentMessage = FfiVecU8;
pub type FfiEncryptedAccountDataList = FfiVec<FfiEncryptedAccountData>;
pub type FfiPublicActionList = FfiVec<FfiPublicAction>;
pub type FfiNullifierCommitmentSetList = FfiVec<FfiNullifierCommitmentSet>;
pub type FfiPrivateActionList = FfiVec<FfiPrivateAction>;

View File

@ -6,9 +6,9 @@ use crate::{
Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockIngestError, Ciphertext,
Commitment, CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType,
IndexerStatus, IndexerSyncState, Nullifier, PrivacyPreservingMessage,
PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction,
ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, Signature, StallReason,
Transaction, ValidityWindow, WitnessSet,
PrivacyPreservingTransaction, PrivateAction, ProgramDeploymentMessage,
ProgramDeploymentTransaction, ProgramId, Proof, PublicActionWithID, PublicKey, PublicMessage,
PublicTransaction, Signature, StallReason, Transaction, ValidityWindow, WitnessSet,
};
// ============================================================================
@ -279,6 +279,26 @@ impl From<PublicMessage> for lee::public_transaction::Message {
}
}
impl From<lee::privacy_preserving_transaction::message::PublicActionWithID> for PublicActionWithID {
fn from(value: lee::privacy_preserving_transaction::message::PublicActionWithID) -> Self {
Self {
account_id: value.account_id.into(),
post_state: value.post_state.into(),
}
}
}
impl From<lee_core::PrivateAction> for PrivateAction {
fn from(value: lee_core::PrivateAction) -> Self {
Self {
nullifier: value.nullifier.into(),
root: value.root.into(),
commitment: value.commitment.into(),
encrypted_post_state: value.encrypted_post_state.into(),
}
}
}
impl From<lee::privacy_preserving_transaction::message::Message> for PrivacyPreservingMessage {
fn from(value: lee::privacy_preserving_transaction::message::Message) -> Self {
let lee::privacy_preserving_transaction::message::Message {
@ -289,80 +309,59 @@ impl From<lee::privacy_preserving_transaction::message::Message> for PrivacyPres
timestamp_validity_window,
} = value;
Self {
public_account_ids: public_actions.iter().map(|a| a.account_id.into()).collect(),
public_actions: public_actions.into_iter().map(Into::into).collect(),
nonces: nonces.iter().map(|x| x.0).collect(),
public_post_states: public_actions
.into_iter()
.map(|a| a.post_state.into())
.collect(),
encrypted_private_post_states: private_actions
.iter()
.map(|a| a.encrypted_post_state.clone().into())
.collect(),
new_commitments: private_actions
.iter()
.map(|a| a.commitment.clone().into())
.collect(),
new_nullifiers: private_actions
.into_iter()
.map(|a| (a.nullifier.into(), a.root.into()))
.collect(),
private_actions: private_actions.into_iter().map(Into::into).collect(),
block_validity_window: block_validity_window.into(),
timestamp_validity_window: timestamp_validity_window.into(),
}
}
}
impl TryFrom<PublicActionWithID>
for lee::privacy_preserving_transaction::message::PublicActionWithID
{
type Error = lee::error::LeeError;
fn try_from(value: PublicActionWithID) -> Result<Self, Self::Error> {
Ok(Self {
account_id: value.account_id.into(),
post_state: value
.post_state
.try_into()
.map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?,
})
}
}
impl From<PrivateAction> for lee_core::PrivateAction {
fn from(value: PrivateAction) -> Self {
Self {
nullifier: value.nullifier.into(),
root: value.root.into(),
commitment: value.commitment.into(),
encrypted_post_state: value.encrypted_post_state.into(),
}
}
}
impl TryFrom<PrivacyPreservingMessage> for lee::privacy_preserving_transaction::message::Message {
type Error = lee::error::LeeError;
fn try_from(value: PrivacyPreservingMessage) -> Result<Self, Self::Error> {
let PrivacyPreservingMessage {
public_account_ids,
public_actions,
nonces,
public_post_states,
encrypted_private_post_states,
new_commitments,
new_nullifiers,
private_actions,
block_validity_window,
timestamp_validity_window,
} = value;
let public_post_states = public_post_states
let public_actions = public_actions
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<lee_core::account::Account>, _>>()
.map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?;
let public_actions = public_account_ids
.into_iter()
.map(Into::into)
.zip(public_post_states)
.map(|(account_id, post_state)| {
lee::privacy_preserving_transaction::message::PublicActionWithID {
account_id,
post_state,
}
})
.collect();
let private_actions = new_commitments
.into_iter()
.map(Into::into)
.zip(
new_nullifiers
.into_iter()
.map(|(n, d)| (n.into(), d.into())),
)
.zip(encrypted_private_post_states.into_iter().map(Into::into))
.map(|((commitment, (nullifier, root)), encrypted_post_state)| {
lee_core::PrivateAction {
nullifier,
root,
commitment,
encrypted_post_state,
}
})
.collect();
.collect::<Result<Vec<_>, _>>()?;
let private_actions = private_actions.into_iter().map(Into::into).collect();
Ok(Self {
public_actions,

View File

@ -226,14 +226,25 @@ pub struct PublicMessage {
pub type InstructionData = Vec<u32>;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct PublicActionWithID {
pub account_id: AccountId,
pub post_state: Account,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct PrivateAction {
pub nullifier: Nullifier,
pub root: CommitmentSetDigest,
pub commitment: Commitment,
pub encrypted_post_state: EncryptedAccountData,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct PrivacyPreservingMessage {
pub public_account_ids: Vec<AccountId>,
pub public_actions: Vec<PublicActionWithID>,
pub nonces: Vec<Nonce>,
pub public_post_states: Vec<Account>,
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub private_actions: Vec<PrivateAction>,
pub block_validity_window: ValidityWindow,
pub timestamp_validity_window: ValidityWindow,
}

View File

@ -11,9 +11,9 @@ use std::{collections::HashMap, sync::Arc, time::Duration};
use indexer_service_protocol::{
Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment,
CommitmentSetDigest, Data, EncryptedAccountData, HashType, IndexerStatus, IndexerSyncState,
PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
ProgramDeploymentTransaction, ProgramId, PublicMessage, PublicTransaction, Signature,
Transaction, ValidityWindow, WitnessSet,
PrivacyPreservingMessage, PrivacyPreservingTransaction, PrivateAction,
ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, PublicActionWithID,
PublicMessage, PublicTransaction, Signature, Transaction, ValidityWindow, WitnessSet,
};
use jsonrpsee::{
core::{SubscriptionResult, async_trait},
@ -300,9 +300,11 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
.values()
.filter(|(tx, _)| match tx {
Transaction::Public(pub_tx) => pub_tx.message.account_ids.contains(&account_id),
Transaction::PrivacyPreserving(priv_tx) => {
priv_tx.message.public_account_ids.contains(&account_id)
}
Transaction::PrivacyPreserving(priv_tx) => priv_tx
.message
.public_actions
.iter()
.any(|action| action.account_id == account_id),
Transaction::ProgramDeployment(_) => false,
})
.cloned()
@ -381,24 +383,26 @@ fn mock_privacy_preserving_tx(
Transaction::PrivacyPreserving(PrivacyPreservingTransaction {
hash: tx_hash,
message: PrivacyPreservingMessage {
public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]],
public_actions: vec![PublicActionWithID {
account_id: account_ids[tx_idx as usize % account_ids.len()],
post_state: Account {
program_owner: ProgramId([1_u32; 8]),
balance: 500,
data: Data(vec![0xdd, 0xee]),
nonce: block_id as u128,
},
}],
nonces: vec![block_id as u128],
public_post_states: vec![Account {
program_owner: ProgramId([1_u32; 8]),
balance: 500,
data: Data(vec![0xdd, 0xee]),
nonce: block_id as u128,
private_actions: vec![PrivateAction {
nullifier: indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
root: CommitmentSetDigest([0xff; 32]),
commitment: Commitment([block_id as u8; 32]),
encrypted_post_state: EncryptedAccountData {
ciphertext: indexer_service_protocol::Ciphertext(vec![0x01, 0x02, 0x03, 0x04]),
epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]),
view_tag: 42,
},
}],
encrypted_private_post_states: vec![EncryptedAccountData {
ciphertext: indexer_service_protocol::Ciphertext(vec![0x01, 0x02, 0x03, 0x04]),
epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]),
view_tag: 42,
}],
new_commitments: vec![Commitment([block_id as u8; 32])],
new_nullifiers: vec![(
indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
CommitmentSetDigest([0xff; 32]),
)],
block_validity_window: ValidityWindow((None, None)),
timestamp_validity_window: ValidityWindow((None, None)),
},