Skip to content

Commit e614128

Browse files
authored
Merge pull request #3355 from ProvableHQ/feat/align_declared_plaintext_type_size
[Feat] Align declared plaintext type sizes during deployment verification
2 parents 6bf800e + adf4a83 commit e614128

6 files changed

Lines changed: 319 additions & 2 deletions

File tree

console/network/src/consensus_heights.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ pub enum ConsensusVersion {
6969
/// V18: Enables native credits record translation, introduces block-wide deployment limits,
7070
/// and enforces canonical subDAG certificate ordering.
7171
V18 = 18,
72-
/// V19: Adds more accurate type checking for the root call.
72+
/// V19: Adds more accurate type checking for the root call, and bounds the size of every
73+
/// `PlaintextType` declared in a deployed program.
7374
V19 = 19,
7475
/// V20: TBD
7576
V20 = 20,

console/network/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ pub trait Network:
210210
/// The maximum number of fields in data (must not exceed u16::MAX).
211211
#[allow(clippy::cast_possible_truncation)]
212212
const MAX_DATA_SIZE_IN_FIELDS: u32 = ((128 * 1024 * 8) / Field::<Self>::SIZE_IN_DATA_BITS) as u32;
213+
/// A list of (consensus_version, size) pairs indicating the maximum size in bits of any single
214+
/// `PlaintextType` declared in a program. This mirrors the runtime budget `to_fields` enforces.
215+
const MAX_PLAINTEXT_TYPE_SIZE_IN_BITS: [(ConsensusVersion, usize); 1] =
216+
[(ConsensusVersion::V19, Self::MAX_DATA_SIZE_IN_FIELDS as usize * Field::<Self>::SIZE_IN_DATA_BITS)];
213217

214218
/// The minimum number of entries in a struct.
215219
const MIN_STRUCT_ENTRIES: usize = 1; // This ensures the struct is not empty.
@@ -355,6 +359,14 @@ pub trait Network:
355359
fn LATEST_MAX_ARRAY_ELEMENTS() -> usize {
356360
Self::MAX_ARRAY_ELEMENTS.last().expect("MAX_ARRAY_ELEMENTS must have at least one entry").1
357361
}
362+
/// Returns the last `MAX_PLAINTEXT_TYPE_SIZE_IN_BITS` value.
363+
#[allow(non_snake_case)]
364+
fn LATEST_MAX_PLAINTEXT_TYPE_SIZE_IN_BITS() -> usize {
365+
Self::MAX_PLAINTEXT_TYPE_SIZE_IN_BITS
366+
.last()
367+
.expect("MAX_PLAINTEXT_TYPE_SIZE_IN_BITS must have at least one entry")
368+
.1
369+
}
358370
/// Returns the last `MAX_CERTIFICATES` value.
359371
#[allow(non_snake_case)]
360372
fn LATEST_MAX_CERTIFICATES() -> u16 {

synthesizer/src/vm/helpers/program.rs

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
use crate::Stack;
1717
use console::{
1818
prelude::{Network, cfg_iter},
19-
program::{Identifier, Locator, ValueType},
19+
program::{EntryType, FinalizeType, Identifier, Locator, PlaintextType, RegisterType, ValueType},
2020
};
2121
use snarkvm_synthesizer_program::{Program, StackTrait};
2222

@@ -102,3 +102,101 @@ pub fn check_future_argument_bit_size<N: Network>(
102102
})
103103
})
104104
}
105+
106+
/// Checks that every `PlaintextType` declared in the program does not exceed the specified maximum size in bits.
107+
pub fn check_program_plaintext_sizes<N: Network>(
108+
program: &Program<N>,
109+
stack: &Stack<N>,
110+
max_bits: usize,
111+
) -> Result<()> {
112+
// Helper to get a struct declaration.
113+
let get_struct = |id: &Identifier<N>| program.get_struct(id).cloned();
114+
115+
// Helper to get an external struct declaration.
116+
let get_external_struct = |locator: &Locator<N>| {
117+
stack.get_external_stack(locator.program_id())?.program().get_struct(locator.resource()).cloned()
118+
};
119+
120+
// Check a single plaintext type against the budget.
121+
let check = |pt: &PlaintextType<N>| -> Result<()> {
122+
let bits = pt.size_in_bits_raw(&get_struct, &get_external_struct)?;
123+
ensure!(
124+
bits <= max_bits,
125+
"Plaintext type '{pt}' exceeds the maximum allowed size in bits ({bits} > {max_bits})"
126+
);
127+
Ok(())
128+
};
129+
130+
// Check function inputs, outputs, and finalize arguments.
131+
for (_, function) in program.functions() {
132+
for input in function.inputs() {
133+
if let ValueType::Constant(pt) | ValueType::Public(pt) | ValueType::Private(pt) = input.value_type() {
134+
check(pt)?;
135+
}
136+
}
137+
for output in function.outputs() {
138+
if let ValueType::Constant(pt) | ValueType::Public(pt) | ValueType::Private(pt) = output.value_type() {
139+
check(pt)?;
140+
}
141+
}
142+
if let Some(finalize) = function.finalize_logic() {
143+
for input in finalize.inputs() {
144+
if let FinalizeType::Plaintext(pt) = input.finalize_type() {
145+
check(pt)?;
146+
}
147+
}
148+
}
149+
}
150+
151+
// Check view inputs and outputs.
152+
for (_, view) in program.views() {
153+
for input in view.inputs() {
154+
if let FinalizeType::Plaintext(pt) = input.finalize_type() {
155+
check(pt)?;
156+
}
157+
}
158+
for output in view.outputs() {
159+
if let FinalizeType::Plaintext(pt) = output.finalize_type() {
160+
check(pt)?;
161+
}
162+
}
163+
}
164+
165+
// Check each struct member.
166+
for (_, struct_) in program.structs() {
167+
for (_, pt) in struct_.members() {
168+
check(pt)?;
169+
}
170+
}
171+
172+
// Check each record entry.
173+
for (_, record) in program.records() {
174+
for (_, entry) in record.entries() {
175+
match entry {
176+
EntryType::Constant(pt) | EntryType::Public(pt) | EntryType::Private(pt) => check(pt)?,
177+
}
178+
}
179+
}
180+
181+
// Check each mapping key and value.
182+
for (_, mapping) in program.mappings() {
183+
check(mapping.key().plaintext_type())?;
184+
check(mapping.value().plaintext_type())?;
185+
}
186+
187+
// Check closure inputs and outputs.
188+
for (_, closure) in program.closures() {
189+
for input in closure.inputs() {
190+
if let RegisterType::Plaintext(pt) = input.register_type() {
191+
check(pt)?;
192+
}
193+
}
194+
for output in closure.outputs() {
195+
if let RegisterType::Plaintext(pt) = output.register_type() {
196+
check(pt)?;
197+
}
198+
}
199+
}
200+
201+
Ok(())
202+
}

