Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ A **state break** is when the same inputs produce a different state root — an
- **Eager genesis initialization.** Writing a new `#[state]` value at genesis adds a key to the tree, changing the genesis (and empty-batch) state root for *every* chain. Only write when non-default, and make a missing value read back as the default (`get(...).unwrap_or_default()`).
- **New metered reads/writes in execution paths.** State accesses are gas-metered, and gas feeds the fee deducted (and written) from the payer. A single extra (or one less!) `StateValue::get` on a transaction or hook path changes gas → fees → balances → state root if it's ever exercised. Be careful on the entire execution path: both changes to existing CallMessage handlers in a module, as well as any state access changes across the STF transaction processing pipeline.
- **Serialization layout of replayed or stored types.** Transactions are replayed from DA during resync, and state items are hashed into the state root; so any type either reachable from `CallMessage` / `RuntimeCall`, or stored in the rollup state, must keep a stable, byte-perfect serialized layout. For both binary codecs used here (borsh for messages, BCS for state), **only** appending enum variants is safe (preserves existing discriminants); you must NOT add, remove, reorder, or retype *struct or tuple* fields, nor insert/reorder enum variants. Genesis-config types decoded from JSON using serde_json are exempt when the new field is `#[serde(default)]`, because `serde_json`'s format is self-describing.
- **State item ordering.** `#[state]` field prefixes are positional within a module: appending a new field is safe, but inserting or reordering shifts the storage keys of all following fields. Module discriminants are pinned in `constants.toml`.
- **State item ordering.** `#[state]` field prefixes are positional within a module: appending a new field is safe, but inserting or reordering shifts the storage keys of all following fields. Module discriminants are pinned in `constants.toml`. Chain metadata (`CHAIN_ID`, `CHAIN_NAME`, `CHAIN_HASH_OVERRIDES`, DA namespaces) lives in `chain-metadata.toml`.

These rules exist to protect *deployed* state and *historical* messages. The release boundary is the `nightly` branch (unless the user says otherwise): commits already in `dev` but not yet in `nightly` are unreleased and safe to change in state-breaking ways. In exceptional cases the user may also confirm that some type or commit is known to not have been used in production yet. Test-only modules are also safe to change.
When in doubt about whether something is deployed, ask the user; by default assume anything in a `nightly` branch is released.
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 2026-07-03
- #3028 **Breaking Change** adds an optional `chain-metadata.toml` tier for per-network metadata (`CHAIN_ID`, `CHAIN_NAME`, `CHAIN_HASH_OVERRIDES`, `BATCH_NAMESPACE`, `PROOF_NAMESPACE`) and stops reading that metadata from `sov-modules-api` macro expansions. Chain values now flow through `Runtime::chain_id()` / `Runtime::chain_hash_overrides()` and explicit build-time `ChainData`, so editing chain metadata only recompiles crates that read it and their dependents. Not state-breaking; transaction verification behavior is unchanged.
- #3028 migration notes: `config_chain_id()` -> `Rt::chain_id()`; `resolve_chain_hashes_for_height()` -> `resolve_chain_hashes(height, Rt::chain_hash_overrides(), default_hash)`; `authenticate` no longer takes `default_chain_hash`; `verify_chain_id` is generic over `Rt`; `get_runtime_schema` and `sov_build::Options::apply` / `apply_defaults` take `ChainData`; `StandardSchemaEndpoint::new` takes `chain_hash_overrides`; `SovApiProofSender::new` and proof-blob serialization helpers take `chain_id`; CLI `--chain-id` is optional and defaults to the runtime chain id.
- #3028 Build: constants are now tracked by generated `include_bytes!(...)` expansions instead of `sov-modules-macros` watching the manifest in its build script, avoiding proc-macro dependency fanout on constants edits. Also declares the manifest-selection env vars as build-script inputs, improves stale manifest path errors, updates Hive to rewrite `chain-metadata*.toml`, and removes the stale `cargo:rerun-if-changed=NULL` watch from `sov-demo-rollup`.

