@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.
Files changed (149) hide show
  1. package/dist/create/package-versions.js +1 -1
  2. package/dist/create/server-source/Cargo.lock +165 -0
  3. package/dist/create/server-source/Cargo.toml +9 -0
  4. package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
  5. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
  6. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
  7. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
  8. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
  9. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
  10. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
  11. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
  12. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
  13. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
  14. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
  15. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
  19. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
  20. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
  21. package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
  22. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
  23. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
  24. package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
  25. package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
  26. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
  27. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
  28. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
  29. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
  30. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
  31. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
  32. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
  33. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
  34. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
  35. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
  36. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
  37. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
  38. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
  39. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
  40. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
  41. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
  42. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
  43. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
  44. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
  45. package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
  46. package/dist/create/server-source/crates/feltdb/src/lib.rs +507 -6
  47. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
  48. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
  49. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
  50. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
  51. package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
  52. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
  53. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
  54. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
  55. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
  56. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
  57. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
  58. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
  59. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
  60. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
  61. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
  62. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +79 -70
  63. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
  64. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
  65. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
  66. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
  67. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
  68. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
  69. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
  70. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
  71. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
  72. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
  73. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
  74. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
  75. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
  76. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
  77. package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
  78. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
  79. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
  80. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
  81. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
  82. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
  83. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
  84. package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
  85. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
  86. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +13 -4
  87. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
  88. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
  89. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
  90. package/dist/create/server-source/crates/feltdb/src/state_model.rs +1514 -537
  91. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
  92. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
  93. package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
  94. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
  95. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
  96. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
  97. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
  98. package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
  99. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
  100. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
  101. package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
  102. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +3 -4
  103. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
  104. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +288 -0
  105. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
  106. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
  107. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
  108. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
  109. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
  110. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +2 -1
  111. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +80 -25
  112. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
  113. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
  114. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
  115. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
  116. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
  117. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
  118. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
  119. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
  120. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
  121. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1032 -0
  122. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
  123. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
  124. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
  125. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
  126. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
  127. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
  128. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
  129. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
  130. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
  131. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
  132. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
  133. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
  134. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
  135. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
  136. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
  137. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
  138. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +2 -1
  139. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
  140. package/dist/create/server-source/crates/feltdb-server/src/main.rs +326 -6
  141. package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-DaNwCLRX.js} +1 -1
  142. package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
  143. package/dist/studio-app/assets/{index-DwgNAIIX.js → index-j8IlhNqJ.js} +1 -1
  144. package/dist/studio-app/index.html +1 -1
  145. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  146. package/dist/workload.d.ts +2 -1
  147. package/dist/workload.d.ts.map +1 -1
  148. package/package.json +1 -1
  149. 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
+ }