synthesizer/src/vm/tests/test_v19/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@
1616
// Tests that the translation-marked variants of Input and Output are checked correctly.
1717
mod translated_type_checks;
1818

19+
// Tests for the V19 plaintext-type size bound.
20+
mod plaintext_size;
21+
1922
use super::*;
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright (c) 2019-2026 Provable Inc.
2+
// This file is part of the snarkVM library.
3+
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at:
7+
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
use super::*;
17+
18+
use crate::Stack;
19+
20+
use console::network::prelude::*;
21+
22+
/// Builds a VM advanced to V19 height and runs `check_program_plaintext_sizes`
23+
/// against `program_text` using the V19 bit budget.
24+
fn run_check_at_v19(program_text: &str) -> Result<()> {
25+
let rng = &mut TestRng::default();
26+
let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V19).unwrap(), rng);
27+
28+
let program = Program::<CurrentNetwork>::from_str(program_text).unwrap();
29+
let stack = Stack::new(vm.process(), &program).unwrap();
30+
let max_bits = CurrentNetwork::LATEST_MAX_PLAINTEXT_TYPE_SIZE_IN_BITS();
31+
check_program_plaintext_sizes(&program, &stack, max_bits)
32+
}
33+
34+
/// A triply-nested 2048x2048x2048 bool array, the largest array type the element limit permits.
35+
/// It is rejected on the type AST, without sampling any leaf.
36+
#[test]
37+
fn test_deeply_nested_array_input_rejected() {
38+
let result = run_check_at_v19(
39+
r"
40+
program nested_array.aleo;
41+
42+
function f:
43+
input r0 as [[[boolean; 2048u32]; 2048u32]; 2048u32].private;
44+
output r0 as [[[boolean; 2048u32]; 2048u32]; 2048u32].private;
45+
46+
constructor:
47+
assert.eq true true;
48+
",
49+
);
50+
let err = result.expect_err("over-cap nested array type must be rejected");
51+
assert!(err.to_string().contains("exceeds the maximum allowed size in bits"), "unexpected error: {err}");
52+
}
53+
54+
/// A program whose function input fits well under the cap is accepted.
55+
#[test]
56+
fn test_under_cap_function_input_accepted() {
57+
run_check_at_v19(
58+
r"
59+
program small.aleo;
60+
61+
function f:
62+
input r0 as [u64; 100u32].private;
63+
output r0 as [u64; 100u32].private;
64+
65+
constructor:
66+
assert.eq true true;
67+
",
68+
)
69+
.expect("under-cap program must pass");
70+
}
71+
72+
/// A struct whose member exceeds the per-type cap is rejected.
73+
#[test]
74+
fn test_over_cap_struct_member_rejected() {
75+
let err = run_check_at_v19(
76+
r"
77+
program big_struct.aleo;
78+
79+
struct big:
80+
huge as [[u64; 2048u32]; 9u32];
81+
82+
function f:
83+
input r0 as u64.private;
84+
output r0 as u64.private;
85+
86+
constructor:
87+
assert.eq true true;
88+
",
89+
)
90+
.expect_err("over-cap struct member must be rejected");
91+
assert!(err.to_string().contains("exceeds the maximum allowed size in bits"), "unexpected error: {err}");
92+
}
93+
94+
/// A record entry that exceeds the per-type cap is rejected.
95+
#[test]
96+
fn test_over_cap_record_entry_rejected() {
97+
let err = run_check_at_v19(
98+
r"
99+
program big_record.aleo;
100+
101+
record big:
102+
owner as address.private;
103+
huge as [[u64; 2048u32]; 9u32].private;
104+
105+
function f:
106+
input r0 as u64.private;
107+
output r0 as u64.private;
108+
109+
constructor:
110+
assert.eq true true;
111+
",
112+
)
113+
.expect_err("over-cap record entry must be rejected");
114+
assert!(err.to_string().contains("exceeds the maximum allowed size in bits"), "unexpected error: {err}");
115+
}
116+
117+
/// A mapping value that exceeds the per-type cap is rejected.
118+
#[test]
119+
fn test_over_cap_mapping_value_rejected() {
120+
let err = run_check_at_v19(
121+
r"
122+
program big_mapping.aleo;
123+
124+
mapping m:
125+
key as u64.public;
126+
value as [[u64; 2048u32]; 9u32].public;
127+
128+
function f:
129+
input r0 as u64.private;
130+
output r0 as u64.private;
131+
132+
constructor:
133+
assert.eq true true;
134+
",
135+
)
136+
.expect_err("over-cap mapping value must be rejected");
137+
assert!(err.to_string().contains("exceeds the maximum allowed size in bits"), "unexpected error: {err}");
138+
}
139+
140+
/// A closure input that exceeds the per-type cap is rejected.
141+
#[test]
142+
fn test_over_cap_closure_input_rejected() {
143+
let err = run_check_at_v19(
144+
r"
145+
program big_closure.aleo;
146+
147+
closure c:
148+
input r0 as [[u64; 2048u32]; 9u32];
149+
is.eq r0 r0 into r1;
150+
output r1 as boolean;
151+
152+
function f:
153+
input r0 as u64.private;
154+
output r0 as u64.private;
155+
156+
constructor:
157+
assert.eq true true;
158+
",
159+
)
160+
.expect_err("over-cap closure input must be rejected");
161+
assert!(err.to_string().contains("exceeds the maximum allowed size in bits"), "unexpected error: {err}");
162+
}
163+
164+
/// A finalize argument that exceeds the per-type cap is rejected.
165+
/// Since async arguments and finalize inputs must agree, the over-cap type also appears as
166+
/// a function input; the function-input check fires first, but the program is still rejected.
167+
/// The legacy `check_future_argument_bit_size` runs only before V14 and permits up to
168+
/// `u16::MAX` bits; this check applies from V19 with a tighter budget.
169+
#[test]
170+
fn test_over_cap_finalize_input_rejected() {
171+
let err = run_check_at_v19(
172+
r"
173+
program big_finalize.aleo;
174+
175+
function f:
176+
input r0 as u64.public;
177+
input r1 as [[u64; 2048u32]; 9u32].public;
178+
async f r0 r1 into r2;
179+
output r2 as big_finalize.aleo/f.future;
180+
181+
finalize f:
182+
input r0 as u64.public;
183+
input r1 as [[u64; 2048u32]; 9u32].public;
184+
assert.eq r0 r0;
185+
186+
constructor:
187+
assert.eq true true;
188+
",
189+
)
190+
.expect_err("over-cap finalize input must be rejected");
191+
assert!(err.to_string().contains("exceeds the maximum allowed size in bits"), "unexpected error: {err}");
192+
}

synthesizer/src/vm/verify.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,17 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
429429
}
430430
}
431431
}
432+
if consensus_version >= ConsensusVersion::V19 {
433+
// Bound the size in bits of every plaintext type declared in the program. This runs
434+
// before deployment verification samples values, since sampling walks the leaves of
435+
// the declared type.
436+
let max_plaintext_type_bits =
437+
consensus_config_value!(N, MAX_PLAINTEXT_TYPE_SIZE_IN_BITS, current_block_height).ok_or_else(
438+
|| anyhow!("Missing consensus config value: MAX_PLAINTEXT_TYPE_SIZE_IN_BITS"),
439+
)?;
440+
let stack = Stack::new(&self.process, deployment.program())?;
441+
check_program_plaintext_sizes(deployment.program(), &stack, max_plaintext_type_bits)?;
442+
}
432443

433444
// Determine if any of the array types exceed the maximum array elements.
434445
// Do not perform this check if the consensus version is beyond the latest version threshold for `MAX_ARRAY_ELEMENTS`.

0 commit comments

Comments
 (0)