# 2026-06-25
- #3018 demo-rollup: split the example into two DA-layer Cargo features — `mock_da` (default; mock + SP1 zkVMs) and `celestia_da` (Risc0) — to cut compile time. The DA layer is selected at compile time while the zkVM stays a runtime `--zk-vm` choice within `mock_da`, so a build only compiles the selected DA's zkVM guests: a default `mock_da` build no longer pulls the Celestia/Risc0 adapters, and a `celestia_da` build skips the SP1 guests. When both DA features are enabled (`--all-features`), mock DA is selected by default. Also drops the unused SP1 Celestia guest build. Example crate only (`publish = false`) — no SDK API, state, or protocol change.
# 2026-06-30
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,9 @@ check-demo-rollup-features: ## Constrained feature-powerset for demo-rollup (nee

check-constant-overriding-is-disabled-in-release-mode:
# Passes in release mode...
SOV_TEST_CONST_OVERRIDE_CHAIN_ID=1 cargo test -p sov-modules-api --profile release-with-opt-level-0 assert_chain_id_was_not_overridden
SOV_TEST_CONST_OVERRIDE_CHAIN_ID=1 cargo test -p sov-test-utils --profile release-with-opt-level-0 assert_chain_id_was_not_overridden
# ...but not with standard test profile
if SOV_TEST_CONST_OVERRIDE_CHAIN_ID=1 cargo test -p sov-modules-api assert_chain_id_was_not_overridden; then \
if SOV_TEST_CONST_OVERRIDE_CHAIN_ID=1 cargo test -p sov-test-utils assert_chain_id_was_not_overridden; then \
echo "Check succeeded, but was expected to fail!"; \
exit 1; \
fi
Expand Down
10 changes: 10 additions & 0 deletions chain-metadata.testing.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Per-network chain metadata for test-mode builds (SOV_TEST_MODE_CONST_MANIFEST),
# split out of constants.testing.toml. See chain-metadata.toml for details.

[constants]
# We use the ID 4321 for demo purposes. Change this value before deploying!
CHAIN_ID = 4321
CHAIN_NAME = "TestChain"
CHAIN_HASH_OVERRIDES = []
BATCH_NAMESPACE = { byte_string = "sov-test-b" }
PROOF_NAMESPACE = { hex = "0x736f762d746573742d70" }
36 changes: 36 additions & 0 deletions chain-metadata.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Per-network chain metadata, split out of constants.toml. These values change
# on every network switch (devnet/testnet/mainnet), so they live in their own
# file: editing it recompiles only the crates whose macro expansions read chain
# metadata (and their dependents), leaving crates that only read constants.toml
# untouched.
#
# If this file is deleted, the same keys are read from constants.toml instead
# (the pre-split layout). Once this file exists, every chain-metadata key must
# be defined here — a chain key found only in constants.toml is a compile error.

[constants]
# We use the ID 4321 for demo purposes. Change this value before deploying!
CHAIN_ID = 4321
CHAIN_NAME = "TestChain"

# Chain hash overrides by rollup block height range.
# When the rollup schema changes (e.g., adding a new transaction type), the CHAIN_HASH changes.
# To avoid rejecting previously signed transactions, define overrides for old chain hashes.
# Format: [{ start_height = <height>, end_height = <height>, chain_hash = "0x...", grace_period = <blocks> }, ...]
# Ranges are [start_height, end_height) - start is inclusive, end is exclusive.
# Falls back to Runtime::CHAIN_HASH when no override applies.
# The optional `grace_period` field (defaults to 0) specifies how many blocks after end_height
# the old hash will still be accepted alongside the new hash, allowing for smooth transitions.
# Example: CHAIN_HASH_OVERRIDES = [{ start_height = 0, end_height = 1000, chain_hash = "0xabc...32_bytes_hex", grace_period = 100 }]
CHAIN_HASH_OVERRIDES = []

# The namespace used by the rollup to store its data.
# Using byte_string to convert "sov-test-b" to ASCII bytes
# Should be 10 bytes long for Celestia
BATCH_NAMESPACE = { byte_string = "sov-test-b" }
# The namespace used by the rollup to store aggregated ZK proofs.
# This is "sov-test-p" encoded as ASCII bytes.
# "hex" format is used as an example, that it is possible to define namespace as any sequence of bytes.
# `python3 -c "import sys; print('0x' + sys.argv[1].encode().hex())" "sov-test-p"`
# Should be 10 bytes long for Celestia
PROOF_NAMESPACE = { hex = "0x736f762d746573742d70" }
6 changes: 2 additions & 4 deletions constants.testing.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ mint = [2, 2]
freeze = [1, 1]

