Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9b92e7c
Add constructors
d0cd Mar 15, 2025
de65282
Add program checksum and edition to stack
d0cd Mar 15, 2025
341078d
Adjust operand
d0cd Mar 15, 2025
0f19c3a
Load operand
d0cd Mar 15, 2025
5906dfe
Set edition to default
d0cd Mar 15, 2025
8f97617
Implement constructors, add basic tests
d0cd Mar 18, 2025
0710e19
Fix and add test
d0cd Mar 18, 2025
091f43f
Fix expectation
d0cd Mar 18, 2025
b28c317
Fix comment
d0cd Mar 18, 2025
9f2e8e0
Cleanup
d0cd Mar 19, 2025
1277470
Feedback
d0cd Mar 20, 2025
d032476
Update good_constructor test, allow other FinalizeOperations
d0cd Mar 20, 2025
2ca3f04
Fine-grained pricing for constructors
d0cd Mar 20, 2025
e2c4f75
Feedback
d0cd Mar 20, 2025
5da640e
Feedback
d0cd Mar 20, 2025
9646b02
Feedback
d0cd Mar 20, 2025
60784ef
Feedback
d0cd Mar 20, 2025
b2dcf99
Feedback
d0cd Mar 20, 2025
8179ffb
Feedback
d0cd Mar 20, 2025
2f35cf9
Feedback
d0cd Mar 20, 2025
ede2b81
Feedback
d0cd Mar 20, 2025
f4aa8a2
Merge branch 'feat/constructors-and-operands' into review/constructor…
d0cd Mar 21, 2025
542adb7
Merge pull request #2658 from ProvableHQ/review/constructors-and-oper…
d0cd Mar 21, 2025
cb1ea9f
Feedback
d0cd Mar 21, 2025
83b319e
Feedback
d0cd Mar 21, 2025
56f27ad
Feedback
d0cd Mar 27, 2025
e0d0882
Remove edition and checksum as keywords
d0cd Mar 27, 2025
fd173bc
Feedback
d0cd Mar 28, 2025
3e0b08c
Removed constructor from reserved keywords
d0cd Apr 15, 2025
01e228e
Merge branch 'staging' into feat/constructors-and-operands
d0cd Apr 18, 2025
c4e8c3b
Merge branch 'staging' into feat/constructors-and-operands
d0cd May 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ledger/block/src/transaction/merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ impl<N: Network> Transaction<N> {
// Ensure the number of functions is within the allowed range.
ensure!(
num_transitions < Self::MAX_TRANSITIONS, // Note: Observe we hold back 1 for the fee.
"Execution must contain less than {num_transitions} transitions, found {}",
"Execution must contain less than {} transitions, found {num_transitions}",
Self::MAX_TRANSITIONS,
);
Ok(())
Expand Down
32 changes: 17 additions & 15 deletions ledger/block/src/transactions/confirmed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,22 @@ impl<N: Network> ConfirmedTransaction<N> {
}
};

