Skip to content

Commit 40aef70

Browse files
authored
fuzz: consistent fixture hashing (#40)
1 parent 1f3c1fc commit 40aef70

File tree

16 files changed

+483
-17
lines changed

16 files changed

+483
-17
lines changed

fuzz/fixture-fd/src/account.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use {
44
super::proto::{AcctState as ProtoAccount, SeedAddress as ProtoSeedAddress},
55
solana_sdk::{
66
account::{Account, AccountSharedData},
7+
keccak::Hasher,
78
pubkey::Pubkey,
89
},
910
};
@@ -107,3 +108,19 @@ impl From<(Pubkey, AccountSharedData)> for ProtoAccount {
107108
}
108109
}
109110
}
111+
112+
pub(crate) fn hash_proto_accounts(hasher: &mut Hasher, accounts: &[ProtoAccount]) {
113+
for account in accounts {
114+
hasher.hash(&account.address);
115+
hasher.hash(&account.owner);
116+
hasher.hash(&account.lamports.to_le_bytes());
117+
hasher.hash(&account.data);
118+
hasher.hash(&[account.executable as u8]);
119+
hasher.hash(&account.rent_epoch.to_le_bytes());
120+
if let Some(seed_addr) = &account.seed_addr {
121+
hasher.hash(&seed_addr.base);
122+
hasher.hash(&seed_addr.seed);
123+
hasher.hash(&seed_addr.owner);
124+
}
125+
}
126+
}

fuzz/fixture-fd/src/context.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use {
55
},
66
crate::account::SeedAddress,
77
solana_sdk::{
8-
account::AccountSharedData, feature_set::FeatureSet, pubkey::Pubkey,
8+
account::AccountSharedData, feature_set::FeatureSet, keccak::Hasher, pubkey::Pubkey,
99
transaction_context::InstructionAccount,
1010
},
1111
};
@@ -118,3 +118,19 @@ impl From<Context> for ProtoContext {
118118
}
119119
}
120120
}
121+
122+
pub(crate) fn hash_proto_context(hasher: &mut Hasher, context: &ProtoContext) {
123+
hasher.hash(&context.program_id);
124+
crate::account::hash_proto_accounts(hasher, &context.accounts);
125+
crate::instr_account::hash_proto_instr_accounts(hasher, &context.instr_accounts);
126+
hasher.hash(&context.data);
127+
hasher.hash(&context.cu_avail.to_le_bytes());
128+
if let Some(slot_context) = &context.slot_context {
129+
hasher.hash(&slot_context.slot.to_le_bytes());
130+
}
131+
if let Some(epoch_context) = &context.epoch_context {
132+
if let Some(features) = &epoch_context.features {
133+
crate::feature_set::hash_proto_feature_set(hasher, features);
134+
}
135+
}
136+
}

fuzz/fixture-fd/src/effects.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use {
44
super::proto::InstrEffects as ProtoEffects,
55
crate::account::SeedAddress,
6-
solana_sdk::{account::AccountSharedData, pubkey::Pubkey},
6+
solana_sdk::{account::AccountSharedData, keccak::Hasher, pubkey::Pubkey},
77
};
88

99
/// Represents the effects of a single instruction.
@@ -65,3 +65,11 @@ impl From<Effects> for ProtoEffects {
6565
}
6666
}
6767
}
68+
69+
pub(crate) fn hash_proto_effects(hasher: &mut Hasher, effects: &ProtoEffects) {
70+
hasher.hash(&effects.result.to_le_bytes());
71+
hasher.hash(&effects.custom_err.to_le_bytes());
72+
crate::account::hash_proto_accounts(hasher, &effects.modified_accounts);
73+
hasher.hash(&effects.cu_avail.to_le_bytes());
74+
hasher.hash(&effects.return_data);
75+
}