[constants]
# We use the ID 4321 for demo purposes. Change this value before deploying!
CHAIN_ID = 4321
CHAIN_NAME = "TestChain"
CHAIN_HASH_OVERRIDES = []
# Chain metadata lives in chain-metadata.testing.toml.

# The number of bytes a transaction can have.
MAX_TX_SIZE=1_048_576

Expand Down
27 changes: 2 additions & 25 deletions constants.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,8 @@ freeze = [1, 1]


[constants]
# We use the ID 4321 for demo purposes. Change this value before deploying!
CHAIN_ID = 4321
CHAIN_NAME = "TestChain"

# Chain hash overrides by rollup block height range.
# When the rollup schema changes (e.g., adding a new transaction type), the CHAIN_HASH changes.
# To avoid rejecting previously signed transactions, define overrides for old chain hashes.
# Format: [{ start_height = <height>, end_height = <height>, chain_hash = "0x...", grace_period = <blocks> }, ...]
# Ranges are [start_height, end_height) - start is inclusive, end is exclusive.
# Falls back to Runtime::CHAIN_HASH when no override applies.
# The optional `grace_period` field (defaults to 0) specifies how many blocks after end_height
# the old hash will still be accepted alongside the new hash, allowing for smooth transitions.
# Example: CHAIN_HASH_OVERRIDES = [{ start_height = 0, end_height = 1000, chain_hash = "0xabc...32_bytes_hex", grace_period = 100 }]
CHAIN_HASH_OVERRIDES = []

# The namespace used by the rollup to store its data.
# Using byte_string to convert "sov-test-b" to ASCII bytes
# Should be 10 bytes long for Celestia
BATCH_NAMESPACE = { byte_string = "sov-test-b" }
# The namespace used by the rollup to store aggregated ZK proofs.
# This is "sov-test-p" encoded as ASCII bytes.
# "hex" format is used as an example, that it is possible to define namespace as any sequence of bytes.
# `python3 -c "import sys; print('0x' + sys.argv[1].encode().hex())" "sov-test-p"`
# Should be 10 bytes long for Celestia
PROOF_NAMESPACE = { hex = "0x736f762d746573742d70" }
# Chain metadata (CHAIN_ID, CHAIN_NAME, CHAIN_HASH_OVERRIDES, BATCH_NAMESPACE,
# PROOF_NAMESPACE) lives in chain-metadata.toml.

# The number of bytes a transaction can have.
MAX_TX_SIZE=1_048_576
Expand Down
7 changes: 6 additions & 1 deletion crates/full-node/sov-rollup-apis/src/endpoints/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub trait SchemaEndpoint: Clone + Send + Sync + 'static {
pub struct StandardSchemaEndpoint<S: Spec> {
schema: serde_json::Value,
default_chain_hash: HexHash,
chain_hash_overrides: &'static [sov_modules_api::ChainHashOverride],
checkpoint_receiver: watch::Receiver<Arc<ConcurrentStateCheckpoint<S>>>,
}

