Skip to content

Commit 78699e8

Browse files
committed
fix!: Validate encoding of bytecode in the registry
We were doing bytecode commitments field worded but all of our code assumed bytecode was byte worded. This ensures that the field array aligns cleanly with 31 bytes per field (zeroed upper bits) and that the last field has zeroes after the remainder bytes. Cost of publishing goes from 208k to 245k gates.
1 parent bdd342e commit 78699e8

File tree

2 files changed

+116
-8
lines changed

2 files changed

+116
-8
lines changed

noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
mod events;
2+
mod validate_bytecode;
23

34
pub contract ContractClassRegistry {
45
use crate::events::class_published::ContractClassPublished;
6+
use crate::validate_bytecode::validate_packed_public_bytecode;
57
use aztec::{
6-
hash::compute_public_bytecode_commitment,
78
oracle::capsules,
89
protocol::{
9-
constants::CONTRACT_CLASS_REGISTRY_BYTECODE_CAPSULE_SLOT,
10-
contract_class_id::ContractClassId, traits::ToField,
10+
constants::{
11+
CONTRACT_CLASS_REGISTRY_BYTECODE_CAPSULE_SLOT,
12+
MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS,
13+
},
14+
contract_class_id::ContractClassId,
15+
traits::ToField,
1116
},
1217
};
1318

@@ -33,17 +38,15 @@ pub contract ContractClassRegistry {
3338

3439
// Safety: We load the bytecode via a capsule, which is unconstrained. In order to ensure the loaded bytecode
3540
// matches the expected one, we recompute the commitment and assert it matches the one provided by the caller.
36-
let mut packed_public_bytecode: [Field; 3000] = unsafe {
41+
let mut packed_public_bytecode: [Field; MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS] = unsafe {
3742
capsules::load(
3843
context.this_address(),
3944
CONTRACT_CLASS_REGISTRY_BYTECODE_CAPSULE_SLOT,
4045
)
4146
.unwrap()
4247
};
43-
// Compute and check the public bytecode commitment
44-
let computed_public_bytecode_commitment =
45-
compute_public_bytecode_commitment(packed_public_bytecode);
46-
assert_eq(computed_public_bytecode_commitment, public_bytecode_commitment);
48+
49+
validate_packed_public_bytecode(packed_public_bytecode, public_bytecode_commitment);
4750

4851
// Compute contract class id from preimage
4952
let contract_class_id = ContractClassId::compute(
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use aztec::{
2+
hash::compute_public_bytecode_commitment,
3+
protocol::constants::MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS,
4+
};
5+
6+
pub(crate) fn validate_packed_public_bytecode(
7+
packed_public_bytecode: [Field; MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS],
8+
public_bytecode_commitment: Field,
9+
) {
10+
validate_bytes_encoding(packed_public_bytecode);
11+
12+
assert_eq(
13+
compute_public_bytecode_commitment(packed_public_bytecode),
14+
public_bytecode_commitment,
15+
);
16+
}
17+
18+
fn validate_bytes_encoding(
19+
packed_public_bytecode: [Field; MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS],
20+
) {
21+
// Validate bit size of bytecode length
22+
packed_public_bytecode[0].assert_max_bit_size::<32>();
23+
// 31 bytes per field, the odd upper bits MUST be 0
24+
for i in 1..MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS {
25+
packed_public_bytecode[i].assert_max_bit_size::<248>();
26+
}
27+
28+
// check that the last field doesn't include garbage after bytes length.
29+
let bytecode_length_in_bytes = packed_public_bytecode[0] as u32;
30+
let not_aligned_bytes = bytecode_length_in_bytes % 31;
31+
32+
let bytecode_length_in_fields =
33+
(bytecode_length_in_bytes / 31) + (not_aligned_bytes != 0) as u32;
34+
35+
// We don't -1 because the first field is the bytes length
36+
let decomposition: [u8; 32] = packed_public_bytecode[bytecode_length_in_fields].to_be_bytes();
37+
38+
let expected_populated_bytes = if not_aligned_bytes == 0 {
39+
31
40+
} else {
41+
not_aligned_bytes
42+
};
43+
44+
let mut should_be_zero = false;
45+
for i in 0..32 {
46+
// +1 since we don't check the MSB (since it was already checked by assert_max_bit_size)
47+
should_be_zero |= i == (expected_populated_bytes + 1);
48+
49+
if should_be_zero {
50+
assert_eq(decomposition[i], 0, "Extra bytes in the last field of the bytecode");
51+
}
52+
}
53+
}
54+
55+
#[test]
56+
fn test_validate_bytes_encoding() {
57+
let mut packed_public_bytecode = [0; MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS];
58+
packed_public_bytecode[0] = 30;
59+
// Upper bits clean, no extra trailing bytes
60+
let bytes = [
61+
0x0, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
62+
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
63+
0x1e, 0x00,
64+
];
65+
packed_public_bytecode[1] = Field::from_be_bytes(bytes);
66+
67+
validate_bytes_encoding(packed_public_bytecode);
68+
}
69+
70+
#[test(should_fail_with = "assert_max_bit_size")]
71+
fn test_should_fail_too_large_bytecode_length() {
72+
let mut packed_public_bytecode = [0; MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS];
73+
packed_public_bytecode[0] = 0x100000000;
74+
validate_bytes_encoding(packed_public_bytecode);
75+
}
76+
77+
#[test(should_fail_with = "assert_max_bit_size")]
78+
fn test_should_fail_populated_upper_bits() {
79+
let mut packed_public_bytecode = [0; MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS];
80+
packed_public_bytecode[0] = 30;
81+
// one upper bit populated
82+
let bytes = [
83+
0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
84+
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
85+
0x1e, 0x00,
86+
];
87+
packed_public_bytecode[1] = Field::from_be_bytes(bytes);
88+
89+
validate_bytes_encoding(packed_public_bytecode);
90+
}
91+
92+
#[test(should_fail_with = "Extra bytes in the last field of the bytecode")]
93+
fn test_should_fail_extra_bytes_in_last_field() {
94+
let mut packed_public_bytecode = [0; MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS];
95+
packed_public_bytecode[0] = 30;
96+
// Last one should be zero
97+
let bytes = [
98+
0x0, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
99+
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
100+
0x1e, 0x01,
101+
];
102+
packed_public_bytecode[1] = Field::from_be_bytes(bytes);
103+
104+
validate_bytes_encoding(packed_public_bytecode);
105+
}

0 commit comments

Comments
 (0)