fuzz/fixture-fd/src/feature_set.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use {
44
super::proto::FeatureSet as ProtoFeatureSet,
5-
solana_sdk::{feature_set::FeatureSet, pubkey::Pubkey},
5+
solana_sdk::{feature_set::FeatureSet, keccak::Hasher, pubkey::Pubkey},
66
};
77

88
// Omit "test features" (they have the same u64 ID).
@@ -51,3 +51,11 @@ impl From<FeatureSet> for ProtoFeatureSet {
5151
Self { features }
5252
}
5353
}
54+
55+
pub(crate) fn hash_proto_feature_set(hasher: &mut Hasher, feature_set: &ProtoFeatureSet) {
56+
let mut features = feature_set.features.clone();
57+
features.sort();
58+
for f in &features {
59+
hasher.hash(&f.to_le_bytes());
60+
}
61+
}

fuzz/fixture-fd/src/instr_account.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use {
44
super::proto::InstrAcct as ProtoInstrAccount,
5-
solana_sdk::transaction_context::InstructionAccount,
5+
solana_sdk::{keccak::Hasher, transaction_context::InstructionAccount},
66
};
77

88
impl From<ProtoInstrAccount> for InstructionAccount {
@@ -37,3 +37,11 @@ impl From<InstructionAccount> for ProtoInstrAccount {
3737
}
3838
}
3939
}
40+
41+
pub(crate) fn hash_proto_instr_accounts(hasher: &mut Hasher, instr_accounts: &[ProtoInstrAccount]) {
42+
for account in instr_accounts {
43+
hasher.hash(&account.index.to_le_bytes());
44+
hasher.hash(&[account.is_signer as u8]);
45+
hasher.hash(&[account.is_writable as u8]);
46+
}
47+
}

fuzz/fixture-fd/src/lib.rs

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use {
2424
context::Context, effects::Effects, metadata::Metadata, proto::InstrFixture as ProtoFixture,
2525
},
2626
mollusk_svm_fuzz_fs::{FsHandler, IntoSerializableFixture, SerializableFixture},
27+
solana_sdk::keccak::{Hash, Hasher},
2728
};
2829

2930
/// A fixture for invoking a single instruction against a simulated SVM
@@ -76,7 +77,22 @@ impl From<Fixture> for ProtoFixture {
7677
}
7778
}
7879

79-
impl SerializableFixture for ProtoFixture {}
80+
impl SerializableFixture for ProtoFixture {
81+
// Manually implemented for deterministic hashes.
82+
fn hash(&self) -> Hash {
83+
let mut hasher = Hasher::default();
84+
if let Some(metadata) = &self.metadata {
85+
crate::metadata::hash_proto_metadata(&mut hasher, metadata);
86+
}
87+
if let Some(input) = &self.input {
88+
crate::context::hash_proto_context(&mut hasher, input);
89+
}
90+
if let Some(output) = &self.output {
91+
crate::effects::hash_proto_effects(&mut hasher, output);
92+
}
93+
hasher.result()
94+
}
95+
}
8096

