mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-25 03:11:21 +00:00
feat(lee): anchor privacy-circuit image ids and migrate all programs to Deploy
Privacy-preserving circuit: - Add ProgramImageClaim, letting a Deploy-created program's real image_id be anchored to its account for privacy-circuit env::verify, with the sequencer independently re-deriving the real image_id via get_program to authenticate the claim (journal reconstruction, same pattern as public_actions pre-states). - Fix compute_public_authorized_pdas and the private circuit's resolve_authorization_and_record_bindings to use the caller's real recovered image_id instead of bijection-guessing it from the caller's account_id, which was wrong for Deploy-created callers. Program storage: - V03State::insert_program (used by both genesis's with_programs and live ProgramDeploymentTransaction execution) now always writes the Deploy two-account shape (ProgramData header + segment), not the legacy PROGRAM_STORAGE_OWNER raw-elf shape. The header stays at the existing bijection address so no dispatch-address reference needed to change; only the segment (never a caller-facing address) moves to its PDA. - Drop the now-redundant ProgramAlreadyExists pre-check in ProgramDeploymentTransaction validation; PDA claiming already prevents redeploying an account. - Migrate remaining tests off ProgramDeploymentTransaction onto native Deploy (sequencer_core, integration_tests' auth_transfer and block_size_limit), adding a shared deploy_targets/deploy_transaction/encoded_tx_size helper. Rebuild artifacts and the prebuilt test fixture for the circuit and program storage changes.
This commit is contained in:
Generated
+3
@@ -4770,6 +4770,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"authenticated_transfer_core",
|
||||
"borsh",
|
||||
"bridge_core",
|
||||
"bridge_lock_core",
|
||||
"bytesize",
|
||||
@@ -4793,7 +4794,9 @@ dependencies = [
|
||||
"logos-blockchain-zone-sdk",
|
||||
"num-bigint 0.4.6",
|
||||
"ping_core",
|
||||
"program_loader_core",
|
||||
"programs",
|
||||
"risc0-binfmt",
|
||||
"risc0-zkvm",
|
||||
"sequencer_core",
|
||||
"sequencer_service",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lee::{
|
||||
AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies,
|
||||
program::Program,
|
||||
AccountId, privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
|
||||
};
|
||||
use wallet::{AccountIdentity, WalletCore};
|
||||
|
||||
@@ -47,8 +46,8 @@ async fn main() {
|
||||
let simple_tail_call = Program::new(simple_tail_call_bytecode.into()).unwrap();
|
||||
let hello_world_bytecode: Vec<u8> = std::fs::read(hello_world_path).unwrap();
|
||||
let hello_world = Program::new(hello_world_bytecode.into()).unwrap();
|
||||
let dependencies: HashMap<ProgramId, Program> =
|
||||
std::iter::once((hello_world.id(), hello_world)).collect();
|
||||
let dependencies: HashMap<AccountId, Program> =
|
||||
std::iter::once((hello_world.id().into(), hello_world)).collect();
|
||||
let program_with_dependencies = ProgramWithDependencies::new(simple_tail_call, dependencies);
|
||||
|
||||
let accounts = vec![AccountIdentity::PrivateOwned(account_id)];
|
||||
|
||||
@@ -40,6 +40,9 @@ sequencer_stake_core.workspace = true
|
||||
programs.workspace = true
|
||||
test_programs.workspace = true
|
||||
testnet_initial_state.workspace = true
|
||||
program_loader_core.workspace = true
|
||||
risc0-binfmt.workspace = true
|
||||
borsh.workspace = true
|
||||
|
||||
logos-blockchain-core.workspace = true
|
||||
logos-blockchain-key-management-system-service.workspace = true
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
use lee_core::account::AccountId;
|
||||
use lee_core::{account::AccountId, program::RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID};
|
||||
use log::info;
|
||||
use sequencer_core::{
|
||||
block_publisher::{Ed25519PublicKey, read_channel_state},
|
||||
@@ -321,3 +321,58 @@ pub async fn wait_for_indexer_to_catch_up(ctx: &TestContext) -> anyhow::Result<u
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
/// Derives the `(header, segment)` account pair `bytecode` would deploy to via `Deploy`, mirroring
|
||||
/// `sequencer_core`'s private test helper of the same name.
|
||||
#[must_use]
|
||||
pub fn deploy_targets(bytecode: &[u8]) -> (AccountId, AccountId) {
|
||||
let loader_id: lee_core::program::ProgramId = RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID.into();
|
||||
let image_id: lee_core::program::ProgramId =
|
||||
risc0_binfmt::compute_image_id(bytecode).unwrap().into();
|
||||
let header =
|
||||
program_loader_core::deploy_header_account_id(loader_id, image_id, 0, AccountId::default());
|
||||
let segment =
|
||||
program_loader_core::deploy_segment_account_id(loader_id, image_id, 0, AccountId::default());
|
||||
(header, segment)
|
||||
}
|
||||
|
||||
/// Builds the `PublicTransaction` that deploys `bytecode` to `(header, segment)`.
|
||||
///
|
||||
/// `(header, segment)` are the targets [`deploy_targets`] derives for it. Tests should invoke
|
||||
/// programs at the returned `header` address afterward, not the program's own bijection
|
||||
/// `AccountId::from(image_id)`.
|
||||
#[must_use]
|
||||
pub fn deploy_transaction(
|
||||
header: AccountId,
|
||||
segment: AccountId,
|
||||
bytecode: Vec<u8>,
|
||||
) -> lee::PublicTransaction {
|
||||
let loader_id: lee_core::program::ProgramId = RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID.into();
|
||||
let message = lee::public_transaction::Message::try_new(
|
||||
loader_id.into(),
|
||||
vec![header, segment],
|
||||
vec![],
|
||||
program_loader_core::Instruction::Deploy { bytecode },
|
||||
)
|
||||
.expect("deploy instruction data should always be serializable");
|
||||
let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
lee::PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
/// The exact wire size the sequencer measures a transaction by (see
|
||||
/// `sequencer_rpc_server_actor::actor::service`'s `send_transaction`).
|
||||
///
|
||||
/// A `Deploy`'s bytecode is transported through `instruction_data` (`Vec<u32>`), and RISC0's
|
||||
/// word-oriented serde doesn't pack `Vec<u8>` efficiently: each byte becomes its own 4-byte word,
|
||||
/// so a `Deploy` transaction's wire size runs ~4x its raw bytecode length. Measuring the real
|
||||
/// encoded size here (rather than guessing from bytecode length) keeps size-sensitive tests
|
||||
/// correct regardless of that encoding overhead.
|
||||
#[must_use]
|
||||
pub fn encoded_tx_size(tx: &common::transaction::LeeTransaction) -> u64 {
|
||||
u64::try_from(
|
||||
borsh::to_vec(tx)
|
||||
.expect("transaction should serialize")
|
||||
.len(),
|
||||
)
|
||||
.expect("transaction size should fit in u64")
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use bytesize::ByteSize;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, private_mention,
|
||||
public_mention,
|
||||
utils::{
|
||||
account_balance, assert_private_commitment_in_state, get_account, new_account, send,
|
||||
sync_private,
|
||||
account_balance, assert_private_commitment_in_state, deploy_targets, deploy_transaction,
|
||||
encoded_tx_size, get_account, new_account, send, sync_private,
|
||||
},
|
||||
verify_commitment_is_in_state,
|
||||
};
|
||||
@@ -22,6 +23,10 @@ use lee_core::{
|
||||
encryption::ViewingPublicKey,
|
||||
};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use test_fixtures::{
|
||||
MultiZoneTestContextBuilder, ZoneTestContextBuilder,
|
||||
config::{MultiNodeTestContextConfig, SequencerPartialConfig},
|
||||
};
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
account::Label,
|
||||
@@ -579,12 +584,29 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
|
||||
|
||||
#[test]
|
||||
async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
|
||||
let faucet_chain_caller = test_programs::faucet_chain_caller();
|
||||
let deploy_tx = LeeTransaction::ProgramDeployment(lee::ProgramDeploymentTransaction::new(
|
||||
lee::program_deployment_transaction::Message::new(faucet_chain_caller.elf().to_owned()),
|
||||
let bytecode = faucet_chain_caller.elf().to_vec();
|
||||
let (faucet_chain_caller_header, faucet_chain_caller_segment) = deploy_targets(&bytecode);
|
||||
let deploy_tx = LeeTransaction::Public(deploy_transaction(
|
||||
faucet_chain_caller_header,
|
||||
faucet_chain_caller_segment,
|
||||
bytecode,
|
||||
));
|
||||
|
||||
// `Deploy`'s bytecode payload runs ~4x its raw size on the wire (see `encoded_tx_size`'s
|
||||
// docs), so the default 1 MiB block size isn't enough headroom for a real guest binary.
|
||||
let tx_size = encoded_tx_size(&deploy_tx);
|
||||
let ctx = MultiZoneTestContextBuilder::default()
|
||||
.with_zone(
|
||||
ZoneTestContextBuilder::new(MultiNodeTestContextConfig::default())
|
||||
.with_sequencer_partial_config(SequencerPartialConfig {
|
||||
max_block_size: ByteSize::b(tx_size + 10 * 1024),
|
||||
..SequencerPartialConfig::default()
|
||||
}),
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
ctx.sequencer_client().send_transaction(deploy_tx).await?;
|
||||
|
||||
log::info!("Waiting for deploy block creation");
|
||||
@@ -619,12 +641,16 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
faucet_chain_caller,
|
||||
[
|
||||
(faucet_program_id, programs::faucet()),
|
||||
(vault_program_id, programs::vault()),
|
||||
(auth_transfer_program_id, programs::authenticated_transfer()),
|
||||
(faucet_program_id.into(), programs::faucet()),
|
||||
(vault_program_id.into(), programs::vault()),
|
||||
(
|
||||
auth_transfer_program_id.into(),
|
||||
programs::authenticated_transfer(),
|
||||
),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
)
|
||||
.with_program_account_id(faucet_chain_caller_header);
|
||||
|
||||
let instruction =
|
||||
Program::serialize_instruction((faucet_program_id, vault_program_id, attacker_id, amount))?;
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use bytesize::ByteSize;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention,
|
||||
utils::{account_balance, get_account, new_account, send, send_claiming_new_account},
|
||||
utils::{
|
||||
account_balance, deploy_targets, deploy_transaction, encoded_tx_size, get_account,
|
||||
new_account, send, send_claiming_new_account,
|
||||
},
|
||||
};
|
||||
use lee::{PublicKey, public_transaction};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use test_fixtures::{
|
||||
MultiZoneTestContextBuilder, ZoneTestContextBuilder,
|
||||
config::{MultiNodeTestContextConfig, SequencerPartialConfig},
|
||||
};
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
account::Label,
|
||||
@@ -387,12 +395,29 @@ async fn cannot_execute_faucet_program() -> Result<()> {
|
||||
|
||||
#[test]
|
||||
async fn user_tx_that_chain_calls_faucet_is_dropped() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
|
||||
let faucet_chain_caller = test_programs::faucet_chain_caller();
|
||||
let deploy_tx = LeeTransaction::ProgramDeployment(lee::ProgramDeploymentTransaction::new(
|
||||
lee::program_deployment_transaction::Message::new(faucet_chain_caller.elf().to_owned()),
|
||||
let bytecode = faucet_chain_caller.elf().to_vec();
|
||||
let (faucet_chain_caller_header, faucet_chain_caller_segment) = deploy_targets(&bytecode);
|
||||
let deploy_tx = LeeTransaction::Public(deploy_transaction(
|
||||
faucet_chain_caller_header,
|
||||
faucet_chain_caller_segment,
|
||||
bytecode,
|
||||
));
|
||||
|
||||
// `Deploy`'s bytecode payload runs ~4x its raw size on the wire (see `encoded_tx_size`'s
|
||||
// docs), so the default 1 MiB block size isn't enough headroom for a real guest binary.
|
||||
let tx_size = encoded_tx_size(&deploy_tx);
|
||||
let ctx = MultiZoneTestContextBuilder::default()
|
||||
.with_zone(
|
||||
ZoneTestContextBuilder::new(MultiNodeTestContextConfig::default())
|
||||
.with_sequencer_partial_config(SequencerPartialConfig {
|
||||
max_block_size: ByteSize::b(tx_size + 10 * 1024),
|
||||
..SequencerPartialConfig::default()
|
||||
}),
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
ctx.sequencer_client().send_transaction(deploy_tx).await?;
|
||||
|
||||
log::info!("Waiting for deploy block creation");
|
||||
@@ -406,7 +431,7 @@ async fn user_tx_that_chain_calls_faucet_is_dropped() -> Result<()> {
|
||||
let amount: u128 = 1;
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
faucet_chain_caller.id().into(),
|
||||
faucet_chain_caller_header,
|
||||
vec![faucet_account_id, attacker_vault_id],
|
||||
vec![],
|
||||
(faucet_program_id, vault_program_id, attacker, amount),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#![expect(
|
||||
clippy::as_conversions,
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
@@ -9,8 +8,12 @@ use std::time::Duration;
|
||||
use anyhow::Result;
|
||||
use bytesize::ByteSize;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, config::SequencerPartialConfig};
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, config::SequencerPartialConfig, deploy_targets,
|
||||
deploy_transaction, encoded_tx_size,
|
||||
};
|
||||
use lee::program::Program;
|
||||
use lee_core::program::RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use test_fixtures::{
|
||||
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
|
||||
@@ -19,12 +22,20 @@ use tokio::test;
|
||||
|
||||
#[test]
|
||||
async fn reject_oversized_transaction() -> Result<()> {
|
||||
let bytecode = test_programs::claimer().elf().to_vec();
|
||||
let (header, segment) = deploy_targets(&bytecode);
|
||||
let tx = LeeTransaction::Public(deploy_transaction(header, segment, bytecode));
|
||||
let tx_size = encoded_tx_size(&tx);
|
||||
|
||||
let ctx = MultiZoneTestContextBuilder::default()
|
||||
.with_zone(
|
||||
ZoneTestContextBuilder::new(MultiNodeTestContextConfig::default())
|
||||
.with_sequencer_partial_config(SequencerPartialConfig {
|
||||
max_num_tx_in_block: 100,
|
||||
max_block_size: ByteSize::mib(1),
|
||||
// Below the transaction's actual size, so it's rejected outright (the
|
||||
// sequencer additionally reserves ~200 bytes of block-header overhead off of
|
||||
// this limit, so being equal to `tx_size` is already enough of a margin).
|
||||
max_block_size: ByteSize::b(tx_size),
|
||||
mempool_max_size: 1000,
|
||||
block_create_timeout: Duration::from_secs(10),
|
||||
priority_fee: sequencer_core::config::default_priority_fee(),
|
||||
@@ -33,19 +44,8 @@ async fn reject_oversized_transaction() -> Result<()> {
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
// Create a transaction that's definitely too large
|
||||
// Block size is 1 MiB (1,048,576 bytes), minus ~200 bytes for header = ~1,048,376 bytes max tx
|
||||
// Create a 1.1 MiB binary to ensure it exceeds the limit
|
||||
let oversized_binary = vec![0_u8; 1100 * 1024]; // 1.1 MiB binary
|
||||
|
||||
let message = lee::program_deployment_transaction::Message::new(oversized_binary);
|
||||
let tx = lee::ProgramDeploymentTransaction::new(message);
|
||||
|
||||
// Try to submit the transaction and expect an error
|
||||
let result = ctx
|
||||
.sequencer_client()
|
||||
.send_transaction(LeeTransaction::ProgramDeployment(tx))
|
||||
.await;
|
||||
let result = ctx.sequencer_client().send_transaction(tx).await;
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
@@ -66,12 +66,18 @@ async fn reject_oversized_transaction() -> Result<()> {
|
||||
|
||||
#[test]
|
||||
async fn accept_transaction_within_limit() -> Result<()> {
|
||||
let bytecode = test_programs::claimer().elf().to_vec();
|
||||
let (header, segment) = deploy_targets(&bytecode);
|
||||
let tx = LeeTransaction::Public(deploy_transaction(header, segment, bytecode));
|
||||
let tx_size = encoded_tx_size(&tx);
|
||||
|
||||
let ctx = MultiZoneTestContextBuilder::default()
|
||||
.with_zone(
|
||||
ZoneTestContextBuilder::new(MultiNodeTestContextConfig::default())
|
||||
.with_sequencer_partial_config(SequencerPartialConfig {
|
||||
max_num_tx_in_block: 100,
|
||||
max_block_size: ByteSize::mib(1),
|
||||
// Comfortably above the transaction's actual size.
|
||||
max_block_size: ByteSize::b(tx_size + 10 * 1024),
|
||||
mempool_max_size: 1000,
|
||||
block_create_timeout: Duration::from_secs(10),
|
||||
priority_fee: sequencer_core::config::default_priority_fee(),
|
||||
@@ -80,17 +86,8 @@ async fn accept_transaction_within_limit() -> Result<()> {
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
// Create a small program deployment that should fit
|
||||
let small_binary = vec![0_u8; 1024]; // 1 KiB binary
|
||||
|
||||
let message = lee::program_deployment_transaction::Message::new(small_binary);
|
||||
let tx = lee::ProgramDeploymentTransaction::new(message);
|
||||
|
||||
// This should succeed
|
||||
let result = ctx
|
||||
.sequencer_client()
|
||||
.send_transaction(LeeTransaction::ProgramDeployment(tx))
|
||||
.await;
|
||||
let result = ctx.sequencer_client().send_transaction(tx).await;
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
@@ -106,10 +103,24 @@ async fn transaction_deferred_to_next_block_when_current_full() -> Result<()> {
|
||||
let claimer = test_programs::claimer();
|
||||
let chain_caller = test_programs::chain_caller();
|
||||
|
||||
// Calculate block size to fit only one of the two transactions, leaving some room for headers
|
||||
let (claimer_header, claimer_segment) = deploy_targets(claimer.elf());
|
||||
let claimer_tx = LeeTransaction::Public(deploy_transaction(
|
||||
claimer_header,
|
||||
claimer_segment,
|
||||
claimer.elf().to_vec(),
|
||||
));
|
||||
|
||||
let (chain_caller_header, chain_caller_segment) = deploy_targets(chain_caller.elf());
|
||||
let chain_caller_tx = LeeTransaction::Public(deploy_transaction(
|
||||
chain_caller_header,
|
||||
chain_caller_segment,
|
||||
chain_caller.elf().to_vec(),
|
||||
));
|
||||
|
||||
// Block size to fit only one of the two transactions, leaving some room for headers
|
||||
// (e.g., 10 KiB)
|
||||
let max_program_size = claimer.elf().len().max(chain_caller.elf().len());
|
||||
let block_size = ByteSize::b((max_program_size + 10 * 1024) as u64);
|
||||
let max_tx_size = encoded_tx_size(&claimer_tx).max(encoded_tx_size(&chain_caller_tx));
|
||||
let block_size = ByteSize::b(max_tx_size + 10 * 1024);
|
||||
|
||||
let ctx = MultiZoneTestContextBuilder::default()
|
||||
.with_zone(
|
||||
@@ -128,20 +139,9 @@ async fn transaction_deferred_to_next_block_when_current_full() -> Result<()> {
|
||||
let initial_block_height = ctx.sequencer_client().get_last_block_id().await?;
|
||||
|
||||
// Submit both program deployments
|
||||
ctx.sequencer_client().send_transaction(claimer_tx).await?;
|
||||
ctx.sequencer_client()
|
||||
.send_transaction(LeeTransaction::ProgramDeployment(
|
||||
lee::ProgramDeploymentTransaction::new(
|
||||
lee::program_deployment_transaction::Message::new(claimer.elf().to_owned()),
|
||||
),
|
||||
))
|
||||
.await?;
|
||||
|
||||
ctx.sequencer_client()
|
||||
.send_transaction(LeeTransaction::ProgramDeployment(
|
||||
lee::ProgramDeploymentTransaction::new(
|
||||
lee::program_deployment_transaction::Message::new(chain_caller.elf().to_owned()),
|
||||
),
|
||||
))
|
||||
.send_transaction(chain_caller_tx)
|
||||
.await?;
|
||||
|
||||
// Wait for first block
|
||||
@@ -153,19 +153,26 @@ async fn transaction_deferred_to_next_block_when_current_full() -> Result<()> {
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
// Check which program is in block 1
|
||||
// Check which program is deployed in a block, by picking out its `Deploy` transactions and
|
||||
// decoding the real `image_id` of each one's bytecode.
|
||||
let get_program_ids = |block: &common::block::Block| -> Vec<lee::ProgramId> {
|
||||
block
|
||||
.body
|
||||
.transactions
|
||||
.iter()
|
||||
.filter_map(|tx| {
|
||||
if let LeeTransaction::ProgramDeployment(deployment) = tx {
|
||||
let bytecode = deployment.message.clone().into_bytecode();
|
||||
Program::new(bytecode.into()).ok().map(|p| p.id())
|
||||
} else {
|
||||
None
|
||||
let LeeTransaction::Public(public_tx) = tx else {
|
||||
return None;
|
||||
};
|
||||
if public_tx.message.program_account_id != RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID {
|
||||
return None;
|
||||
}
|
||||
let loader_core::Instruction::Deploy { bytecode } =
|
||||
risc0_zkvm::serde::from_slice::<loader_core::Instruction, u32>(
|
||||
&public_tx.message.instruction_data,
|
||||
)
|
||||
.ok()?;
|
||||
Program::new(bytecode.into()).ok().map(|p| p.id())
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
@@ -159,9 +159,9 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
lee::privacy_preserving_transaction::circuit::ProgramWithDependencies::new(
|
||||
programs::bridge(),
|
||||
[
|
||||
(vault_program_id, programs::vault()),
|
||||
(vault_program_id.into(), programs::vault()),
|
||||
(
|
||||
programs::authenticated_transfer().id(),
|
||||
programs::authenticated_transfer().id().into(),
|
||||
programs::authenticated_transfer(),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -168,7 +168,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
|
||||
let auth_transfer_program = ProgramWithDependencies::new(auth_transfer.clone(), [].into());
|
||||
let spend_program =
|
||||
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into());
|
||||
ProgramWithDependencies::new(proxy, [(auth_transfer_id.into(), auth_transfer)].into());
|
||||
|
||||
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_vpk, 0);
|
||||
let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_vpk, 1);
|
||||
|
||||
@@ -4,13 +4,14 @@ use std::{
|
||||
};
|
||||
|
||||
use lee_core::{
|
||||
Identifier, InputAccountIdentity, NullifierPublicKey, PrivateWitness, WitnessKind,
|
||||
Identifier, InputAccountIdentity, NullifierPublicKey, PrivateWitness, ProgramImageClaim,
|
||||
WitnessKind,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
encryption::ViewingPublicKey,
|
||||
program::{
|
||||
AccountPostState, BlockValidityWindow, CallerData, ChainedCall, Claim,
|
||||
DEFAULT_PROGRAM_OWNER, MAX_NUMBER_CHAINED_CALLS, PdaSeed, ProgramId, ProgramOutput,
|
||||
TimestampValidityWindow, validate_execution,
|
||||
AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_OWNER,
|
||||
MAX_NUMBER_CHAINED_CALLS, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow,
|
||||
validate_execution,
|
||||
},
|
||||
};
|
||||
use risc0_zkvm::{guest::env, serde::to_vec};
|
||||
@@ -51,9 +52,7 @@ pub struct ExecutionState {
|
||||
/// `AccountId::for_private_pda(program_id, seed, npk, vpk, identifier) ==
|
||||
/// pre_state.account_id`.
|
||||
private_pda_by_position: HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
|
||||
/// The set containing non-PDA accounts authorized at their first sight, anywhere in the
|
||||
/// call tree, remaining authorized throughout all calls.
|
||||
globally_authorized: HashSet<AccountId>,
|
||||
authorized_accounts: HashSet<AccountId>,
|
||||
}
|
||||
|
||||
impl ExecutionState {
|
||||
@@ -62,6 +61,7 @@ impl ExecutionState {
|
||||
account_identities: &[InputAccountIdentity],
|
||||
program_id: ProgramId,
|
||||
program_outputs: Vec<ProgramOutput>,
|
||||
program_image_claims: &[ProgramImageClaim],
|
||||
) -> Self {
|
||||
// Build position → (npk, identifier) map for private-PDA pre_states, indexed by position
|
||||
// in `account_identities`. The vec is documented as 1:1 with the program's pre_state
|
||||
@@ -114,7 +114,7 @@ impl ExecutionState {
|
||||
private_pda_bound_positions: HashMap::new(),
|
||||
pda_family_binding: HashMap::new(),
|
||||
private_pda_by_position,
|
||||
globally_authorized: HashSet::new(),
|
||||
authorized_accounts: HashSet::new(),
|
||||
};
|
||||
|
||||
let Some(first_output) = program_outputs.first() else {
|
||||
@@ -127,17 +127,14 @@ impl ExecutionState {
|
||||
pre_states: first_output.pre_states.clone(),
|
||||
pda_seeds: Vec::new(),
|
||||
};
|
||||
let initial_caller_data = CallerData {
|
||||
caller_account_id: None,
|
||||
authorized_accounts: HashSet::new(),
|
||||
};
|
||||
let mut chained_calls =
|
||||
VecDeque::<(ChainedCall, CallerData)>::from_iter([(initial_call, initial_caller_data)]);
|
||||
let mut chained_calls = VecDeque::from_iter([(initial_call, None, None)]);
|
||||
|
||||
let mut program_outputs_iter = program_outputs.into_iter();
|
||||
let mut chain_calls_counter = 0;
|
||||
|
||||
while let Some((chained_call, caller_data)) = chained_calls.pop_front() {
|
||||
while let Some((chained_call, caller_account_id, caller_image_id)) =
|
||||
chained_calls.pop_front()
|
||||
{
|
||||
assert!(
|
||||
chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
|
||||
"Max chained calls depth is exceeded"
|
||||
@@ -147,12 +144,18 @@ impl ExecutionState {
|
||||
panic!("Insufficient program outputs for chained calls");
|
||||
};
|
||||
|
||||
// Recover the real `ProgramId` (RISC0 image id) from the account's address: on this
|
||||
// branch every program account lives at the direct `AccountId::from(program_id)`
|
||||
// bijection, so this round-trip is exact. Needed wherever proof verification/PDA
|
||||
// derivation requires the underlying image id rather than the dispatch-facing
|
||||
// `AccountId`.
|
||||
let current_program_id = ProgramId::from(chained_call.program_account_id);
|
||||
// The real `image_id` for this dispatch address. A `Deploy`-created program's
|
||||
// address doesn't encode its image id (unlike a legacy program's, where the
|
||||
// `AccountId::from(program_id)` bijection is exact by construction), so its real
|
||||
// image id has to come from a claim instead — see `ProgramImageClaim`'s doc comment
|
||||
// for how that claim gets anchored to real chain state (by the sequencer, not here).
|
||||
let current_program_id = program_image_claims
|
||||
.iter()
|
||||
.find(|claim| claim.account_id == chained_call.program_account_id)
|
||||
.map_or_else(
|
||||
|| ProgramId::from(chained_call.program_account_id),
|
||||
|claim| claim.image_id,
|
||||
);
|
||||
|
||||
// Check that instruction data in chained call is the instruction data in program output
|
||||
assert_eq!(
|
||||
@@ -180,7 +183,7 @@ impl ExecutionState {
|
||||
// by spoofing caller_account_id (e.g. passing caller_account_id = self_account_id
|
||||
// to bypass access control checks).
|
||||
assert_eq!(
|
||||
program_output.caller_account_id, caller_data.caller_account_id,
|
||||
program_output.caller_account_id, caller_account_id,
|
||||
"Program output caller_account_id does not match actual caller"
|
||||
);
|
||||
|
||||
@@ -195,25 +198,22 @@ impl ExecutionState {
|
||||
panic!("Invalid program behavior in program {current_program_id:?}: {err}");
|
||||
}
|
||||
|
||||
let authorized_accounts = execution_state.validate_and_sync_states(
|
||||
for next_call in program_output.chained_calls.iter().rev() {
|
||||
chained_calls.push_front((
|
||||
next_call.clone(),
|
||||
Some(chained_call.program_account_id),
|
||||
Some(current_program_id),
|
||||
));
|
||||
}
|
||||
|
||||
execution_state.validate_and_sync_states(
|
||||
account_identities,
|
||||
current_program_id,
|
||||
caller_data,
|
||||
caller_image_id,
|
||||
&chained_call.pda_seeds,
|
||||
program_output.pre_states,
|
||||
program_output.post_states,
|
||||
);
|
||||
|
||||
for next_call in program_output.chained_calls.into_iter().rev() {
|
||||
// Push the call with newly-authorized account set.
|
||||
chained_calls.push_front((
|
||||
next_call,
|
||||
CallerData {
|
||||
caller_account_id: Some(chained_call.program_account_id),
|
||||
authorized_accounts: authorized_accounts.clone(),
|
||||
},
|
||||
));
|
||||
}
|
||||
chain_calls_counter = chain_calls_counter.checked_add(1).expect(
|
||||
"Chain calls counter should not overflow as it checked before incrementing",
|
||||
);
|
||||
@@ -264,20 +264,16 @@ impl ExecutionState {
|
||||
}
|
||||
|
||||
/// Validate program pre and post states and populate the execution state.
|
||||
///
|
||||
/// Return the set of authorized accounts as the result of the processed
|
||||
/// call.
|
||||
fn validate_and_sync_states(
|
||||
&mut self,
|
||||
account_identities: &[InputAccountIdentity],
|
||||
program_id: ProgramId,
|
||||
caller: CallerData,
|
||||
caller_image_id: Option<ProgramId>,
|
||||
caller_pda_seeds: &[PdaSeed],
|
||||
output_pre_states: Vec<AccountWithMetadata>,
|
||||
output_post_states: Vec<AccountPostState>,
|
||||
) -> HashSet<AccountId> {
|
||||
let mut authorized_output_accounts = Vec::new();
|
||||
for (mut pre, mut post) in output_pre_states.into_iter().zip(output_post_states) {
|
||||
) {
|
||||
for (pre, mut post) in output_pre_states.into_iter().zip(output_post_states) {
|
||||
let pre_account_id = pre.account_id;
|
||||
let pre_is_authorized = pre.is_authorized;
|
||||
let post_states_entry = self.post_states.entry(pre.account_id);
|
||||
@@ -300,26 +296,35 @@ impl ExecutionState {
|
||||
"Inconsistent pre state for account {pre_account_id}",
|
||||
);
|
||||
|
||||
let pre_state_position = self
|
||||
let (previous_is_authorized, pre_state_position) = self
|
||||
.pre_states
|
||||
.iter()
|
||||
.position(|acc| acc.account_id == pre_account_id)
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Pre state must exist in execution state for account {pre_account_id}",
|
||||
)
|
||||
});
|
||||
.enumerate()
|
||||
.find(|(_, acc)| acc.account_id == pre_account_id)
|
||||
.map_or_else(
|
||||
|| {
|
||||
panic!(
|
||||
"Pre state must exist in execution state for account {pre_account_id}",
|
||||
)
|
||||
},
|
||||
|(pos, acc)| (acc.is_authorized, pos),
|
||||
);
|
||||
|
||||
assert_authorization_and_record_bindings(
|
||||
let is_authorized = resolve_authorization_and_record_bindings(
|
||||
&mut self.pda_family_binding,
|
||||
&mut self.private_pda_bound_positions,
|
||||
&self.private_pda_by_position,
|
||||
&self.globally_authorized,
|
||||
&caller,
|
||||
caller_pda_seeds,
|
||||
&mut self.authorized_accounts,
|
||||
pre_account_id,
|
||||
pre_state_position,
|
||||
pre_is_authorized,
|
||||
caller_image_id,
|
||||
caller_pda_seeds,
|
||||
previous_is_authorized,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
pre_is_authorized, is_authorized,
|
||||
"Inconsistent authorization for account {pre_account_id}",
|
||||
);
|
||||
}
|
||||
Entry::Vacant(_) => {
|
||||
@@ -368,50 +373,10 @@ impl ExecutionState {
|
||||
pre_account_id,
|
||||
);
|
||||
}
|
||||
let has_private_pda_witness = self
|
||||
.private_pda_by_position
|
||||
.contains_key(&pre_state_position);
|
||||
if has_private_pda_witness {
|
||||
assert_authorization_and_record_bindings(
|
||||
&mut self.pda_family_binding,
|
||||
&mut self.private_pda_bound_positions,
|
||||
&self.private_pda_by_position,
|
||||
&self.globally_authorized,
|
||||
&caller,
|
||||
caller_pda_seeds,
|
||||
pre_account_id,
|
||||
pre_state_position,
|
||||
pre_is_authorized,
|
||||
);
|
||||
}
|
||||
if !has_private_pda_witness
|
||||
&& authorize_first_sight_without_pda_witness(
|
||||
&mut self.pda_family_binding,
|
||||
&mut self.globally_authorized,
|
||||
&caller,
|
||||
caller_pda_seeds,
|
||||
pre_account_id,
|
||||
pre_is_authorized,
|
||||
)
|
||||
{
|
||||
// authorize_first_sight_without_pda_witness is only true for PDAs
|
||||
// which will be recorded in output journal.
|
||||
//
|
||||
// Since we are in a privacy circuit, the verifier cannot
|
||||
// replay the transaction to see which public PDAs were
|
||||
// actually authorized. We mark them false as the
|
||||
// verifier checks regular account signatures as well.
|
||||
pre.is_authorized = false;
|
||||
}
|
||||
self.pre_states.push(pre);
|
||||
}
|
||||
}
|
||||
|
||||
// If an account it authorized, push it to the autorized set.
|
||||
if pre_is_authorized {
|
||||
authorized_output_accounts.push(pre_account_id);
|
||||
}
|
||||
|
||||
if let Some(claim) = post.required_claim() {
|
||||
// The invoked program can only claim accounts with default program id.
|
||||
assert_eq!(
|
||||
@@ -495,10 +460,6 @@ impl ExecutionState {
|
||||
|
||||
post_states_entry.insert_entry(post.into_account());
|
||||
}
|
||||
|
||||
let mut authorized_accounts = caller.authorized_accounts;
|
||||
authorized_accounts.extend(authorized_output_accounts);
|
||||
authorized_accounts
|
||||
}
|
||||
|
||||
/// Consume self and yield the validity windows, the per-position PDA seed/program map
|
||||
@@ -582,131 +543,65 @@ fn bind_private_pda_position(
|
||||
}
|
||||
}
|
||||
|
||||
/// Match `account_id` against the caller's seeds under the public-PDA derivation. `None`
|
||||
/// if no appropriate authorization given.
|
||||
fn match_caller_seed_as_public_pda(
|
||||
caller: &CallerData,
|
||||
caller_pda_seeds: &[PdaSeed],
|
||||
account_id: AccountId,
|
||||
) -> Option<(PdaSeed, ProgramId)> {
|
||||
// Recover the real `ProgramId` (RISC0 image id): on this branch every program account lives
|
||||
// at the direct `AccountId::from(program_id)` bijection, so this round-trip is exact.
|
||||
// `for_public_pda`'s derivation formula is pinned to the caller's actual image id, not its
|
||||
// dispatch-facing `AccountId`.
|
||||
let caller_program_id = ProgramId::from(caller.caller_account_id?);
|
||||
// Costy for calls with multiple seeds in one call.
|
||||
caller_pda_seeds.iter().find_map(|seed| {
|
||||
if AccountId::for_public_pda(&caller_program_id, seed) == account_id {
|
||||
return Some((*seed, caller_program_id));
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Match `account_id` against the caller's seeds interpreted as private-PDA derivations, using the
|
||||
/// (npk, vpk, identifier) supplied for this position. `None` when the position carries no
|
||||
/// private-PDA witness.
|
||||
fn match_caller_seed_as_private_pda(
|
||||
private_pda_by_position: &HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
|
||||
caller: &CallerData,
|
||||
caller_pda_seeds: &[PdaSeed],
|
||||
account_id: AccountId,
|
||||
pre_state_position: usize,
|
||||
) -> Option<(PdaSeed, ProgramId)> {
|
||||
let (npk, vpk, identifier) = private_pda_by_position.get(&pre_state_position)?;
|
||||
let caller_program_id = ProgramId::from(caller.caller_account_id?);
|
||||
// Costy for calls with multiple seeds in one call.
|
||||
caller_pda_seeds.iter().find_map(|seed| {
|
||||
if AccountId::for_private_pda(&caller_program_id, seed, npk, vpk, *identifier) == account_id
|
||||
{
|
||||
return Some((*seed, caller_program_id));
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Judge a non-private-PDA `pre_state` at its first sighting and resolve its journal mask.
|
||||
///
|
||||
/// Either the account is a public PDA in which case the public mask should be changed, or
|
||||
/// it is a regular account. For PDAs, we assert the family bindings. For regular accounts,
|
||||
/// add to global authorization set.
|
||||
fn authorize_first_sight_without_pda_witness(
|
||||
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
|
||||
globally_authorized: &mut HashSet<AccountId>,
|
||||
caller: &CallerData,
|
||||
caller_pda_seeds: &[PdaSeed],
|
||||
pre_account_id: AccountId,
|
||||
pre_is_authorized: bool,
|
||||
) -> bool {
|
||||
if let Some((seed, caller_program_id)) =
|
||||
match_caller_seed_as_public_pda(caller, caller_pda_seeds, pre_account_id)
|
||||
{
|
||||
assert!(
|
||||
pre_is_authorized,
|
||||
"Caller-seeded public PDA must be declared authorized at first sight: {pre_account_id}"
|
||||
);
|
||||
assert_family_binding(pda_family_binding, caller_program_id, seed, pre_account_id);
|
||||
true
|
||||
} else {
|
||||
// If an authorized account is a non-PDA one, it is globally authorized.
|
||||
if pre_is_authorized {
|
||||
globally_authorized.insert(pre_account_id);
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// When a caller seed matches, also records the `(caller, seed) → account_id` family binding
|
||||
/// and, for the private form, marks the position in `private_pda_bound_positions`. Free
|
||||
/// function so callers can pass individual `&mut self.*` field borrows without holding a borrow
|
||||
/// on the surrounding struct's other fields.
|
||||
/// Resolve the authorization state of a `pre_state` seen again in a chained call and record
|
||||
/// any resulting bindings. Returns `true` if the `pre_state` is authorized through either a
|
||||
/// previously-seen authorization or a matching caller seed (under the public or private
|
||||
/// derivation). When a caller seed matches, also records the `(caller, seed) → account_id`
|
||||
/// family binding and, for the private form, marks the position in
|
||||
/// `private_pda_bound_positions`. Only reachable when `caller_image_id.is_some()`,
|
||||
/// top-level flows have no caller-emitted seeds, so binding at top level must come from the
|
||||
/// claim path. Free function so callers can pass individual `&mut self.*` field borrows
|
||||
/// without holding a borrow on the surrounding struct's other fields.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "breaking out a context struct does not buy us anything here"
|
||||
)]
|
||||
fn assert_authorization_and_record_bindings(
|
||||
fn resolve_authorization_and_record_bindings(
|
||||
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
|
||||
private_pda_bound_positions: &mut HashMap<usize, (ProgramId, PdaSeed)>,
|
||||
private_pda_by_position: &HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
|
||||
globally_authorized: &HashSet<AccountId>,
|
||||
caller: &CallerData,
|
||||
caller_pda_seeds: &[PdaSeed],
|
||||
authorized_accounts: &mut HashSet<AccountId>,
|
||||
pre_account_id: AccountId,
|
||||
pre_state_position: usize,
|
||||
pre_is_authorized: bool,
|
||||
) {
|
||||
caller_image_id: Option<ProgramId>,
|
||||
caller_pda_seeds: &[PdaSeed],
|
||||
previous_is_authorized: bool,
|
||||
) -> bool {
|
||||
// `for_public_pda`/`for_private_pda`'s derivation formula is pinned to the caller's real
|
||||
// image id, not its dispatch-facing `AccountId` — a `Deploy`-created caller's address doesn't
|
||||
// encode it, so `caller_image_id` must be the recovered real image id (see
|
||||
// `derive_from_outputs`'s `current_program_id`), not a bijection round-trip.
|
||||
let matched_caller_seed: Option<(PdaSeed, bool, ProgramId)> =
|
||||
match_caller_seed_as_public_pda(caller, caller_pda_seeds, pre_account_id)
|
||||
.map(|(seed, caller_program_id)| (seed, false, caller_program_id))
|
||||
.or_else(|| {
|
||||
match_caller_seed_as_private_pda(
|
||||
private_pda_by_position,
|
||||
caller,
|
||||
caller_pda_seeds,
|
||||
pre_account_id,
|
||||
pre_state_position,
|
||||
)
|
||||
.map(|(seed, caller_program_id)| (seed, true, caller_program_id))
|
||||
});
|
||||
caller_image_id.and_then(|caller| {
|
||||
caller_pda_seeds.iter().find_map(|seed| {
|
||||
if AccountId::for_public_pda(&caller, seed) == pre_account_id {
|
||||
return Some((*seed, false, caller));
|
||||
}
|
||||
if let Some((npk, vpk, identifier)) =
|
||||
private_pda_by_position.get(&pre_state_position)
|
||||
&& AccountId::for_private_pda(&caller, seed, npk, vpk, *identifier)
|
||||
== pre_account_id
|
||||
{
|
||||
return Some((*seed, true, caller));
|
||||
}
|
||||
None
|
||||
})
|
||||
});
|
||||
|
||||
if let Some((seed, is_private_form, caller_program_id)) = matched_caller_seed {
|
||||
assert_family_binding(pda_family_binding, caller_program_id, seed, pre_account_id);
|
||||
if let Some((seed, is_private_form, caller)) = matched_caller_seed {
|
||||
assert_family_binding(pda_family_binding, caller, seed, pre_account_id);
|
||||
if is_private_form {
|
||||
bind_private_pda_position(
|
||||
private_pda_bound_positions,
|
||||
pre_state_position,
|
||||
caller_program_id,
|
||||
seed,
|
||||
);
|
||||
bind_private_pda_position(private_pda_bound_positions, pre_state_position, caller, seed);
|
||||
}
|
||||
}
|
||||
|
||||
let is_authorized = matched_caller_seed.is_some()
|
||||
|| globally_authorized.contains(&pre_account_id)
|
||||
|| caller.authorized_accounts.contains(&pre_account_id);
|
||||
if authorized_accounts.contains(&pre_account_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
pre_is_authorized, is_authorized,
|
||||
"Inconsistent authorization for account {pre_account_id}",
|
||||
);
|
||||
let authorized = previous_is_authorized || matched_caller_seed.is_some();
|
||||
if authorized {
|
||||
authorized_accounts.insert(pre_account_id);
|
||||
}
|
||||
authorized
|
||||
}
|
||||
|
||||
@@ -10,15 +10,19 @@ fn main() {
|
||||
account_identities,
|
||||
program_id,
|
||||
dummy_inputs,
|
||||
program_image_claims,
|
||||
} = env::read();
|
||||
|
||||
let execution_state = execution_state::ExecutionState::derive_from_outputs(
|
||||
&account_identities,
|
||||
program_id,
|
||||
program_outputs,
|
||||
&program_image_claims,
|
||||
);
|
||||
|
||||
let output = output::compute_circuit_output(execution_state, &account_identities, dummy_inputs);
|
||||
let mut output =
|
||||
output::compute_circuit_output(execution_state, &account_identities, dummy_inputs);
|
||||
output.program_image_claims = program_image_claims;
|
||||
|
||||
env::commit(&output);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ pub fn compute_circuit_output(
|
||||
private_actions: Vec::new(),
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
// Set by the caller (`main.rs`) — this function has no need to know about it.
|
||||
program_image_claims: Vec::new(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -4,11 +4,28 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::{
|
||||
AuthorizationSecretKey, Commitment, CommitmentSetDigest, Identifier, MembershipProof,
|
||||
Nullifier, NullifierPublicKey, NullifierSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
encryption::{EncryptedAccountData, ViewTag, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow},
|
||||
};
|
||||
|
||||
/// A claim that `account_id`'s program account currently has `image_id`.
|
||||
///
|
||||
/// Supplied by the prover as circuit input (untrusted), used inside the circuit for
|
||||
/// `env::verify` in place of a `Deploy`-created program's address (which, unlike a legacy
|
||||
/// program's, doesn't encode its image id), and echoed unchanged into the circuit's output.
|
||||
/// Anchoring `image_id` to `account_id` is **not** enforced inside the circuit — it's enforced
|
||||
/// by the sequencer, which independently checks every claim against real chain state
|
||||
/// (`V03State::get_program`) before accepting the proof. A side effect of this, for now: every
|
||||
/// program invoked anywhere in a private transaction's call graph is publicly visible via this
|
||||
/// claim list.
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
|
||||
pub struct ProgramImageClaim {
|
||||
pub account_id: AccountId,
|
||||
pub image_id: ProgramId,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PrivacyPreservingCircuitInput {
|
||||
/// Outputs of the program execution.
|
||||
@@ -21,6 +38,9 @@ pub struct PrivacyPreservingCircuitInput {
|
||||
/// Program ID.
|
||||
pub program_id: ProgramId,
|
||||
pub dummy_inputs: Vec<DummyInput>,
|
||||
/// Real `image_id`s for every `Deploy`-created program invoked in the call graph, keyed by
|
||||
/// account id. See [`ProgramImageClaim`].
|
||||
pub program_image_claims: Vec<ProgramImageClaim>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
@@ -169,6 +189,9 @@ pub struct PrivacyPreservingCircuitOutput {
|
||||
pub private_actions: Vec<PrivateAction>,
|
||||
pub block_validity_window: BlockValidityWindow,
|
||||
pub timestamp_validity_window: TimestampValidityWindow,
|
||||
/// Unchanged echo of [`PrivacyPreservingCircuitInput::program_image_claims`] — what the
|
||||
/// receipt actually commits to, so the sequencer can check it against real chain state.
|
||||
pub program_image_claims: Vec<ProgramImageClaim>,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "host", test))]
|
||||
@@ -267,6 +290,10 @@ mod tests {
|
||||
}],
|
||||
block_validity_window: (1..).into(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
program_image_claims: vec![ProgramImageClaim {
|
||||
account_id: AccountId::new([3; 32]),
|
||||
image_id: [4; 8],
|
||||
}],
|
||||
};
|
||||
let bytes = output.to_bytes();
|
||||
let output_from_slice: PrivacyPreservingCircuitOutput = from_slice(&bytes).unwrap();
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
pub use circuit_io::{
|
||||
DummyInput, InputAccountIdentity, NullifierWitness, PrivacyPreservingCircuitInput,
|
||||
PrivacyPreservingCircuitOutput, PrivateAction, PrivateWitness, PublicAction, WitnessKind,
|
||||
PrivacyPreservingCircuitOutput, PrivateAction, PrivateWitness, ProgramImageClaim, PublicAction,
|
||||
WitnessKind,
|
||||
};
|
||||
pub use commitment::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof,
|
||||
|
||||
@@ -22,7 +22,19 @@ pub const PROGRAM_STORAGE_OWNER: AccountId = AccountId::new([0xFF; 32]);
|
||||
|
||||
/// Reserved `AccountId` for the native "Deploy" dispatch shortcut.
|
||||
///
|
||||
/// `SHA256("/LEE/v0.3/AccountId/State/" || "DeploymentProgram")`, each padded to 32 bytes.
|
||||
/// `SHA256(domain_separator || label)`, where `domain_separator` is
|
||||
/// `/LEE/v0.3/AccountId/State/` and `label` is `DeploymentProgram`, each padded with trailing
|
||||
/// zero bytes to 32 bytes before concatenation — the same domain-separation construction used
|
||||
/// throughout this module, just with no variable input, since this is a single fixed address
|
||||
/// rather than a per-caller derivation.
|
||||
///
|
||||
/// Dispatch recognizes this exact `AccountId` and runs the deploy logic as native Rust instead
|
||||
/// of interpreting a guest ELF: computing a program's image id inside the zkVM costs roughly
|
||||
/// 1,400-1,500 cycles per byte of deployed bytecode (measured against every real program in
|
||||
/// this repo), pushing a real deployment to 500M-900M cycles against the 32M public-execution
|
||||
/// cap, whereas the equivalent native computation costs low tens of milliseconds. A caller
|
||||
/// targets this address directly as a `Message`/`ChainedCall`'s `AccountId`, same as any other
|
||||
/// program.
|
||||
pub const RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID: AccountId = AccountId::new(hex!(
|
||||
"599e2c6c2b89ff39bc3094b3276f1fcaa7173800a71d9896a1ba9bd1458a91c9"
|
||||
));
|
||||
@@ -31,12 +43,51 @@ pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
|
||||
|
||||
pub type ProgramId = [u32; 8];
|
||||
|
||||
/// Derives the `AccountId` under which a program's data is stored, directly from its
|
||||
/// `ProgramId`, by reinterpreting the 8 little-endian `u32` words as 32 raw bytes.
|
||||
/// The account-data layout of a program's header account, deployed via the `Deploy` native
|
||||
/// dispatch shortcut (see [`RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID`]).
|
||||
///
|
||||
/// A 1:1, information-preserving mapping (both types are exactly 32 bytes) rather than a
|
||||
/// hash — `ProgramId` is already content-derived (RISC0's `image_id`), so no extra domain
|
||||
/// separation is needed just to use it as a `HashMap<AccountId, Account>` key.
|
||||
/// Deliberately holds only small, fixed-size fields — never the program's bytecode, which lives
|
||||
/// in a separate account (see `program_loader_core::deploy_segment_account_id`). Keeping the two
|
||||
/// apart means anything that needs to authenticate a program's *identity* (e.g. the
|
||||
/// privacy-preserving circuit confirming which `image_id` an `AccountId` currently maps to) only
|
||||
/// ever has to read this handful of bytes, not the full program — the only account-authentication
|
||||
/// primitive available today is whole-account equality, so what's bundled into one account sets
|
||||
/// the floor for how cheap that authentication can be.
|
||||
///
|
||||
/// `image_id` is read fresh from this account on every dispatch/verification rather than
|
||||
/// re-derived from the account's address, specifically so that upgrading a program (writing a
|
||||
/// new `image_id` into the same, stable `AccountId`) is the entire upgrade mechanism — no
|
||||
/// separate "which version" bookkeeping needed.
|
||||
///
|
||||
/// Lives here rather than in `program_loader_core` so that `V03State::get_program` — a generic,
|
||||
/// program-agnostic lookup — can decode it without depending on a specific program's crate.
|
||||
/// `program_loader_core` re-exports this type.
|
||||
#[derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct ProgramData {
|
||||
pub image_id: ProgramId,
|
||||
pub segment_number: u32,
|
||||
pub update_auth: AccountId,
|
||||
}
|
||||
|
||||
impl TryFrom<&crate::account::Data> for ProgramData {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn try_from(data: &crate::account::Data) -> Result<Self, Self::Error> {
|
||||
BorshDeserialize::try_from_slice(data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ProgramData> for crate::account::Data {
|
||||
fn from(program_data: &ProgramData) -> Self {
|
||||
let mut data = Vec::with_capacity(std::mem::size_of_val(program_data));
|
||||
BorshSerialize::serialize(program_data, &mut data)
|
||||
.expect("borsh serialization should not fail");
|
||||
Self::try_from(data).expect("elf must fit under DATA_MAX_LENGTH")
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO: This is a temporary conversion; will be removed once `Program` to `Account`
|
||||
/// migration is complete.
|
||||
impl From<ProgramId> for AccountId {
|
||||
fn from(program_id: ProgramId) -> Self {
|
||||
let bytes: Vec<u8> = program_id
|
||||
@@ -244,12 +295,6 @@ impl AccountId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CallerData {
|
||||
pub caller_account_id: Option<AccountId>,
|
||||
pub authorized_accounts: HashSet<AccountId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct ChainedCall {
|
||||
/// The `AccountId` of the program to execute.
|
||||
@@ -682,17 +727,12 @@ pub enum ExecutionValidationError {
|
||||
/// `pre_state`.
|
||||
#[must_use]
|
||||
pub fn compute_public_authorized_pdas(
|
||||
caller_account_id: Option<AccountId>,
|
||||
caller_image_id: Option<ProgramId>,
|
||||
pda_seeds: &[PdaSeed],
|
||||
) -> HashSet<AccountId> {
|
||||
let Some(caller) = caller_account_id else {
|
||||
let Some(caller) = caller_image_id else {
|
||||
return HashSet::new();
|
||||
};
|
||||
// Recover the real `ProgramId` (RISC0 image id): on this branch every program account lives
|
||||
// at the direct `AccountId::from(program_id)` bijection, so this round-trip is exact.
|
||||
// `for_public_pda`'s derivation formula is pinned to the caller's actual image id, not its
|
||||
// dispatch-facing `AccountId`.
|
||||
let caller = ProgramId::from(caller);
|
||||
pda_seeds
|
||||
.iter()
|
||||
.map(|seed| AccountId::for_public_pda(&caller, seed))
|
||||
|
||||
@@ -326,7 +326,7 @@ fn for_private_account_dispatches_correctly() {
|
||||
fn compute_public_authorized_pdas_with_seeds() {
|
||||
let caller: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let result = compute_public_authorized_pdas(Some(caller.into()), &[seed]);
|
||||
let result = compute_public_authorized_pdas(Some(caller), &[seed]);
|
||||
let expected = AccountId::for_public_pda(&caller, &seed);
|
||||
assert!(result.contains(&expected));
|
||||
assert_eq!(result.len(), 1);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use std::io;
|
||||
|
||||
use lee_core::{
|
||||
account::{Account, AccountId},
|
||||
program::ProgramId,
|
||||
};
|
||||
use lee_core::account::{Account, AccountId};
|
||||
use thiserror::Error;
|
||||
|
||||
#[macro_export]
|
||||
@@ -68,9 +65,6 @@ pub enum LeeError {
|
||||
#[error("Invalid program bytecode")]
|
||||
InvalidProgramBytecode(#[source] anyhow::Error),
|
||||
|
||||
#[error("Program already exists")]
|
||||
ProgramAlreadyExists,
|
||||
|
||||
#[error("Chain of calls is too long")]
|
||||
MaxChainedCallsDepthExceeded,
|
||||
|
||||
@@ -129,8 +123,8 @@ pub enum InvalidProgramBehaviorError {
|
||||
#[error("Default account {account_id} was modified without being claimed")]
|
||||
DefaultAccountModifiedWithoutClaim { account_id: AccountId },
|
||||
|
||||
#[error("Called program {program_id:?} which is not listed in dependencies")]
|
||||
UndeclaredProgramDependency { program_id: ProgramId },
|
||||
#[error("Called program {account_id} which is not listed in dependencies")]
|
||||
UndeclaredProgramDependency { account_id: AccountId },
|
||||
|
||||
#[error(
|
||||
"Account {account_id} was declared in the transaction but is missing from the program output"
|
||||
|
||||
@@ -3,9 +3,9 @@ use std::collections::{HashMap, VecDeque};
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
DummyInput, InputAccountIdentity, PrivacyPreservingCircuitInput,
|
||||
PrivacyPreservingCircuitOutput,
|
||||
PrivacyPreservingCircuitOutput, ProgramImageClaim,
|
||||
account::{AccountId, AccountWithMetadata},
|
||||
program::{ChainedCall, InstructionData, ProgramId, ProgramOutput},
|
||||
program::{ChainedCall, InstructionData, ProgramOutput},
|
||||
};
|
||||
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
|
||||
|
||||
@@ -43,18 +43,32 @@ impl Proof {
|
||||
#[derive(Clone)]
|
||||
pub struct ProgramWithDependencies {
|
||||
pub program: Program,
|
||||
/// Where `program` is dispatched at. Defaults to `AccountId::from(program.id())` (correct
|
||||
/// for a legacy, bijection-addressed program); override via
|
||||
/// [`Self::with_program_account_id`] for a program deployed to a PDA (e.g. via `Deploy`).
|
||||
pub program_account_id: AccountId,
|
||||
// TODO: avoid having a copy of the bytecode of each dependency.
|
||||
pub dependencies: HashMap<ProgramId, Program>,
|
||||
pub dependencies: HashMap<AccountId, Program>,
|
||||
}
|
||||
|
||||
impl ProgramWithDependencies {
|
||||
#[must_use]
|
||||
pub const fn new(program: Program, dependencies: HashMap<ProgramId, Program>) -> Self {
|
||||
pub fn new(program: Program, dependencies: HashMap<AccountId, Program>) -> Self {
|
||||
let program_account_id = AccountId::from(program.id());
|
||||
Self {
|
||||
program,
|
||||
program_account_id,
|
||||
dependencies,
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides the address `program` is dispatched at, for a program whose address isn't
|
||||
/// derived from its own image id (e.g. deployed via `Deploy` to a PDA).
|
||||
#[must_use]
|
||||
pub const fn with_program_account_id(mut self, program_account_id: AccountId) -> Self {
|
||||
self.program_account_id = program_account_id;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Program> for ProgramWithDependencies {
|
||||
@@ -89,13 +103,33 @@ pub fn execute_and_prove_with_padded_inputs(
|
||||
) -> Result<(PrivacyPreservingCircuitOutput, Proof), LeeError> {
|
||||
let ProgramWithDependencies {
|
||||
program: initial_program,
|
||||
program_account_id: initial_program_account_id,
|
||||
dependencies,
|
||||
} = program_with_dependencies;
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
let mut program_outputs = Vec::new();
|
||||
|
||||
// Real `image_id`s for every program in this call graph whose address doesn't already
|
||||
// determine it — i.e. every `Deploy`-created program, PDA-addressed rather than
|
||||
// bijection-addressed. A legacy program needs no claim at all: `ProgramId::from(account_id)`
|
||||
// is already exact for it, by construction, with nothing to authenticate. See
|
||||
// `ProgramImageClaim` and `execution_state.rs`'s matching bijection fallback.
|
||||
let program_image_claims: Vec<ProgramImageClaim> =
|
||||
std::iter::once((*initial_program_account_id, initial_program.id()))
|
||||
.chain(
|
||||
dependencies
|
||||
.iter()
|
||||
.map(|(account_id, program)| (*account_id, program.id())),
|
||||
)
|
||||
.filter(|(account_id, image_id)| *account_id != AccountId::from(*image_id))
|
||||
.map(|(account_id, image_id)| ProgramImageClaim {
|
||||
account_id,
|
||||
image_id,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let initial_call = ChainedCall {
|
||||
program_account_id: AccountId::from(initial_program.id()),
|
||||
program_account_id: *initial_program_account_id,
|
||||
instruction_data,
|
||||
pre_states,
|
||||
pda_seeds: vec![],
|
||||
@@ -110,6 +144,7 @@ pub fn execute_and_prove_with_padded_inputs(
|
||||
|
||||
let inner_receipt = execute_and_prove_program(
|
||||
program,
|
||||
chained_call.program_account_id,
|
||||
caller_account_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
@@ -127,10 +162,9 @@ pub fn execute_and_prove_with_padded_inputs(
|
||||
env_builder.add_assumption(inner_receipt);
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
let new_call_program_id = ProgramId::from(new_call.program_account_id);
|
||||
let next_program = dependencies.get(&new_call_program_id).ok_or(
|
||||
let next_program = dependencies.get(&new_call.program_account_id).ok_or(
|
||||
InvalidProgramBehaviorError::UndeclaredProgramDependency {
|
||||
program_id: new_call_program_id,
|
||||
account_id: new_call.program_account_id,
|
||||
},
|
||||
)?;
|
||||
chained_calls.push_front((
|
||||
@@ -150,6 +184,7 @@ pub fn execute_and_prove_with_padded_inputs(
|
||||
account_identities,
|
||||
program_id: program_with_dependencies.program.id(),
|
||||
dummy_inputs,
|
||||
program_image_claims,
|
||||
};
|
||||
|
||||
env_builder.write(&circuit_input).unwrap();
|
||||
@@ -173,6 +208,7 @@ pub fn execute_and_prove_with_padded_inputs(
|
||||
|
||||
fn execute_and_prove_program(
|
||||
program: &Program,
|
||||
self_account_id: AccountId,
|
||||
caller_account_id: Option<AccountId>,
|
||||
pre_states: &[AccountWithMetadata],
|
||||
instruction_data: &InstructionData,
|
||||
@@ -180,7 +216,7 @@ fn execute_and_prove_program(
|
||||
// Write inputs to the program
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
Program::write_inputs(
|
||||
AccountId::from(program.id()),
|
||||
self_account_id,
|
||||
caller_account_id,
|
||||
pre_states,
|
||||
instruction_data,
|
||||
|
||||
@@ -381,7 +381,7 @@ fn circuit_fails_when_chained_validity_windows_have_empty_intersection() {
|
||||
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
validity_window_chain_caller,
|
||||
[(validity_window.id(), validity_window)].into(),
|
||||
[(validity_window.id().into(), validity_window)].into(),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
@@ -465,7 +465,7 @@ fn private_pda_init() {
|
||||
|
||||
let auth_id = simple_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, simple_transfer)].into());
|
||||
ProgramWithDependencies::new(program, [(auth_id.into(), simple_transfer)].into());
|
||||
|
||||
// is_withdraw=false triggers init path (1 pre-state)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, false)).unwrap();
|
||||
@@ -509,7 +509,7 @@ fn private_pda_withdraw() {
|
||||
|
||||
let auth_id = simple_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, simple_transfer)].into());
|
||||
ProgramWithDependencies::new(program, [(auth_id.into(), simple_transfer)].into());
|
||||
|
||||
// is_withdraw=true, amount=0 (PDA has no balance yet)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, true)).unwrap();
|
||||
@@ -960,8 +960,10 @@ fn pda_update_attempt(
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, declare_authorized, pda_id);
|
||||
let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(simple_transfer_id, simple_transfer)].into());
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
program,
|
||||
[(simple_transfer_id.into(), simple_transfer)].into(),
|
||||
);
|
||||
|
||||
execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, PrivateAction,
|
||||
ProgramImageClaim,
|
||||
account::{Account, Nonce},
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
@@ -24,6 +25,10 @@ pub struct Message {
|
||||
pub private_actions: Vec<PrivateAction>,
|
||||
pub block_validity_window: BlockValidityWindow,
|
||||
pub timestamp_validity_window: TimestampValidityWindow,
|
||||
/// Real `image_id`s claimed for every `Deploy`-created program invoked in this transaction's
|
||||
/// call graph — see [`ProgramImageClaim`]. The sequencer checks each against real chain
|
||||
/// state before accepting the transaction.
|
||||
pub program_image_claims: Vec<ProgramImageClaim>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Message {
|
||||
@@ -52,6 +57,7 @@ impl std::fmt::Debug for Message {
|
||||
.field("private_actions", &private_actions)
|
||||
.field("block_validity_window", &self.block_validity_window)
|
||||
.field("timestamp_validity_window", &self.timestamp_validity_window)
|
||||
.field("program_image_claims", &self.program_image_claims)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -73,6 +79,7 @@ impl Message {
|
||||
private_actions: output.private_actions,
|
||||
block_validity_window: output.block_validity_window,
|
||||
timestamp_validity_window: output.timestamp_validity_window,
|
||||
program_image_claims: output.program_image_claims,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +175,7 @@ pub mod tests {
|
||||
}],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
program_image_claims: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +187,7 @@ pub mod tests {
|
||||
private_actions: vec![],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
program_image_claims: vec![],
|
||||
};
|
||||
|
||||
// empty vec fields: u32 len=0
|
||||
@@ -187,6 +196,7 @@ pub mod tests {
|
||||
let private_actions_bytes: &[u8] = &[0, 0, 0, 0];
|
||||
// validity windows: unbounded = {from: None (0_u8), to: None (0_u8)}
|
||||
let unbounded_window_bytes: &[u8] = &[0, 0];
|
||||
let program_image_claims_bytes: &[u8] = &[0, 0, 0, 0];
|
||||
|
||||
let expected_borsh_vec: Vec<u8> = [
|
||||
public_actions_bytes,
|
||||
@@ -194,6 +204,7 @@ pub mod tests {
|
||||
private_actions_bytes,
|
||||
unbounded_window_bytes, // block_validity_window
|
||||
unbounded_window_bytes, // timestamp_validity_window
|
||||
program_image_claims_bytes,
|
||||
]
|
||||
.concat();
|
||||
let expected_borsh: &[u8] = &expected_borsh_vec;
|
||||
|
||||
@@ -54,6 +54,7 @@ impl Program {
|
||||
|
||||
pub(crate) fn execute(
|
||||
&self,
|
||||
self_account_id: AccountId,
|
||||
caller_account_id: Option<AccountId>,
|
||||
pre_states: &[AccountWithMetadata],
|
||||
instruction_data: &InstructionData,
|
||||
@@ -62,7 +63,7 @@ impl Program {
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
env_builder.session_limit(Some(MAX_NUM_CYCLES_PUBLIC_EXECUTION));
|
||||
Self::write_inputs(
|
||||
AccountId::from(self.id),
|
||||
self_account_id,
|
||||
caller_account_id,
|
||||
pre_states,
|
||||
instruction_data,
|
||||
|
||||
@@ -26,7 +26,12 @@ fn program_execution() {
|
||||
..Account::default()
|
||||
};
|
||||
let program_output = program
|
||||
.execute(None, &[sender, recipient], &instruction_data)
|
||||
.execute(
|
||||
AccountId::from(program.id()),
|
||||
None,
|
||||
&[sender, recipient],
|
||||
&instruction_data,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap();
|
||||
|
||||
@@ -5,7 +5,9 @@ use lee_core::{
|
||||
BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
|
||||
Timestamp,
|
||||
account::{Account, AccountId, Data},
|
||||
program::PROGRAM_STORAGE_OWNER,
|
||||
program::{
|
||||
PROGRAM_STORAGE_OWNER, ProgramData, ProgramId, RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -112,6 +114,14 @@ impl BorshDeserialize for NullifierSet {
|
||||
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
pub struct V03State {
|
||||
/// Deployed programs live here too, as `Account`s findable via [`Self::get_program`], which
|
||||
/// recognizes two shapes: legacy `ProgramDeploymentTransaction`-deployed programs, keyed by
|
||||
/// `AccountId::from(program_id)` (see that impl's doc comment) with the raw elf in
|
||||
/// `Account.data` and `program_owner` set to [`PROGRAM_STORAGE_OWNER`]; and `Deploy`-created
|
||||
/// programs (including every genesis-seeded builtin, via [`Self::insert_program`]), which
|
||||
/// live across two accounts owned by [`RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID`] — a header
|
||||
/// account whose `Account.data` is a borsh-encoded [`ProgramData`] (the current `image_id`,
|
||||
/// small and fixed-size), and a separate segment PDA holding the raw elf.
|
||||
public_state: HashMap<AccountId, Account>,
|
||||
private_state: (CommitmentSet, NullifierSet),
|
||||
}
|
||||
@@ -193,15 +203,46 @@ impl V03State {
|
||||
self
|
||||
}
|
||||
|
||||
/// Seeds a program directly into state in the same two-account shape a `Deploy` dispatch
|
||||
/// produces (see [`Self::get_program`]), skipping the dispatch/proving machinery genesis has
|
||||
/// no signer to drive. The header account is placed at `AccountId::from(image_id)` rather
|
||||
/// than the loader-PDA address a live `Deploy` would use for it — deliberately, so a
|
||||
/// genesis-seeded program keeps its well-known dispatch address — while the segment account
|
||||
/// still lives at the exact PDA [`Self::get_program`] derives from the header's content,
|
||||
/// since that address is never a caller-facing well-known address to begin with.
|
||||
pub(crate) fn insert_program(&mut self, program: &Program) {
|
||||
let account_id = AccountId::from(program.id());
|
||||
let account = Account {
|
||||
program_owner: PROGRAM_STORAGE_OWNER,
|
||||
let image_id = program.id();
|
||||
let segment_number = 0;
|
||||
let update_auth = AccountId::default();
|
||||
|
||||
let header_account_id = AccountId::from(image_id);
|
||||
let header_account = Account {
|
||||
program_owner: RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID,
|
||||
data: Data::from(&ProgramData {
|
||||
image_id,
|
||||
segment_number,
|
||||
update_auth,
|
||||
}),
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let loader_id = ProgramId::from(RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID);
|
||||
let segment_account_id = loader_core::deploy_segment_account_id(
|
||||
loader_id,
|
||||
image_id,
|
||||
segment_number,
|
||||
update_auth,
|
||||
);
|
||||
let segment_account = Account {
|
||||
program_owner: RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID,
|
||||
data: Data::try_from(program.elf().to_vec())
|
||||
.expect("elf must fit under DATA_MAX_LENGTH"),
|
||||
..Account::default()
|
||||
};
|
||||
self.public_state.insert(account_id, account);
|
||||
|
||||
self.public_state.insert(header_account_id, header_account);
|
||||
self.public_state
|
||||
.insert(segment_account_id, segment_account);
|
||||
}
|
||||
|
||||
pub fn apply_state_diff(&mut self, diff: ValidatedStateDiff) {
|
||||
@@ -258,7 +299,7 @@ impl V03State {
|
||||
&mut self,
|
||||
tx: &ProgramDeploymentTransaction,
|
||||
) -> Result<(), LeeError> {
|
||||
let diff = ValidatedStateDiff::from_program_deployment_transaction(tx, self)?;
|
||||
let diff = ValidatedStateDiff::from_program_deployment_transaction(tx)?;
|
||||
self.apply_state_diff(diff);
|
||||
Ok(())
|
||||
}
|
||||
@@ -281,16 +322,45 @@ impl V03State {
|
||||
self.public_state.get(&account_id)
|
||||
}
|
||||
|
||||
/// Looks up a deployed program's storage account by its `AccountId`, verifying it is
|
||||
/// actually owned by [`PROGRAM_STORAGE_OWNER`].
|
||||
/// Looks up a deployed program's real `image_id` and bytecode by its `AccountId`,
|
||||
/// recognizing both ways a program can come to exist:
|
||||
///
|
||||
/// An account that lacks this ownership isn't a deployed program, whatever its contents —
|
||||
/// - Owned by [`PROGRAM_STORAGE_OWNER`]: the legacy `ProgramDeploymentTransaction` path, where
|
||||
/// `account.data` is the raw ELF directly and `AccountId::from(image_id)` is the account's
|
||||
/// address by construction.
|
||||
/// - Owned by [`RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID`]: deployed via the native `Deploy`
|
||||
/// dispatch shortcut. `account.data` decodes as a [`ProgramData`] header holding the real
|
||||
/// `image_id`; the bytecode itself lives in a second, separately-addressed segment account
|
||||
/// derived from that header (see `loader_core::deploy_segment_account_id`).
|
||||
///
|
||||
/// Returning the real `image_id` — rather than callers deriving one from the address, which
|
||||
/// is only valid for the legacy path — is what makes upgrading a `Deploy`-created program
|
||||
/// possible: the address never changes, only the `image_id` written into its header account
|
||||
/// does.
|
||||
///
|
||||
/// An account that matches neither owner isn't a deployed program, whatever its contents —
|
||||
/// this is the single place that distinction is enforced, so callers never have to remember
|
||||
/// to re-check it themselves.
|
||||
#[must_use]
|
||||
pub fn get_program(&self, program_account_id: AccountId) -> Option<&Account> {
|
||||
pub fn get_program(&self, program_account_id: AccountId) -> Option<(ProgramId, Vec<u8>)> {
|
||||
let account = self.get_account_by_id_ref(program_account_id)?;
|
||||
(account.program_owner == PROGRAM_STORAGE_OWNER).then_some(account)
|
||||
if account.program_owner == PROGRAM_STORAGE_OWNER {
|
||||
return Some((ProgramId::from(program_account_id), account.data.to_vec()));
|
||||
}
|
||||
if account.program_owner == RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID {
|
||||
let header = ProgramData::try_from(&account.data).ok()?;
|
||||
let loader_id = ProgramId::from(RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID);
|
||||
let segment_account_id = loader_core::deploy_segment_account_id(
|
||||
loader_id,
|
||||
header.image_id,
|
||||
header.segment_number,
|
||||
header.update_auth,
|
||||
);
|
||||
let segment = self.get_account_by_id_ref(segment_account_id)?;
|
||||
return (segment.program_owner == RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID)
|
||||
.then(|| (header.image_id, segment.data.to_vec()));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
||||
@@ -498,7 +498,8 @@ fn caller_pda_seeds_authorize_private_pda_for_callee() {
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let callee_id = callee.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(delegator, [(callee_id, callee)].into());
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(delegator, [(callee_id.into(), callee)].into());
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre_state],
|
||||
@@ -530,7 +531,8 @@ fn caller_pda_seeds_with_wrong_seed_rejects_private_pda_for_callee() {
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let callee_id = callee.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(delegator, [(callee_id, callee)].into());
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(delegator, [(callee_id.into(), callee)].into());
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre_state],
|
||||
@@ -557,7 +559,7 @@ fn sibling_declaring_delegated_pda(pda_is_authorized: bool) -> Result<(), LeeErr
|
||||
let sibling_id = sibling.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
delegator,
|
||||
[(callee_id, callee), (sibling_id, sibling)].into(),
|
||||
[(callee_id.into(), callee), (sibling_id.into(), sibling)].into(),
|
||||
);
|
||||
|
||||
execute_and_prove(
|
||||
@@ -605,7 +607,7 @@ fn delegated_pda_stays_authorized_in_delegated_subtree() {
|
||||
let callee_id = callee.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
delegator,
|
||||
[(forwarder_id, forwarder), (callee_id, callee)].into(),
|
||||
[(forwarder_id.into(), forwarder), (callee_id.into(), callee)].into(),
|
||||
);
|
||||
let no_sibling: Option<(ProgramId, Option<bool>)> = None;
|
||||
|
||||
@@ -650,7 +652,7 @@ fn holder_authorization_survives_across_sibling_calls() {
|
||||
let sibling_id = sibling.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
delegator,
|
||||
[(callee_id, callee), (sibling_id, sibling)].into(),
|
||||
[(callee_id.into(), callee), (sibling_id.into(), sibling)].into(),
|
||||
);
|
||||
|
||||
execute_and_prove(
|
||||
@@ -699,7 +701,7 @@ fn inherited_scope_passes_through_intermediate_calls() {
|
||||
let callee_id = callee.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
delegator,
|
||||
[(forwarder_id, forwarder), (callee_id, callee)].into(),
|
||||
[(forwarder_id.into(), forwarder), (callee_id.into(), callee)].into(),
|
||||
);
|
||||
let no_sibling: Option<(ProgramId, Option<bool>)> = None;
|
||||
let forward_through_undeclaring_call = Program::serialize_instruction((
|
||||
@@ -748,7 +750,7 @@ fn undeclaring_private_delegation(
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let callee_id = callee.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(delegator, [(callee_id, callee)].into());
|
||||
let program_with_deps = ProgramWithDependencies::new(delegator, [(callee_id.into(), callee)].into());
|
||||
|
||||
execute_and_prove(
|
||||
vec![pre_state],
|
||||
@@ -831,7 +833,7 @@ fn undeclaring_public_delegation(
|
||||
let sibling_id = sibling.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
delegator,
|
||||
[(callee_id, callee), (sibling_id, sibling)].into(),
|
||||
[(callee_id.into(), callee), (sibling_id.into(), sibling)].into(),
|
||||
);
|
||||
|
||||
execute_and_prove(
|
||||
@@ -1334,7 +1336,7 @@ fn two_private_pda_family_members_receive_and_spend() {
|
||||
|
||||
let spend_with_deps = ProgramWithDependencies::new(
|
||||
proxy,
|
||||
[(simple_transfer_id, simple_transfer.clone())].into(),
|
||||
[(simple_transfer_id.into(), simple_transfer.clone())].into(),
|
||||
);
|
||||
|
||||
let funder_id = funder_keys.account_id();
|
||||
|
||||
@@ -419,7 +419,7 @@ fn private_chained_call(number_of_calls: u32) {
|
||||
|
||||
let mut dependencies = HashMap::new();
|
||||
|
||||
dependencies.insert(simple_transfers.id(), simple_transfers);
|
||||
dependencies.insert(simple_transfers.id().into(), simple_transfers);
|
||||
let program_with_deps = ProgramWithDependencies::new(chain_caller, dependencies);
|
||||
|
||||
let from_new_nonce = Nonce::default().private_account_nonce_increment(&from_keys.nsk());
|
||||
|
||||
@@ -506,7 +506,7 @@ fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() {
|
||||
let instruction = (balance_to_transfer, simple_transfers.id());
|
||||
|
||||
let mut dependencies = HashMap::new();
|
||||
dependencies.insert(simple_transfers.id(), simple_transfers);
|
||||
dependencies.insert(simple_transfers.id().into(), simple_transfers);
|
||||
let program_with_deps = ProgramWithDependencies::new(malicious_program, dependencies);
|
||||
|
||||
// Act - execute the malicious program - this should fail during proving
|
||||
|
||||
@@ -5,10 +5,11 @@ use std::{
|
||||
};
|
||||
|
||||
use lee_core::{
|
||||
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, PublicAction, Timestamp,
|
||||
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, ProgramImageClaim,
|
||||
PublicAction, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{
|
||||
CallerData, ChainedCall, Claim, DEFAULT_PROGRAM_OWNER, ProgramId, ProgramOutput,
|
||||
ChainedCall, Claim, DEFAULT_PROGRAM_OWNER, ProgramId, ProgramOutput,
|
||||
RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID, compute_public_authorized_pdas, validate_execution,
|
||||
},
|
||||
};
|
||||
@@ -99,6 +100,7 @@ impl ValidatedStateDiff {
|
||||
|
||||
let initial_caller_data = CallerData {
|
||||
caller_account_id: None,
|
||||
caller_image_id: None,
|
||||
authorized_accounts: signer_account_ids.iter().copied().collect(),
|
||||
};
|
||||
|
||||
@@ -112,22 +114,25 @@ impl ValidatedStateDiff {
|
||||
LeeError::MaxChainedCallsDepthExceeded
|
||||
);
|
||||
|
||||
// Recover the real `ProgramId` (RISC0 image id) from the account's address: on this
|
||||
// branch every program account lives at the direct `AccountId::from(program_id)`
|
||||
// bijection, so this round-trip is exact. Needed wherever execution/PDA derivation
|
||||
// requires the underlying image id rather than the dispatch-facing `AccountId`.
|
||||
let program_id = ProgramId::from(chained_call.program_account_id);
|
||||
|
||||
debug!(
|
||||
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
|
||||
chained_call.program_account_id,
|
||||
chained_call.pre_states,
|
||||
chained_call.instruction_data
|
||||
);
|
||||
let mut program_output = if chained_call.program_account_id
|
||||
let (program_id, mut program_output) = if chained_call.program_account_id
|
||||
== RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID
|
||||
{
|
||||
// Runs `Deploy` as native Rust instead of interpreting a guest ELF.
|
||||
// Runs `Deploy` as native Rust instead of interpreting a guest ELF — see
|
||||
// `RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID`'s doc comment for why. The
|
||||
// loader's own identity is this fixed reserved `AccountId`, unlike an
|
||||
// ordinary program's, so recovering it via the bijection is exact — there's
|
||||
// no separate "real image id" to look up, Deploy isn't itself upgradeable.
|
||||
let program_id = ProgramId::from(RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID);
|
||||
// `execute_deploy` validates its input via `assert!`/`.expect(...)`, exactly
|
||||
// like every guest program in this codebase, relying here on `catch_unwind` to
|
||||
// play the same role the zkVM executor plays for a real guest: converting a
|
||||
// rejected input into a graceful `Err` instead of unwinding past this call.
|
||||
let program_loader_core::Instruction::Deploy { bytecode } =
|
||||
risc0_zkvm::serde::from_slice(&chained_call.instruction_data).map_err(|e| {
|
||||
LeeError::InvalidInput(format!("invalid Deploy instruction: {e}"))
|
||||
@@ -139,25 +144,35 @@ impl ValidatedStateDiff {
|
||||
.map_err(|_panic_payload| {
|
||||
LeeError::ProgramExecutionFailed("Deploy rejected the given input".into())
|
||||
})?;
|
||||
ProgramOutput::new(
|
||||
chained_call.program_account_id,
|
||||
caller_data.caller_account_id,
|
||||
chained_call.instruction_data.clone(),
|
||||
chained_call.pre_states.clone(),
|
||||
post_states,
|
||||
(
|
||||
program_id,
|
||||
ProgramOutput::new(
|
||||
chained_call.program_account_id,
|
||||
caller_data.caller_account_id,
|
||||
chained_call.instruction_data.clone(),
|
||||
chained_call.pre_states.clone(),
|
||||
post_states,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
let Some(program_account) = state.get_program(chained_call.program_account_id)
|
||||
// The real `image_id`, sourced from the program's own account rather than
|
||||
// guessed from its address — see `V03State::get_program`'s doc comment for
|
||||
// why that distinction matters once a program's address can outlive its
|
||||
// current bytecode (upgrades).
|
||||
let Some((program_id, elf)) = state.get_program(chained_call.program_account_id)
|
||||
else {
|
||||
return Err(LeeError::InvalidInput("Unknown program".into()));
|
||||
};
|
||||
let program =
|
||||
Program::new_unchecked(program_id, Cow::Owned(program_account.data.to_vec()));
|
||||
program.execute(
|
||||
caller_data.caller_account_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?
|
||||
let program = Program::new_unchecked(program_id, Cow::Owned(elf));
|
||||
(
|
||||
program_id,
|
||||
program.execute(
|
||||
chained_call.program_account_id,
|
||||
caller_data.caller_account_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?,
|
||||
)
|
||||
};
|
||||
debug!(
|
||||
"Program {:?} output: {:?}",
|
||||
@@ -165,7 +180,7 @@ impl ValidatedStateDiff {
|
||||
);
|
||||
|
||||
let authorized_pdas = compute_public_authorized_pdas(
|
||||
caller_data.caller_account_id,
|
||||
caller_data.caller_image_id,
|
||||
&chained_call.pda_seeds,
|
||||
);
|
||||
|
||||
@@ -313,6 +328,7 @@ impl ValidatedStateDiff {
|
||||
new_call,
|
||||
CallerData {
|
||||
caller_account_id: Some(chained_call.program_account_id),
|
||||
caller_image_id: Some(program_id),
|
||||
authorized_accounts: authorized_accounts.clone(),
|
||||
},
|
||||
));
|
||||
@@ -444,6 +460,7 @@ impl ValidatedStateDiff {
|
||||
|
||||
// 4. Proof verification
|
||||
check_privacy_preserving_circuit_proof_is_valid(
|
||||
state,
|
||||
&witness_set.proof,
|
||||
&public_pre_states,
|
||||
message,
|
||||
@@ -473,13 +490,9 @@ impl ValidatedStateDiff {
|
||||
|
||||
pub fn from_program_deployment_transaction(
|
||||
tx: &ProgramDeploymentTransaction,
|
||||
state: &V03State,
|
||||
) -> Result<Self, LeeError> {
|
||||
// TODO: remove clone
|
||||
let program = Program::new(tx.message.bytecode.clone().into())?;
|
||||
if state.get_program(AccountId::from(program.id())).is_some() {
|
||||
return Err(LeeError::ProgramAlreadyExists);
|
||||
}
|
||||
Ok(Self(StateDiff {
|
||||
signer_account_ids: vec![],
|
||||
public_diff: HashMap::new(),
|
||||
@@ -503,6 +516,17 @@ impl ValidatedStateDiff {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CallerData {
|
||||
caller_account_id: Option<AccountId>,
|
||||
/// The caller's real `image_id`, recovered when the caller itself was dispatched (see
|
||||
/// `V03State::get_program`) rather than guessed from `caller_account_id` via the bijection —
|
||||
/// needed wherever PDA derivation requires the caller's actual identity, since a
|
||||
/// `Deploy`-created caller's address doesn't encode it.
|
||||
caller_image_id: Option<ProgramId>,
|
||||
authorized_accounts: HashSet<AccountId>,
|
||||
}
|
||||
|
||||
fn authenticate_public_transaction_signers(
|
||||
tx: &PublicTransaction,
|
||||
state: &V03State,
|
||||
@@ -535,10 +559,30 @@ fn authenticate_public_transaction_signers(
|
||||
}
|
||||
|
||||
fn check_privacy_preserving_circuit_proof_is_valid(
|
||||
state: &V03State,
|
||||
proof: &Proof,
|
||||
public_pre_states: &[AccountWithMetadata],
|
||||
message: &Message,
|
||||
) -> Result<(), LeeError> {
|
||||
// Anchor each claimed image_id to real chain state: reconstruct the claims using the
|
||||
// program's *actual* current image_id (via `get_program`), not the message's own claim. If
|
||||
// the claim was wrong, the reconstructed journal won't match what the receipt actually
|
||||
// committed to, and `proof.is_valid_for` below fails — the same mechanism `public_actions`
|
||||
// already relies on for authenticating account content against real state.
|
||||
let program_image_claims = message
|
||||
.program_image_claims
|
||||
.iter()
|
||||
.map(|claim| {
|
||||
let (image_id, _elf) = state.get_program(claim.account_id).ok_or_else(|| {
|
||||
LeeError::InvalidInput(format!("Unknown program {}", claim.account_id))
|
||||
})?;
|
||||
Ok(ProgramImageClaim {
|
||||
account_id: claim.account_id,
|
||||
image_id,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, LeeError>>()?;
|
||||
|
||||
let output = PrivacyPreservingCircuitOutput {
|
||||
public_actions: public_pre_states
|
||||
.iter()
|
||||
@@ -552,6 +596,7 @@ fn check_privacy_preserving_circuit_proof_is_valid(
|
||||
private_actions: message.private_actions.clone(),
|
||||
block_validity_window: message.block_validity_window,
|
||||
timestamp_validity_window: message.timestamp_validity_window,
|
||||
program_image_claims,
|
||||
};
|
||||
proof
|
||||
.is_valid_for(&output)
|
||||
|
||||
@@ -161,7 +161,7 @@ fn privacy_malicious_programs_cannot_drain_public_victim() {
|
||||
let at = crate::test_methods::simple_balance_transfer();
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
crate::test_methods::malicious_injector(),
|
||||
[(p2.id(), p2), (at.id(), at)].into(),
|
||||
[(p2.id().into(), p2), (at.id().into(), at)].into(),
|
||||
);
|
||||
|
||||
// account_identities order must match self.pre_states as built by the circuit:
|
||||
@@ -322,7 +322,7 @@ fn privacy_malicious_programs_cannot_drain_private_victim() {
|
||||
let at = crate::test_methods::simple_balance_transfer();
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
crate::test_methods::malicious_injector(),
|
||||
[(p2.id(), p2), (at.id(), at)].into(),
|
||||
[(p2.id().into(), p2), (at.id().into(), at)].into(),
|
||||
);
|
||||
|
||||
// account_identities order must match self.pre_states as built by the circuit:
|
||||
@@ -529,6 +529,7 @@ fn privacy_garbage_proof_is_rejected() {
|
||||
}],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
program_image_claims: vec![],
|
||||
};
|
||||
|
||||
// Garbage proof bytes: not a valid borsh-encoded `InnerReceipt`.
|
||||
|
||||
@@ -117,7 +117,7 @@ impl LeeTransaction {
|
||||
tx, state, block_id, timestamp,
|
||||
),
|
||||
Self::ProgramDeployment(tx) => {
|
||||
ValidatedStateDiff::from_program_deployment_transaction(tx, state)
|
||||
ValidatedStateDiff::from_program_deployment_transaction(tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl Into
|
||||
private_actions,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
program_image_claims,
|
||||
} = message;
|
||||
let private_action_count = private_actions.len();
|
||||
let public_account_ids: Vec<_> = public_actions
|
||||
@@ -80,6 +81,9 @@ pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl Into
|
||||
.map(|action| action.account_id)
|
||||
.collect();
|
||||
let public_account_count = public_account_ids.len();
|
||||
// Every program invoked in a private transaction's call graph is publicly visible via this
|
||||
// claim list — see `ProgramImageClaim`.
|
||||
let programs_invoked_count = program_image_claims.len();
|
||||
let WitnessSet {
|
||||
signatures_and_public_keys: _,
|
||||
proof,
|
||||
@@ -112,6 +116,10 @@ pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl Into
|
||||
<span class="info-label">"Timestamp Validity Window:"</span>
|
||||
<span class="info-value">{timestamp_validity_window.to_string()}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">"Programs Invoked:"</span>
|
||||
<span class="info-value">{programs_invoked_count.to_string()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>"Public Accounts"</h3>
|
||||
|
||||
@@ -260,12 +260,33 @@ typedef struct FfiVec_FfiPrivateAction {
|
||||
|
||||
typedef struct FfiVec_FfiPrivateAction FfiPrivateActionList;
|
||||
|
||||
/**
|
||||
* Program ID - 8 u32 values (32 bytes total).
|
||||
*/
|
||||
typedef struct FfiProgramId {
|
||||
uint32_t data[8];
|
||||
} FfiProgramId;
|
||||
|
||||
typedef struct FfiProgramImageClaim {
|
||||
FfiAccountId account_id;
|
||||
struct FfiProgramId image_id;
|
||||
} FfiProgramImageClaim;
|
||||
|
||||
typedef struct FfiVec_FfiProgramImageClaim {
|
||||
struct FfiProgramImageClaim *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiProgramImageClaim;
|
||||
|
||||
typedef struct FfiVec_FfiProgramImageClaim FfiProgramImageClaimList;
|
||||
|
||||
typedef struct FfiPrivacyPreservingMessage {
|
||||
FfiPublicActionList public_actions;
|
||||
FfiNonceList nonces;
|
||||
FfiPrivateActionList private_actions;
|
||||
uint64_t block_validity_window[2];
|
||||
uint64_t timestamp_validity_window[2];
|
||||
FfiProgramImageClaimList program_image_claims;
|
||||
} FfiPrivacyPreservingMessage;
|
||||
|
||||
typedef FfiVecU8 FfiProof;
|
||||
|
||||
@@ -2,17 +2,18 @@ use indexer_service_protocol::{
|
||||
AccountId, Ciphertext, Commitment, CommitmentSetDigest, EncryptedAccountData,
|
||||
EphemeralPublicKey, HashType, Nullifier, PrivacyPreservingMessage,
|
||||
PrivacyPreservingTransaction, PrivateAction, ProgramDeploymentMessage,
|
||||
ProgramDeploymentTransaction, Proof, PublicActionWithID, PublicKey, PublicMessage,
|
||||
PublicTransaction, Signature, Transaction, ValidityWindow, WitnessSet,
|
||||
ProgramDeploymentTransaction, ProgramImageClaim, Proof, PublicActionWithID, PublicKey,
|
||||
PublicMessage, PublicTransaction, Signature, Transaction, ValidityWindow, WitnessSet,
|
||||
};
|
||||
|
||||
use crate::api::types::{
|
||||
FfiAccountId, FfiBytes32, FfiHashType, FfiOption, FfiPublicKey, FfiSignature, FfiVec,
|
||||
FfiAccountId, FfiBytes32, FfiHashType, FfiOption, FfiProgramId, FfiPublicKey, FfiSignature,
|
||||
FfiVec,
|
||||
account::FfiAccount,
|
||||
vectors::{
|
||||
FfiAccountIdList, FfiInstructionDataList, FfiNonceList, FfiPrivateActionList,
|
||||
FfiProgramDeploymentMessage, FfiProof, FfiPublicActionList, FfiSignaturePubKeyList,
|
||||
FfiVecU8,
|
||||
FfiProgramDeploymentMessage, FfiProgramImageClaimList, FfiProof, FfiPublicActionList,
|
||||
FfiSignaturePubKeyList, FfiVecU8,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -193,6 +194,18 @@ impl From<Box<FfiPrivateTransactionBody>> for PrivacyPreservingTransaction {
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
program_image_claims: {
|
||||
let std_vec: Vec<_> = value.message.program_image_claims.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| ProgramImageClaim {
|
||||
account_id: AccountId {
|
||||
value: ffi_val.account_id.data,
|
||||
},
|
||||
image_id: ffi_val.image_id.data.into(),
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
block_validity_window: cast_ffi_validity_window(
|
||||
value.message.block_validity_window,
|
||||
),
|
||||
@@ -238,6 +251,21 @@ impl From<PublicActionWithID> for FfiPublicAction {
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiProgramImageClaim {
|
||||
pub account_id: FfiAccountId,
|
||||
pub image_id: FfiProgramId,
|
||||
}
|
||||
|
||||
impl From<ProgramImageClaim> for FfiProgramImageClaim {
|
||||
fn from(value: ProgramImageClaim) -> Self {
|
||||
Self {
|
||||
account_id: value.account_id.into(),
|
||||
image_id: value.image_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiPrivateAction {
|
||||
pub nullifier: FfiBytes32,
|
||||
@@ -268,6 +296,7 @@ pub struct FfiPrivacyPreservingMessage {
|
||||
pub private_actions: FfiPrivateActionList,
|
||||
pub block_validity_window: [u64; 2],
|
||||
pub timestamp_validity_window: [u64; 2],
|
||||
pub program_image_claims: FfiProgramImageClaimList,
|
||||
}
|
||||
|
||||
impl From<PrivacyPreservingMessage> for FfiPrivacyPreservingMessage {
|
||||
@@ -278,6 +307,7 @@ impl From<PrivacyPreservingMessage> for FfiPrivacyPreservingMessage {
|
||||
private_actions,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
program_image_claims,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
@@ -298,6 +328,11 @@ impl From<PrivacyPreservingMessage> for FfiPrivacyPreservingMessage {
|
||||
.into(),
|
||||
block_validity_window: cast_validity_window(block_validity_window),
|
||||
timestamp_validity_window: cast_validity_window(timestamp_validity_window),
|
||||
program_image_claims: program_image_claims
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::api::types::{
|
||||
FfiAccountId, FfiNonce, FfiVec,
|
||||
transaction::{FfiPrivateAction, FfiPublicAction, FfiSignaturePubKeyEntry, FfiTransaction},
|
||||
transaction::{
|
||||
FfiPrivateAction, FfiProgramImageClaim, FfiPublicAction, FfiSignaturePubKeyEntry,
|
||||
FfiTransaction,
|
||||
},
|
||||
};
|
||||
|
||||
pub type FfiVecU8 = FfiVec<u8>;
|
||||
@@ -22,3 +25,5 @@ pub type FfiProgramDeploymentMessage = FfiVecU8;
|
||||
pub type FfiPublicActionList = FfiVec<FfiPublicAction>;
|
||||
|
||||
pub type FfiPrivateActionList = FfiVec<FfiPrivateAction>;
|
||||
|
||||
pub type FfiProgramImageClaimList = FfiVec<FfiProgramImageClaim>;
|
||||
|
||||
@@ -7,7 +7,8 @@ use crate::{
|
||||
Commitment, CommitmentSetDigest, CrossZoneHalt, Data, EncryptedAccountData, EphemeralPublicKey,
|
||||
HashType, IndexerStatus, IndexerSyncState, Nullifier, PeerHealth, PeerStatus,
|
||||
PrivacyPreservingMessage, PrivacyPreservingTransaction, PrivateAction,
|
||||
ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, Proof, PublicActionWithID,
|
||||
ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, ProgramImageClaim, Proof,
|
||||
PublicActionWithID,
|
||||
PublicKey, PublicMessage, PublicTransaction, Signature, StallReason, Transaction,
|
||||
ValidityWindow, WitnessSet,
|
||||
};
|
||||
@@ -28,6 +29,24 @@ impl From<ProgramId> for [u32; 8] {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee_core::ProgramImageClaim> for ProgramImageClaim {
|
||||
fn from(value: lee_core::ProgramImageClaim) -> Self {
|
||||
Self {
|
||||
account_id: value.account_id.into(),
|
||||
image_id: value.image_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProgramImageClaim> for lee_core::ProgramImageClaim {
|
||||
fn from(value: ProgramImageClaim) -> Self {
|
||||
Self {
|
||||
account_id: value.account_id.into(),
|
||||
image_id: value.image_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee_core::account::AccountId> for AccountId {
|
||||
fn from(value: lee_core::account::AccountId) -> Self {
|
||||
Self {
|
||||
@@ -308,6 +327,7 @@ impl From<lee::privacy_preserving_transaction::message::Message> for PrivacyPres
|
||||
private_actions,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
program_image_claims,
|
||||
} = value;
|
||||
Self {
|
||||
public_actions: public_actions.into_iter().map(Into::into).collect(),
|
||||
@@ -315,6 +335,7 @@ impl From<lee::privacy_preserving_transaction::message::Message> for PrivacyPres
|
||||
private_actions: private_actions.into_iter().map(Into::into).collect(),
|
||||
block_validity_window: block_validity_window.into(),
|
||||
timestamp_validity_window: timestamp_validity_window.into(),
|
||||
program_image_claims: program_image_claims.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -356,6 +377,7 @@ impl TryFrom<PrivacyPreservingMessage> for lee::privacy_preserving_transaction::
|
||||
private_actions,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
program_image_claims,
|
||||
} = value;
|
||||
|
||||
let public_actions = public_actions
|
||||
@@ -377,6 +399,7 @@ impl TryFrom<PrivacyPreservingMessage> for lee::privacy_preserving_transaction::
|
||||
timestamp_validity_window: timestamp_validity_window
|
||||
.try_into()
|
||||
.map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?,
|
||||
program_image_claims: program_image_claims.into_iter().map(Into::into).collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +250,13 @@ pub struct PrivacyPreservingMessage {
|
||||
pub private_actions: Vec<PrivateAction>,
|
||||
pub block_validity_window: ValidityWindow,
|
||||
pub timestamp_validity_window: ValidityWindow,
|
||||
pub program_image_claims: Vec<ProgramImageClaim>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ProgramImageClaim {
|
||||
pub account_id: AccountId,
|
||||
pub image_id: ProgramId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -409,6 +409,7 @@ fn mock_privacy_preserving_tx(
|
||||
}],
|
||||
block_validity_window: ValidityWindow((None, None)),
|
||||
timestamp_validity_window: ValidityWindow((None, None)),
|
||||
program_image_claims: vec![],
|
||||
},
|
||||
witness_set: WitnessSet {
|
||||
signatures_and_public_keys: vec![],
|
||||
|
||||
@@ -1,54 +1,71 @@
|
||||
pub use lee_core::program::PdaSeed;
|
||||
pub use lee_core::program::{PdaSeed, ProgramData};
|
||||
use lee_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, Claim, ProgramId},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const DEPLOY_SEED_DOMAIN_SEPARATOR: AccountId =
|
||||
AccountId::new(*b"/LEZ/v0.3/LoaderDeploySeed/00000");
|
||||
const DEPLOY_HEADER_SEED_DOMAIN_SEPARATOR: AccountId =
|
||||
AccountId::new(*b"/LEZ/v0.3/LoaderDeployHeaderSeed");
|
||||
const DEPLOY_SEGMENT_SEED_DOMAIN_SEPARATOR: AccountId =
|
||||
AccountId::new(*b"/LEZ/v0.3/LoaderDeploySegmentSee");
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Instruction {
|
||||
/// Deploys a new program, claiming its `ProgramData` account as a PDA of the loader.
|
||||
/// Deploys a new program: writes its `ProgramData` header and one bytecode segment, each
|
||||
/// claimed as a PDA of the loader.
|
||||
///
|
||||
/// Required accounts (1):
|
||||
/// - The target `ProgramData` PDA account (must be `Account::default()`)
|
||||
/// Required accounts (2), in order:
|
||||
/// - The target `ProgramData` header PDA account (must be `Account::default()`)
|
||||
/// - The target segment PDA account holding the raw bytecode (must be `Account::default()`)
|
||||
Deploy { bytecode: Vec<u8> },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, borsh::BorshSerialize, borsh::BorshDeserialize)]
|
||||
pub struct ProgramData {
|
||||
pub image_id: ProgramId,
|
||||
pub segment_number: u32,
|
||||
pub update_auth: AccountId,
|
||||
pub elf_segment: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TryFrom<&Data> for ProgramData {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn try_from(data: &Data) -> Result<Self, Self::Error> {
|
||||
borsh::BorshDeserialize::try_from_slice(data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ProgramData> for Data {
|
||||
fn from(program_data: &ProgramData) -> Self {
|
||||
let mut data = Vec::with_capacity(std::mem::size_of_val(program_data));
|
||||
borsh::BorshSerialize::serialize(program_data, &mut data)
|
||||
.expect("borsh serialization should not fail");
|
||||
Self::try_from(data).expect("elf must fit under DATA_MAX_LENGTH")
|
||||
}
|
||||
}
|
||||
|
||||
/// Derives the PDA seed for a deployed program's `ProgramData` account.
|
||||
/// Derives the PDA seed for a deployed program's `ProgramData` header account.
|
||||
///
|
||||
/// Domain-separated from other PDA-seed derivations in the codebase so that a `deploy_pda_seed`
|
||||
/// output can never collide with a seed meant for a different purpose, even if the input triple
|
||||
/// happened to coincide.
|
||||
/// Combines the program's content-derived identity (`image_id`), its position in a (currently
|
||||
/// always single-segment) split (`segment_number`), and who may redeploy it (`update_auth`).
|
||||
///
|
||||
/// Domain-separated from other PDA-seed derivations in the codebase, including
|
||||
/// [`deploy_segment_pda_seed`], so a header seed can never collide with a segment seed (or
|
||||
/// anything else) even when the input triple coincides.
|
||||
#[must_use]
|
||||
pub fn deploy_pda_seed(
|
||||
pub fn deploy_header_pda_seed(
|
||||
image_id: ProgramId,
|
||||
segment_number: u32,
|
||||
update_auth: AccountId,
|
||||
) -> PdaSeed {
|
||||
deploy_seed(
|
||||
DEPLOY_HEADER_SEED_DOMAIN_SEPARATOR,
|
||||
image_id,
|
||||
segment_number,
|
||||
update_auth,
|
||||
)
|
||||
}
|
||||
|
||||
/// Derives the PDA seed for a deployed program's bytecode segment account.
|
||||
///
|
||||
/// Same inputs as [`deploy_header_pda_seed`], domain-separated so the two never collide. Kept as
|
||||
/// a distinct account from the header specifically so that authenticating a program's identity
|
||||
/// (e.g. for privacy-preserving proof verification) never has to touch its bytecode: the only
|
||||
/// account-authentication primitive available is whole-account equality, so what's bundled into
|
||||
/// one account sets the floor for how cheap that authentication can be.
|
||||
#[must_use]
|
||||
pub fn deploy_segment_pda_seed(
|
||||
image_id: ProgramId,
|
||||
segment_number: u32,
|
||||
update_auth: AccountId,
|
||||
) -> PdaSeed {
|
||||
deploy_seed(
|
||||
DEPLOY_SEGMENT_SEED_DOMAIN_SEPARATOR,
|
||||
image_id,
|
||||
segment_number,
|
||||
update_auth,
|
||||
)
|
||||
}
|
||||
|
||||
fn deploy_seed(
|
||||
domain_separator: AccountId,
|
||||
image_id: ProgramId,
|
||||
segment_number: u32,
|
||||
update_auth: AccountId,
|
||||
@@ -56,7 +73,7 @@ pub fn deploy_pda_seed(
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
|
||||
let mut bytes = [0_u8; 32 + 32 + 4 + 32];
|
||||
bytes[0..32].copy_from_slice(DEPLOY_SEED_DOMAIN_SEPARATOR.as_ref());
|
||||
bytes[0..32].copy_from_slice(domain_separator.as_ref());
|
||||
let image_id_bytes: &[u8] =
|
||||
bytemuck::try_cast_slice(&image_id).expect("ProgramId should be castable to &[u8]");
|
||||
bytes[32..64].copy_from_slice(image_id_bytes);
|
||||
@@ -72,7 +89,7 @@ pub fn deploy_pda_seed(
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn deploy_account_id(
|
||||
pub fn deploy_header_account_id(
|
||||
loader_program_id: ProgramId,
|
||||
image_id: ProgramId,
|
||||
segment_number: u32,
|
||||
@@ -80,12 +97,27 @@ pub fn deploy_account_id(
|
||||
) -> AccountId {
|
||||
AccountId::for_public_pda(
|
||||
&loader_program_id,
|
||||
&deploy_pda_seed(image_id, segment_number, update_auth),
|
||||
&deploy_header_pda_seed(image_id, segment_number, update_auth),
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn deploy_segment_account_id(
|
||||
loader_program_id: ProgramId,
|
||||
image_id: ProgramId,
|
||||
segment_number: u32,
|
||||
update_auth: AccountId,
|
||||
) -> AccountId {
|
||||
AccountId::for_public_pda(
|
||||
&loader_program_id,
|
||||
&deploy_segment_pda_seed(image_id, segment_number, update_auth),
|
||||
)
|
||||
}
|
||||
|
||||
/// Executes the `Deploy` instruction: verifies `bytecode` decodes as a valid RISC0 program
|
||||
/// binary, derives its `ProgramData` PDA, and claims it.
|
||||
/// binary, derives its header and segment PDAs, and claims both. Called natively from
|
||||
/// dispatch's `RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID` shortcut (see that constant's doc
|
||||
/// comment in `lee_core::program`).
|
||||
#[must_use]
|
||||
pub fn execute_deploy(
|
||||
self_program_id: ProgramId,
|
||||
@@ -97,32 +129,54 @@ pub fn execute_deploy(
|
||||
.into();
|
||||
let segment_number = 0_u32;
|
||||
let update_auth = AccountId::default();
|
||||
let seed = deploy_pda_seed(image_id, segment_number, update_auth);
|
||||
let pda = AccountId::for_public_pda(&self_program_id, &seed);
|
||||
let header_seed = deploy_header_pda_seed(image_id, segment_number, update_auth);
|
||||
let segment_seed = deploy_segment_pda_seed(image_id, segment_number, update_auth);
|
||||
let header_pda = AccountId::for_public_pda(&self_program_id, &header_seed);
|
||||
let segment_pda = AccountId::for_public_pda(&self_program_id, &segment_seed);
|
||||
|
||||
let [target] = pre_states
|
||||
let [header_target, segment_target] = pre_states
|
||||
.try_into()
|
||||
.expect("Deploy requires exactly 1 account");
|
||||
.expect("Deploy requires exactly 2 accounts");
|
||||
|
||||
assert_eq!(target.account_id, pda, "wrong deployment target account");
|
||||
assert_eq!(
|
||||
target.account,
|
||||
header_target.account_id, header_pda,
|
||||
"wrong deployment header target account"
|
||||
);
|
||||
assert_eq!(
|
||||
header_target.account,
|
||||
Account::default(),
|
||||
"program already deployed"
|
||||
"program header already deployed"
|
||||
);
|
||||
assert_eq!(
|
||||
segment_target.account_id, segment_pda,
|
||||
"wrong deployment segment target account"
|
||||
);
|
||||
assert_eq!(
|
||||
segment_target.account,
|
||||
Account::default(),
|
||||
"program segment already deployed"
|
||||
);
|
||||
|
||||
let program_data = ProgramData {
|
||||
image_id,
|
||||
segment_number,
|
||||
update_auth,
|
||||
elf_segment: bytecode,
|
||||
};
|
||||
|
||||
vec![AccountPostState::new_claimed(
|
||||
Account {
|
||||
data: Data::from(&program_data),
|
||||
..Account::default()
|
||||
},
|
||||
Claim::Pda(seed),
|
||||
)]
|
||||
vec![
|
||||
AccountPostState::new_claimed(
|
||||
Account {
|
||||
data: Data::from(&program_data),
|
||||
..Account::default()
|
||||
},
|
||||
Claim::Pda(header_seed),
|
||||
),
|
||||
AccountPostState::new_claimed(
|
||||
Account {
|
||||
data: Data::try_from(bytecode).expect("elf must fit under DATA_MAX_LENGTH"),
|
||||
..Account::default()
|
||||
},
|
||||
Claim::Pda(segment_seed),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1672,8 +1672,12 @@ async fn user_tx_that_chain_calls_clock_is_dropped() {
|
||||
|
||||
let clock_chain_caller = test_programs::clock_chain_caller();
|
||||
// Deploy the clock_chain_caller test program.
|
||||
let deploy_tx = LeeTransaction::ProgramDeployment(lee::ProgramDeploymentTransaction::new(
|
||||
lee::program_deployment_transaction::Message::new(clock_chain_caller.elf().to_owned()),
|
||||
let bytecode = clock_chain_caller.elf().to_vec();
|
||||
let (clock_chain_caller_header, clock_chain_caller_segment) = deploy_targets(&bytecode);
|
||||
let deploy_tx = LeeTransaction::Public(deploy_transaction(
|
||||
clock_chain_caller_header,
|
||||
clock_chain_caller_segment,
|
||||
bytecode,
|
||||
));
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, deploy_tx))
|
||||
@@ -1684,12 +1688,11 @@ async fn user_tx_that_chain_calls_clock_is_dropped() {
|
||||
// Build a user transaction that invokes clock_chain_caller, which in turn chain-calls the
|
||||
// clock program with the clock accounts. The sequencer should detect that the resulting
|
||||
// state diff modifies clock accounts and drop the transaction.
|
||||
let clock_chain_caller_id = test_programs::clock_chain_caller().id();
|
||||
let clock_program_id = programs::clock().id();
|
||||
let timestamp: u64 = 0;
|
||||
|
||||
let message = lee::public_transaction::Message::try_new(
|
||||
clock_chain_caller_id.into(),
|
||||
clock_chain_caller_header,
|
||||
system_accounts::clock_account_ids().to_vec(),
|
||||
vec![], // no signers
|
||||
(clock_program_id, timestamp),
|
||||
@@ -3666,11 +3669,26 @@ fn the_bootstrap_sequencer_can_request_an_unstake_of_its_genesis_stake() {
|
||||
);
|
||||
}
|
||||
|
||||
fn deploy_transaction(target: AccountId, bytecode: Vec<u8>) -> PublicTransaction {
|
||||
/// Derives the `(header, segment)` account pair `bytecode` would deploy to.
|
||||
fn deploy_targets(bytecode: &[u8]) -> (AccountId, AccountId) {
|
||||
let loader_id: ProgramId = RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID.into();
|
||||
let image_id: ProgramId = risc0_binfmt::compute_image_id(bytecode).unwrap().into();
|
||||
let header =
|
||||
program_loader_core::deploy_header_account_id(loader_id, image_id, 0, AccountId::default());
|
||||
let segment =
|
||||
program_loader_core::deploy_segment_account_id(loader_id, image_id, 0, AccountId::default());
|
||||
(header, segment)
|
||||
}
|
||||
|
||||
fn deploy_transaction(
|
||||
header: AccountId,
|
||||
segment: AccountId,
|
||||
bytecode: Vec<u8>,
|
||||
) -> PublicTransaction {
|
||||
let loader_id: ProgramId = RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID.into();
|
||||
let message = lee::public_transaction::Message::try_new(
|
||||
loader_id.into(),
|
||||
vec![target],
|
||||
vec![header, segment],
|
||||
vec![],
|
||||
program_loader_core::Instruction::Deploy { bytecode },
|
||||
)
|
||||
@@ -3681,51 +3699,86 @@ fn deploy_transaction(target: AccountId, bytecode: Vec<u8>) -> PublicTransaction
|
||||
|
||||
#[test]
|
||||
fn loader_deploys_program() {
|
||||
let loader_id: ProgramId = RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID.into();
|
||||
let mut state = V03State::new();
|
||||
|
||||
let bytecode = test_programs::claimer().elf().to_vec();
|
||||
let image_id: ProgramId = risc0_binfmt::compute_image_id(&bytecode).unwrap().into();
|
||||
let target =
|
||||
program_loader_core::deploy_account_id(loader_id, image_id, 0, AccountId::default());
|
||||
let (header, segment) = deploy_targets(&bytecode);
|
||||
|
||||
assert_eq!(state.get_account_by_id(target), Account::default());
|
||||
assert_eq!(state.get_account_by_id(header), Account::default());
|
||||
assert_eq!(state.get_account_by_id(segment), Account::default());
|
||||
|
||||
let tx = deploy_transaction(target, bytecode.clone());
|
||||
let tx = deploy_transaction(header, segment, bytecode.clone());
|
||||
state
|
||||
.transition_from_public_transaction(&tx, 1, 0)
|
||||
.expect("Deploy should succeed against an unclaimed target");
|
||||
.expect("Deploy should succeed against unclaimed targets");
|
||||
|
||||
let deployed = state.get_account_by_id(target);
|
||||
let deployed_header = state.get_account_by_id(header);
|
||||
assert_eq!(
|
||||
deployed.program_owner,
|
||||
deployed_header.program_owner,
|
||||
RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID
|
||||
);
|
||||
|
||||
let program_data = program_loader_core::ProgramData::try_from(&deployed.data)
|
||||
.expect("deployed account data should decode as ProgramData");
|
||||
let program_data = program_loader_core::ProgramData::try_from(&deployed_header.data)
|
||||
.expect("deployed header account data should decode as ProgramData");
|
||||
assert_eq!(program_data.image_id, image_id);
|
||||
assert_eq!(program_data.segment_number, 0);
|
||||
assert_eq!(program_data.update_auth, AccountId::default());
|
||||
assert_eq!(program_data.elf_segment, bytecode);
|
||||
|
||||
let deployed_segment = state.get_account_by_id(segment);
|
||||
assert_eq!(
|
||||
deployed_segment.program_owner,
|
||||
RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID
|
||||
);
|
||||
assert_eq!(deployed_segment.data.to_vec(), bytecode);
|
||||
}
|
||||
|
||||
/// A `Deploy`-created program must be a fully ordinary dispatch target afterward: `get_program`
|
||||
/// has to find it by decoding the `ProgramData` header and locating its separate segment account,
|
||||
/// and dispatch has to actually execute it.
|
||||
#[test]
|
||||
fn loader_deployed_program_is_invocable_via_dispatch() {
|
||||
let mut state = V03State::new();
|
||||
|
||||
let bytecode = test_programs::claimer().elf().to_vec();
|
||||
let (header, segment) = deploy_targets(&bytecode);
|
||||
|
||||
let tx = deploy_transaction(header, segment, bytecode);
|
||||
state
|
||||
.transition_from_public_transaction(&tx, 1, 0)
|
||||
.expect("Deploy should succeed against unclaimed targets");
|
||||
|
||||
// `claimer` claims its one pre_state account with `Claim::Authorized`, which requires the
|
||||
// account to be signed for and to start out default-owned.
|
||||
let key = PrivateKey::try_new([7; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
state.force_insert_account(account_id, Account::default());
|
||||
|
||||
let message =
|
||||
lee::public_transaction::Message::try_new(header, vec![account_id], vec![Nonce(0)], ())
|
||||
.unwrap();
|
||||
let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[&key]);
|
||||
let invoke_tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state
|
||||
.transition_from_public_transaction(&invoke_tx, 2, 0)
|
||||
.expect("a Deploy-created program must be dispatchable and executable");
|
||||
|
||||
assert_eq!(state.get_account_by_id(account_id).program_owner, header);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loader_rejects_redeploying_an_already_deployed_program() {
|
||||
let loader_id: ProgramId = RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID.into();
|
||||
let mut state = V03State::new();
|
||||
|
||||
let bytecode = test_programs::claimer().elf().to_vec();
|
||||
let image_id: ProgramId = risc0_binfmt::compute_image_id(&bytecode).unwrap().into();
|
||||
let target =
|
||||
program_loader_core::deploy_account_id(loader_id, image_id, 0, AccountId::default());
|
||||
let (header, segment) = deploy_targets(&bytecode);
|
||||
|
||||
let tx = deploy_transaction(target, bytecode.clone());
|
||||
let tx = deploy_transaction(header, segment, bytecode.clone());
|
||||
state
|
||||
.transition_from_public_transaction(&tx, 1, 0)
|
||||
.expect("First deploy should succeed");
|
||||
|
||||
let tx = deploy_transaction(target, bytecode);
|
||||
let tx = deploy_transaction(header, segment, bytecode);
|
||||
let result = state.transition_from_public_transaction(&tx, 2, 0);
|
||||
|
||||
assert!(
|
||||
@@ -3738,12 +3791,13 @@ fn loader_rejects_redeploying_an_already_deployed_program() {
|
||||
fn loader_rejects_invalid_bytecode() {
|
||||
let mut state = V03State::new();
|
||||
|
||||
// execute_deploy panics on compute_image_id before it ever looks at the target account, so
|
||||
// any account works here.
|
||||
// execute_deploy panics on compute_image_id before it ever looks at the target accounts, so
|
||||
// any accounts work here.
|
||||
let bytecode = b"this is not a valid RISC0 program binary".to_vec();
|
||||
let target = AccountId::new([7; 32]);
|
||||
let header = AccountId::new([7; 32]);
|
||||
let segment = AccountId::new([8; 32]);
|
||||
|
||||
let tx = deploy_transaction(target, bytecode);
|
||||
let tx = deploy_transaction(header, segment, bytecode);
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(
|
||||
@@ -3757,10 +3811,11 @@ fn loader_rejects_wrong_target_account() {
|
||||
let mut state = V03State::new();
|
||||
|
||||
let bytecode = test_programs::claimer().elf().to_vec();
|
||||
let (_correct_header, segment) = deploy_targets(&bytecode);
|
||||
// Deliberately not the PDA this bytecode's image_id would derive to.
|
||||
let wrong_target = AccountId::new([7; 32]);
|
||||
let wrong_header = AccountId::new([7; 32]);
|
||||
|
||||
let tx = deploy_transaction(wrong_target, bytecode);
|
||||
let tx = deploy_transaction(wrong_header, segment, bytecode);
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(
|
||||
@@ -3775,14 +3830,12 @@ fn loader_rejects_wrong_number_of_accounts() {
|
||||
let mut state = V03State::new();
|
||||
|
||||
let bytecode = test_programs::claimer().elf().to_vec();
|
||||
let image_id: ProgramId = risc0_binfmt::compute_image_id(&bytecode).unwrap().into();
|
||||
let target =
|
||||
program_loader_core::deploy_account_id(loader_id, image_id, 0, AccountId::default());
|
||||
let (header, segment) = deploy_targets(&bytecode);
|
||||
let extra = AccountId::new([9; 32]);
|
||||
|
||||
let message = lee::public_transaction::Message::try_new(
|
||||
loader_id.into(),
|
||||
vec![target, extra],
|
||||
vec![header, segment, extra],
|
||||
vec![],
|
||||
program_loader_core::Instruction::Deploy { bytecode },
|
||||
)
|
||||
@@ -3821,8 +3874,7 @@ fn loader_deploys_program_via_chained_call() {
|
||||
|
||||
let bytecode = test_programs::claimer().elf().to_vec();
|
||||
let image_id: ProgramId = risc0_binfmt::compute_image_id(&bytecode).unwrap().into();
|
||||
let target =
|
||||
program_loader_core::deploy_account_id(loader_id, image_id, 0, AccountId::default());
|
||||
let (header, segment) = deploy_targets(&bytecode);
|
||||
|
||||
let inner_instruction_data =
|
||||
lee::program::Program::serialize_instruction(program_loader_core::Instruction::Deploy {
|
||||
@@ -3832,7 +3884,7 @@ fn loader_deploys_program_via_chained_call() {
|
||||
|
||||
let message = lee::public_transaction::Message::try_new(
|
||||
forwarder.id().into(),
|
||||
vec![target],
|
||||
vec![header, segment],
|
||||
vec![],
|
||||
(loader_id, inner_instruction_data),
|
||||
)
|
||||
@@ -3844,14 +3896,16 @@ fn loader_deploys_program_via_chained_call() {
|
||||
.transition_from_public_transaction(&tx, 1, 0)
|
||||
.expect("Deploy via chained call should succeed");
|
||||
|
||||
let deployed = state.get_account_by_id(target);
|
||||
let deployed_header = state.get_account_by_id(header);
|
||||
assert_eq!(
|
||||
deployed.program_owner,
|
||||
deployed_header.program_owner,
|
||||
RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID
|
||||
);
|
||||
|
||||
let program_data = program_loader_core::ProgramData::try_from(&deployed.data)
|
||||
.expect("deployed account data should decode as ProgramData");
|
||||
let program_data = program_loader_core::ProgramData::try_from(&deployed_header.data)
|
||||
.expect("deployed header account data should decode as ProgramData");
|
||||
assert_eq!(program_data.image_id, image_id);
|
||||
assert_eq!(program_data.elf_segment, bytecode);
|
||||
|
||||
let deployed_segment = state.get_account_by_id(segment);
|
||||
assert_eq!(deployed_segment.data.to_vec(), bytecode);
|
||||
}
|
||||
|
||||
@@ -88,13 +88,10 @@ impl TryFrom<&FfiProgramWithDependencies> for ProgramWithDependencies {
|
||||
.ok_or(WalletFfiError::NullPointer)?
|
||||
.try_into()?;
|
||||
|
||||
program_map.insert(program_dep.id(), program_dep);
|
||||
program_map.insert(program_dep.id().into(), program_dep);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
program: orig_program,
|
||||
dependencies: program_map,
|
||||
})
|
||||
Ok(Self::new(orig_program, program_map))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +223,6 @@ impl Ata<'_> {
|
||||
fn ata_with_token_dependency() -> ProgramWithDependencies {
|
||||
let token = programs::token();
|
||||
let mut deps = HashMap::new();
|
||||
deps.insert(token.id(), token);
|
||||
deps.insert(token.id().into(), token);
|
||||
ProgramWithDependencies::new(programs::ata(), deps)
|
||||
}
|
||||
|
||||
@@ -132,6 +132,6 @@ impl Vault<'_> {
|
||||
fn vault_with_auth_dependency() -> ProgramWithDependencies {
|
||||
let auth_transfer = programs::authenticated_transfer();
|
||||
let mut deps = HashMap::new();
|
||||
deps.insert(auth_transfer.id(), auth_transfer);
|
||||
deps.insert(auth_transfer.id().into(), auth_transfer);
|
||||
ProgramWithDependencies::new(programs::vault(), deps)
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ fn prove_chain_caller(
|
||||
let auth_transfer = programs::authenticated_transfer();
|
||||
let auth_transfer_id = auth_transfer.id();
|
||||
let mut deps = HashMap::new();
|
||||
deps.insert(auth_transfer.id(), auth_transfer);
|
||||
deps.insert(auth_transfer.id().into(), auth_transfer);
|
||||
let pwd = ProgramWithDependencies::new(chain_caller, deps);
|
||||
|
||||
// Both accounts pre-claimed by auth_transfer. chain_caller doesn't
|
||||
|
||||
Reference in New Issue
Block a user