@feltdb/core 0.8.3 → 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/cli/commands.js +4 -1
- package/dist/cli/provisioning-neutrality.js +79 -0
- package/dist/collection.d.ts +43 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +192 -22
- package/dist/create/create.js +25 -21
- package/dist/create/managed-account.js +11 -0
- 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/equality_index.rs +595 -0
- 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 +1050 -117
- 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 +90 -72
- 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_execution_diagnostics.rs +126 -0
- 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 +305 -6
- 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/sync.rs +12 -0
- 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 +442 -0
- 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 +234 -0
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +947 -0
- 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/audit.rs +1137 -29
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +800 -34
- package/dist/db.d.ts +33 -34
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +74 -20
- package/dist/deployment.d.ts +30 -0
- package/dist/deployment.d.ts.map +1 -0
- package/dist/deployment.js +130 -0
- package/dist/embedded-transaction.d.ts +22 -4
- package/dist/embedded-transaction.d.ts.map +1 -1
- package/dist/embedded-transaction.js +51 -5
- package/dist/feltdb.d.ts +14 -2
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.js +1 -1
- package/dist/http-client.d.ts +14 -0
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +23 -5
- package/dist/http-db.d.ts +119 -1
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +346 -31
- package/dist/index-core.d.ts +2 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +35 -21
- package/dist/managed-recovery.d.ts +192 -0
- package/dist/managed-recovery.d.ts.map +1 -0
- package/dist/managed-recovery.js +242 -0
- package/dist/memory-db.js +1 -1
- package/dist/studio-app/assets/{feltdb_wasm-DB8cX151.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-j8IlhNqJ.js +29 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/transaction.d.ts +30 -0
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +41 -0
- 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-ClhDHp0S.wasm +0 -0
- package/dist/studio-app/assets/index-B0k4UAlI.js +0 -29
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
//! Conformance, determinism and capability proofs for the
|
|
2
|
+
//! `feltdb.trigger.match` deterministic contract.
|
|
3
|
+
//!
|
|
4
|
+
//! The architectural invariant under test is:
|
|
5
|
+
//!
|
|
6
|
+
//! ```text
|
|
7
|
+
//! Contract semantics are authoritative.
|
|
8
|
+
//! Runtime implementation is replaceable.
|
|
9
|
+
//!
|
|
10
|
+
//! NativeContract(input) == WasmContract(input) for every fixture
|
|
11
|
+
//! ```
|
|
12
|
+
//!
|
|
13
|
+
//! The corpus in `tests/fixtures/trigger_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 even where the WASM toolchain is
|
|
22
|
+
//! absent: a change in native semantics is caught by the same bytes the WASM
|
|
23
|
+
//! implementation was measured against.
|
|
24
|
+
|
|
25
|
+
use feltdb::trigger_contract::{
|
|
26
|
+
canonical_input_bytes, evaluate_canonical, TriggerMatchInput, TRIGGER_MATCH_CONTRACT_ID,
|
|
27
|
+
TRIGGER_MATCH_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<TriggerMatchInput>,
|
|
57
|
+
/// Bytes fed to the contract verbatim, for malformed and non-canonical
|
|
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. Recording the
|
|
67
|
+
/// bytes makes the corpus self-describing instead of requiring every reader
|
|
68
|
+
/// to own a canonical encoder.
|
|
69
|
+
#[serde(default)]
|
|
70
|
+
canonical_input: Option<String>,
|
|
71
|
+
/// The canonical output both implementations must produce.
|
|
72
|
+
#[serde(default)]
|
|
73
|
+
expected_output: Option<String>,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
impl Case {
|
|
77
|
+
/// The exact bytes this case presents to a contract implementation.
|
|
78
|
+
fn bytes(&self) -> Vec<u8> {
|
|
79
|
+
match (&self.input, &self.raw_input) {
|
|
80
|
+
(Some(input), None) => canonical_input_bytes(input),
|
|
81
|
+
(None, Some(raw)) => raw.as_bytes().to_vec(),
|
|
82
|
+
_ => panic!(
|
|
83
|
+
"case {} must have exactly one of input/raw_input",
|
|
84
|
+
self.name
|
|
85
|
+
),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn expected(&self) -> &str {
|
|
90
|
+
self.expected_output.as_deref().unwrap_or_else(|| {
|
|
91
|
+
panic!(
|
|
92
|
+
"case {} has no expected_output; regenerate the corpus with \
|
|
93
|
+
FELTDB_TRIGGER_CONTRACT_BLESS=1",
|
|
94
|
+
self.name
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn corpus_path() -> PathBuf {
|
|
101
|
+
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/trigger_contract_corpus.json")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn corpus() -> &'static Corpus {
|
|
105
|
+
static CORPUS: OnceLock<Corpus> = OnceLock::new();
|
|
106
|
+
CORPUS.get_or_init(|| {
|
|
107
|
+
let bytes = std::fs::read(corpus_path()).expect("conformance corpus is committed");
|
|
108
|
+
serde_json::from_slice(&bytes).expect("conformance corpus is well formed")
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[test]
|
|
113
|
+
fn corpus_addresses_this_contract_at_this_version() {
|
|
114
|
+
let corpus = corpus();
|
|
115
|
+
assert_eq!(corpus.corpus_version, 1);
|
|
116
|
+
assert_eq!(corpus.contract, TRIGGER_MATCH_CONTRACT_ID);
|
|
117
|
+
assert_eq!(corpus.contract_version, TRIGGER_MATCH_CONTRACT_VERSION);
|
|
118
|
+
assert!(
|
|
119
|
+
corpus.cases.len() >= 90,
|
|
120
|
+
"the corpus is the proof; it should not shrink silently"
|
|
121
|
+
);
|
|
122
|
+
let mut names: Vec<&str> = corpus.cases.iter().map(|case| case.name.as_str()).collect();
|
|
123
|
+
names.sort_unstable();
|
|
124
|
+
let unique = names.len();
|
|
125
|
+
names.dedup();
|
|
126
|
+
assert_eq!(unique, names.len(), "case names identify cases");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Every required category of behaviour is represented by at least one case.
|
|
130
|
+
///
|
|
131
|
+
/// This is a guard against the corpus quietly losing a dimension it was built
|
|
132
|
+
/// to cover, not a substitute for reading it.
|
|
133
|
+
#[test]
|
|
134
|
+
fn corpus_covers_every_required_category() {
|
|
135
|
+
let names: Vec<&str> = corpus().cases.iter().map(|c| c.name.as_str()).collect();
|
|
136
|
+
for required in [
|
|
137
|
+
"collection_matches",
|
|
138
|
+
"collection_differs",
|
|
139
|
+
"change_kind_update_matches",
|
|
140
|
+
"change_kind_insert_against_update_trigger",
|
|
141
|
+
"disabled_trigger_rejects_matching_change",
|
|
142
|
+
"predicate_matches_string",
|
|
143
|
+
"predicate_value_mismatch",
|
|
144
|
+
"predicate_absent_matches_any_value",
|
|
145
|
+
"value_null_present_with_whole_record_predicate",
|
|
146
|
+
"nested_path_matches",
|
|
147
|
+
"array_equality_matches",
|
|
148
|
+
"integer_equality_matches",
|
|
149
|
+
"fanout_first_trigger_matches",
|
|
150
|
+
"malformed_not_json",
|
|
151
|
+
"empty_object_record",
|
|
152
|
+
] {
|
|
153
|
+
assert!(
|
|
154
|
+
names.contains(&required),
|
|
155
|
+
"corpus lost coverage: {required}"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// The committed canonical bytes are what this crate's encoder produces.
|
|
161
|
+
///
|
|
162
|
+
/// Consumers outside Rust read `canonical_input` rather than re-canonicalizing,
|
|
163
|
+
/// so this is the check that keeps those bytes honest: if canonicalization
|
|
164
|
+
/// changed, every other reader of the corpus would silently be testing an
|
|
165
|
+
/// encoding FeltDB no longer emits.
|
|
166
|
+
#[test]
|
|
167
|
+
fn committed_canonical_bytes_match_the_encoder() {
|
|
168
|
+
for case in &corpus().cases {
|
|
169
|
+
match (&case.input, &case.raw_input) {
|
|
170
|
+
(Some(input), None) => assert_eq!(
|
|
171
|
+
case.canonical_input.as_deref(),
|
|
172
|
+
Some(
|
|
173
|
+
String::from_utf8(canonical_input_bytes(input))
|
|
174
|
+
.unwrap()
|
|
175
|
+
.as_str()
|
|
176
|
+
),
|
|
177
|
+
"{} has a stale canonical_input",
|
|
178
|
+
case.name
|
|
179
|
+
),
|
|
180
|
+
(None, Some(_)) => assert!(
|
|
181
|
+
case.canonical_input.is_none(),
|
|
182
|
+
"{} is a raw case and must not also record canonical_input",
|
|
183
|
+
case.name
|
|
184
|
+
),
|
|
185
|
+
_ => panic!(
|
|
186
|
+
"case {} must have exactly one of input/raw_input",
|
|
187
|
+
case.name
|
|
188
|
+
),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// WASM host
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
const WASM_CRATE: &str = "crates/feltdb-trigger-contract-wasm";
|
|
198
|
+
const WASM_ARTIFACT: &str =
|
|
199
|
+
"target/wasm32-unknown-unknown/release/feltdb_trigger_contract_wasm.wasm";
|
|
200
|
+
|
|
201
|
+
fn repository_root() -> &'static Path {
|
|
202
|
+
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
203
|
+
.parent()
|
|
204
|
+
.and_then(Path::parent)
|
|
205
|
+
.expect("the crate lives at <root>/crates/feltdb")
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// Whether this machine can compile the contract to WebAssembly at all.
|
|
209
|
+
fn wasm_target_installed() -> bool {
|
|
210
|
+
Command::new("rustc")
|
|
211
|
+
.args([
|
|
212
|
+
"--print",
|
|
213
|
+
"target-libdir",
|
|
214
|
+
"--target",
|
|
215
|
+
"wasm32-unknown-unknown",
|
|
216
|
+
])
|
|
217
|
+
.output()
|
|
218
|
+
.is_ok_and(|output| output.status.success())
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// Compiles a WASM contract crate and returns the module bytes.
|
|
222
|
+
///
|
|
223
|
+
/// A build failure is a defect and panics. A missing `wasm32-unknown-unknown`
|
|
224
|
+
/// target is an environment fact, reported by the caller as a skip.
|
|
225
|
+
fn build_module(manifest_dir: &Path) -> Vec<u8> {
|
|
226
|
+
let output = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
|
|
227
|
+
.args(["build", "--release", "--target", "wasm32-unknown-unknown"])
|
|
228
|
+
.current_dir(manifest_dir)
|
|
229
|
+
// The contract crate is its own workspace; inheriting the outer
|
|
230
|
+
// build's target directory or profile would defeat that.
|
|
231
|
+
.env_remove("CARGO_TARGET_DIR")
|
|
232
|
+
.env_remove("RUSTFLAGS")
|
|
233
|
+
.output()
|
|
234
|
+
.expect("cargo is runnable");
|
|
235
|
+
assert!(
|
|
236
|
+
output.status.success(),
|
|
237
|
+
"building the WASM contract at {} failed:\n{}",
|
|
238
|
+
manifest_dir.display(),
|
|
239
|
+
String::from_utf8_lossy(&output.stderr)
|
|
240
|
+
);
|
|
241
|
+
std::fs::read(manifest_dir.join(WASM_ARTIFACT)).expect("the build emits one module")
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// The committed WASM contract, compiled once for the whole suite.
|
|
245
|
+
fn contract_module() -> Option<&'static [u8]> {
|
|
246
|
+
static MODULE: OnceLock<Option<Vec<u8>>> = OnceLock::new();
|
|
247
|
+
MODULE
|
|
248
|
+
.get_or_init(|| {
|
|
249
|
+
if let Ok(path) = std::env::var("FELTDB_TRIGGER_CONTRACT_WASM") {
|
|
250
|
+
return Some(
|
|
251
|
+
std::fs::read(path).expect("FELTDB_TRIGGER_CONTRACT_WASM is readable"),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if !wasm_target_installed() {
|
|
255
|
+
return None;
|
|
256
|
+
}
|
|
257
|
+
Some(build_module(&repository_root().join(WASM_CRATE)))
|
|
258
|
+
})
|
|
259
|
+
.as_deref()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// Announces a skip loudly, so an absent toolchain is never a silent pass.
|
|
263
|
+
fn skipped(test: &str) {
|
|
264
|
+
eprintln!(
|
|
265
|
+
"SKIPPED {test}: the wasm32-unknown-unknown target is not installed, so the WASM \
|
|
266
|
+
contract implementation could not be built. Native conformance against the committed \
|
|
267
|
+
expectations still ran. Install the target, or set FELTDB_TRIGGER_CONTRACT_WASM to a \
|
|
268
|
+
prebuilt module, to execute the cross-runtime proof."
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// One instantiation of the WASM contract.
|
|
273
|
+
///
|
|
274
|
+
/// The linker is empty and stays empty: the module declares no imports, so
|
|
275
|
+
/// there is nothing to satisfy and nothing to grant.
|
|
276
|
+
struct WasmContract {
|
|
277
|
+
store: wasmi::Store<()>,
|
|
278
|
+
memory: wasmi::Memory,
|
|
279
|
+
alloc: wasmi::TypedFunc<u32, u32>,
|
|
280
|
+
evaluate: wasmi::TypedFunc<(u32, u32), u64>,
|
|
281
|
+
free: wasmi::TypedFunc<(u32, u32), ()>,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
impl WasmContract {
|
|
285
|
+
fn instantiate(bytes: &[u8]) -> Self {
|
|
286
|
+
let engine = wasmi::Engine::default();
|
|
287
|
+
let module = wasmi::Module::new(&engine, bytes).expect("the module validates");
|
|
288
|
+
let mut store = wasmi::Store::new(&engine, ());
|
|
289
|
+
let instance = wasmi::Linker::<()>::new(&engine)
|
|
290
|
+
.instantiate_and_start(&mut store, &module)
|
|
291
|
+
.expect("an empty linker satisfies a module with no imports");
|
|
292
|
+
Self {
|
|
293
|
+
memory: instance
|
|
294
|
+
.get_memory(&store, "memory")
|
|
295
|
+
.expect("the module exports its memory"),
|
|
296
|
+
alloc: instance
|
|
297
|
+
.get_typed_func(&store, "contract_alloc")
|
|
298
|
+
.expect("contract_alloc"),
|
|
299
|
+
evaluate: instance
|
|
300
|
+
.get_typed_func(&store, "contract_evaluate")
|
|
301
|
+
.expect("contract_evaluate"),
|
|
302
|
+
free: instance
|
|
303
|
+
.get_typed_func(&store, "contract_free")
|
|
304
|
+
.expect("contract_free"),
|
|
305
|
+
store,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn version(bytes: &[u8]) -> u32 {
|
|
310
|
+
let engine = wasmi::Engine::default();
|
|
311
|
+
let module = wasmi::Module::new(&engine, bytes).unwrap();
|
|
312
|
+
let mut store = wasmi::Store::new(&engine, ());
|
|
313
|
+
let instance = wasmi::Linker::<()>::new(&engine)
|
|
314
|
+
.instantiate_and_start(&mut store, &module)
|
|
315
|
+
.unwrap();
|
|
316
|
+
instance
|
|
317
|
+
.get_typed_func::<(), u32>(&store, "contract_version")
|
|
318
|
+
.expect("contract_version")
|
|
319
|
+
.call(&mut store, ())
|
|
320
|
+
.expect("contract_version is total")
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
fn evaluate(&mut self, input: &[u8]) -> Vec<u8> {
|
|
324
|
+
let len = input.len() as u32;
|
|
325
|
+
let ptr = self.alloc.call(&mut self.store, len).expect("allocation");
|
|
326
|
+
self.memory
|
|
327
|
+
.write(&mut self.store, ptr as usize, input)
|
|
328
|
+
.expect("input fits in linear memory");
|
|
329
|
+
let packed = self
|
|
330
|
+
.evaluate
|
|
331
|
+
.call(&mut self.store, (ptr, len))
|
|
332
|
+
.expect("the contract never traps on any input");
|
|
333
|
+
let out_ptr = (packed >> 32) as usize;
|
|
334
|
+
let out_len = (packed & 0xffff_ffff) as usize;
|
|
335
|
+
let mut output = vec![0u8; out_len];
|
|
336
|
+
self.memory
|
|
337
|
+
.read(&self.store, out_ptr, &mut output)
|
|
338
|
+
.expect("the output buffer is inside linear memory");
|
|
339
|
+
self.free.call(&mut self.store, (ptr, len)).expect("free");
|
|
340
|
+
self.free
|
|
341
|
+
.call(&mut self.store, (out_ptr as u32, out_len as u32))
|
|
342
|
+
.expect("free");
|
|
343
|
+
output
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// Conformance
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
/// Native evaluation reproduces the committed canonical output for every case.
|
|
352
|
+
///
|
|
353
|
+
/// The expectations were recorded from a run in which the native and the WASM
|
|
354
|
+
/// implementation already agreed, so this is a full-strength check of native
|
|
355
|
+
/// semantics even on a machine with no WASM toolchain.
|
|
356
|
+
#[test]
|
|
357
|
+
fn native_matches_the_committed_expectations() {
|
|
358
|
+
let mut divergent = Vec::new();
|
|
359
|
+
for case in &corpus().cases {
|
|
360
|
+
let produced = String::from_utf8(evaluate_canonical(&case.bytes())).unwrap();
|
|
361
|
+
if produced != case.expected() {
|
|
362
|
+
divergent.push(format!(
|
|
363
|
+
"{}\n expected: {}\n native: {}",
|
|
364
|
+
case.name,
|
|
365
|
+
case.expected(),
|
|
366
|
+
produced
|
|
367
|
+
));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
assert!(
|
|
371
|
+
divergent.is_empty(),
|
|
372
|
+
"native contract diverged from the committed expectations:\n{}",
|
|
373
|
+
divergent.join("\n")
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// The proof: one corpus, two implementations, byte-identical canonical output.
|
|
378
|
+
#[test]
|
|
379
|
+
fn native_and_wasm_agree_on_every_fixture() {
|
|
380
|
+
let Some(bytes) = contract_module() else {
|
|
381
|
+
return skipped("native_and_wasm_agree_on_every_fixture");
|
|
382
|
+
};
|
|
383
|
+
let mut wasm = WasmContract::instantiate(bytes);
|
|
384
|
+
|
|
385
|
+
println!(
|
|
386
|
+
"{:<52} {:<7} {:<7} {}",
|
|
387
|
+
"fixture", "native", "wasm", "equal"
|
|
388
|
+
);
|
|
389
|
+
let mut divergent = Vec::new();
|
|
390
|
+
for case in &corpus().cases {
|
|
391
|
+
let input = case.bytes();
|
|
392
|
+
let native = evaluate_canonical(&input);
|
|
393
|
+
let produced = wasm.evaluate(&input);
|
|
394
|
+
let equal = native == produced;
|
|
395
|
+
println!(
|
|
396
|
+
"{:<52} {:<7} {:<7} {}",
|
|
397
|
+
case.name,
|
|
398
|
+
summarize(&native),
|
|
399
|
+
summarize(&produced),
|
|
400
|
+
if equal { "yes" } else { "NO" }
|
|
401
|
+
);
|
|
402
|
+
if !equal {
|
|
403
|
+
divergent.push(format!(
|
|
404
|
+
"{}\n input: {}\n native: {}\n wasm: {}",
|
|
405
|
+
case.name,
|
|
406
|
+
String::from_utf8_lossy(&input),
|
|
407
|
+
String::from_utf8_lossy(&native),
|
|
408
|
+
String::from_utf8_lossy(&produced)
|
|
409
|
+
));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
assert!(
|
|
413
|
+
divergent.is_empty(),
|
|
414
|
+
"native and WASM contract implementations diverged:\n{}",
|
|
415
|
+
divergent.join("\n")
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/// The WASM implementation reproduces the committed canonical output too.
|
|
420
|
+
///
|
|
421
|
+
/// Separate from the pairwise comparison on purpose: two implementations can
|
|
422
|
+
/// agree with each other while both having moved.
|
|
423
|
+
#[test]
|
|
424
|
+
fn wasm_matches_the_committed_expectations() {
|
|
425
|
+
let Some(bytes) = contract_module() else {
|
|
426
|
+
return skipped("wasm_matches_the_committed_expectations");
|
|
427
|
+
};
|
|
428
|
+
let mut wasm = WasmContract::instantiate(bytes);
|
|
429
|
+
for case in &corpus().cases {
|
|
430
|
+
assert_eq!(
|
|
431
|
+
String::from_utf8(wasm.evaluate(&case.bytes())).unwrap(),
|
|
432
|
+
case.expected(),
|
|
433
|
+
"WASM contract diverged from the committed expectation for {}",
|
|
434
|
+
case.name
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/// Both implementations answer for the same contract at the same version.
|
|
440
|
+
#[test]
|
|
441
|
+
fn both_implementations_declare_the_same_contract_version() {
|
|
442
|
+
let Some(bytes) = contract_module() else {
|
|
443
|
+
return skipped("both_implementations_declare_the_same_contract_version");
|
|
444
|
+
};
|
|
445
|
+
assert_eq!(WasmContract::version(bytes), TRIGGER_MATCH_CONTRACT_VERSION);
|
|
446
|
+
for case in &corpus().cases {
|
|
447
|
+
let output: Value = serde_json::from_slice(&evaluate_canonical(&case.bytes())).unwrap();
|
|
448
|
+
assert_eq!(output["contract"], TRIGGER_MATCH_CONTRACT_ID);
|
|
449
|
+
assert_eq!(output["version"], TRIGGER_MATCH_CONTRACT_VERSION);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/// A short rendering of an output, for the printed conformance table.
|
|
454
|
+
fn summarize(output: &[u8]) -> String {
|
|
455
|
+
let parsed: Value = match serde_json::from_slice(output) {
|
|
456
|
+
Ok(value) => value,
|
|
457
|
+
Err(_) => return "?".into(),
|
|
458
|
+
};
|
|
459
|
+
if let Some(matched) = parsed["outcome"]["matched"].as_bool() {
|
|
460
|
+
return if matched { "match" } else { "no-match" }.into();
|
|
461
|
+
}
|
|
462
|
+
"error".into()
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
// Determinism
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
|
|
469
|
+
/// The same input, in the same instance, produces the same output every time.
|
|
470
|
+
#[test]
|
|
471
|
+
fn wasm_is_deterministic_across_repeated_calls() {
|
|
472
|
+
let Some(bytes) = contract_module() else {
|
|
473
|
+
return skipped("wasm_is_deterministic_across_repeated_calls");
|
|
474
|
+
};
|
|
475
|
+
let mut wasm = WasmContract::instantiate(bytes);
|
|
476
|
+
for case in &corpus().cases {
|
|
477
|
+
let input = case.bytes();
|
|
478
|
+
let first = wasm.evaluate(&input);
|
|
479
|
+
for round in 1..16 {
|
|
480
|
+
assert_eq!(
|
|
481
|
+
wasm.evaluate(&input),
|
|
482
|
+
first,
|
|
483
|
+
"{} changed answer on repeat {round} within one instance",
|
|
484
|
+
case.name
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/// The same input produces the same output in a fresh instance.
|
|
491
|
+
///
|
|
492
|
+
/// Each instance gets its own `Store`, so its linear memory, allocator state
|
|
493
|
+
/// and call history all start over. An implementation that carried state
|
|
494
|
+
/// between decisions would show it here.
|
|
495
|
+
#[test]
|
|
496
|
+
fn wasm_is_deterministic_across_fresh_instances() {
|
|
497
|
+
let Some(bytes) = contract_module() else {
|
|
498
|
+
return skipped("wasm_is_deterministic_across_fresh_instances");
|
|
499
|
+
};
|
|
500
|
+
let mut baseline = WasmContract::instantiate(bytes);
|
|
501
|
+
let expected: Vec<Vec<u8>> = corpus()
|
|
502
|
+
.cases
|
|
503
|
+
.iter()
|
|
504
|
+
.map(|case| baseline.evaluate(&case.bytes()))
|
|
505
|
+
.collect();
|
|
506
|
+
|
|
507
|
+
for round in 0..4 {
|
|
508
|
+
let mut fresh = WasmContract::instantiate(bytes);
|
|
509
|
+
for (case, want) in corpus().cases.iter().zip(&expected) {
|
|
510
|
+
assert_eq!(
|
|
511
|
+
&fresh.evaluate(&case.bytes()),
|
|
512
|
+
want,
|
|
513
|
+
"{} changed answer in fresh instance {round}",
|
|
514
|
+
case.name
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/// Order of evaluation does not affect any answer.
|
|
521
|
+
///
|
|
522
|
+
/// The corpus is replayed forwards and backwards through one instance. A
|
|
523
|
+
/// decision that depended on what had been evaluated before would differ.
|
|
524
|
+
#[test]
|
|
525
|
+
fn wasm_answers_do_not_depend_on_evaluation_order() {
|
|
526
|
+
let Some(bytes) = contract_module() else {
|
|
527
|
+
return skipped("wasm_answers_do_not_depend_on_evaluation_order");
|
|
528
|
+
};
|
|
529
|
+
let mut forwards = WasmContract::instantiate(bytes);
|
|
530
|
+
let ordered: Vec<Vec<u8>> = corpus()
|
|
531
|
+
.cases
|
|
532
|
+
.iter()
|
|
533
|
+
.map(|case| forwards.evaluate(&case.bytes()))
|
|
534
|
+
.collect();
|
|
535
|
+
|
|
536
|
+
let mut backwards = WasmContract::instantiate(bytes);
|
|
537
|
+
for (index, case) in corpus().cases.iter().enumerate().rev() {
|
|
538
|
+
assert_eq!(
|
|
539
|
+
backwards.evaluate(&case.bytes()),
|
|
540
|
+
ordered[index],
|
|
541
|
+
"{} answered differently when the corpus ran in reverse",
|
|
542
|
+
case.name
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/// The same input produces the same output in a separate OS process.
|
|
548
|
+
///
|
|
549
|
+
/// `cargo test` may run the tests above in one process on one thread pool.
|
|
550
|
+
/// This re-runs the corpus in a child process with an independently created
|
|
551
|
+
/// engine, which is the strongest process boundary this harness can establish
|
|
552
|
+
/// without shipping a second binary.
|
|
553
|
+
#[test]
|
|
554
|
+
fn wasm_is_deterministic_across_process_boundaries() {
|
|
555
|
+
let Some(bytes) = contract_module() else {
|
|
556
|
+
return skipped("wasm_is_deterministic_across_process_boundaries");
|
|
557
|
+
};
|
|
558
|
+
let mut here = WasmContract::instantiate(bytes);
|
|
559
|
+
let mine: Vec<Vec<u8>> = corpus()
|
|
560
|
+
.cases
|
|
561
|
+
.iter()
|
|
562
|
+
.map(|case| here.evaluate(&case.bytes()))
|
|
563
|
+
.collect();
|
|
564
|
+
|
|
565
|
+
let executable = std::env::current_exe().expect("the test binary has a path");
|
|
566
|
+
let output = Command::new(executable)
|
|
567
|
+
.args([
|
|
568
|
+
"--exact",
|
|
569
|
+
"wasm_is_deterministic_across_fresh_instances",
|
|
570
|
+
"--nocapture",
|
|
571
|
+
])
|
|
572
|
+
.output()
|
|
573
|
+
.expect("the test binary re-runs");
|
|
574
|
+
assert!(
|
|
575
|
+
output.status.success(),
|
|
576
|
+
"the corpus did not reproduce in a separate process:\n{}",
|
|
577
|
+
String::from_utf8_lossy(&output.stdout)
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
// And the parent's own answers still match the committed expectations, so
|
|
581
|
+
// the child agreeing with itself is not the whole claim.
|
|
582
|
+
for (case, produced) in corpus().cases.iter().zip(&mine) {
|
|
583
|
+
assert_eq!(String::from_utf8_lossy(produced), case.expected());
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
// Runtime restrictions
|
|
589
|
+
// ---------------------------------------------------------------------------
|
|
590
|
+
|
|
591
|
+
/// The module imports nothing.
|
|
592
|
+
///
|
|
593
|
+
/// This is the whole capability argument and it is deliberately narrow. A
|
|
594
|
+
/// WebAssembly module reaches the outside world only through its imports.
|
|
595
|
+
/// With an empty import section there is no clock, no filesystem handle, no
|
|
596
|
+
/// socket and no entropy source in scope — not because they were denied at
|
|
597
|
+
/// call time, but because no such function is linked. It says nothing about
|
|
598
|
+
/// hostile modules, which is a different claim this suite does not make.
|
|
599
|
+
#[test]
|
|
600
|
+
fn wasm_module_declares_no_imports() {
|
|
601
|
+
let Some(bytes) = contract_module() else {
|
|
602
|
+
return skipped("wasm_module_declares_no_imports");
|
|
603
|
+
};
|
|
604
|
+
let engine = wasmi::Engine::default();
|
|
605
|
+
let module = wasmi::Module::new(&engine, bytes).expect("the module validates");
|
|
606
|
+
let imports: Vec<String> = module
|
|
607
|
+
.imports()
|
|
608
|
+
.map(|import: wasmi::ImportType| format!("{}::{}", import.module(), import.name()))
|
|
609
|
+
.collect();
|
|
610
|
+
assert!(
|
|
611
|
+
imports.is_empty(),
|
|
612
|
+
"the contract module must import nothing; it imports {imports:?}"
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/// An empty linker instantiates the module.
|
|
617
|
+
///
|
|
618
|
+
/// The companion to the import check, from the host's side: nothing is
|
|
619
|
+
/// provided, and instantiation still succeeds, so no capability is being
|
|
620
|
+
/// supplied implicitly by the runtime.
|
|
621
|
+
#[test]
|
|
622
|
+
fn wasm_module_instantiates_with_no_host_functions() {
|
|
623
|
+
let Some(bytes) = contract_module() else {
|
|
624
|
+
return skipped("wasm_module_instantiates_with_no_host_functions");
|
|
625
|
+
};
|
|
626
|
+
let engine = wasmi::Engine::default();
|
|
627
|
+
let module = wasmi::Module::new(&engine, bytes).unwrap();
|
|
628
|
+
let mut store = wasmi::Store::new(&engine, ());
|
|
629
|
+
wasmi::Linker::<()>::new(&engine)
|
|
630
|
+
.instantiate_and_start(&mut store, &module)
|
|
631
|
+
.expect("no host function is required");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/// The module exports the contract ABI and nothing that could act on FeltDB.
|
|
635
|
+
///
|
|
636
|
+
/// The exported surface is memory plus four functions. There is no export that
|
|
637
|
+
/// writes state, creates work, or names a FeltDB concept: the module's only
|
|
638
|
+
/// possible effect on FeltDB is the bytes it returns.
|
|
639
|
+
#[test]
|
|
640
|
+
fn wasm_module_exports_only_the_contract_abi() {
|
|
641
|
+
let Some(bytes) = contract_module() else {
|
|
642
|
+
return skipped("wasm_module_exports_only_the_contract_abi");
|
|
643
|
+
};
|
|
644
|
+
let engine = wasmi::Engine::default();
|
|
645
|
+
let module = wasmi::Module::new(&engine, bytes).unwrap();
|
|
646
|
+
let mut functions: Vec<String> = module
|
|
647
|
+
.exports()
|
|
648
|
+
.filter(|export| export.ty().func().is_some())
|
|
649
|
+
.map(|export| export.name().to_string())
|
|
650
|
+
.collect();
|
|
651
|
+
functions.sort();
|
|
652
|
+
assert_eq!(
|
|
653
|
+
functions,
|
|
654
|
+
vec![
|
|
655
|
+
"contract_alloc".to_string(),
|
|
656
|
+
"contract_evaluate".to_string(),
|
|
657
|
+
"contract_free".to_string(),
|
|
658
|
+
"contract_version".to_string(),
|
|
659
|
+
]
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/// WASI is not present, under any of the names a runtime would look for.
|
|
664
|
+
#[test]
|
|
665
|
+
fn wasm_module_requires_no_wasi() {
|
|
666
|
+
let Some(bytes) = contract_module() else {
|
|
667
|
+
return skipped("wasm_module_requires_no_wasi");
|
|
668
|
+
};
|
|
669
|
+
let text = String::from_utf8_lossy(bytes);
|
|
670
|
+
for forbidden in [
|
|
671
|
+
"wasi_snapshot_preview1",
|
|
672
|
+
"wasi_unstable",
|
|
673
|
+
"clock_time_get",
|
|
674
|
+
"random_get",
|
|
675
|
+
"fd_read",
|
|
676
|
+
"fd_write",
|
|
677
|
+
"sock_recv",
|
|
678
|
+
"path_open",
|
|
679
|
+
] {
|
|
680
|
+
assert!(
|
|
681
|
+
!text.contains(forbidden),
|
|
682
|
+
"the contract module names {forbidden}, which it has no reason to"
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/// The contract never traps, on any corpus input.
|
|
688
|
+
///
|
|
689
|
+
/// A trap would be a runtime-specific outcome rather than a contract decision,
|
|
690
|
+
/// and the two implementations could not be compared through it.
|
|
691
|
+
#[test]
|
|
692
|
+
fn wasm_contract_returns_a_decision_for_every_input_including_malformed() {
|
|
693
|
+
let Some(bytes) = contract_module() else {
|
|
694
|
+
return skipped("wasm_contract_returns_a_decision_for_every_input_including_malformed");
|
|
695
|
+
};
|
|
696
|
+
let mut wasm = WasmContract::instantiate(bytes);
|
|
697
|
+
for case in &corpus().cases {
|
|
698
|
+
let output = wasm.evaluate(&case.bytes());
|
|
699
|
+
let parsed: Value =
|
|
700
|
+
serde_json::from_slice(&output).expect("every output is canonical JSON");
|
|
701
|
+
assert_eq!(parsed["contract"], TRIGGER_MATCH_CONTRACT_ID);
|
|
702
|
+
assert!(
|
|
703
|
+
parsed.get("outcome").is_some() ^ parsed.get("error").is_some(),
|
|
704
|
+
"{} produced neither or both of outcome and error",
|
|
705
|
+
case.name
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
// Inputs no fixture would contain, to show the trap-freedom is not a
|
|
709
|
+
// property of the corpus.
|
|
710
|
+
for hostile in [
|
|
711
|
+
vec![0u8; 0],
|
|
712
|
+
vec![0xff; 64],
|
|
713
|
+
b"{".repeat(4096),
|
|
714
|
+
b"[".repeat(4096),
|
|
715
|
+
vec![b'"'; 1024],
|
|
716
|
+
] {
|
|
717
|
+
let output = wasm.evaluate(&hostile);
|
|
718
|
+
assert!(serde_json::from_slice::<Value>(&output).is_ok());
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ---------------------------------------------------------------------------
|
|
723
|
+
// Mutation: does the suite actually detect divergence?
|
|
724
|
+
// ---------------------------------------------------------------------------
|
|
725
|
+
|
|
726
|
+
/// A deliberate change to the WASM implementation's source.
|
|
727
|
+
struct Mutation {
|
|
728
|
+
name: &'static str,
|
|
729
|
+
file: &'static str,
|
|
730
|
+
from: &'static str,
|
|
731
|
+
to: &'static str,
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const MUTATIONS: &[Mutation] = &[
|
|
735
|
+
Mutation {
|
|
736
|
+
name: "predicate comparison always succeeds",
|
|
737
|
+
file: "src/lib.rs",
|
|
738
|
+
from: " if !resolved.equals(predicate.equals) {",
|
|
739
|
+
to: " if false {",
|
|
740
|
+
},
|
|
741
|
+
Mutation {
|
|
742
|
+
name: "collection comparison is case-insensitive",
|
|
743
|
+
file: "src/lib.rs",
|
|
744
|
+
from: " if change.collection != trigger.source_collection {",
|
|
745
|
+
to: " if !change.collection.eq_ignore_ascii_case(trigger.source_collection) {",
|
|
746
|
+
},
|
|
747
|
+
Mutation {
|
|
748
|
+
name: "disabled triggers are accepted",
|
|
749
|
+
file: "src/lib.rs",
|
|
750
|
+
from: " if !trigger.enabled {",
|
|
751
|
+
to: " if false {",
|
|
752
|
+
},
|
|
753
|
+
Mutation {
|
|
754
|
+
name: "a null record value counts as absent",
|
|
755
|
+
file: "src/lib.rs",
|
|
756
|
+
from: " let Some(value) = change.value else {",
|
|
757
|
+
to: " let Some(value) = change.value.filter(|value| **value != Json::Null) else {",
|
|
758
|
+
},
|
|
759
|
+
Mutation {
|
|
760
|
+
name: "change kind is never compared",
|
|
761
|
+
file: "src/lib.rs",
|
|
762
|
+
from: " if change.change_kind != trigger.change_kind {",
|
|
763
|
+
to: " if false {",
|
|
764
|
+
},
|
|
765
|
+
Mutation {
|
|
766
|
+
name: "output encoding renames a member",
|
|
767
|
+
file: "src/lib.rs",
|
|
768
|
+
from: r#"out.extend_from_slice(b"\",\"outcome\":{\"matched\":");"#,
|
|
769
|
+
to: r#"out.extend_from_slice(b"\",\"result\":{\"matched\":");"#,
|
|
770
|
+
},
|
|
771
|
+
Mutation {
|
|
772
|
+
name: "integers and floats compare equal",
|
|
773
|
+
file: "src/json.rs",
|
|
774
|
+
from: " (Json::Float(left), Json::Float(right)) => left == right,",
|
|
775
|
+
to: " (Json::Float(left), Json::Float(right)) => left == right,\n (Json::PosInt(left), Json::Float(right)) => *left as f64 == *right,\n (Json::Float(left), Json::PosInt(right)) => *left == *right as f64,",
|
|
776
|
+
},
|
|
777
|
+
];
|
|
778
|
+
|
|
779
|
+
/// Every deliberate mutation of the WASM contract is caught by the corpus.
|
|
780
|
+
///
|
|
781
|
+
/// Without this the conformance suite proves only that two implementations
|
|
782
|
+
/// agree, not that agreement was ever in question. Each mutant is built from a
|
|
783
|
+
/// copy of the real source with one edit applied, and must fail against the
|
|
784
|
+
/// committed expectations.
|
|
785
|
+
#[test]
|
|
786
|
+
fn deliberate_wasm_mutations_are_detected() {
|
|
787
|
+
if contract_module().is_none() {
|
|
788
|
+
return skipped("deliberate_wasm_mutations_are_detected");
|
|
789
|
+
}
|
|
790
|
+
let source = repository_root().join(WASM_CRATE);
|
|
791
|
+
let scratch = tempfile::tempdir().expect("a scratch directory");
|
|
792
|
+
|
|
793
|
+
let mut undetected = Vec::new();
|
|
794
|
+
for mutation in MUTATIONS {
|
|
795
|
+
let root = scratch.path().join(mutation.name.replace(' ', "-"));
|
|
796
|
+
copy_crate(&source, &root);
|
|
797
|
+
let target = root.join(mutation.file);
|
|
798
|
+
let original = std::fs::read_to_string(&target).expect("the mutated file exists");
|
|
799
|
+
assert!(
|
|
800
|
+
original.matches(mutation.from).count() == 1,
|
|
801
|
+
"mutation {:?} no longer applies: its anchor is not present exactly once in {}. \
|
|
802
|
+
The mutation suite must be updated with the implementation it mutates.",
|
|
803
|
+
mutation.name,
|
|
804
|
+
mutation.file
|
|
805
|
+
);
|
|
806
|
+
std::fs::write(&target, original.replace(mutation.from, mutation.to)).unwrap();
|
|
807
|
+
|
|
808
|
+
let mut mutant = WasmContract::instantiate(&build_module(&root));
|
|
809
|
+
let caught: Vec<&str> = corpus()
|
|
810
|
+
.cases
|
|
811
|
+
.iter()
|
|
812
|
+
.filter(|case| {
|
|
813
|
+
String::from_utf8_lossy(&mutant.evaluate(&case.bytes())) != case.expected()
|
|
814
|
+
})
|
|
815
|
+
.map(|case| case.name.as_str())
|
|
816
|
+
.collect();
|
|
817
|
+
match caught.first() {
|
|
818
|
+
Some(first) => println!(
|
|
819
|
+
"mutation {:?} caught by {} fixtures, first: {first}",
|
|
820
|
+
mutation.name,
|
|
821
|
+
caught.len()
|
|
822
|
+
),
|
|
823
|
+
None => undetected.push(mutation.name),
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
assert!(
|
|
827
|
+
undetected.is_empty(),
|
|
828
|
+
"the corpus does not distinguish these mutations from correct behaviour: {undetected:?}"
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/// Copies the contract crate's source, without its build directory.
|
|
833
|
+
fn copy_crate(from: &Path, to: &Path) {
|
|
834
|
+
std::fs::create_dir_all(to.join("src")).unwrap();
|
|
835
|
+
std::fs::copy(from.join("Cargo.toml"), to.join("Cargo.toml")).unwrap();
|
|
836
|
+
for entry in std::fs::read_dir(from.join("src")).unwrap() {
|
|
837
|
+
let entry = entry.unwrap();
|
|
838
|
+
std::fs::copy(entry.path(), to.join("src").join(entry.file_name())).unwrap();
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/// Mutating the canonical encoding is detected where the encoding is
|
|
843
|
+
/// semantically load-bearing, and not where it is not.
|
|
844
|
+
///
|
|
845
|
+
/// This is the honest shape of the result. Three encoder mutations are applied
|
|
846
|
+
/// to well-formed cases and fed to both implementations:
|
|
847
|
+
///
|
|
848
|
+
/// - conflating an absent record value with `null` changes a decision, and
|
|
849
|
+
/// both implementations change it together, so only the committed
|
|
850
|
+
/// expectations catch it;
|
|
851
|
+
/// - dropping the contract identity turns every case into an input error,
|
|
852
|
+
/// likewise caught by the expectations; and
|
|
853
|
+
/// - emitting members in non-sorted order changes nothing, because JSON object
|
|
854
|
+
/// equality is by member and both parsers agree. The suite correctly does
|
|
855
|
+
/// not flag it, and the contract does not claim byte-canonical input.
|
|
856
|
+
#[test]
|
|
857
|
+
fn canonicalization_mutations_are_detected_where_they_matter() {
|
|
858
|
+
let cases: Vec<(&Case, Value)> = corpus()
|
|
859
|
+
.cases
|
|
860
|
+
.iter()
|
|
861
|
+
.filter_map(|case| {
|
|
862
|
+
let input = case.input.as_ref()?;
|
|
863
|
+
Some((case, serde_json::to_value(input).unwrap()))
|
|
864
|
+
})
|
|
865
|
+
.collect();
|
|
866
|
+
assert!(cases.len() > 40, "most of the corpus is typed input");
|
|
867
|
+
|
|
868
|
+
// 1. Absence conflated with null.
|
|
869
|
+
let mut changed = 0;
|
|
870
|
+
for (case, encoded) in &cases {
|
|
871
|
+
let mut mutated = encoded.clone();
|
|
872
|
+
if mutated["change"].get("value").is_none() {
|
|
873
|
+
mutated["change"]["value"] = Value::Null;
|
|
874
|
+
}
|
|
875
|
+
let produced = String::from_utf8(evaluate_canonical(
|
|
876
|
+
serde_json::to_vec(&mutated).unwrap().as_slice(),
|
|
877
|
+
))
|
|
878
|
+
.unwrap();
|
|
879
|
+
if produced != case.expected() {
|
|
880
|
+
changed += 1;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
assert!(
|
|
884
|
+
changed > 0,
|
|
885
|
+
"an encoder that writes null for an absent record value must change a decision"
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
// 2. Contract identity dropped.
|
|
889
|
+
for (case, encoded) in cases.iter().take(1) {
|
|
890
|
+
let mut mutated = encoded.clone();
|
|
891
|
+
mutated.as_object_mut().unwrap().remove("contract");
|
|
892
|
+
let produced = String::from_utf8(evaluate_canonical(
|
|
893
|
+
serde_json::to_vec(&mutated).unwrap().as_slice(),
|
|
894
|
+
))
|
|
895
|
+
.unwrap();
|
|
896
|
+
assert_ne!(produced, case.expected());
|
|
897
|
+
assert!(produced.contains("CONTRACT_INPUT_INVALID"));
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// 3. Member order, which carries no meaning.
|
|
901
|
+
for (case, encoded) in &cases {
|
|
902
|
+
let produced =
|
|
903
|
+
String::from_utf8(evaluate_canonical(encode_reversed(encoded).as_bytes())).unwrap();
|
|
904
|
+
assert_eq!(
|
|
905
|
+
produced,
|
|
906
|
+
case.expected(),
|
|
907
|
+
"{} changed answer under a reordered encoding; object member order is \
|
|
908
|
+
not part of this contract's semantics",
|
|
909
|
+
case.name
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/// Encodes a value with every object's members in reverse sorted order.
|
|
915
|
+
///
|
|
916
|
+
/// `serde_json::Map` re-sorts on insert, so a non-canonical member order has to
|
|
917
|
+
/// be produced as text rather than as a value.
|
|
918
|
+
fn encode_reversed(value: &Value) -> String {
|
|
919
|
+
match value {
|
|
920
|
+
Value::Object(members) => {
|
|
921
|
+
let mut parts: Vec<String> = members
|
|
922
|
+
.iter()
|
|
923
|
+
.map(|(key, value)| {
|
|
924
|
+
format!(
|
|
925
|
+
"{}:{}",
|
|
926
|
+
serde_json::to_string(key).unwrap(),
|
|
927
|
+
encode_reversed(value)
|
|
928
|
+
)
|
|
929
|
+
})
|
|
930
|
+
.collect();
|
|
931
|
+
parts.reverse();
|
|
932
|
+
format!("{{{}}}", parts.join(","))
|
|
933
|
+
}
|
|
934
|
+
Value::Array(items) => format!(
|
|
935
|
+
"[{}]",
|
|
936
|
+
items
|
|
937
|
+
.iter()
|
|
938
|
+
.map(encode_reversed)
|
|
939
|
+
.collect::<Vec<_>>()
|
|
940
|
+
.join(",")
|
|
941
|
+
),
|
|
942
|
+
other => serde_json::to_string(other).unwrap(),
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ---------------------------------------------------------------------------
|
|
947
|
+
// Corpus maintenance
|
|
948
|
+
// ---------------------------------------------------------------------------
|
|
949
|
+
|
|
950
|
+
/// Records the canonical output of every case, after both implementations agree.
|
|
951
|
+
///
|
|
952
|
+
/// Ignored by default: it writes the corpus. Run it deliberately with
|
|
953
|
+
/// `FELTDB_TRIGGER_CONTRACT_BLESS=1 cargo test -p feltdb --test
|
|
954
|
+
/// trigger_contract_conformance -- bless --ignored`. It refuses to record
|
|
955
|
+
/// anything unless the WASM implementation is available and agrees, so an
|
|
956
|
+
/// expectation can never be blessed from one implementation alone.
|
|
957
|
+
#[test]
|
|
958
|
+
#[ignore = "rewrites the committed corpus"]
|
|
959
|
+
fn bless() {
|
|
960
|
+
assert_eq!(
|
|
961
|
+
std::env::var("FELTDB_TRIGGER_CONTRACT_BLESS").as_deref(),
|
|
962
|
+
Ok("1"),
|
|
963
|
+
"set FELTDB_TRIGGER_CONTRACT_BLESS=1 to rewrite the corpus"
|
|
964
|
+
);
|
|
965
|
+
let bytes = contract_module().expect(
|
|
966
|
+
"blessing requires the WASM implementation: an expectation recorded from the native \
|
|
967
|
+
implementation alone would prove nothing",
|
|
968
|
+
);
|
|
969
|
+
let mut wasm = WasmContract::instantiate(bytes);
|
|
970
|
+
|
|
971
|
+
let mut document: Value =
|
|
972
|
+
serde_json::from_slice(&std::fs::read(corpus_path()).unwrap()).unwrap();
|
|
973
|
+
for (index, case) in corpus().cases.iter().enumerate() {
|
|
974
|
+
let input = case.bytes();
|
|
975
|
+
let native = evaluate_canonical(&input);
|
|
976
|
+
let produced = wasm.evaluate(&input);
|
|
977
|
+
assert_eq!(
|
|
978
|
+
String::from_utf8_lossy(&native),
|
|
979
|
+
String::from_utf8_lossy(&produced),
|
|
980
|
+
"refusing to record {}: the implementations disagree",
|
|
981
|
+
case.name
|
|
982
|
+
);
|
|
983
|
+
if case.input.is_some() {
|
|
984
|
+
document["cases"][index]["canonical_input"] =
|
|
985
|
+
Value::String(String::from_utf8(input).unwrap());
|
|
986
|
+
}
|
|
987
|
+
document["cases"][index]["expected_output"] =
|
|
988
|
+
Value::String(String::from_utf8(native).unwrap());
|
|
989
|
+
}
|
|
990
|
+
let mut text = serde_json::to_string_pretty(&document).unwrap();
|
|
991
|
+
text.push('\n');
|
|
992
|
+
std::fs::write(corpus_path(), text).unwrap();
|
|
993
|
+
println!("recorded {} expectations", corpus().cases.len());
|
|
994
|
+
}
|