8197
impl IntoSerializableFixture for Fixture {
8298
type Fixture = ProtoFixture;
@@ -85,3 +101,97 @@ impl IntoSerializableFixture for Fixture {
85101
Into::into(self)
86102
}
87103
}
104+
105+
#[cfg(test)]
106+
mod tests {
107+
use {
108+
super::{proto::InstrFixture, Fixture},
109+
crate::{
110+
context::{Context, EpochContext, SlotContext},
111+
effects::Effects,
112+
metadata::Metadata,
113+
},
114+
mollusk_svm_fuzz_fs::SerializableFixture,
115+
solana_sdk::{
116+
account::AccountSharedData, feature_set::FeatureSet, keccak::Hash, pubkey::Pubkey,
117+
transaction_context::InstructionAccount,
118+
},
119+
};
120+
121+
fn produce_hash(fixture: &Fixture) -> Hash {
122+
let proto_fixture: InstrFixture = fixture.clone().into();
123+
proto_fixture.hash()
124+
}
125+
126+
#[test]
127+
fn test_consistent_hashing() {
128+
const ITERATIONS: usize = 1000;
129+
130+
let program_id = Pubkey::default();
131+
let accounts = vec![
132+
(
133+
Pubkey::new_unique(),
134+
AccountSharedData::new(42, 42, &Pubkey::default()),
135+
None,
136+
),
137+
(
138+
Pubkey::new_unique(),
139+
AccountSharedData::new(42, 42, &Pubkey::default()),
140+
None,
141+
),
142+
(
143+
Pubkey::new_unique(),
144+
AccountSharedData::new(42, 42, &Pubkey::default()),
145+
None,
146+
),
147+
(
148+
Pubkey::new_unique(),
149+
AccountSharedData::new(42, 42, &Pubkey::default()),
150+
None,
151+
),
152+
];
153+
let instruction_accounts = accounts
154+
.iter()
155+
.enumerate()
156+
.map(|(i, _)| InstructionAccount {
157+
index_in_transaction: i as u16,
158+
index_in_caller: i as u16,
159+
index_in_callee: i as u16,
160+
is_signer: false,
161+
is_writable: true,
162+
})
163+
.collect::<Vec<_>>();
164+
let instruction_data = vec![4; 24];
165+
let slot_context = SlotContext { slot: 42 };
166+
let epoch_context = EpochContext {
167+
feature_set: FeatureSet::all_enabled(),
168+
};
169+
170+
let metadata = Metadata {
171+
entrypoint: String::from("Hello, world!"),
172+
};
173+
let context = Context {
174+
program_id,
175+
accounts,
176+
instruction_accounts,
177+
instruction_data,
178+
compute_units_available: 200_000,
179+
slot_context,
180+
epoch_context,
181+
};
182+
let effects = Effects::default();
183+
184+
let fixture = Fixture {
185+
metadata: Some(metadata),
186+
input: context,
187+
output: effects,
188+
};
189+
190+
let mut last_hash = produce_hash(&fixture);
191+
for _ in 0..ITERATIONS {
192+
let new_hash = produce_hash(&fixture);
193+
assert_eq!(last_hash, new_hash);
194+
last_hash = new_hash;
195+
}
196+
}
197+
}

fuzz/fixture-fd/src/metadata.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Program invocation metadata.
22
3-
use super::proto::FixtureMetadata as ProtoFixtureMetadata;
3+
use {super::proto::FixtureMetadata as ProtoFixtureMetadata, solana_sdk::keccak::Hasher};
44

55
#[derive(Clone, Debug, Default, PartialEq)]
66
pub struct Metadata {
@@ -23,3 +23,7 @@ impl From<Metadata> for ProtoFixtureMetadata {
2323
}
2424
}
2525
}
26+
27+
pub(crate) fn hash_proto_metadata(hasher: &mut Hasher, metadata: &ProtoFixtureMetadata) {
28+
hasher.hash(metadata.fn_entrypoint.as_bytes());
29+
}

fuzz/fixture/src/account.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use {
44
super::proto::AcctState as ProtoAccount,
55
solana_sdk::{
66
account::{Account, AccountSharedData},
7+
keccak::Hasher,
78
pubkey::Pubkey,
89
},
910
};
@@ -58,3 +59,14 @@ impl From<(Pubkey, AccountSharedData)> for ProtoAccount {
5859
}
5960
}
6061
}
62+
63+
pub(crate) fn hash_proto_accounts(hasher: &mut Hasher, accounts: &[ProtoAccount]) {
64+
for account in accounts {
65+
hasher.hash(&account.address);
66+
hasher.hash(&account.owner);
67+
hasher.hash(&account.lamports.to_le_bytes());
68+
hasher.hash(&account.data);
69+
hasher.hash(&[account.executable as u8]);
70+
hasher.hash(&account.rent_epoch.to_le_bytes());
71+
}
72+
}

0 commit comments

Comments
 (0)