Skip to content

Commit e750000

Browse files
sanityclaude
andcommitted
fix: prevent race condition by pre-storing parameters when contract is received
The core issue was that when a contract is received from the network and stored, its parameters were not immediately available in state_store. If an UPDATE arrived before the state was fully stored, it would fail with 'missing contract parameters' because UpdateQuery doesn't pass the contract. The fix pre-stores an empty state with the contract's parameters immediately when a new contract is stored. This ensures parameters are always available for subsequent operations, even if they arrive before the real state is stored. This empty state is then replaced with the actual state during validation. Fixes #1838 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent b3c9b7c commit e750000

File tree

1 file changed

+44
-13
lines changed

1 file changed

+44
-13
lines changed

crates/core/src/contract/executor/runtime.rs

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,30 +71,60 @@ impl ContractExecutor for Executor<Runtime> {
7171
})?
7272
};
7373

74-
let remove_if_fail = if self
74+
// Track if we stored a new contract
75+
let (remove_if_fail, contract_was_provided) = if self
7576
.runtime
7677
.contract_store
7778
.fetch_contract(&key, &params)
7879
.is_none()
7980
{
80-
let code = code.ok_or_else(|| {
81-
ExecutorError::request(StdContractError::MissingContract { key: key.into() })
82-
})?;
83-
84-
tracing::debug!("Storing new contract - key={}", key);
85-
86-
self.runtime
87-
.contract_store
88-
.store_contract(code.clone())
89-
.map_err(ExecutorError::other)?;
81+
if let Some(ref contract_code) = code {
82+
tracing::debug!("Storing new contract - key={}", key);
9083

91-
true
84+
self.runtime
85+
.contract_store
86+
.store_contract(contract_code.clone())
87+
.map_err(ExecutorError::other)?;
88+
(true, true)
89+
} else {
90+
return Err(ExecutorError::request(StdContractError::MissingContract {
91+
key: key.into(),
92+
}));
93+
}
9294
} else {
93-
false
95+
(false, code.is_some())
9496
};
9597

9698
let is_new_contract = self.state_store.get(&key).await.is_err();
9799

100+
// CRITICAL FIX for #1838: When we just stored a new contract, immediately store
101+
// its parameters with an empty state if no state exists yet. This prevents the
102+
// race condition where UPDATE arrives before params are available.
103+
if remove_if_fail && is_new_contract && contract_was_provided {
104+
// We just stored a new contract and there's no state yet
105+
// Store empty state with params immediately to make them available
106+
tracing::debug!(
107+
contract = %key,
108+
"Storing parameters for newly received contract to prevent race condition"
109+
);
110+
111+
// Store a minimal empty state just to ensure params are persisted
112+
// This will be replaced with the actual state below
113+
let empty_state = WrappedState::new(Vec::new());
114+
if let Err(e) = self
115+
.state_store
116+
.store(key, empty_state, params.clone())
117+
.await
118+
{
119+
tracing::warn!(
120+
contract = %key,
121+
error = %e,
122+
"Failed to pre-store parameters for new contract"
123+
);
124+
// Don't fail here, continue with normal flow
125+
}
126+
}
127+
98128
let mut updates = match update {
99129
Either::Left(incoming_state) => {
100130
let result = self
@@ -114,6 +144,7 @@ impl ContractExecutor for Executor<Runtime> {
114144
if is_new_contract {
115145
tracing::debug!("Contract is new, storing initial state");
116146
let state_to_store = incoming_state.clone();
147+
// This will overwrite the empty state we stored above (if any)
117148
self.state_store
118149
.store(key, state_to_store, params.clone())
119150
.await

0 commit comments

Comments
 (0)