// Count the number of `InitializeMapping` and `UpdateKeyValue` finalize operations.
let (num_initialize_mappings, num_update_key_values) =
finalize_operations.iter().try_fold((0, 0), |(init, update), operation| match operation {
FinalizeOperation::InitializeMapping(..) => Ok((init + 1, update)),
FinalizeOperation::UpdateKeyValue(..) => Ok((init, update + 1)),
// Count the number of `InitializeMapping` and `*KeyValue` finalize operations.
let (num_initialize_mappings, num_key_values) =
finalize_operations.iter().try_fold((0, 0), |(init, key_value), operation| match operation {
FinalizeOperation::InitializeMapping(..) => Ok((init + 1, key_value)),
FinalizeOperation::InsertKeyValue(..) // At the time of writing, `InsertKeyValue` is only used in tests. However, it is added for completeness, as it is a valid operation.
| FinalizeOperation::RemoveKeyValue(..)
Comment thread
vicsn marked this conversation as resolved.
| FinalizeOperation::UpdateKeyValue(..) => Ok((init, key_value + 1)),
op => {
bail!("Transaction '{}' (deploy) contains an invalid finalize operation ({op})", transaction.id())
}
})?;

// Perform safety checks on the finalize operations.
{
// Ensure the number of finalize operations matches the number of 'InitializeMapping' and 'UpdateKeyValue' finalize operations.
if num_initialize_mappings + num_update_key_values != finalize_operations.len() {
// Ensure the number of finalize operations matches the number of 'InitializeMapping' and '*KeyValue' finalize operations.
if num_initialize_mappings + num_key_values != finalize_operations.len() {
bail!(
"Transaction '{}' (deploy) must contain '{}' operations",
transaction.id(),
Expand All @@ -79,14 +81,14 @@ impl<N: Network> ConfirmedTransaction<N> {
program.mappings().len(),
)
}
// Ensure the number of finalize operations matches the number of 'UpdateKeyValue' finalize operations.
if num_update_key_values != fee.num_finalize_operations() {
bail!(
"Transaction '{}' (deploy) must contain {} 'UpdateKeyValue' operations (found '{num_update_key_values}')",
transaction.id(),
fee.num_finalize_operations()
);
}
// Ensure the number of fee finalize operations lower bounds the number of '*KeyValue' finalize operations.
// The lower bound is due to the fact that constructors can issue '*KeyValue' operations as part of the deployment.
ensure!(
fee.num_finalize_operations() <= num_key_values,
"Transaction '{}' (deploy) must contain at least {} 'UpdateKeyValue' operations (found '{num_key_values}')",
transaction.id(),
fee.num_finalize_operations()
);
}

// Return the accepted deploy transaction.
Expand Down
7 changes: 6 additions & 1 deletion ledger/store/src/program/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -600,11 +600,16 @@ impl<N: Network, P: FinalizeStorage<N>> FinalizeStore<N, P> {
}

impl<N: Network, P: FinalizeStorage<N>> FinalizeStoreTrait<N> for FinalizeStore<N, P> {
/// Returns `true` if the given `program ID` and `mapping name` exist.
/// Returns `true` if the given `program ID` and `mapping name` is confirmed to exist.
fn contains_mapping_confirmed(&self, program_id: &ProgramID<N>, mapping_name: &Identifier<N>) -> Result<bool> {
self.storage.contains_mapping_confirmed(program_id, mapping_name)
}

/// Returns `true` if the given `program ID` and `mapping name` exist.
fn contains_mapping_speculative(&self, program_id: &ProgramID<N>, mapping_name: &Identifier<N>) -> Result<bool> {
self.storage.contains_mapping_speculative(program_id, mapping_name)
}

/// Returns `true` if the given `program ID`, `mapping name`, and `key` exist.
fn contains_key_speculative(
&self,
Expand Down
28 changes: 23 additions & 5 deletions synthesizer/process/src/cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ use console::{
program::{FinalizeType, Identifier, LiteralType, PlaintextType},
};
use ledger_block::{Deployment, Execution, Transaction};
use synthesizer_program::{CastType, Command, Finalize, Instruction, Operand, StackProgram};
use synthesizer_program::{CastType, Command, Finalize, Instruction, Operand, Program, StackProgram};

/// Returns the *minimum* cost in microcredits to publish the given deployment (total cost, (storage cost, synthesis cost, namespace cost)).
pub fn deployment_cost<N: Network>(deployment: &Deployment<N>) -> Result<(u64, (u64, u64, u64))> {
/// Returns the *minimum* cost in microcredits to publish the given deployment (total cost, (storage cost, synthesis cost, constructor cost, namespace cost)).
pub fn deployment_cost<N: Network>(deployment: &Deployment<N>) -> Result<(u64, (u64, u64, u64, u64))> {
// Determine the number of bytes in the deployment.
let size_in_bytes = deployment.size_in_bytes()?;
// Retrieve the program ID.
Expand All @@ -44,7 +44,10 @@ pub fn deployment_cost<N: Network>(deployment: &Deployment<N>) -> Result<(u64, (
// Compute the synthesis cost in microcredits.
let synthesis_cost = num_combined_variables.saturating_add(num_combined_constraints) * N::SYNTHESIS_FEE_MULTIPLIER;

// Compute the namespace cost in credits: 10^(10 - num_characters).
// Compute the constructor cost in microcredits.
let constructor_cost = constructor_cost_in_microcredits(deployment.program())?;

// Compute the namespace cost in microcredits: 10^(10 - num_characters) * 1e6
let namespace_cost = 10u64
.checked_pow(10u32.saturating_sub(num_characters))
.ok_or(anyhow!("The namespace cost computation overflowed for a deployment"))?
Expand All @@ -53,10 +56,11 @@ pub fn deployment_cost<N: Network>(deployment: &Deployment<N>) -> Result<(u64, (
// Compute the total cost in microcredits.
let total_cost = storage_cost
.checked_add(synthesis_cost)
.and_then(|x| x.checked_add(constructor_cost))
.and_then(|x| x.checked_add(namespace_cost))
.ok_or(anyhow!("The total cost computation overflowed for a deployment"))?;

Ok((total_cost, (storage_cost, synthesis_cost, namespace_cost)))
Ok((total_cost, (storage_cost, synthesis_cost, constructor_cost, namespace_cost)))
Comment thread
vicsn marked this conversation as resolved.
}

/// Returns the *minimum* cost in microcredits to publish the given execution (total cost, (storage cost, finalize cost)).
Expand Down Expand Up @@ -405,6 +409,20 @@ pub fn cost_per_command<N: Network>(
}
}

/// Returns the minimum number of microcredits required to run the constructor in the given program.
/// Each command in a constructor costs 100_000 microcredits.
/// If a constructor does not exist, no cost is incurred.
pub fn constructor_cost_in_microcredits<N: Network>(program: &Program<N>) -> Result<u64> {
match program.constructor() {
Some(constructor) => {
let num_commands = constructor.commands().len() as u64;
let cost = num_commands.checked_mul(100_000).ok_or(anyhow!("Constructor cost overflowed"))?;
Comment thread
d0cd marked this conversation as resolved.
Outdated
Ok(cost)
}
None => Ok(0),
}
}

/// Returns the minimum number of microcredits required to run the finalize.
pub fn cost_in_microcredits_v2<N: Network>(stack: &Stack<N>, function_name: &Identifier<N>) -> Result<u64> {
cost_in_microcredits(stack, function_name, ConsensusFeeVersion::V2)
Expand Down
106 changes: 105 additions & 1 deletion synthesizer/process/src/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,17 @@ impl<N: Network> Process<N> {
// Initialize the mapping.
finalize_operations.push(store.initialize_mapping(*program_id, *mapping.name())?);
}
finish!(timer, "Initialize the program mappings");
lap!(timer, "Initialize the program mappings");

// If the program has a constructor, execute it and extend the finalize operations.
Comment thread
d0cd marked this conversation as resolved.
// This must happen after the mappings are initialized as the constructor may depend on them.
if deployment.program().contains_constructor() {
let operations = finalize_constructor(state, store, &stack, *fee.transition_id())?;
finalize_operations.extend(operations);
lap!(timer, "Execute the constructor");
}

finish!(timer, "Finished finalizing the deployment");
// Return the stack and finalize operations.
Ok((stack, finalize_operations))
})
Expand Down Expand Up @@ -183,6 +192,101 @@ fn finalize_fee_transition<N: Network, P: FinalizeStorage<N>>(
}
}

/// Finalizes the constructor.
fn finalize_constructor<N: Network, P: FinalizeStorage<N>>(
state: FinalizeGlobalState,
store: &FinalizeStore<N, P>,
stack: &Stack<N>,
transition_id: N::TransitionID,
) -> Result<Vec<FinalizeOperation<N>>> {
// Retrieve the program ID.
let program_id = stack.program_id();
#[cfg(debug_assertions)]
println!("Finalizing constructor for {}...", stack.program_id());

// Initialize a list for finalize operations.
let mut finalize_operations = Vec::new();

// Initialize a nonce for the constructor registers.
// Currently, this nonce is set to zero for every constructor.
let nonce = 0;

// Get the constructor logic. If the program does not have a constructor, return early.
let Some(constructor) = stack.program().constructor() else {
return Ok(finalize_operations);
};

// Get the constructor types.
let constructor_types = stack.get_constructor_types()?.clone();

// Initialize the finalize registers.
let mut registers = FinalizeRegisters::new(state, transition_id, *program_id.name(), constructor_types, nonce);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auditors: The parameters used to initialize the FinalizeRegisters here should be reviewed for correctness and soundness.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@d0cd make sure to go over all unresolved comments and copy them over in the new PR you're making.

@d0cd d0cd May 6, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gonna keep this PR, but I think we're all resolved in this one no?


// Initialize a counter for the commands.
let mut counter = 0;

// Evaluate the commands.
while counter < constructor.commands().len() {
Comment thread
raychu86 marked this conversation as resolved.
// Retrieve the command.
let command = &constructor.commands()[counter];
// Finalize the command.
match &command {
Command::BranchEq(branch_eq) => {
let result =
try_vm_runtime!(|| branch_to(counter, branch_eq, constructor.positions(), stack, &registers));
match result {
Ok(Ok(new_counter)) => {
counter = new_counter;
}
// If the evaluation fails, bail and return the error.
Ok(Err(error)) => bail!("'constructor' failed to evaluate command ({command}): {error}"),
// If the evaluation fails, bail and return the error.
Err(_) => bail!("'constructor' failed to evaluate command ({command})"),
}
}
Command::BranchNeq(branch_neq) => {
let result =
try_vm_runtime!(|| branch_to(counter, branch_neq, constructor.positions(), stack, &registers));
match result {
Ok(Ok(new_counter)) => {
counter = new_counter;
}
// If the evaluation fails, bail and return the error.
Ok(Err(error)) => bail!("'constructor' failed to evaluate command ({command}): {error}"),
// If the evaluation fails, bail and return the error.
Err(_) => bail!("'constructor' failed to evaluate command ({command})"),
}
}
Command::Await(_) => {
bail!("Cannot `await` a Future in a constructor")
}
_ => {
let result = try_vm_runtime!(|| command.finalize(stack, store, &mut registers));
match result {
// If the evaluation succeeds with an operation, add it to the list.
Ok(Ok(Some(finalize_operation))) => finalize_operations.push(finalize_operation),
// If the evaluation succeeds with no operation, continue.
Ok(Ok(None)) => {}
// If the evaluation fails, bail and return the error.
Ok(Err(error)) => {
println!("'constructor' failed to evaluate command ({command}): {error}");
bail!("'constructor' failed to evaluate command ({command}): {error}")
}
// If the evaluation fails, bail and return the error.
Err(_) => {
println!("'constructor' failed to evaluate command ({command})");
bail!("'constructor' failed to evaluate command ({command})")
}
}
counter += 1;
}
};
}

// Return the finalize operations.
Ok(finalize_operations)
}

/// Finalizes the given transition.
fn finalize_transition<N: Network, P: FinalizeStorage<N>>(
state: FinalizeGlobalState,
Expand Down
32 changes: 32 additions & 0 deletions synthesizer/process/src/stack/evaluate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,22 @@ impl<N: Network> StackEvaluate<N> for Stack<N> {
Operand::BlockHeight => bail!("Cannot retrieve the block height from a closure scope."),
// If the operand is the network id, throw an error.
Operand::NetworkID => bail!("Cannot retrieve the network ID from a closure scope."),
// If the operand is the program checksum, retrieve the checksum from the stack.
Operand::Checksum(program_id) => {
let checksum = match program_id {
Some(program_id) => *self.get_external_stack(program_id)?.program_checksum(),
None => *self.program_checksum(),
};
Ok(Value::Plaintext(Plaintext::from(Literal::Field(checksum))))
}
// If the operand is the program edition, retrieve the edition from the stack.
Operand::Edition(program_id) => {
let edition = match program_id {
Some(program_id) => *self.get_external_stack(program_id)?.program_edition(),
None => *self.program_edition(),
};
Ok(Value::Plaintext(Plaintext::from(Literal::U16(edition))))
}
}
})
.collect();
Expand Down Expand Up @@ -218,6 +234,22 @@ impl<N: Network> StackEvaluate<N> for Stack<N> {
Operand::BlockHeight => bail!("Cannot retrieve the block height from a function scope."),
// If the operand is the network id, throw an error.
Operand::NetworkID => bail!("Cannot retrieve the network ID from a function scope."),
// If the operand is the program checksum, retrieve the checksum from the stack.
Operand::Checksum(program_id) => {
let checksum = match program_id {
Some(program_id) => *self.get_external_stack(program_id)?.program_checksum(),
None => *self.program_checksum(),
};
Ok(Value::Plaintext(Plaintext::from(Literal::Field(checksum))))
}
// If the operand is the program edition, retrieve the edition from the stack.
Operand::Edition(program_id) => {
let edition = match program_id {
Some(program_id) => *self.get_external_stack(program_id)?.program_edition(),
None => *self.program_edition(),
};
Ok(Value::Plaintext(Plaintext::from(Literal::U16(edition))))
}
}
})
.collect::<Result<Vec<_>>>()?;
Expand Down
40 changes: 40 additions & 0 deletions synthesizer/process/src/stack/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,26 @@ impl<N: Network> StackExecute<N> for Stack<N> {
Operand::NetworkID => {
bail!("Illegal operation: cannot retrieve the network id in a closure scope")
}
// If the operand is the checksum, retrieve the checksum from the stack.
Operand::Checksum(program_id) => {
let checksum = match program_id {
Some(program_id) => *self.get_external_stack(program_id)?.program_checksum(),
None => *self.program_checksum(),
};
Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::Field(
circuit::Field::new(circuit::Mode::Constant, checksum),
))))
}
// If the operand is the edition, retrieve the edition from the stack.
Operand::Edition(program_id) => {
let edition = match program_id {
Some(program_id) => *self.get_external_stack(program_id)?.program_edition(),
None => *self.program_edition(),
};
Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::U16(
circuit::U16::new(circuit::Mode::Constant, edition),
))))
}
}
})
.collect();
Expand Down Expand Up @@ -355,6 +375,26 @@ impl<N: Network> StackExecute<N> for Stack<N> {
Operand::NetworkID => {
bail!("Illegal operation: cannot retrieve the network id in a function scope")
}
// If the operand is the checksum, retrieve the checksum from the stack.
Operand::Checksum(program_id) => {
let checksum = match program_id {
Comment thread
vicsn marked this conversation as resolved.
Some(program_id) => *self.get_external_stack(program_id)?.program_checksum(),
None => *self.program_checksum(),
};
Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::Field(
circuit::Field::new(circuit::Mode::Constant, checksum),
))))
}
// If the operand is the edition, retrieve the edition from the stack.
Operand::Edition(program_id) => {
let edition = match program_id {
Some(program_id) => *self.get_external_stack(program_id)?.program_edition(),
None => *self.program_edition(),
};
Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::U16(
circuit::U16::new(circuit::Mode::Constant, edition),
))))
}
}
})
.collect::<Result<Vec<_>>>()?;
Expand Down
Loading