@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
|
@@ -25,7 +25,8 @@ use crate::{
|
|
|
25
25
|
policy_evaluation::{
|
|
26
26
|
Actor, AuthorizationState, PolicyEvaluator, PolicySubject, RecordAuthorizationContext,
|
|
27
27
|
},
|
|
28
|
-
AtomicMutation, AtomicPrecondition, FeltDb,
|
|
28
|
+
AtomicMutation, AtomicPrecondition, FeltDb, FlowError, PreconditionFailure, RecordPrecondition,
|
|
29
|
+
StoredRow,
|
|
29
30
|
};
|
|
30
31
|
use serde::{Deserialize, Serialize};
|
|
31
32
|
use serde_json::{Map, Value};
|
|
@@ -853,6 +854,12 @@ pub struct StateFailure {
|
|
|
853
854
|
pub transaction: Option<String>,
|
|
854
855
|
#[serde(default)]
|
|
855
856
|
pub causal_position: Option<u64>,
|
|
857
|
+
/// The structured predicate failure, when the refusal was a record
|
|
858
|
+
/// precondition. Named `failure` because that is what the direct
|
|
859
|
+
/// authority's refused transaction already calls it: one refusal, one
|
|
860
|
+
/// shape, whichever endpoint answered.
|
|
861
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
862
|
+
pub failure: Option<Value>,
|
|
856
863
|
}
|
|
857
864
|
impl StateFailure {
|
|
858
865
|
fn new(code: &str, message: &str) -> Self {
|
|
@@ -864,6 +871,7 @@ impl StateFailure {
|
|
|
864
871
|
resource: None,
|
|
865
872
|
transaction: None,
|
|
866
873
|
causal_position: None,
|
|
874
|
+
failure: None,
|
|
867
875
|
}
|
|
868
876
|
}
|
|
869
877
|
fn storage(error: impl std::fmt::Display) -> Self {
|
|
@@ -1611,10 +1619,47 @@ pub struct TransactionOperation {
|
|
|
1611
1619
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
1612
1620
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
1613
1621
|
pub enum TransactionPrecondition {
|
|
1614
|
-
CollectionVersion {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1622
|
+
CollectionVersion {
|
|
1623
|
+
collection: String,
|
|
1624
|
+
version: u64,
|
|
1625
|
+
},
|
|
1626
|
+
StateVersion {
|
|
1627
|
+
version: u64,
|
|
1628
|
+
},
|
|
1629
|
+
ApplicationRevision {
|
|
1630
|
+
revision_id: String,
|
|
1631
|
+
},
|
|
1632
|
+
CausalCursor {
|
|
1633
|
+
cursor: BTreeMap<String, u64>,
|
|
1634
|
+
},
|
|
1635
|
+
/// What must be true of one record for the transaction to commit.
|
|
1636
|
+
///
|
|
1637
|
+
/// Distinct from every variant above, and evaluated in a different place
|
|
1638
|
+
/// for a reason: those compare counters that can be read before the commit
|
|
1639
|
+
/// lock is taken, while this one compares the fields a caller reads back —
|
|
1640
|
+
/// the document's `__version`, its authority epoch, its lease — and must
|
|
1641
|
+
/// therefore be evaluated *inside* the same lock as the writes. Checking it
|
|
1642
|
+
/// against the pre-commit snapshot would be a client-side check wearing the
|
|
1643
|
+
/// authority's name: between the read and the write, another writer
|
|
1644
|
+
/// commits.
|
|
1645
|
+
///
|
|
1646
|
+
/// It is also what makes `putIfAbsent` and `updateIfVersion` mean the same
|
|
1647
|
+
/// thing against a managed authority as against the file runtime. Mapping
|
|
1648
|
+
/// a caller's `expectedVersion` onto `if_version` would not: that fences
|
|
1649
|
+
/// the row's internal storage sequence, which is a different number that
|
|
1650
|
+
/// happens to share a name.
|
|
1651
|
+
Record {
|
|
1652
|
+
collection: String,
|
|
1653
|
+
id: String,
|
|
1654
|
+
#[serde(default)]
|
|
1655
|
+
require_absent: bool,
|
|
1656
|
+
#[serde(default)]
|
|
1657
|
+
expected_version: Option<u64>,
|
|
1658
|
+
#[serde(default)]
|
|
1659
|
+
expected_epoch: Option<u64>,
|
|
1660
|
+
#[serde(default)]
|
|
1661
|
+
expected_lease_id: Option<String>,
|
|
1662
|
+
},
|
|
1618
1663
|
}
|
|
1619
1664
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
1620
1665
|
pub struct TransactionRequest {
|
|
@@ -2056,8 +2101,47 @@ fn execute_transaction_with_policies(
|
|
|
2056
2101
|
});
|
|
2057
2102
|
}
|
|
2058
2103
|
let snapshot = db.state_rows().map_err(StateFailure::storage)?;
|
|
2104
|
+
let namespace = request
|
|
2105
|
+
.state_namespace
|
|
2106
|
+
.as_deref()
|
|
2107
|
+
.unwrap_or(&request.application_id)
|
|
2108
|
+
.to_string();
|
|
2109
|
+
// Record preconditions are collected here and evaluated by the store,
|
|
2110
|
+
// inside the same lock as the writes. Evaluating them against `snapshot`
|
|
2111
|
+
// alongside the counters below would reintroduce exactly the gap they
|
|
2112
|
+
// exist to close.
|
|
2113
|
+
let mut record_preconditions: Vec<RecordPrecondition> = Vec::new();
|
|
2059
2114
|
for condition in &request.preconditions {
|
|
2060
2115
|
let (resource, expected, actual, passed) = match condition {
|
|
2116
|
+
TransactionPrecondition::Record {
|
|
2117
|
+
collection,
|
|
2118
|
+
id,
|
|
2119
|
+
require_absent,
|
|
2120
|
+
expected_version,
|
|
2121
|
+
expected_epoch,
|
|
2122
|
+
expected_lease_id,
|
|
2123
|
+
} => {
|
|
2124
|
+
let precondition = RecordPrecondition {
|
|
2125
|
+
capability: format!("{namespace}:{collection}"),
|
|
2126
|
+
key: id.clone(),
|
|
2127
|
+
require_absent: *require_absent,
|
|
2128
|
+
expected_version: *expected_version,
|
|
2129
|
+
expected_epoch: *expected_epoch,
|
|
2130
|
+
expected_lease_id: expected_lease_id.clone(),
|
|
2131
|
+
};
|
|
2132
|
+
if precondition.is_empty() {
|
|
2133
|
+
// Refused rather than ignored: a precondition that
|
|
2134
|
+
// constrains nothing reads as protection and provides none.
|
|
2135
|
+
let mut failure = StateFailure::new(
|
|
2136
|
+
"VALIDATION_FAILED",
|
|
2137
|
+
&format!("precondition on {collection}:{id} constrains nothing"),
|
|
2138
|
+
);
|
|
2139
|
+
failure.resource = Some(format!("{collection}:{id}"));
|
|
2140
|
+
return Err(failure);
|
|
2141
|
+
}
|
|
2142
|
+
record_preconditions.push(precondition);
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2061
2145
|
TransactionPrecondition::CollectionVersion {
|
|
2062
2146
|
collection,
|
|
2063
2147
|
version,
|
|
@@ -2350,15 +2434,37 @@ fn execute_transaction_with_policies(
|
|
|
2350
2434
|
}
|
|
2351
2435
|
}
|
|
2352
2436
|
let commit = db
|
|
2353
|
-
.
|
|
2437
|
+
.apply_atomic_transaction_guarded(
|
|
2354
2438
|
&storage_transaction_id,
|
|
2355
2439
|
Some(&payload_hash),
|
|
2356
2440
|
request.causal_parent,
|
|
2357
2441
|
&preconditions,
|
|
2442
|
+
&record_preconditions,
|
|
2358
2443
|
&mutations,
|
|
2359
2444
|
Some(serde_json::json!({"transaction_id":transaction_id.clone(),"subject":request.authorization.subject.clone(),"tenant":request.tenant_id.clone(),"application":request.application_id.clone(),"revision":request.revision_id.clone(),"schema_version":request.schema_version,"operations":request.operations.len(),"authorization_decisions":["state:write allowed"],"causal_parent":request.causal_parent,"result":"committed"})),
|
|
2360
2445
|
)
|
|
2361
2446
|
.map_err(|error| {
|
|
2447
|
+
// A record precondition that did not hold is a lost race, and it
|
|
2448
|
+
// says which predicate failed and what the authority holds. That
|
|
2449
|
+
// detail is the difference between "re-read and decide again" and
|
|
2450
|
+
// "this deployment is broken", so it is carried through as
|
|
2451
|
+
// structure rather than flattened into the message.
|
|
2452
|
+
if let FlowError::PreconditionFailed(refusal) = &error {
|
|
2453
|
+
let mut failure =
|
|
2454
|
+
StateFailure::new("PRECONDITION_FAILED", &error.to_string());
|
|
2455
|
+
failure.transaction = Some(transaction_id.clone());
|
|
2456
|
+
failure.causal_position = db.sequence().ok();
|
|
2457
|
+
failure.resource = Some(refusal.key().to_string());
|
|
2458
|
+
failure.failure = serde_json::to_value(refusal.as_ref()).ok();
|
|
2459
|
+
if let PreconditionFailure::Version {
|
|
2460
|
+
expected, actual, ..
|
|
2461
|
+
} = refusal.as_ref()
|
|
2462
|
+
{
|
|
2463
|
+
failure.expected = Some(Value::from(*expected));
|
|
2464
|
+
failure.actual = Some(Value::from(*actual));
|
|
2465
|
+
}
|
|
2466
|
+
return failure;
|
|
2467
|
+
}
|
|
2362
2468
|
let text = error.to_string();
|
|
2363
2469
|
if text.contains("PRECONDITION_FAILED") {
|
|
2364
2470
|
let mut failure = StateFailure::new("PRECONDITION_FAILED", &text);
|
|
@@ -2665,6 +2771,199 @@ mod tests {
|
|
|
2665
2771
|
"CONFLICT"
|
|
2666
2772
|
);
|
|
2667
2773
|
}
|
|
2774
|
+
/// A conditional write through the canonical contract has to mean the same
|
|
2775
|
+
/// thing it means everywhere else in FeltDB, or `putIfAbsent` and
|
|
2776
|
+
/// `updateIfVersion` become file-runtime privileges.
|
|
2777
|
+
///
|
|
2778
|
+
/// The predicate under test is the document's `__version` -- the number a
|
|
2779
|
+
/// caller reads back -- not the row's internal storage sequence. Those are
|
|
2780
|
+
/// two different numbers that happen to share a name, and a fence on the
|
|
2781
|
+
/// wrong one is worse than no fence, because it reads as protection.
|
|
2782
|
+
#[test]
|
|
2783
|
+
fn a_record_precondition_fences_the_document_version() {
|
|
2784
|
+
let db = db("record-precondition");
|
|
2785
|
+
let schema = schema();
|
|
2786
|
+
let write = |id: &str,
|
|
2787
|
+
transaction: &str,
|
|
2788
|
+
title: &str,
|
|
2789
|
+
preconditions: Vec<TransactionPrecondition>,
|
|
2790
|
+
kind: TransactionOperationKind| {
|
|
2791
|
+
execute_transaction(
|
|
2792
|
+
&db,
|
|
2793
|
+
&schema,
|
|
2794
|
+
&TransactionRequest {
|
|
2795
|
+
transaction_id: Some(transaction.into()),
|
|
2796
|
+
tenant_id: "tenant".into(),
|
|
2797
|
+
application_id: "app".into(),
|
|
2798
|
+
revision_id: "rev".into(),
|
|
2799
|
+
schema_version: 1,
|
|
2800
|
+
state_namespace: None,
|
|
2801
|
+
causal_parent: None,
|
|
2802
|
+
authorization: auth(),
|
|
2803
|
+
preconditions,
|
|
2804
|
+
operations: vec![TransactionOperation {
|
|
2805
|
+
kind,
|
|
2806
|
+
collection: "incidents".into(),
|
|
2807
|
+
id: id.into(),
|
|
2808
|
+
value: serde_json::json!({ "title": title }),
|
|
2809
|
+
if_version: None,
|
|
2810
|
+
}],
|
|
2811
|
+
},
|
|
2812
|
+
None,
|
|
2813
|
+
)
|
|
2814
|
+
};
|
|
2815
|
+
let fence = |version: Option<u64>, absent: bool| {
|
|
2816
|
+
vec![TransactionPrecondition::Record {
|
|
2817
|
+
collection: "incidents".into(),
|
|
2818
|
+
id: "i1".into(),
|
|
2819
|
+
require_absent: absent,
|
|
2820
|
+
expected_version: version,
|
|
2821
|
+
expected_epoch: None,
|
|
2822
|
+
expected_lease_id: None,
|
|
2823
|
+
}]
|
|
2824
|
+
};
|
|
2825
|
+
let version = || {
|
|
2826
|
+
db.state_rows()
|
|
2827
|
+
.unwrap()
|
|
2828
|
+
.into_iter()
|
|
2829
|
+
.find(|row| row.capability == "app:incidents" && row.key == "i1" && !row.deleted)
|
|
2830
|
+
.and_then(|row| row.value.get("__version").and_then(Value::as_u64))
|
|
2831
|
+
};
|
|
2832
|
+
|
|
2833
|
+
// A conditional create is a create at version 1.
|
|
2834
|
+
write(
|
|
2835
|
+
"i1",
|
|
2836
|
+
"create",
|
|
2837
|
+
"First",
|
|
2838
|
+
fence(None, true),
|
|
2839
|
+
TransactionOperationKind::Insert,
|
|
2840
|
+
)
|
|
2841
|
+
.expect("the record is absent, so the create commits");
|
|
2842
|
+
assert_eq!(version(), Some(1));
|
|
2843
|
+
|
|
2844
|
+
// The same create again is a lost race. Whichever check catches it,
|
|
2845
|
+
// the answer is a conflict and nothing was written.
|
|
2846
|
+
let refused = write(
|
|
2847
|
+
"i1",
|
|
2848
|
+
"create-again",
|
|
2849
|
+
"Second",
|
|
2850
|
+
fence(None, true),
|
|
2851
|
+
TransactionOperationKind::Insert,
|
|
2852
|
+
)
|
|
2853
|
+
.expect_err("the record now exists");
|
|
2854
|
+
assert!(
|
|
2855
|
+
refused.code == "CONFLICT" || refused.code == "PRECONDITION_FAILED",
|
|
2856
|
+
"a lost creation race is a conflict, got {}",
|
|
2857
|
+
refused.code,
|
|
2858
|
+
);
|
|
2859
|
+
assert_eq!(version(), Some(1), "a refused transaction writes nothing");
|
|
2860
|
+
|
|
2861
|
+
// A guard on a record the transaction does not write is evaluated
|
|
2862
|
+
// inside the commit lock, and says which predicate failed.
|
|
2863
|
+
let guarded = write(
|
|
2864
|
+
"i2",
|
|
2865
|
+
"guarded",
|
|
2866
|
+
"Other",
|
|
2867
|
+
fence(None, true),
|
|
2868
|
+
TransactionOperationKind::Insert,
|
|
2869
|
+
)
|
|
2870
|
+
.expect_err("i1 exists, so the guard fails and i2 is never written");
|
|
2871
|
+
assert_eq!(guarded.code, "PRECONDITION_FAILED");
|
|
2872
|
+
assert_eq!(
|
|
2873
|
+
guarded
|
|
2874
|
+
.failure
|
|
2875
|
+
.as_ref()
|
|
2876
|
+
.and_then(|value| value.get("predicate").and_then(Value::as_str)),
|
|
2877
|
+
Some("present"),
|
|
2878
|
+
);
|
|
2879
|
+
assert!(
|
|
2880
|
+
db.state_rows()
|
|
2881
|
+
.unwrap()
|
|
2882
|
+
.iter()
|
|
2883
|
+
.all(|row| row.key != "i2" || row.deleted),
|
|
2884
|
+
"a refused guard leaves the staged write unwritten",
|
|
2885
|
+
);
|
|
2886
|
+
|
|
2887
|
+
// A stale fence conflicts and names what the authority actually holds.
|
|
2888
|
+
let stale = write(
|
|
2889
|
+
"i1",
|
|
2890
|
+
"stale",
|
|
2891
|
+
"Third",
|
|
2892
|
+
fence(Some(7), false),
|
|
2893
|
+
TransactionOperationKind::Update,
|
|
2894
|
+
)
|
|
2895
|
+
.expect_err("version 7 is not the version on record");
|
|
2896
|
+
assert_eq!(stale.code, "PRECONDITION_FAILED");
|
|
2897
|
+
assert_eq!(stale.expected, Some(Value::from(7)));
|
|
2898
|
+
assert_eq!(stale.actual, Some(Value::from(1)));
|
|
2899
|
+
assert_eq!(version(), Some(1));
|
|
2900
|
+
|
|
2901
|
+
// The fenced write commits and the authority owns the next version.
|
|
2902
|
+
write(
|
|
2903
|
+
"i1",
|
|
2904
|
+
"fenced",
|
|
2905
|
+
"Fourth",
|
|
2906
|
+
fence(Some(1), false),
|
|
2907
|
+
TransactionOperationKind::Update,
|
|
2908
|
+
)
|
|
2909
|
+
.expect("version 1 is the version on record");
|
|
2910
|
+
assert_eq!(version(), Some(2));
|
|
2911
|
+
|
|
2912
|
+
// Replaying a committed transaction id applies nothing, so it cannot
|
|
2913
|
+
// advance the version a second time.
|
|
2914
|
+
write(
|
|
2915
|
+
"i1",
|
|
2916
|
+
"fenced",
|
|
2917
|
+
"Fourth",
|
|
2918
|
+
fence(Some(1), false),
|
|
2919
|
+
TransactionOperationKind::Update,
|
|
2920
|
+
)
|
|
2921
|
+
.expect("a duplicate transaction id is not a failure");
|
|
2922
|
+
assert_eq!(version(), Some(2));
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
/// A precondition that constrains nothing is refused rather than accepted,
|
|
2926
|
+
/// because one that is silently ignored reads as protection and provides
|
|
2927
|
+
/// none.
|
|
2928
|
+
#[test]
|
|
2929
|
+
fn an_empty_record_precondition_is_refused() {
|
|
2930
|
+
let db = db("empty-precondition");
|
|
2931
|
+
let schema = schema();
|
|
2932
|
+
let failure = execute_transaction(
|
|
2933
|
+
&db,
|
|
2934
|
+
&schema,
|
|
2935
|
+
&TransactionRequest {
|
|
2936
|
+
transaction_id: Some("empty".into()),
|
|
2937
|
+
tenant_id: "tenant".into(),
|
|
2938
|
+
application_id: "app".into(),
|
|
2939
|
+
revision_id: "rev".into(),
|
|
2940
|
+
schema_version: 1,
|
|
2941
|
+
state_namespace: None,
|
|
2942
|
+
causal_parent: None,
|
|
2943
|
+
authorization: auth(),
|
|
2944
|
+
preconditions: vec![TransactionPrecondition::Record {
|
|
2945
|
+
collection: "incidents".into(),
|
|
2946
|
+
id: "i1".into(),
|
|
2947
|
+
require_absent: false,
|
|
2948
|
+
expected_version: None,
|
|
2949
|
+
expected_epoch: None,
|
|
2950
|
+
expected_lease_id: None,
|
|
2951
|
+
}],
|
|
2952
|
+
operations: vec![TransactionOperation {
|
|
2953
|
+
kind: TransactionOperationKind::Insert,
|
|
2954
|
+
collection: "incidents".into(),
|
|
2955
|
+
id: "i1".into(),
|
|
2956
|
+
value: serde_json::json!({ "title": "First" }),
|
|
2957
|
+
if_version: None,
|
|
2958
|
+
}],
|
|
2959
|
+
},
|
|
2960
|
+
None,
|
|
2961
|
+
)
|
|
2962
|
+
.expect_err("a precondition that constrains nothing is a caller error");
|
|
2963
|
+
assert_eq!(failure.code, "VALIDATION_FAILED");
|
|
2964
|
+
assert!(db.state_rows().unwrap().iter().all(|row| row.deleted));
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2668
2967
|
#[test]
|
|
2669
2968
|
fn validation_failure_rolls_back_every_operation() {
|
|
2670
2969
|
let db = db("rollback");
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
//! The deterministic contract behind FeltDB's semantic state diff.
|
|
2
|
+
//!
|
|
3
|
+
//! This is the second specimen of the pattern established by
|
|
4
|
+
//! [`crate::trigger_contract`]: a decision FeltDB already makes, drawn behind a
|
|
5
|
+
//! runtime-independent boundary so that more than one implementation can be
|
|
6
|
+
//! held to it.
|
|
7
|
+
//!
|
|
8
|
+
//! ```text
|
|
9
|
+
//! canonical state A + canonical state B
|
|
10
|
+
//! │
|
|
11
|
+
//! ┌──────────────┴──────────────┐
|
|
12
|
+
//! │ │
|
|
13
|
+
//! native evaluation WASM evaluation
|
|
14
|
+
//! │ │
|
|
15
|
+
//! └──────────────┬──────────────┘
|
|
16
|
+
//! │
|
|
17
|
+
//! canonical diff bytes
|
|
18
|
+
//! ```
|
|
19
|
+
//!
|
|
20
|
+
//! # This module adds no diff semantics
|
|
21
|
+
//!
|
|
22
|
+
//! [`SemanticDiff::compute`] is untouched and remains authoritative. Everything
|
|
23
|
+
//! here is envelope: identity, version, canonical encoding, and the decision to
|
|
24
|
+
//! report an unusable input rather than fail. If this module and
|
|
25
|
+
//! `SemanticDiff::compute` ever disagree, `SemanticDiff::compute` is right.
|
|
26
|
+
//!
|
|
27
|
+
//! # Determinism budget
|
|
28
|
+
//!
|
|
29
|
+
//! Evaluation reads nothing but its two states. No clock, filesystem, network,
|
|
30
|
+
//! random source, process identity, authentication context, authority metadata
|
|
31
|
+
//! or process-global mutable state participates, and the audit that produced
|
|
32
|
+
//! this contract found none in the implementation either.
|
|
33
|
+
//!
|
|
34
|
+
//! # What the audit established about the semantics
|
|
35
|
+
//!
|
|
36
|
+
//! These are properties of the existing implementation, recorded here because
|
|
37
|
+
//! the contract is defined against them rather than against an idea of what a
|
|
38
|
+
//! diff should do:
|
|
39
|
+
//!
|
|
40
|
+
//! - **Arrays are positional.** A reorder is not detected; `[1,2] -> [2,1]`
|
|
41
|
+
//! reports `Changed` at index 0 and at index 1. Elements past the shorter
|
|
42
|
+
//! length become `Added` or `Removed` at their index.
|
|
43
|
+
//! - **Absence and null are different, in both directions.** `missing -> value`
|
|
44
|
+
//! is `Added`, `null -> value` is `Changed`, `value -> missing` is `Removed`,
|
|
45
|
+
//! `value -> null` is `Changed`.
|
|
46
|
+
//! - **`old_value` and `new_value` are `Option`, encoded as `null` when absent.**
|
|
47
|
+
//! That is ambiguous with a JSON null on its own, and `kind` is what
|
|
48
|
+
//! disambiguates: `missing -> null` is `Added` with `new_value: null`.
|
|
49
|
+
//! - **Numbers compare by `serde_json` variant.** `42` and `42.0` differ, so
|
|
50
|
+
//! they report `Changed`; `-0` is a float and so differs from the integer
|
|
51
|
+
//! `0`; `-0.0` and `0.0` are equal under IEEE equality and report nothing.
|
|
52
|
+
//! - **A type transition is one `Changed`,** carrying both whole values, at the
|
|
53
|
+
//! path where it happens — including at the empty path, for the states
|
|
54
|
+
//! themselves.
|
|
55
|
+
//! - **Ordering is total.** Changes sort by path, then by kind. Paths are
|
|
56
|
+
//! unique within one diff, so the ordering does not depend on sort stability.
|
|
57
|
+
|
|
58
|
+
use crate::state_model::SemanticDiff;
|
|
59
|
+
use serde::{Deserialize, Serialize};
|
|
60
|
+
use serde_json::Value;
|
|
61
|
+
use sha2::{Digest, Sha256};
|
|
62
|
+
|
|
63
|
+
/// Stable identity of this contract.
|
|
64
|
+
pub const STATE_DIFF_CONTRACT_ID: &str = "feltdb.state.diff";
|
|
65
|
+
|
|
66
|
+
/// Version of this contract's input shape, output shape and semantics.
|
|
67
|
+
///
|
|
68
|
+
/// The output shape is [`SemanticDiff`]'s own serialization, so a change to
|
|
69
|
+
/// that type is a change to this contract and must bump this.
|
|
70
|
+
pub const STATE_DIFF_CONTRACT_VERSION: u32 = 1;
|
|
71
|
+
|
|
72
|
+
/// The complete deterministic input: two states, and the contract's identity.
|
|
73
|
+
///
|
|
74
|
+
/// Members are declared alphabetically so the canonical struct encoding matches
|
|
75
|
+
/// the sorted-key encoding `serde_json` gives every JSON object.
|
|
76
|
+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
77
|
+
#[serde(deny_unknown_fields)]
|
|
78
|
+
pub struct StateDiffInput {
|
|
79
|
+
pub contract: String,
|
|
80
|
+
pub state_a: Value,
|
|
81
|
+
pub state_b: Value,
|
|
82
|
+
pub version: u32,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
impl StateDiffInput {
|
|
86
|
+
/// Builds an input stamped with this contract's identity and version.
|
|
87
|
+
pub fn new(state_a: Value, state_b: Value) -> Self {
|
|
88
|
+
Self {
|
|
89
|
+
contract: STATE_DIFF_CONTRACT_ID.to_string(),
|
|
90
|
+
state_a,
|
|
91
|
+
state_b,
|
|
92
|
+
version: STATE_DIFF_CONTRACT_VERSION,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Why an input could not be evaluated.
|
|
98
|
+
///
|
|
99
|
+
/// Three codes, matching the vocabulary [`crate::trigger_contract`] uses. As
|
|
100
|
+
/// there, an error carries a code and nothing else: a free-form message would
|
|
101
|
+
/// be a divergence surface between implementations that says nothing about the
|
|
102
|
+
/// semantics under test.
|
|
103
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
104
|
+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
|
105
|
+
pub enum ContractErrorCode {
|
|
106
|
+
/// The bytes are not a well-formed instance of this contract's input.
|
|
107
|
+
ContractInputInvalid,
|
|
108
|
+
/// The input declares a different contract.
|
|
109
|
+
ContractIdMismatch,
|
|
110
|
+
/// The input declares a version this implementation does not implement.
|
|
111
|
+
ContractVersionUnsupported,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// The error body of a contract output.
|
|
115
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
116
|
+
#[serde(deny_unknown_fields)]
|
|
117
|
+
pub struct ContractErrorBody {
|
|
118
|
+
pub code: ContractErrorCode,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// The complete deterministic output.
|
|
122
|
+
///
|
|
123
|
+
/// Exactly one of `diff` and `error` is present. `diff` is [`SemanticDiff`]
|
|
124
|
+
/// serialized exactly as it already serializes — this contract does not
|
|
125
|
+
/// reshape it, because the existing semantic result *is* the contract.
|
|
126
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
127
|
+
#[serde(deny_unknown_fields)]
|
|
128
|
+
pub struct StateDiffOutput {
|
|
129
|
+
pub contract: String,
|
|
130
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
131
|
+
pub diff: Option<SemanticDiff>,
|
|
132
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
133
|
+
pub error: Option<ContractErrorBody>,
|
|
134
|
+
pub version: u32,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
impl StateDiffOutput {
|
|
138
|
+
fn computed(diff: SemanticDiff) -> Self {
|
|
139
|
+
Self {
|
|
140
|
+
contract: STATE_DIFF_CONTRACT_ID.to_string(),
|
|
141
|
+
diff: Some(diff),
|
|
142
|
+
error: None,
|
|
143
|
+
version: STATE_DIFF_CONTRACT_VERSION,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn failure(code: ContractErrorCode) -> Self {
|
|
148
|
+
Self {
|
|
149
|
+
contract: STATE_DIFF_CONTRACT_ID.to_string(),
|
|
150
|
+
diff: None,
|
|
151
|
+
error: Some(ContractErrorBody { code }),
|
|
152
|
+
version: STATE_DIFF_CONTRACT_VERSION,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// The runtime that executed a contract, recorded in evidence.
|
|
158
|
+
///
|
|
159
|
+
/// Only [`ContractRuntime::Native`] is wired anywhere. As with the trigger
|
|
160
|
+
/// contract, the WASM variant exists so a recorded runtime can be named
|
|
161
|
+
/// honestly; it is not a selection switch.
|
|
162
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
163
|
+
#[serde(rename_all = "snake_case")]
|
|
164
|
+
pub enum ContractRuntime {
|
|
165
|
+
Native,
|
|
166
|
+
Wasm,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
impl ContractRuntime {
|
|
170
|
+
pub fn as_str(self) -> &'static str {
|
|
171
|
+
match self {
|
|
172
|
+
Self::Native => "native",
|
|
173
|
+
Self::Wasm => "wasm",
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// The deterministic contract, natively implemented.
|
|
179
|
+
///
|
|
180
|
+
/// A thin adapter over the existing implementation, deliberately holding no
|
|
181
|
+
/// diff logic of its own.
|
|
182
|
+
pub fn diff_states(state_a: &Value, state_b: &Value) -> SemanticDiff {
|
|
183
|
+
SemanticDiff::compute(state_a, state_b)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// The canonical byte encoding of a contract input.
|
|
187
|
+
pub fn canonical_input_bytes(input: &StateDiffInput) -> Vec<u8> {
|
|
188
|
+
serde_json::to_vec(input).expect("contract input is always serializable")
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// The canonical byte encoding of a contract output.
|
|
192
|
+
pub fn canonical_output_bytes(output: &StateDiffOutput) -> Vec<u8> {
|
|
193
|
+
serde_json::to_vec(output).expect("contract output is always serializable")
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// Content identity of a contract input, for provenance.
|
|
197
|
+
pub fn input_digest(input: &StateDiffInput) -> String {
|
|
198
|
+
format!("sha256:{:x}", Sha256::digest(canonical_input_bytes(input)))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// The runtime-independent entry point: canonical bytes in, canonical bytes out.
|
|
202
|
+
///
|
|
203
|
+
/// This is the exact shape a foreign runtime implements. It never panics and
|
|
204
|
+
/// never returns anything but a canonical output encoding, so an unusable input
|
|
205
|
+
/// is a contract decision rather than a host-specific failure mode.
|
|
206
|
+
pub fn evaluate_canonical(input: &[u8]) -> Vec<u8> {
|
|
207
|
+
canonical_output_bytes(&evaluate_canonical_output(input))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// [`evaluate_canonical`] before encoding, for callers that want the value.
|
|
211
|
+
pub fn evaluate_canonical_output(input: &[u8]) -> StateDiffOutput {
|
|
212
|
+
let Ok(parsed) = serde_json::from_slice::<StateDiffInput>(input) else {
|
|
213
|
+
return StateDiffOutput::failure(ContractErrorCode::ContractInputInvalid);
|
|
214
|
+
};
|
|
215
|
+
if parsed.contract != STATE_DIFF_CONTRACT_ID {
|
|
216
|
+
return StateDiffOutput::failure(ContractErrorCode::ContractIdMismatch);
|
|
217
|
+
}
|
|
218
|
+
if parsed.version != STATE_DIFF_CONTRACT_VERSION {
|
|
219
|
+
return StateDiffOutput::failure(ContractErrorCode::ContractVersionUnsupported);
|
|
220
|
+
}
|
|
221
|
+
StateDiffOutput::computed(diff_states(&parsed.state_a, &parsed.state_b))
|
|
222
|
+
}
|