@feltdb/core 0.8.4 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +165 -0
- package/dist/create/server-source/Cargo.toml +9 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
- package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
- package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
- package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
- package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
- package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
- package/dist/create/server-source/crates/feltdb/src/lib.rs +507 -6
- package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
- package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +79 -70
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
- package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
- package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
- package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
- package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +13 -4
- package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
- package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +1514 -537
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
- package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
- package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
- package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +3 -4
- package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +288 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
- package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
- package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +2 -1
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +80 -25
- package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
- package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
- package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1032 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
- package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
- package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
- package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
- package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
- package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
- package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
- package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
- package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
- package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
- package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +2 -1
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +326 -6
- package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-DaNwCLRX.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
- package/dist/studio-app/assets/{index-DwgNAIIX.js → index-j8IlhNqJ.js} +1 -1
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workload.d.ts +2 -1
- package/dist/workload.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
//! Conformance, determinism and capability proofs for the `feltdb.state.diff`
|
|
2
|
+
//! deterministic contract.
|
|
3
|
+
//!
|
|
4
|
+
//! The second specimen of the pattern proved for `feltdb.trigger.match`:
|
|
5
|
+
//!
|
|
6
|
+
//! ```text
|
|
7
|
+
//! Contract semantics are authoritative.
|
|
8
|
+
//! Runtime implementation is replaceable.
|
|
9
|
+
//!
|
|
10
|
+
//! NativeStateDiff(a, b) == WasmStateDiff(a, b) for every fixture
|
|
11
|
+
//! ```
|
|
12
|
+
//!
|
|
13
|
+
//! The corpus in `tests/fixtures/state_diff_contract_corpus.json` is committed
|
|
14
|
+
//! rather than generated, and carries the canonical output each case must
|
|
15
|
+
//! produce. Three comparisons run over it:
|
|
16
|
+
//!
|
|
17
|
+
//! 1. native output == committed expectation (runs everywhere);
|
|
18
|
+
//! 2. WASM output == committed expectation (needs `wasm32-unknown-unknown`);
|
|
19
|
+
//! 3. native output == WASM output, byte for byte.
|
|
20
|
+
//!
|
|
21
|
+
//! The first makes the suite meaningful where the WASM toolchain is absent: a
|
|
22
|
+
//! change in the native diff's semantics is caught by the same bytes the WASM
|
|
23
|
+
//! implementation was measured against.
|
|
24
|
+
|
|
25
|
+
use feltdb::state_diff_contract::{
|
|
26
|
+
canonical_input_bytes, evaluate_canonical, StateDiffInput, STATE_DIFF_CONTRACT_ID,
|
|
27
|
+
STATE_DIFF_CONTRACT_VERSION,
|
|
28
|
+
};
|
|
29
|
+
use serde::Deserialize;
|
|
30
|
+
use serde_json::Value;
|
|
31
|
+
use std::path::{Path, PathBuf};
|
|
32
|
+
use std::process::Command;
|
|
33
|
+
use std::sync::OnceLock;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Corpus
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
#[derive(Debug, Deserialize)]
|
|
40
|
+
struct Corpus {
|
|
41
|
+
corpus_version: u32,
|
|
42
|
+
contract: String,
|
|
43
|
+
contract_version: u32,
|
|
44
|
+
#[allow(dead_code)]
|
|
45
|
+
about: String,
|
|
46
|
+
cases: Vec<Case>,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[derive(Debug, Deserialize)]
|
|
50
|
+
struct Case {
|
|
51
|
+
name: String,
|
|
52
|
+
#[allow(dead_code)]
|
|
53
|
+
why: String,
|
|
54
|
+
/// A well-formed contract input, canonically encoded by the harness.
|
|
55
|
+
#[serde(default)]
|
|
56
|
+
input: Option<StateDiffInput>,
|
|
57
|
+
/// Bytes fed to the contract verbatim, for non-canonical and malformed
|
|
58
|
+
/// encodings that a typed value cannot express.
|
|
59
|
+
#[serde(default)]
|
|
60
|
+
raw_input: Option<String>,
|
|
61
|
+
/// The canonical encoding of `input`, recorded so that every consumer of
|
|
62
|
+
/// the corpus presents the same bytes.
|
|
63
|
+
///
|
|
64
|
+
/// A consumer in another language cannot always reproduce it: JavaScript
|
|
65
|
+
/// has one number type, so it cannot encode the difference between `42` and
|
|
66
|
+
/// `42.0` that this contract's value equality depends on.
|
|
67
|
+
#[serde(default)]
|
|
68
|
+
canonical_input: Option<String>,
|
|
69
|
+
/// The canonical output both implementations must produce.
|
|
70
|
+
#[serde(default)]
|
|
71
|
+
expected_output: Option<String>,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl Case {
|
|
75
|
+
/// The exact bytes this case presents to a contract implementation.
|
|
76
|
+
fn bytes(&self) -> Vec<u8> {
|
|
77
|
+
match (&self.input, &self.raw_input) {
|
|
78
|
+
(Some(input), None) => canonical_input_bytes(input),
|
|
79
|
+
(None, Some(raw)) => raw.as_bytes().to_vec(),
|
|
80
|
+
_ => panic!(
|
|
81
|
+
"case {} must have exactly one of input/raw_input",
|
|
82
|
+
self.name
|
|
83
|
+
),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn expected(&self) -> &str {
|
|
88
|
+
self.expected_output.as_deref().unwrap_or_else(|| {
|
|
89
|
+
panic!(
|
|
90
|
+
"case {} has no expected_output; regenerate the corpus with \
|
|
91
|
+
FELTDB_STATE_DIFF_CONTRACT_BLESS=1",
|
|
92
|
+
self.name
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fn corpus_path() -> PathBuf {
|
|
99
|
+
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/state_diff_contract_corpus.json")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn corpus() -> &'static Corpus {
|
|
103
|
+
static CORPUS: OnceLock<Corpus> = OnceLock::new();
|
|
104
|
+
CORPUS.get_or_init(|| {
|
|
105
|
+
let bytes = std::fs::read(corpus_path()).expect("conformance corpus is committed");
|
|
106
|
+
serde_json::from_slice(&bytes).expect("conformance corpus is well formed")
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[test]
|
|
111
|
+
fn corpus_addresses_this_contract_at_this_version() {
|
|
112
|
+
let corpus = corpus();
|
|
113
|
+
assert_eq!(corpus.corpus_version, 1);
|
|
114
|
+
assert_eq!(corpus.contract, STATE_DIFF_CONTRACT_ID);
|
|
115
|
+
assert_eq!(corpus.contract_version, STATE_DIFF_CONTRACT_VERSION);
|
|
116
|
+
assert!(
|
|
117
|
+
corpus.cases.len() >= 110,
|
|
118
|
+
"the corpus is the proof; it should not shrink silently"
|
|
119
|
+
);
|
|
120
|
+
let mut names: Vec<&str> = corpus.cases.iter().map(|case| case.name.as_str()).collect();
|
|
121
|
+
names.sort_unstable();
|
|
122
|
+
let unique = names.len();
|
|
123
|
+
names.dedup();
|
|
124
|
+
assert_eq!(unique, names.len(), "case names identify cases");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Every required category of behaviour is represented by at least one case.
|
|
128
|
+
///
|
|
129
|
+
/// A guard against the corpus quietly losing a dimension it was built to
|
|
130
|
+
/// cover, not a substitute for reading it.
|
|
131
|
+
#[test]
|
|
132
|
+
fn corpus_covers_every_required_category() {
|
|
133
|
+
let names: Vec<&str> = corpus().cases.iter().map(|c| c.name.as_str()).collect();
|
|
134
|
+
for required in [
|
|
135
|
+
// basic
|
|
136
|
+
"identical_objects",
|
|
137
|
+
"both_empty_objects",
|
|
138
|
+
"empty_to_populated_object",
|
|
139
|
+
"populated_to_empty_object",
|
|
140
|
+
"single_scalar_replacement",
|
|
141
|
+
// object changes
|
|
142
|
+
"object_addition",
|
|
143
|
+
"object_removal",
|
|
144
|
+
"object_replacement",
|
|
145
|
+
"nested_addition",
|
|
146
|
+
"nested_removal",
|
|
147
|
+
"nested_replacement",
|
|
148
|
+
"multiple_independent_changes",
|
|
149
|
+
"deeply_nested_change",
|
|
150
|
+
"unusual_object_keys",
|
|
151
|
+
"unicode_keys_and_values",
|
|
152
|
+
// null and missing
|
|
153
|
+
"missing_to_value",
|
|
154
|
+
"value_to_missing",
|
|
155
|
+
"missing_to_null",
|
|
156
|
+
"null_to_missing",
|
|
157
|
+
"null_to_value",
|
|
158
|
+
"value_to_null",
|
|
159
|
+
// arrays
|
|
160
|
+
"empty_to_populated_array",
|
|
161
|
+
"array_append_one",
|
|
162
|
+
"array_remove_tail",
|
|
163
|
+
"array_index_replacement",
|
|
164
|
+
"array_multiple_index_changes",
|
|
165
|
+
"array_nested_change",
|
|
166
|
+
"array_to_object",
|
|
167
|
+
"object_to_array",
|
|
168
|
+
"array_reorder_is_positional",
|
|
169
|
+
// numbers
|
|
170
|
+
"integer_change",
|
|
171
|
+
"float_change",
|
|
172
|
+
"zero_to_positive",
|
|
173
|
+
"negative_zero_int_against_zero",
|
|
174
|
+
"large_unsigned",
|
|
175
|
+
"float_extremes",
|
|
176
|
+
// structural
|
|
177
|
+
"many_members",
|
|
178
|
+
"repeated_nested_modifications",
|
|
179
|
+
"changes_at_related_paths",
|
|
180
|
+
"mixed_container_shapes_at_depth",
|
|
181
|
+
// invalid
|
|
182
|
+
"malformed_not_json",
|
|
183
|
+
"malformed_duplicate_envelope_member",
|
|
184
|
+
"malformed_leading_zero_number",
|
|
185
|
+
"malformed_truncated_object",
|
|
186
|
+
] {
|
|
187
|
+
assert!(
|
|
188
|
+
names.contains(&required),
|
|
189
|
+
"corpus lost coverage: {required}"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// The committed canonical bytes are what this crate's encoder produces.
|
|
195
|
+
///
|
|
196
|
+
/// Consumers outside Rust read `canonical_input` rather than re-canonicalizing,
|
|
197
|
+
/// so this is the check that keeps those bytes honest: if canonicalization
|
|
198
|
+
/// changed, every other reader of the corpus would silently be testing an
|
|
199
|
+
/// encoding FeltDB no longer emits.
|
|
200
|
+
#[test]
|
|
201
|
+
fn committed_canonical_bytes_match_the_encoder() {
|
|
202
|
+
for case in &corpus().cases {
|
|
203
|
+
match (&case.input, &case.raw_input) {
|
|
204
|
+
(Some(input), None) => assert_eq!(
|
|
205
|
+
case.canonical_input.as_deref(),
|
|
206
|
+
Some(
|
|
207
|
+
String::from_utf8(canonical_input_bytes(input))
|
|
208
|
+
.unwrap()
|
|
209
|
+
.as_str()
|
|
210
|
+
),
|
|
211
|
+
"{} has a stale canonical_input",
|
|
212
|
+
case.name
|
|
213
|
+
),
|
|
214
|
+
(None, Some(_)) => assert!(
|
|
215
|
+
case.canonical_input.is_none(),
|
|
216
|
+
"{} is a raw case and must not also record canonical_input",
|
|
217
|
+
case.name
|
|
218
|
+
),
|
|
219
|
+
_ => panic!(
|
|
220
|
+
"case {} must have exactly one of input/raw_input",
|
|
221
|
+
case.name
|
|
222
|
+
),
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// WASM host
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
const WASM_CRATE: &str = "crates/feltdb-state-diff-contract-wasm";
|
|
232
|
+
const WASM_ARTIFACT: &str =
|
|
233
|
+
"target/wasm32-unknown-unknown/release/feltdb_state_diff_contract_wasm.wasm";
|
|
234
|
+
|
|
235
|
+
fn repository_root() -> &'static Path {
|
|
236
|
+
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
237
|
+
.parent()
|
|
238
|
+
.and_then(Path::parent)
|
|
239
|
+
.expect("the crate lives at <root>/crates/feltdb")
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Whether this machine can compile the contract to WebAssembly at all.
|
|
243
|
+
fn wasm_target_installed() -> bool {
|
|
244
|
+
Command::new("rustc")
|
|
245
|
+
.args([
|
|
246
|
+
"--print",
|
|
247
|
+
"target-libdir",
|
|
248
|
+
"--target",
|
|
249
|
+
"wasm32-unknown-unknown",
|
|
250
|
+
])
|
|
251
|
+
.output()
|
|
252
|
+
.is_ok_and(|output| output.status.success())
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// Compiles a WASM contract crate and returns the module bytes.
|
|
256
|
+
///
|
|
257
|
+
/// A build failure is a defect and panics. A missing `wasm32-unknown-unknown`
|
|
258
|
+
/// target is an environment fact, reported by the caller as a skip.
|
|
259
|
+
fn build_module(manifest_dir: &Path) -> Vec<u8> {
|
|
260
|
+
let output = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
|
|
261
|
+
.args(["build", "--release", "--target", "wasm32-unknown-unknown"])
|
|
262
|
+
.current_dir(manifest_dir)
|
|
263
|
+
// The contract crate is its own workspace; inheriting the outer
|
|
264
|
+
// build's target directory or flags would defeat that.
|
|
265
|
+
.env_remove("CARGO_TARGET_DIR")
|
|
266
|
+
.env_remove("RUSTFLAGS")
|
|
267
|
+
.output()
|
|
268
|
+
.expect("cargo is runnable");
|
|
269
|
+
assert!(
|
|
270
|
+
output.status.success(),
|
|
271
|
+
"building the WASM contract at {} failed:\n{}",
|
|
272
|
+
manifest_dir.display(),
|
|
273
|
+
String::from_utf8_lossy(&output.stderr)
|
|
274
|
+
);
|
|
275
|
+
std::fs::read(manifest_dir.join(WASM_ARTIFACT)).expect("the build emits one module")
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// The committed WASM contract, compiled once for the whole suite.
|
|
279
|
+
fn contract_module() -> Option<&'static [u8]> {
|
|
280
|
+
static MODULE: OnceLock<Option<Vec<u8>>> = OnceLock::new();
|
|
281
|
+
MODULE
|
|
282
|
+
.get_or_init(|| {
|
|
283
|
+
if let Ok(path) = std::env::var("FELTDB_STATE_DIFF_CONTRACT_WASM") {
|
|
284
|
+
return Some(
|
|
285
|
+
std::fs::read(path).expect("FELTDB_STATE_DIFF_CONTRACT_WASM is readable"),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if !wasm_target_installed() {
|
|
289
|
+
return None;
|
|
290
|
+
}
|
|
291
|
+
Some(build_module(&repository_root().join(WASM_CRATE)))
|
|
292
|
+
})
|
|
293
|
+
.as_deref()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// Announces a skip loudly, so an absent toolchain is never a silent pass.
|
|
297
|
+
fn skipped(test: &str) {
|
|
298
|
+
eprintln!(
|
|
299
|
+
"SKIPPED {test}: the wasm32-unknown-unknown target is not installed, so the WASM \
|
|
300
|
+
contract implementation could not be built. Native conformance against the committed \
|
|
301
|
+
expectations still ran. Install the target, or set FELTDB_STATE_DIFF_CONTRACT_WASM to a \
|
|
302
|
+
prebuilt module, to execute the cross-runtime proof."
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// One instantiation of the WASM contract.
|
|
307
|
+
///
|
|
308
|
+
/// The linker is empty and stays empty: the module declares no imports, so
|
|
309
|
+
/// there is nothing to satisfy and nothing to grant.
|
|
310
|
+
struct WasmContract {
|
|
311
|
+
store: wasmi::Store<()>,
|
|
312
|
+
memory: wasmi::Memory,
|
|
313
|
+
alloc: wasmi::TypedFunc<u32, u32>,
|
|
314
|
+
evaluate: wasmi::TypedFunc<(u32, u32), u64>,
|
|
315
|
+
free: wasmi::TypedFunc<(u32, u32), ()>,
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
impl WasmContract {
|
|
319
|
+
fn instantiate(bytes: &[u8]) -> Self {
|
|
320
|
+
let engine = wasmi::Engine::default();
|
|
321
|
+
let module = wasmi::Module::new(&engine, bytes).expect("the module validates");
|
|
322
|
+
let mut store = wasmi::Store::new(&engine, ());
|
|
323
|
+
let instance = wasmi::Linker::<()>::new(&engine)
|
|
324
|
+
.instantiate_and_start(&mut store, &module)
|
|
325
|
+
.expect("an empty linker satisfies a module with no imports");
|
|
326
|
+
Self {
|
|
327
|
+
memory: instance
|
|
328
|
+
.get_memory(&store, "memory")
|
|
329
|
+
.expect("the module exports its memory"),
|
|
330
|
+
alloc: instance
|
|
331
|
+
.get_typed_func(&store, "contract_alloc")
|
|
332
|
+
.expect("contract_alloc"),
|
|
333
|
+
evaluate: instance
|
|
334
|
+
.get_typed_func(&store, "contract_evaluate")
|
|
335
|
+
.expect("contract_evaluate"),
|
|
336
|
+
free: instance
|
|
337
|
+
.get_typed_func(&store, "contract_free")
|
|
338
|
+
.expect("contract_free"),
|
|
339
|
+
store,
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
fn version(bytes: &[u8]) -> u32 {
|
|
344
|
+
let engine = wasmi::Engine::default();
|
|
345
|
+
let module = wasmi::Module::new(&engine, bytes).unwrap();
|
|
346
|
+
let mut store = wasmi::Store::new(&engine, ());
|
|
347
|
+
let instance = wasmi::Linker::<()>::new(&engine)
|
|
348
|
+
.instantiate_and_start(&mut store, &module)
|
|
349
|
+
.unwrap();
|
|
350
|
+
instance
|
|
351
|
+
.get_typed_func::<(), u32>(&store, "contract_version")
|
|
352
|
+
.expect("contract_version")
|
|
353
|
+
.call(&mut store, ())
|
|
354
|
+
.expect("contract_version is total")
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
fn evaluate(&mut self, input: &[u8]) -> Vec<u8> {
|
|
358
|
+
let len = input.len() as u32;
|
|
359
|
+
let ptr = self.alloc.call(&mut self.store, len).expect("allocation");
|
|
360
|
+
self.memory
|
|
361
|
+
.write(&mut self.store, ptr as usize, input)
|
|
362
|
+
.expect("input fits in linear memory");
|
|
363
|
+
let packed = self
|
|
364
|
+
.evaluate
|
|
365
|
+
.call(&mut self.store, (ptr, len))
|
|
366
|
+
.expect("the contract never traps on any input");
|
|
367
|
+
let out_ptr = (packed >> 32) as usize;
|
|
368
|
+
let out_len = (packed & 0xffff_ffff) as usize;
|
|
369
|
+
let mut output = vec![0u8; out_len];
|
|
370
|
+
self.memory
|
|
371
|
+
.read(&self.store, out_ptr, &mut output)
|
|
372
|
+
.expect("the output buffer is inside linear memory");
|
|
373
|
+
self.free.call(&mut self.store, (ptr, len)).expect("free");
|
|
374
|
+
self.free
|
|
375
|
+
.call(&mut self.store, (out_ptr as u32, out_len as u32))
|
|
376
|
+
.expect("free");
|
|
377
|
+
output
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// Conformance
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
/// Native evaluation reproduces the committed canonical output for every case.
|
|
386
|
+
#[test]
|
|
387
|
+
fn native_matches_the_committed_expectations() {
|
|
388
|
+
let mut divergent = Vec::new();
|
|
389
|
+
for case in &corpus().cases {
|
|
390
|
+
let produced = String::from_utf8(evaluate_canonical(&case.bytes())).unwrap();
|
|
391
|
+
if produced != case.expected() {
|
|
392
|
+
divergent.push(format!(
|
|
393
|
+
"{}\n expected: {}\n native: {}",
|
|
394
|
+
case.name,
|
|
395
|
+
case.expected(),
|
|
396
|
+
produced
|
|
397
|
+
));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
assert!(
|
|
401
|
+
divergent.is_empty(),
|
|
402
|
+
"native contract diverged from the committed expectations:\n{}",
|
|
403
|
+
divergent.join("\n")
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/// The proof: one corpus, two implementations, byte-identical canonical output.
|
|
408
|
+
#[test]
|
|
409
|
+
fn native_and_wasm_agree_on_every_fixture() {
|
|
410
|
+
let Some(bytes) = contract_module() else {
|
|
411
|
+
return skipped("native_and_wasm_agree_on_every_fixture");
|
|
412
|
+
};
|
|
413
|
+
let mut wasm = WasmContract::instantiate(bytes);
|
|
414
|
+
|
|
415
|
+
println!(
|
|
416
|
+
"{:<48} {:<9} {:<9} {}",
|
|
417
|
+
"fixture", "native", "wasm", "equal"
|
|
418
|
+
);
|
|
419
|
+
let mut divergent = Vec::new();
|
|
420
|
+
for case in &corpus().cases {
|
|
421
|
+
let input = case.bytes();
|
|
422
|
+
let native = evaluate_canonical(&input);
|
|
423
|
+
let produced = wasm.evaluate(&input);
|
|
424
|
+
let equal = native == produced;
|
|
425
|
+
println!(
|
|
426
|
+
"{:<48} {:<9} {:<9} {}",
|
|
427
|
+
case.name,
|
|
428
|
+
summarize(&native),
|
|
429
|
+
summarize(&produced),
|
|
430
|
+
if equal { "yes" } else { "NO" }
|
|
431
|
+
);
|
|
432
|
+
if !equal {
|
|
433
|
+
divergent.push(format!(
|
|
434
|
+
"{}\n input: {}\n native: {}\n wasm: {}",
|
|
435
|
+
case.name,
|
|
436
|
+
String::from_utf8_lossy(&input),
|
|
437
|
+
String::from_utf8_lossy(&native),
|
|
438
|
+
String::from_utf8_lossy(&produced)
|
|
439
|
+
));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
assert!(
|
|
443
|
+
divergent.is_empty(),
|
|
444
|
+
"native and WASM contract implementations diverged:\n{}",
|
|
445
|
+
divergent.join("\n")
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/// The WASM implementation reproduces the committed canonical output too.
|
|
450
|
+
///
|
|
451
|
+
/// Separate from the pairwise comparison on purpose: two implementations can
|
|
452
|
+
/// agree with each other while both having moved.
|
|
453
|
+
#[test]
|
|
454
|
+
fn wasm_matches_the_committed_expectations() {
|
|
455
|
+
let Some(bytes) = contract_module() else {
|
|
456
|
+
return skipped("wasm_matches_the_committed_expectations");
|
|
457
|
+
};
|
|
458
|
+
let mut wasm = WasmContract::instantiate(bytes);
|
|
459
|
+
for case in &corpus().cases {
|
|
460
|
+
assert_eq!(
|
|
461
|
+
String::from_utf8(wasm.evaluate(&case.bytes())).unwrap(),
|
|
462
|
+
case.expected(),
|
|
463
|
+
"WASM contract diverged from the committed expectation for {}",
|
|
464
|
+
case.name
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/// Both implementations answer for the same contract at the same version.
|
|
470
|
+
#[test]
|
|
471
|
+
fn both_implementations_declare_the_same_contract_version() {
|
|
472
|
+
let Some(bytes) = contract_module() else {
|
|
473
|
+
return skipped("both_implementations_declare_the_same_contract_version");
|
|
474
|
+
};
|
|
475
|
+
assert_eq!(WasmContract::version(bytes), STATE_DIFF_CONTRACT_VERSION);
|
|
476
|
+
for case in &corpus().cases {
|
|
477
|
+
let output: Value = serde_json::from_slice(&evaluate_canonical(&case.bytes())).unwrap();
|
|
478
|
+
assert_eq!(output["contract"], STATE_DIFF_CONTRACT_ID);
|
|
479
|
+
assert_eq!(output["version"], STATE_DIFF_CONTRACT_VERSION);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/// The native adapter adds nothing to the existing implementation.
|
|
484
|
+
///
|
|
485
|
+
/// The contract must be the diff FeltDB already computes, not a second one that
|
|
486
|
+
/// happens to agree. This runs both entry points over the whole corpus.
|
|
487
|
+
#[test]
|
|
488
|
+
fn the_contract_is_the_existing_semantic_diff() {
|
|
489
|
+
use feltdb::state_diff_contract::diff_states;
|
|
490
|
+
use feltdb::state_model::SemanticDiff;
|
|
491
|
+
|
|
492
|
+
let mut checked = 0;
|
|
493
|
+
for case in &corpus().cases {
|
|
494
|
+
let Some(input) = case.input.as_ref() else {
|
|
495
|
+
continue;
|
|
496
|
+
};
|
|
497
|
+
let through_contract = diff_states(&input.state_a, &input.state_b);
|
|
498
|
+
let direct = SemanticDiff::compute(&input.state_a, &input.state_b);
|
|
499
|
+
assert_eq!(
|
|
500
|
+
serde_json::to_string(&through_contract).unwrap(),
|
|
501
|
+
serde_json::to_string(&direct).unwrap(),
|
|
502
|
+
"the contract adapter changed the result for {}",
|
|
503
|
+
case.name
|
|
504
|
+
);
|
|
505
|
+
checked += 1;
|
|
506
|
+
}
|
|
507
|
+
assert!(checked > 70, "most of the corpus is typed input");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/// A short rendering of an output, for the printed conformance table.
|
|
511
|
+
fn summarize(output: &[u8]) -> String {
|
|
512
|
+
let parsed: Value = match serde_json::from_slice(output) {
|
|
513
|
+
Ok(value) => value,
|
|
514
|
+
Err(_) => return "?".into(),
|
|
515
|
+
};
|
|
516
|
+
match parsed["diff"]["changes"].as_array() {
|
|
517
|
+
Some(changes) => format!("{} chg", changes.len()),
|
|
518
|
+
None => "error".into(),
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
// Float rendering
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
/// The WASM float writer reproduces `serde_json`'s, over far more values than
|
|
527
|
+
/// the corpus can hold.
|
|
528
|
+
///
|
|
529
|
+
/// This is the riskiest part of the second implementation. The contract's
|
|
530
|
+
/// output embeds whole input values, so the WASM module has to render doubles
|
|
531
|
+
/// exactly as `ryu` does — and neither Rust's `Display` nor its `Debug` will
|
|
532
|
+
/// do it: they disagree with `ryu` on presentation (`1e16` against `1e+16`)
|
|
533
|
+
/// and, on a fraction of values, on which shortest representation to pick.
|
|
534
|
+
///
|
|
535
|
+
/// The sample is restricted to doubles `serde_json` parses back from its own
|
|
536
|
+
/// output, for the reason
|
|
537
|
+
/// [`serde_json_does_not_round_trip_every_double`] records: outside that set
|
|
538
|
+
/// the two implementations disagree about which double the bytes denote,
|
|
539
|
+
/// before any rendering happens, and this test would be measuring that instead.
|
|
540
|
+
///
|
|
541
|
+
/// One diff carries the whole array as a single changed value, so this drives
|
|
542
|
+
/// the real module over the whole sample in a handful of calls.
|
|
543
|
+
#[test]
|
|
544
|
+
fn wasm_renders_floats_exactly_as_serde_json_does() {
|
|
545
|
+
let Some(bytes) = contract_module() else {
|
|
546
|
+
return skipped("wasm_renders_floats_exactly_as_serde_json_does");
|
|
547
|
+
};
|
|
548
|
+
let mut wasm = WasmContract::instantiate(bytes);
|
|
549
|
+
|
|
550
|
+
let mut sample: Vec<f64> = vec![
|
|
551
|
+
0.0,
|
|
552
|
+
-0.0,
|
|
553
|
+
1.0,
|
|
554
|
+
-1.0,
|
|
555
|
+
100.0,
|
|
556
|
+
0.5,
|
|
557
|
+
0.125,
|
|
558
|
+
3.141592653589793,
|
|
559
|
+
2.718281828459045,
|
|
560
|
+
1e15,
|
|
561
|
+
1e16,
|
|
562
|
+
1e17,
|
|
563
|
+
1e21,
|
|
564
|
+
1e100,
|
|
565
|
+
1e308,
|
|
566
|
+
1e-1,
|
|
567
|
+
1e-4,
|
|
568
|
+
1e-5,
|
|
569
|
+
1e-6,
|
|
570
|
+
1e-7,
|
|
571
|
+
1e-100,
|
|
572
|
+
5e-324,
|
|
573
|
+
f64::MAX,
|
|
574
|
+
f64::MIN,
|
|
575
|
+
f64::MIN_POSITIVE,
|
|
576
|
+
0.1,
|
|
577
|
+
0.2,
|
|
578
|
+
0.30000000000000004,
|
|
579
|
+
9007199254740992.0,
|
|
580
|
+
];
|
|
581
|
+
// A deterministic sweep over bit patterns, and over decimals of the kind
|
|
582
|
+
// records actually hold.
|
|
583
|
+
let mut seed: u64 = 0x243F6A8885A308D3;
|
|
584
|
+
for _ in 0..12_000 {
|
|
585
|
+
seed = seed
|
|
586
|
+
.wrapping_mul(6364136223846793005)
|
|
587
|
+
.wrapping_add(1442695040888963407);
|
|
588
|
+
let value = f64::from_bits(seed);
|
|
589
|
+
if value.is_finite() {
|
|
590
|
+
sample.push(value);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
for step in -6_000i64..6_000 {
|
|
594
|
+
sample.push(step as f64 / 1000.0);
|
|
595
|
+
}
|
|
596
|
+
let offered = sample.len();
|
|
597
|
+
sample.retain(|value| serde_json_round_trips(*value));
|
|
598
|
+
let checked = sample.len();
|
|
599
|
+
assert!(
|
|
600
|
+
checked > 12_000,
|
|
601
|
+
"the sample should still be large after excluding what serde_json cannot round trip"
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
for chunk in sample.chunks(4_000) {
|
|
605
|
+
let state_a = Value::Array(chunk.iter().map(|v| Value::from(*v)).collect());
|
|
606
|
+
let input = canonical_input_bytes(&StateDiffInput::new(state_a, Value::from(0)));
|
|
607
|
+
let native = evaluate_canonical(&input);
|
|
608
|
+
let produced = wasm.evaluate(&input);
|
|
609
|
+
if native != produced {
|
|
610
|
+
// Report the first differing value rather than a 100 KB diff.
|
|
611
|
+
let native_text = String::from_utf8_lossy(&native);
|
|
612
|
+
let wasm_text = String::from_utf8_lossy(&produced);
|
|
613
|
+
let at = native_text
|
|
614
|
+
.char_indices()
|
|
615
|
+
.zip(wasm_text.char_indices())
|
|
616
|
+
.find(|((_, a), (_, b))| a != b)
|
|
617
|
+
.map(|((index, _), _)| index)
|
|
618
|
+
.unwrap_or(0);
|
|
619
|
+
let from = at.saturating_sub(40);
|
|
620
|
+
panic!(
|
|
621
|
+
"the WASM float writer diverged from serde_json:\n native: …{}…\n wasm: …{}…",
|
|
622
|
+
&native_text[from..(at + 40).min(native_text.len())],
|
|
623
|
+
&wasm_text[from..(at + 40).min(wasm_text.len())]
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
println!("float rendering agrees on {checked} of {offered} sampled doubles");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/// Whether `serde_json` reads back the double it itself wrote.
|
|
631
|
+
fn serde_json_round_trips(value: f64) -> bool {
|
|
632
|
+
let text = serde_json::to_string(&Value::from(value)).expect("finite");
|
|
633
|
+
serde_json::from_str::<Value>(&text)
|
|
634
|
+
.ok()
|
|
635
|
+
.and_then(|parsed| parsed.as_f64())
|
|
636
|
+
.is_some_and(|parsed| parsed.to_bits() == value.to_bits())
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/// `serde_json` does not parse back every double it writes, and that bounds
|
|
640
|
+
/// what this contract can claim.
|
|
641
|
+
///
|
|
642
|
+
/// `serde_json`'s float parser is accurate to within one unit in the last
|
|
643
|
+
/// place but is not correctly rounded, which is its documented default: exact
|
|
644
|
+
/// round-tripping is the opt-in `float_roundtrip` feature, at roughly twice the
|
|
645
|
+
/// parsing cost. The WASM implementation uses `core`'s `dec2flt`, which *is*
|
|
646
|
+
/// correctly rounded, so for the affected literals the two implementations
|
|
647
|
+
/// disagree about which double the bytes denote — before either renders
|
|
648
|
+
/// anything.
|
|
649
|
+
///
|
|
650
|
+
/// This test exists so that boundary is measured and visible rather than
|
|
651
|
+
/// assumed. It asserts only that short, ordinary decimals — the numbers real
|
|
652
|
+
/// records hold — are unaffected. It deliberately does not assert a rate: that
|
|
653
|
+
/// is a property of the dependency and may change when it is upgraded.
|
|
654
|
+
#[test]
|
|
655
|
+
fn serde_json_does_not_round_trip_every_double() {
|
|
656
|
+
let mut lost = 0usize;
|
|
657
|
+
let mut total = 0usize;
|
|
658
|
+
let mut example = None;
|
|
659
|
+
let mut seed: u64 = 0x243F6A8885A308D3;
|
|
660
|
+
for _ in 0..200_000 {
|
|
661
|
+
seed = seed
|
|
662
|
+
.wrapping_mul(6364136223846793005)
|
|
663
|
+
.wrapping_add(1442695040888963407);
|
|
664
|
+
let value = f64::from_bits(seed);
|
|
665
|
+
if !value.is_finite() {
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
total += 1;
|
|
669
|
+
if !serde_json_round_trips(value) {
|
|
670
|
+
lost += 1;
|
|
671
|
+
if example.is_none() {
|
|
672
|
+
example = Some(serde_json::to_string(&Value::from(value)).unwrap());
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
println!(
|
|
677
|
+
"serde_json {} of {total} random doubles do not survive its own \
|
|
678
|
+
serialize/parse round trip (example: {})",
|
|
679
|
+
lost,
|
|
680
|
+
example.as_deref().unwrap_or("none")
|
|
681
|
+
);
|
|
682
|
+
assert!(
|
|
683
|
+
lost > 0,
|
|
684
|
+
"if serde_json now round trips every double, the restriction on the float rendering \
|
|
685
|
+
test and the contract's stated input domain can both be removed"
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
// The values records actually hold are unaffected, which is what makes the
|
|
689
|
+
// contract useful despite the restriction.
|
|
690
|
+
for ordinary in [
|
|
691
|
+
0.0, -0.0, 0.5, 0.125, 1.0, 2.5, 42.0, 99.99, 1e-5, 1e15, 1e16, 1e-6, 3.14, 2.718,
|
|
692
|
+
1234.5678, -0.001, 100.0, 0.1, 0.2, 0.3,
|
|
693
|
+
] {
|
|
694
|
+
assert!(
|
|
695
|
+
serde_json_round_trips(ordinary),
|
|
696
|
+
"an ordinary decimal must round trip: {ordinary}"
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
// Determinism
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
/// The same input, in the same instance, produces the same output every time.
|
|
706
|
+
#[test]
|
|
707
|
+
fn wasm_is_deterministic_across_repeated_calls() {
|
|
708
|
+
let Some(bytes) = contract_module() else {
|
|
709
|
+
return skipped("wasm_is_deterministic_across_repeated_calls");
|
|
710
|
+
};
|
|
711
|
+
let mut wasm = WasmContract::instantiate(bytes);
|
|
712
|
+
for case in &corpus().cases {
|
|
713
|
+
let input = case.bytes();
|
|
714
|
+
let first = wasm.evaluate(&input);
|
|
715
|
+
for round in 1..16 {
|
|
716
|
+
assert_eq!(
|
|
717
|
+
wasm.evaluate(&input),
|
|
718
|
+
first,
|
|
719
|
+
"{} changed answer on repeat {round} within one instance",
|
|
720
|
+
case.name
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/// Repeated native evaluation is also stable.
|
|
727
|
+
///
|
|
728
|
+
/// Cheap, and it forecloses the reading in which only the WASM side was ever
|
|
729
|
+
/// at risk of varying.
|
|
730
|
+
#[test]
|
|
731
|
+
fn native_is_deterministic_across_repeated_calls() {
|
|
732
|
+
for case in &corpus().cases {
|
|
733
|
+
let input = case.bytes();
|
|
734
|
+
let first = evaluate_canonical(&input);
|
|
735
|
+
for round in 1..16 {
|
|
736
|
+
assert_eq!(
|
|
737
|
+
evaluate_canonical(&input),
|
|
738
|
+
first,
|
|
739
|
+
"{} changed answer on native repeat {round}",
|
|
740
|
+
case.name
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/// The same input produces the same output in a fresh instance.
|
|
747
|
+
///
|
|
748
|
+
/// Each instance gets its own `Store`, so its linear memory, allocator state
|
|
749
|
+
/// and call history all start over. An implementation that carried state
|
|
750
|
+
/// between diffs would show it here.
|
|
751
|
+
#[test]
|
|
752
|
+
fn wasm_is_deterministic_across_fresh_instances() {
|
|
753
|
+
let Some(bytes) = contract_module() else {
|
|
754
|
+
return skipped("wasm_is_deterministic_across_fresh_instances");
|
|
755
|
+
};
|
|
756
|
+
let mut baseline = WasmContract::instantiate(bytes);
|
|
757
|
+
let expected: Vec<Vec<u8>> = corpus()
|
|
758
|
+
.cases
|
|
759
|
+
.iter()
|
|
760
|
+
.map(|case| baseline.evaluate(&case.bytes()))
|
|
761
|
+
.collect();
|
|
762
|
+
|
|
763
|
+
for round in 0..4 {
|
|
764
|
+
let mut fresh = WasmContract::instantiate(bytes);
|
|
765
|
+
for (case, want) in corpus().cases.iter().zip(&expected) {
|
|
766
|
+
assert_eq!(
|
|
767
|
+
&fresh.evaluate(&case.bytes()),
|
|
768
|
+
want,
|
|
769
|
+
"{} changed answer in fresh instance {round}",
|
|
770
|
+
case.name
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/// Order of evaluation does not affect any answer.
|
|
777
|
+
#[test]
|
|
778
|
+
fn wasm_answers_do_not_depend_on_evaluation_order() {
|
|
779
|
+
let Some(bytes) = contract_module() else {
|
|
780
|
+
return skipped("wasm_answers_do_not_depend_on_evaluation_order");
|
|
781
|
+
};
|
|
782
|
+
let mut forwards = WasmContract::instantiate(bytes);
|
|
783
|
+
let ordered: Vec<Vec<u8>> = corpus()
|
|
784
|
+
.cases
|
|
785
|
+
.iter()
|
|
786
|
+
.map(|case| forwards.evaluate(&case.bytes()))
|
|
787
|
+
.collect();
|
|
788
|
+
|
|
789
|
+
let mut backwards = WasmContract::instantiate(bytes);
|
|
790
|
+
for (index, case) in corpus().cases.iter().enumerate().rev() {
|
|
791
|
+
assert_eq!(
|
|
792
|
+
backwards.evaluate(&case.bytes()),
|
|
793
|
+
ordered[index],
|
|
794
|
+
"{} answered differently when the corpus ran in reverse",
|
|
795
|
+
case.name
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/// The same input produces the same output in a separate OS process.
|
|
801
|
+
#[test]
|
|
802
|
+
fn wasm_is_deterministic_across_process_boundaries() {
|
|
803
|
+
let Some(bytes) = contract_module() else {
|
|
804
|
+
return skipped("wasm_is_deterministic_across_process_boundaries");
|
|
805
|
+
};
|
|
806
|
+
let mut here = WasmContract::instantiate(bytes);
|
|
807
|
+
let mine: Vec<Vec<u8>> = corpus()
|
|
808
|
+
.cases
|
|
809
|
+
.iter()
|
|
810
|
+
.map(|case| here.evaluate(&case.bytes()))
|
|
811
|
+
.collect();
|
|
812
|
+
|
|
813
|
+
let executable = std::env::current_exe().expect("the test binary has a path");
|
|
814
|
+
let output = Command::new(executable)
|
|
815
|
+
.args([
|
|
816
|
+
"--exact",
|
|
817
|
+
"wasm_is_deterministic_across_fresh_instances",
|
|
818
|
+
"--nocapture",
|
|
819
|
+
])
|
|
820
|
+
.output()
|
|
821
|
+
.expect("the test binary re-runs");
|
|
822
|
+
assert!(
|
|
823
|
+
output.status.success(),
|
|
824
|
+
"the corpus did not reproduce in a separate process:\n{}",
|
|
825
|
+
String::from_utf8_lossy(&output.stdout)
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
for (case, produced) in corpus().cases.iter().zip(&mine) {
|
|
829
|
+
assert_eq!(String::from_utf8_lossy(produced), case.expected());
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
// Runtime restrictions
|
|
835
|
+
// ---------------------------------------------------------------------------
|
|
836
|
+
|
|
837
|
+
/// The module imports nothing.
|
|
838
|
+
///
|
|
839
|
+
/// This is the whole capability argument and it is deliberately narrow. A
|
|
840
|
+
/// WebAssembly module reaches the outside world only through its imports. With
|
|
841
|
+
/// an empty import section there is no clock, no filesystem handle, no socket,
|
|
842
|
+
/// no entropy source and no FeltDB authority state in scope — not because
|
|
843
|
+
/// access was denied at call time, but because no such function is linked. It
|
|
844
|
+
/// says nothing about hostile modules, which this suite does not test.
|
|
845
|
+
#[test]
|
|
846
|
+
fn wasm_module_declares_no_imports() {
|
|
847
|
+
let Some(bytes) = contract_module() else {
|
|
848
|
+
return skipped("wasm_module_declares_no_imports");
|
|
849
|
+
};
|
|
850
|
+
let engine = wasmi::Engine::default();
|
|
851
|
+
let module = wasmi::Module::new(&engine, bytes).expect("the module validates");
|
|
852
|
+
let imports: Vec<String> = module
|
|
853
|
+
.imports()
|
|
854
|
+
.map(|import: wasmi::ImportType| format!("{}::{}", import.module(), import.name()))
|
|
855
|
+
.collect();
|
|
856
|
+
assert!(
|
|
857
|
+
imports.is_empty(),
|
|
858
|
+
"the contract module must import nothing; it imports {imports:?}"
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/// An empty linker instantiates the module.
|
|
863
|
+
#[test]
|
|
864
|
+
fn wasm_module_instantiates_with_no_host_functions() {
|
|
865
|
+
let Some(bytes) = contract_module() else {
|
|
866
|
+
return skipped("wasm_module_instantiates_with_no_host_functions");
|
|
867
|
+
};
|
|
868
|
+
let engine = wasmi::Engine::default();
|
|
869
|
+
let module = wasmi::Module::new(&engine, bytes).unwrap();
|
|
870
|
+
let mut store = wasmi::Store::new(&engine, ());
|
|
871
|
+
wasmi::Linker::<()>::new(&engine)
|
|
872
|
+
.instantiate_and_start(&mut store, &module)
|
|
873
|
+
.expect("no host function is required");
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/// The module exports the contract ABI and nothing that could act on FeltDB.
|
|
877
|
+
#[test]
|
|
878
|
+
fn wasm_module_exports_only_the_contract_abi() {
|
|
879
|
+
let Some(bytes) = contract_module() else {
|
|
880
|
+
return skipped("wasm_module_exports_only_the_contract_abi");
|
|
881
|
+
};
|
|
882
|
+
let engine = wasmi::Engine::default();
|
|
883
|
+
let module = wasmi::Module::new(&engine, bytes).unwrap();
|
|
884
|
+
let mut functions: Vec<String> = module
|
|
885
|
+
.exports()
|
|
886
|
+
.filter(|export| export.ty().func().is_some())
|
|
887
|
+
.map(|export| export.name().to_string())
|
|
888
|
+
.collect();
|
|
889
|
+
functions.sort();
|
|
890
|
+
assert_eq!(
|
|
891
|
+
functions,
|
|
892
|
+
vec![
|
|
893
|
+
"contract_alloc".to_string(),
|
|
894
|
+
"contract_evaluate".to_string(),
|
|
895
|
+
"contract_free".to_string(),
|
|
896
|
+
"contract_version".to_string(),
|
|
897
|
+
]
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/// WASI is not present, under any of the names a runtime would look for.
|
|
902
|
+
#[test]
|
|
903
|
+
fn wasm_module_requires_no_wasi() {
|
|
904
|
+
let Some(bytes) = contract_module() else {
|
|
905
|
+
return skipped("wasm_module_requires_no_wasi");
|
|
906
|
+
};
|
|
907
|
+
let text = String::from_utf8_lossy(bytes);
|
|
908
|
+
for forbidden in [
|
|
909
|
+
"wasi_snapshot_preview1",
|
|
910
|
+
"wasi_unstable",
|
|
911
|
+
"clock_time_get",
|
|
912
|
+
"random_get",
|
|
913
|
+
"fd_read",
|
|
914
|
+
"fd_write",
|
|
915
|
+
"sock_recv",
|
|
916
|
+
"path_open",
|
|
917
|
+
] {
|
|
918
|
+
assert!(
|
|
919
|
+
!text.contains(forbidden),
|
|
920
|
+
"the contract module names {forbidden}, which it has no reason to"
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/// The contract never traps, on any corpus input.
|
|
926
|
+
#[test]
|
|
927
|
+
fn wasm_contract_returns_a_result_for_every_input_including_malformed() {
|
|
928
|
+
let Some(bytes) = contract_module() else {
|
|
929
|
+
return skipped("wasm_contract_returns_a_result_for_every_input_including_malformed");
|
|
930
|
+
};
|
|
931
|
+
let mut wasm = WasmContract::instantiate(bytes);
|
|
932
|
+
for case in &corpus().cases {
|
|
933
|
+
let output = wasm.evaluate(&case.bytes());
|
|
934
|
+
let parsed: Value =
|
|
935
|
+
serde_json::from_slice(&output).expect("every output is canonical JSON");
|
|
936
|
+
assert_eq!(parsed["contract"], STATE_DIFF_CONTRACT_ID);
|
|
937
|
+
assert!(
|
|
938
|
+
parsed.get("diff").is_some() ^ parsed.get("error").is_some(),
|
|
939
|
+
"{} produced neither or both of diff and error",
|
|
940
|
+
case.name
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
// Inputs no fixture would contain, to show trap-freedom is not a property
|
|
944
|
+
// of the corpus. Nesting past the shared recursion bound is included.
|
|
945
|
+
for hostile in [
|
|
946
|
+
vec![0u8; 0],
|
|
947
|
+
vec![0xff; 64],
|
|
948
|
+
b"{".repeat(4096),
|
|
949
|
+
b"[".repeat(4096),
|
|
950
|
+
vec![b'"'; 1024],
|
|
951
|
+
format!(
|
|
952
|
+
r#"{{"contract":"feltdb.state.diff","state_a":{}{},"state_b":1,"version":1}}"#,
|
|
953
|
+
"[".repeat(300),
|
|
954
|
+
"]".repeat(300)
|
|
955
|
+
)
|
|
956
|
+
.into_bytes(),
|
|
957
|
+
] {
|
|
958
|
+
let output = wasm.evaluate(&hostile);
|
|
959
|
+
assert!(serde_json::from_slice::<Value>(&output).is_ok());
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// ---------------------------------------------------------------------------
|
|
964
|
+
// Mutation: does the suite actually detect divergence?
|
|
965
|
+
// ---------------------------------------------------------------------------
|
|
966
|
+
|
|
967
|
+
/// A deliberate change to the WASM implementation's source.
|
|
968
|
+
struct Mutation {
|
|
969
|
+
name: &'static str,
|
|
970
|
+
file: &'static str,
|
|
971
|
+
from: &'static str,
|
|
972
|
+
to: &'static str,
|
|
973
|
+
/// Whether the corpus must catch it.
|
|
974
|
+
///
|
|
975
|
+
/// One mutation is expected *not* to be caught, and that expectation is
|
|
976
|
+
/// part of the contract: it changes an ordering the contract does not
|
|
977
|
+
/// make semantic.
|
|
978
|
+
detected: bool,
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const MUTATIONS: &[Mutation] = &[
|
|
982
|
+
Mutation {
|
|
983
|
+
name: "additions are reported as changes",
|
|
984
|
+
file: "src/diff.rs",
|
|
985
|
+
from: " kind: ChangeKind::Added,\n old_value: None,\n new_value: Some(new_value.clone()),\n });\n path.pop();",
|
|
986
|
+
to: " kind: ChangeKind::Changed,\n old_value: None,\n new_value: Some(new_value.clone()),\n });\n path.pop();",
|
|
987
|
+
detected: true,
|
|
988
|
+
},
|
|
989
|
+
Mutation {
|
|
990
|
+
name: "object removals are not reported",
|
|
991
|
+
file: "src/diff.rs",
|
|
992
|
+
from: " None => changes.push(Change {\n path: path.clone(),\n kind: ChangeKind::Removed,",
|
|
993
|
+
to: " None => changes.push(Change {\n path: path.clone(),\n kind: ChangeKind::Added,",
|
|
994
|
+
detected: true,
|
|
995
|
+
},
|
|
996
|
+
Mutation {
|
|
997
|
+
name: "unequal integers are not reported as changed",
|
|
998
|
+
file: "src/diff.rs",
|
|
999
|
+
from: " (Json::PosInt(a), Json::PosInt(b)) if a == b => {}",
|
|
1000
|
+
to: " (Json::PosInt(_), Json::PosInt(_)) => {}",
|
|
1001
|
+
detected: true,
|
|
1002
|
+
},
|
|
1003
|
+
Mutation {
|
|
1004
|
+
name: "object path components are dropped",
|
|
1005
|
+
file: "src/diff.rs",
|
|
1006
|
+
from: " for (key, old_value) in a {\n path.push(PathComponent::Key(key.clone()));",
|
|
1007
|
+
to: " for (key, old_value) in a {\n path.push(PathComponent::Key(alloc::string::String::from(\"x\")));",
|
|
1008
|
+
detected: true,
|
|
1009
|
+
},
|
|
1010
|
+
Mutation {
|
|
1011
|
+
name: "array indexes are off by one",
|
|
1012
|
+
file: "src/diff.rs",
|
|
1013
|
+
from: " for (index, (old_value, new_value)) in a.iter().zip(b.iter()).enumerate() {\n path.push(PathComponent::Index(index));",
|
|
1014
|
+
to: " for (index, (old_value, new_value)) in a.iter().zip(b.iter()).enumerate() {\n path.push(PathComponent::Index(index + 1));",
|
|
1015
|
+
detected: true,
|
|
1016
|
+
},
|
|
1017
|
+
Mutation {
|
|
1018
|
+
name: "array removals start at the wrong index",
|
|
1019
|
+
file: "src/diff.rs",
|
|
1020
|
+
from: " for (index, old_value) in a.iter().enumerate().skip(b.len()) {",
|
|
1021
|
+
to: " for (index, old_value) in a.iter().enumerate().skip(b.len() + 1) {",
|
|
1022
|
+
detected: true,
|
|
1023
|
+
},
|
|
1024
|
+
Mutation {
|
|
1025
|
+
name: "old values are not emitted",
|
|
1026
|
+
file: "src/diff.rs",
|
|
1027
|
+
from: " (_, _) => changes.push(Change {\n path: path.clone(),\n kind: ChangeKind::Changed,\n old_value: Some(old.clone()),",
|
|
1028
|
+
to: " (_, _) => changes.push(Change {\n path: path.clone(),\n kind: ChangeKind::Changed,\n old_value: None,",
|
|
1029
|
+
detected: true,
|
|
1030
|
+
},
|
|
1031
|
+
Mutation {
|
|
1032
|
+
name: "new values are not emitted",
|
|
1033
|
+
file: "src/diff.rs",
|
|
1034
|
+
from: " kind: ChangeKind::Changed,\n old_value: Some(old.clone()),\n new_value: Some(new.clone()),",
|
|
1035
|
+
to: " kind: ChangeKind::Changed,\n old_value: Some(old.clone()),\n new_value: None,",
|
|
1036
|
+
detected: true,
|
|
1037
|
+
},
|
|
1038
|
+
Mutation {
|
|
1039
|
+
name: "null against null is reported as a change",
|
|
1040
|
+
file: "src/diff.rs",
|
|
1041
|
+
from: " (Json::Null, Json::Null) => {}",
|
|
1042
|
+
to: " (Json::Null, Json::Null) if false => {}",
|
|
1043
|
+
detected: true,
|
|
1044
|
+
},
|
|
1045
|
+
Mutation {
|
|
1046
|
+
name: "integers and floats compare equal",
|
|
1047
|
+
file: "src/diff.rs",
|
|
1048
|
+
from: " (Json::Float(a), Json::Float(b)) if a == b => {}",
|
|
1049
|
+
to: " (Json::Float(a), Json::Float(b)) if a == b => {}\n (Json::PosInt(a), Json::Float(b)) if *a as f64 == *b => {}\n (Json::Float(a), Json::PosInt(b)) if *a == *b as f64 => {}",
|
|
1050
|
+
detected: true,
|
|
1051
|
+
},
|
|
1052
|
+
Mutation {
|
|
1053
|
+
name: "the result is not sorted by path",
|
|
1054
|
+
file: "src/diff.rs",
|
|
1055
|
+
from: " changes.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.kind.cmp(&b.kind)));",
|
|
1056
|
+
to: " changes.sort_by(|a, b| b.path.cmp(&a.path).then_with(|| a.kind.cmp(&b.kind)));",
|
|
1057
|
+
detected: true,
|
|
1058
|
+
},
|
|
1059
|
+
Mutation {
|
|
1060
|
+
name: "floats are rendered by Rust rather than by ryu's rule",
|
|
1061
|
+
file: "src/json.rs",
|
|
1062
|
+
from: " if (-5..=15).contains(&exponent) {",
|
|
1063
|
+
to: " if (-4..=15).contains(&exponent) {",
|
|
1064
|
+
detected: true,
|
|
1065
|
+
},
|
|
1066
|
+
Mutation {
|
|
1067
|
+
name: "strings skip an escape serde_json emits",
|
|
1068
|
+
file: "src/json.rs",
|
|
1069
|
+
from: " '\\t' => out.extend_from_slice(b\"\\\\t\"),",
|
|
1070
|
+
to: " '\\t' => out.extend_from_slice(b\"\\t\"),",
|
|
1071
|
+
detected: true,
|
|
1072
|
+
},
|
|
1073
|
+
// The one mutation that must NOT be caught. Additions are emitted while
|
|
1074
|
+
// walking the new object's members and the whole result is sorted
|
|
1075
|
+
// afterwards; paths are unique within a diff, so the order they were
|
|
1076
|
+
// emitted in cannot survive into the output. If this were ever detected,
|
|
1077
|
+
// the contract would have accidentally made an internal traversal order
|
|
1078
|
+
// part of its semantics.
|
|
1079
|
+
Mutation {
|
|
1080
|
+
name: "object additions are emitted in reverse order before sorting",
|
|
1081
|
+
file: "src/diff.rs",
|
|
1082
|
+
from: " for (key, new_value) in b {\n if old.get(key).is_none() {",
|
|
1083
|
+
to: " for (key, new_value) in b.iter().rev() {\n if old.get(key).is_none() {",
|
|
1084
|
+
detected: false,
|
|
1085
|
+
},
|
|
1086
|
+
];
|
|
1087
|
+
|
|
1088
|
+
/// Every deliberate mutation of the WASM contract lands where it should.
|
|
1089
|
+
///
|
|
1090
|
+
/// Without this the conformance suite proves only that two implementations
|
|
1091
|
+
/// agree, not that agreement was ever in question. Each mutant is built from a
|
|
1092
|
+
/// copy of the real source with one edit applied. Twelve must fail against the
|
|
1093
|
+
/// committed expectations; one must not, because it changes an order the
|
|
1094
|
+
/// contract deliberately does not make semantic.
|
|
1095
|
+
#[test]
|
|
1096
|
+
fn deliberate_wasm_mutations_land_where_they_should() {
|
|
1097
|
+
if contract_module().is_none() {
|
|
1098
|
+
return skipped("deliberate_wasm_mutations_land_where_they_should");
|
|
1099
|
+
}
|
|
1100
|
+
let source = repository_root().join(WASM_CRATE);
|
|
1101
|
+
let scratch = tempfile::tempdir().expect("a scratch directory");
|
|
1102
|
+
|
|
1103
|
+
let mut wrong = Vec::new();
|
|
1104
|
+
for mutation in MUTATIONS {
|
|
1105
|
+
let root = scratch.path().join(mutation.name.replace(' ', "-"));
|
|
1106
|
+
copy_crate(&source, &root);
|
|
1107
|
+
let target = root.join(mutation.file);
|
|
1108
|
+
let original = std::fs::read_to_string(&target).expect("the mutated file exists");
|
|
1109
|
+
assert!(
|
|
1110
|
+
original.matches(mutation.from).count() == 1,
|
|
1111
|
+
"mutation {:?} no longer applies: its anchor is not present exactly once in {}. \
|
|
1112
|
+
The mutation suite must be updated with the implementation it mutates.",
|
|
1113
|
+
mutation.name,
|
|
1114
|
+
mutation.file
|
|
1115
|
+
);
|
|
1116
|
+
std::fs::write(&target, original.replace(mutation.from, mutation.to)).unwrap();
|
|
1117
|
+
|
|
1118
|
+
let mut mutant = WasmContract::instantiate(&build_module(&root));
|
|
1119
|
+
let caught: Vec<&str> = corpus()
|
|
1120
|
+
.cases
|
|
1121
|
+
.iter()
|
|
1122
|
+
.filter(|case| {
|
|
1123
|
+
String::from_utf8_lossy(&mutant.evaluate(&case.bytes())) != case.expected()
|
|
1124
|
+
})
|
|
1125
|
+
.map(|case| case.name.as_str())
|
|
1126
|
+
.collect();
|
|
1127
|
+
|
|
1128
|
+
match (mutation.detected, caught.first()) {
|
|
1129
|
+
(true, Some(first)) => println!(
|
|
1130
|
+
"mutation {:?} caught by {} fixtures, first: {first}",
|
|
1131
|
+
mutation.name,
|
|
1132
|
+
caught.len()
|
|
1133
|
+
),
|
|
1134
|
+
(false, None) => println!(
|
|
1135
|
+
"mutation {:?} correctly NOT caught: the order it changes is not semantic",
|
|
1136
|
+
mutation.name
|
|
1137
|
+
),
|
|
1138
|
+
(true, None) => wrong.push(format!(
|
|
1139
|
+
"{:?} was not caught, but the corpus should distinguish it",
|
|
1140
|
+
mutation.name
|
|
1141
|
+
)),
|
|
1142
|
+
(false, Some(_)) => wrong.push(format!(
|
|
1143
|
+
"{:?} was caught by {} fixtures, but it changes an order the contract does not \
|
|
1144
|
+
make semantic; the contract has accidentally fixed an internal traversal order",
|
|
1145
|
+
mutation.name,
|
|
1146
|
+
caught.len()
|
|
1147
|
+
)),
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
assert!(
|
|
1151
|
+
wrong.is_empty(),
|
|
1152
|
+
"mutation outcomes were wrong:\n{}",
|
|
1153
|
+
wrong.join("\n")
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/// Copies the contract crate's source, without its build directory.
|
|
1158
|
+
fn copy_crate(from: &Path, to: &Path) {
|
|
1159
|
+
std::fs::create_dir_all(to.join("src")).unwrap();
|
|
1160
|
+
std::fs::copy(from.join("Cargo.toml"), to.join("Cargo.toml")).unwrap();
|
|
1161
|
+
for entry in std::fs::read_dir(from.join("src")).unwrap() {
|
|
1162
|
+
let entry = entry.unwrap();
|
|
1163
|
+
std::fs::copy(entry.path(), to.join("src").join(entry.file_name())).unwrap();
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/// Mutating the input encoding is detected where the encoding is semantically
|
|
1168
|
+
/// load-bearing, and not where it is not.
|
|
1169
|
+
///
|
|
1170
|
+
/// - Reordering members changes nothing, because a JSON object denotes a map
|
|
1171
|
+
/// and both implementations decode it to one. The suite must *not* flag it.
|
|
1172
|
+
/// - Swapping the two states changes the diff's direction, which is entirely
|
|
1173
|
+
/// semantic. The suite must flag it.
|
|
1174
|
+
/// - Dropping the contract identity turns every case into an input error.
|
|
1175
|
+
#[test]
|
|
1176
|
+
fn input_encoding_mutations_are_detected_where_they_matter() {
|
|
1177
|
+
let typed: Vec<(&Case, Value)> = corpus()
|
|
1178
|
+
.cases
|
|
1179
|
+
.iter()
|
|
1180
|
+
.filter_map(|case| {
|
|
1181
|
+
let input = case.input.as_ref()?;
|
|
1182
|
+
Some((case, serde_json::to_value(input).unwrap()))
|
|
1183
|
+
})
|
|
1184
|
+
.collect();
|
|
1185
|
+
assert!(typed.len() > 70, "most of the corpus is typed input");
|
|
1186
|
+
|
|
1187
|
+
// 1. Member order, which carries no meaning.
|
|
1188
|
+
for (case, encoded) in &typed {
|
|
1189
|
+
let produced =
|
|
1190
|
+
String::from_utf8(evaluate_canonical(encode_reversed(encoded).as_bytes())).unwrap();
|
|
1191
|
+
assert_eq!(
|
|
1192
|
+
produced,
|
|
1193
|
+
case.expected(),
|
|
1194
|
+
"{} changed answer under a reordered encoding; object member order is not part of \
|
|
1195
|
+
this contract's semantics",
|
|
1196
|
+
case.name
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// 2. Direction, which carries all of it.
|
|
1201
|
+
let mut direction_matters = 0;
|
|
1202
|
+
for (case, encoded) in &typed {
|
|
1203
|
+
let mut swapped = encoded.clone();
|
|
1204
|
+
let object = swapped.as_object_mut().unwrap();
|
|
1205
|
+
let a = object["state_a"].clone();
|
|
1206
|
+
let b = object["state_b"].clone();
|
|
1207
|
+
object["state_a"] = b;
|
|
1208
|
+
object["state_b"] = a;
|
|
1209
|
+
let produced = String::from_utf8(evaluate_canonical(
|
|
1210
|
+
serde_json::to_vec(&swapped).unwrap().as_slice(),
|
|
1211
|
+
))
|
|
1212
|
+
.unwrap();
|
|
1213
|
+
if produced != case.expected() {
|
|
1214
|
+
direction_matters += 1;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
assert!(
|
|
1218
|
+
direction_matters > 50,
|
|
1219
|
+
"the diff is direction sensitive; swapping the states should change most results, \
|
|
1220
|
+
but it changed only {direction_matters}"
|
|
1221
|
+
);
|
|
1222
|
+
|
|
1223
|
+
// 3. Contract identity dropped.
|
|
1224
|
+
for (case, encoded) in typed.iter().take(1) {
|
|
1225
|
+
let mut mutated = encoded.clone();
|
|
1226
|
+
mutated.as_object_mut().unwrap().remove("contract");
|
|
1227
|
+
let produced = String::from_utf8(evaluate_canonical(
|
|
1228
|
+
serde_json::to_vec(&mutated).unwrap().as_slice(),
|
|
1229
|
+
))
|
|
1230
|
+
.unwrap();
|
|
1231
|
+
assert_ne!(produced, case.expected());
|
|
1232
|
+
assert!(produced.contains("CONTRACT_INPUT_INVALID"));
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/// Encodes a value with every object's members in reverse sorted order.
|
|
1237
|
+
///
|
|
1238
|
+
/// `serde_json::Map` re-sorts on insert, so a non-canonical member order has to
|
|
1239
|
+
/// be produced as text rather than as a value.
|
|
1240
|
+
fn encode_reversed(value: &Value) -> String {
|
|
1241
|
+
match value {
|
|
1242
|
+
Value::Object(members) => {
|
|
1243
|
+
let mut parts: Vec<String> = members
|
|
1244
|
+
.iter()
|
|
1245
|
+
.map(|(key, value)| {
|
|
1246
|
+
format!(
|
|
1247
|
+
"{}:{}",
|
|
1248
|
+
serde_json::to_string(key).unwrap(),
|
|
1249
|
+
encode_reversed(value)
|
|
1250
|
+
)
|
|
1251
|
+
})
|
|
1252
|
+
.collect();
|
|
1253
|
+
parts.reverse();
|
|
1254
|
+
format!("{{{}}}", parts.join(","))
|
|
1255
|
+
}
|
|
1256
|
+
Value::Array(items) => format!(
|
|
1257
|
+
"[{}]",
|
|
1258
|
+
items
|
|
1259
|
+
.iter()
|
|
1260
|
+
.map(encode_reversed)
|
|
1261
|
+
.collect::<Vec<_>>()
|
|
1262
|
+
.join(",")
|
|
1263
|
+
),
|
|
1264
|
+
other => serde_json::to_string(other).unwrap(),
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// ---------------------------------------------------------------------------
|
|
1269
|
+
// Corpus maintenance
|
|
1270
|
+
// ---------------------------------------------------------------------------
|
|
1271
|
+
|
|
1272
|
+
/// Records the canonical output of every case, after both implementations agree.
|
|
1273
|
+
///
|
|
1274
|
+
/// Ignored by default: it writes the corpus. Run it deliberately with
|
|
1275
|
+
/// `FELTDB_STATE_DIFF_CONTRACT_BLESS=1 cargo test -p feltdb --test
|
|
1276
|
+
/// state_diff_contract_conformance -- bless --ignored`. It refuses to record
|
|
1277
|
+
/// anything unless the WASM implementation is available and agrees, so an
|
|
1278
|
+
/// expectation can never be blessed from one implementation alone.
|
|
1279
|
+
#[test]
|
|
1280
|
+
#[ignore = "rewrites the committed corpus"]
|
|
1281
|
+
fn bless() {
|
|
1282
|
+
assert_eq!(
|
|
1283
|
+
std::env::var("FELTDB_STATE_DIFF_CONTRACT_BLESS").as_deref(),
|
|
1284
|
+
Ok("1"),
|
|
1285
|
+
"set FELTDB_STATE_DIFF_CONTRACT_BLESS=1 to rewrite the corpus"
|
|
1286
|
+
);
|
|
1287
|
+
let bytes = contract_module().expect(
|
|
1288
|
+
"blessing requires the WASM implementation: an expectation recorded from the native \
|
|
1289
|
+
implementation alone would prove nothing",
|
|
1290
|
+
);
|
|
1291
|
+
let mut wasm = WasmContract::instantiate(bytes);
|
|
1292
|
+
|
|
1293
|
+
let mut document: Value =
|
|
1294
|
+
serde_json::from_slice(&std::fs::read(corpus_path()).unwrap()).unwrap();
|
|
1295
|
+
for (index, case) in corpus().cases.iter().enumerate() {
|
|
1296
|
+
let input = case.bytes();
|
|
1297
|
+
let native = evaluate_canonical(&input);
|
|
1298
|
+
let produced = wasm.evaluate(&input);
|
|
1299
|
+
assert_eq!(
|
|
1300
|
+
String::from_utf8_lossy(&native),
|
|
1301
|
+
String::from_utf8_lossy(&produced),
|
|
1302
|
+
"refusing to record {}: the implementations disagree",
|
|
1303
|
+
case.name
|
|
1304
|
+
);
|
|
1305
|
+
if case.input.is_some() {
|
|
1306
|
+
document["cases"][index]["canonical_input"] =
|
|
1307
|
+
Value::String(String::from_utf8(input).unwrap());
|
|
1308
|
+
}
|
|
1309
|
+
document["cases"][index]["expected_output"] =
|
|
1310
|
+
Value::String(String::from_utf8(native).unwrap());
|
|
1311
|
+
}
|
|
1312
|
+
let mut text = serde_json::to_string_pretty(&document).unwrap();
|
|
1313
|
+
text.push('\n');
|
|
1314
|
+
std::fs::write(corpus_path(), text).unwrap();
|
|
1315
|
+
println!("recorded {} expectations", corpus().cases.len());
|
|
1316
|
+
}
|