Expand All @@ -74,15 +75,18 @@ impl<S: Spec> StandardSchemaEndpoint<S> {
/// # Arguments
/// * `schema` - The schema to return
/// * `default_chain_hash` - The default chain hash (from `Runtime::CHAIN_HASH`)
/// * `chain_hash_overrides` - The height-ranged overrides (from `Runtime::chain_hash_overrides()`)
/// * `checkpoint_receiver` - Receiver for state checkpoints to read current height
pub fn new(
schema: &Schema,
default_chain_hash: HexHash,
chain_hash_overrides: &'static [sov_modules_api::ChainHashOverride],
checkpoint_receiver: watch::Receiver<Arc<ConcurrentStateCheckpoint<S>>>,
) -> anyhow::Result<Self> {
Ok(Self {
schema: serde_json::to_value(schema)?,
default_chain_hash,
chain_hash_overrides,
checkpoint_receiver,
})
}
Expand All @@ -106,8 +110,9 @@ impl<S: Spec> SchemaEndpoint for StandardSchemaEndpoint<S> {
let height = checkpoint.rollup_height_to_access();

// Resolve the chain hash for the current height
let resolved = sov_modules_api::capabilities::resolve_chain_hashes_for_height(
let resolved = sov_modules_api::runtime::resolve_chain_hashes(
height.get(),
self.chain_hash_overrides,
self.default_chain_hash.0,
);

Expand Down
7 changes: 6 additions & 1 deletion crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,12 @@ impl<S: Spec, R: Runtime<S>> SimulateEndpoint for SovereignSimulate<S, R> {
let ws_gas_meter = auth_tx_data.gas_meter(gas_price, <S::Gas>::MAX);
let working_set = WorkingSet::create_working_set(scratchpad, &auth_tx_data, ws_gas_meter);

let schema = get_runtime_schema::<S, R>().map_err(SimulateError::SchemaConstruction)?;
let schema =
get_runtime_schema::<S, R>(sov_modules_api::sov_universal_wallet::schema::ChainData {
chain_id: config_value!("CHAIN_ID"),
chain_name: config_value!("CHAIN_NAME").to_string(),
})
.map_err(SimulateError::SchemaConstruction)?;
let call_bytes = schema.json_to_borsh(
schema.rollup_expected_index(RollupRoots::RuntimeCall)?,
&params.call.to_string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,13 @@ mod schema_generation {
static SCHEMA: OnceLock<Schema> = OnceLock::new();

SCHEMA.get_or_init(|| {
get_runtime_schema::<S, SchemaGenRuntime<S>>()
.expect("Failed to generate test runtime schema")
get_runtime_schema::<S, SchemaGenRuntime<S>>(
sov_modules_api::sov_universal_wallet::schema::ChainData {
chain_id: sov_modules_api::macros::config_value!("CHAIN_ID"),
chain_name: sov_modules_api::macros::config_value!("CHAIN_NAME").to_string(),
},
)
.expect("Failed to generate test runtime schema")
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use sov_chain_state::ChainState;
use sov_mock_da::MockDaSpec;
use sov_mock_zkvm::MockZkvmHost;
use sov_modules_api::capabilities::RollupHeight;
use sov_modules_api::macros::config_value;
use sov_modules_api::{
Amount, ApiStateAccessor, DaSpec, ProofOutcome, SerializedAttestation, SerializedChallenge,
Spec, StateTransitionPublicData,
Expand Down Expand Up @@ -190,9 +191,12 @@ pub(crate) fn make_attestation_blob(
let serialized_attestation = SerializedAttestation::from_attestation(&attestation).unwrap();

borsh::to_vec(
&serialize_attestation_blob_with_metadata::<S>(serialized_attestation)
.unwrap()
.0,
&serialize_attestation_blob_with_metadata::<S>(
serialized_attestation,
config_value!("CHAIN_ID"),
)
.unwrap()
.0,
)
.unwrap()
}
Expand Down Expand Up @@ -288,9 +292,13 @@ pub(crate) fn make_challenge_blob(
};

borsh::to_vec(
&serialize_challenge_blob_with_metadata::<S>(serialized_challenge, challenge_slot)
.unwrap()
.0,
&serialize_challenge_blob_with_metadata::<S>(
serialized_challenge,
challenge_slot,
config_value!("CHAIN_ID"),
)
.unwrap()
.0,
)
.unwrap()
}
Original file line number Diff line number Diff line change
Expand Up @@ -448,11 +448,7 @@ where
}
EvmAuthenticatorInput::Standard(tx) => {
let (tx_and_raw_hash, auth_data, runtime_call) =
sov_modules_api::capabilities::authenticate::<_, S, Rt>(
&tx.data,
&Rt::CHAIN_HASH,
state,
)?;
sov_modules_api::capabilities::authenticate::<_, S, Rt>(&tx.data, state)?;

Ok((
tx_and_raw_hash,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use serde::Serialize;
use sov_bank::{config_gas_token_id, Bank};
use sov_chain_state::ChainState;
use sov_modules_api::macros::config_value;
use sov_modules_api::prelude::UnwrapInfallible;
use sov_modules_api::registration_lib::StakeRegistration;
use sov_modules_api::{
Expand Down Expand Up @@ -143,7 +144,7 @@ pub(crate) fn serialize_proof_with_commitment<T: Serialize>(
};

borsh::to_vec(
&serialize_proof_blob_with_metadata::<S>(serialized_proof)
&serialize_proof_blob_with_metadata::<S>(serialized_proof, config_value!("CHAIN_ID"))
.unwrap()
.0,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use sov_evm::{
AccountData, EthereumAuthenticator, EvmChainSpec, EvmGenesisConfig, RlpEvmTransaction, SpecId,
};
use sov_evm_test_utils::LegacySimpleStorage;
use sov_modules_api::capabilities::{config_chain_id, TransactionAuthenticator, UniquenessData};
use sov_modules_api::capabilities::{TransactionAuthenticator, UniquenessData};
use sov_modules_api::macros::config_value;
use sov_modules_api::transaction::{Transaction, UnsignedTransaction};
use sov_modules_api::{EncodeCall, RawTx};
Expand Down Expand Up @@ -104,7 +104,7 @@ pub(crate) fn generate_value_setter_uniqueness_tx(

let transaction = UnsignedTransaction::new(
runtime_msg,
config_chain_id(),
config_value!("CHAIN_ID"),
TEST_DEFAULT_MAX_PRIORITY_FEE,
TEST_DEFAULT_MAX_FEE,
uniqueness,
Expand Down
4 changes: 2 additions & 2 deletions crates/module-system/sov-cli/src/workflows/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,14 @@ where

let intermediate_repr: RT::CliStringRepr<U> = match self {
TransactionLoadWorkflow::FromFile(file) => {
chain_id = file.chain_id();
chain_id = file.chain_id().unwrap_or_else(RT::chain_id);
max_priority_fee_bips = file.max_priority_fee_bips();
max_fee = file.max_fee();
gas_limit = file.gas_limit().map(|m| m.to_vec());
file.try_into().map_err(Into::<anyhow::Error>::into)?
}
TransactionLoadWorkflow::FromString(json) => {
chain_id = json.chain_id();
chain_id = json.chain_id().unwrap_or_else(RT::chain_id);
max_priority_fee_bips = json.max_priority_fee_bips();
max_fee = json.max_fee();
gas_limit = json.gas_limit().map(|m| m.to_vec());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ fn default_file_name_arg_for_test(path: &str) -> FileNameArg {
let test_path = make_test_path(path);
FileNameArg {
path: test_path.to_str().unwrap().into(),
chain_id: 0,
chain_id: Some(0),
max_priority_fee_bips: 0,
max_fee: Amount::ZERO,
gas_limit: None,
Expand All @@ -402,7 +402,7 @@ fn default_json_string_arg_for_test(path: impl AsRef<Path>) -> JsonStringArg {
let test_path = make_test_path(path);
JsonStringArg {
json: std::fs::read_to_string(test_path).unwrap(),
chain_id: 0,
chain_id: Some(0),
max_priority_fee_bips: 0,
max_fee: Amount::ZERO,
gas_limit: None,
Expand Down
12 changes: 4 additions & 8 deletions crates/module-system/sov-eip712-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,7 @@ where
match input {
Eip712AuthenticatorInput::Eip712(tx) => authenticate::<_, S, Rt, SP>(&tx.data, state),
Eip712AuthenticatorInput::Standard(tx) => {
sov_modules_api::capabilities::authenticate::<_, S, Rt>(
&tx.data,
&Rt::CHAIN_HASH,
state,
)
sov_modules_api::capabilities::authenticate::<_, S, Rt>(&tx.data, state)
}
}
}
Expand Down Expand Up @@ -194,7 +190,7 @@ where
pub fn authenticate<
Accessor: ProvableStateReader<User, Spec = S>,
S: Spec<CryptoSpec: Secp256k1CryptoSpec>,
D: DispatchCall<Spec = S>,
D: Runtime<S>,
SP: SchemaProvider,
>(
raw_tx: &[u8],
Expand Down Expand Up @@ -237,7 +233,7 @@ pub fn authenticate<

fn verify_and_decode_tx<
S: Spec<CryptoSpec: Secp256k1CryptoSpec>,
D: DispatchCall<Spec = S>,
D: Runtime<S>,
SP: SchemaProvider,
>(
raw_tx_hash: TxHash,
Expand All @@ -255,7 +251,7 @@ fn verify_and_decode_tx<
}
};

verify_chain_id(details, raw_tx_hash)?;
verify_chain_id::<S, D>(details, raw_tx_hash)?;
verify_eip712_signature::<S, D, SP>(&tx, raw_tx_hash, meter)?;

let tx_and_raw_hash = AuthenticatedTransactionAndRawHash {
Expand Down
Loading
Loading