From 96f4869065b82f3b7e0eaf251e01a6ba9dc3dd07 Mon Sep 17 00:00:00 2001 From: steve preston Date: Thu, 16 Apr 2026 10:41:54 -0700 Subject: [PATCH 1/2] feat: configurable native token metadata (on-chain, API, CLI) Co-Authored-By: Claude --- README.md | 12 ++ casper/src/main/resources/Registry.rho | 1 + casper/src/main/resources/TokenMetadata.rhox | 49 +++++ casper/src/rust/casper.rs | 9 + casper/src/rust/casper_conf.rs | 57 ++++++ .../src/rust/engine/approve_block_protocol.rs | 6 + .../rust/engine/block_approver_protocol.rs | 18 ++ casper/src/rust/engine/casper_launch.rs | 54 ++++++ casper/src/rust/engine/initializing.rs | 17 ++ .../genesis/contracts/standard_deploys.rs | 34 ++++ casper/src/rust/genesis/genesis.rs | 28 ++- .../rust/test_utils/util/genesis_builder.rs | 3 + casper/src/rust/util/mod.rs | 1 + casper/src/rust/util/token_metadata_check.rs | 167 ++++++++++++++++++ .../src/test/resources/TokenMetadataTest.rho | 37 ++++ .../block_creator_memory_profile_spec.rs | 6 + ...pute_parents_post_state_regression_spec.rs | 9 + .../engine/block_approver_protocol_test.rs | 15 ++ casper/tests/engine/setup.rs | 3 + casper/tests/genesis/contracts/mod.rs | 1 + .../genesis/contracts/token_metadata_spec.rs | 23 +++ casper/tests/genesis/genesis_test.rs | 3 + casper/tests/util/genesis_builder.rs | 3 + docker/README.md | 20 ++- docker/minikube/setup.md | 9 +- docker/observer.yml | 3 + docker/shard.yml | 15 ++ docker/standalone.yml | 3 + docs/README.md | 8 + docs/casper/CONSENSUS_PROTOCOL.md | 8 +- docs/node/README.md | 53 +++++- .../main/protobuf/DeployServiceCommon.proto | 21 ++- node/src/main/resources/defaults.conf | 21 +++ node/src/rust/api/deploy_grpc_service_v1.rs | 12 ++ node/src/rust/api/web_api.rs | 22 +++ .../commandline/config_mapper.rs | 34 +++- .../rust/configuration/commandline/options.rs | 19 ++ node/src/rust/configuration/mod.rs | 9 + node/src/rust/diagnostics/tests.rs | 3 + node/src/rust/runtime/api_servers.rs | 6 + node/src/rust/runtime/setup.rs | 9 + 41 files changed, 811 insertions(+), 20 deletions(-) create mode 100644 casper/src/main/resources/TokenMetadata.rhox create mode 100644 casper/src/rust/util/token_metadata_check.rs create mode 100644 casper/src/test/resources/TokenMetadataTest.rho create mode 100644 casper/tests/genesis/contracts/token_metadata_spec.rs diff --git a/README.md b/README.md index b0a43c359..d4e0065db 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,18 @@ Reference configs: - [docker/conf/default.conf](docker/conf/default.conf) - Minimal shard override - [Consensus Configuration Guide](https://github.com/F1R3FLY-io/system-integration/blob/main/docs/consensus-configuration.md) - FTT, synchrony threshold semantics, finalization formula, recommended values +### Native Token + +Each chain's native token identity is configured before genesis and baked into the on-chain `TokenMetadata` contract. These values are **immutable after genesis**. + +| Config Field | CLI Flag | Default | +|---|---|---| +| `native-token-name` | `--native-token-name` | `F1R3CAP` | +| `native-token-symbol` | `--native-token-symbol` | `F1R3` | +| `native-token-decimals` | `--native-token-decimals` | `8` | + +After genesis, queryable via `/api/status` or on-chain at `rho:system:tokenMetadata`. Joiners verify their config matches the on-chain values at startup. + ## Documentation Detailed architecture and API documentation for each crate is available in [docs/](docs/README.md): diff --git a/casper/src/main/resources/Registry.rho b/casper/src/main/resources/Registry.rho index d0c71ee17..b5f25315a 100644 --- a/casper/src/main/resources/Registry.rho +++ b/casper/src/main/resources/Registry.rho @@ -485,6 +485,7 @@ in { `rho:system:authKey` : `rho:id:1qw5ehmq1x49dey4eadr1h4ncm361w3536asho7dr38iyookwcsp6i`, `rho:system:makeMint` : `rho:id:asysrwfgzf8bf7sxkiowp4b3tcsy4f8ombi3w96ysox4u3qdmn1wbc`, `rho:system:pos` : `rho:id:m3xk7h8r54dtqtwsrnxqzhe81baswey66nzw6m533nyd45ptyoybqr`, + `rho:system:tokenMetadata` : `rho:id:jutz139igo7x5gpby3ri1dhwcwnbdghd6onuys3wfnqhhkwffxbfsn`, `rho:vault:system` : `rho:id:6zcfqnwnaqcwpeyuysx1rm48ndr6sgsbbgjuwf45i5nor3io7dr76j`, `rho:vault:multiSig` : `rho:id:b9s6j3xeobgset4ndn64hje64grfcj7a43eekb3fh43yso5ujiecfn` } { diff --git a/casper/src/main/resources/TokenMetadata.rhox b/casper/src/main/resources/TokenMetadata.rhox new file mode 100644 index 000000000..3d0806cc8 --- /dev/null +++ b/casper/src/main/resources/TokenMetadata.rhox @@ -0,0 +1,49 @@ +/* + The table below describes the required computations and their dependencies + + No. | Dependency | Computation method | Result + ----+------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------- + 1. | | given | sk = 8f9a1c3b2d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a + 2. | | given | timestamp = 1737500000000 + 3. | | lastNonce | nonce = 9223372036854775807 + 4. | 1, | secp256k1 | pk = 04856ecb339aa2462bc25f4d4d5f5e5317c58e366222bed85f488c576f423ff7136ba22c4198b1b7b8d5d2c61722b948fee9d8d8e10e468dc52b490d3393444fd0 + 5. | 2, 4, 3, | registry | value = (1737500000000, 04856ecb339aa2462bc25f4d4d5f5e5317c58e366222bed85f488c576f423ff7136ba22c4198b1b7b8d5d2c61722b948fee9d8d8e10e468dc52b490d3393444fd0, 9223372036854775807) + 6. | 5, | protobuf | toSign = 2a65aa01620a092a071080fc8fb191650a462a44ca014104856ecb339aa2462bc25f4d4d5f5e5317c58e366222bed85f488c576f423ff7136ba22c4198b1b7b8d5d2c61722b948fee9d8d8e10e468dc52b490d3393444fd00a0d2a0b10feffffffffffffffff01 + 7. | 6, 1, | secp256k1 | sig = 3044022065f630061a79b4ae846a705faf6c96dc3f97d90bb9f1dd1dec8d8581d27f50d902207d99a932eee9a9743bef7c7148db938c4ec34bdd4d99b1445db98bc0a61ec851 + 8. | 4, | registry | uri = rho:id:jutz139igo7x5gpby3ri1dhwcwnbdghd6onuys3wfnqhhkwffxbfsn + ----+------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------- + */ +new + TokenMetadata, + rs(`rho:registry:insertSigned:secp256k1`), + uriOut +in { + // Returns the full name of the native token (e.g. "F1R3CAP"). + contract TokenMetadata(@"name", ret) = { + ret!("$$nativeTokenName$$") + } | + + // Returns the native token ticker symbol (e.g. "F1R3"). + contract TokenMetadata(@"symbol", ret) = { + ret!("$$nativeTokenSymbol$$") + } | + + // Returns the number of decimal places used to display the token + // (e.g. 8 means 1 token = 10^8 dust). + contract TokenMetadata(@"decimals", ret) = { + ret!($$nativeTokenDecimals$$) + } | + + // Returns all token metadata as a tuple (name, symbol, decimals) for + // callers that want a single round-trip. + contract TokenMetadata(@"all", ret) = { + ret!(("$$nativeTokenName$$", "$$nativeTokenSymbol$$", $$nativeTokenDecimals$$)) + } | + + rs!( + "04856ecb339aa2462bc25f4d4d5f5e5317c58e366222bed85f488c576f423ff7136ba22c4198b1b7b8d5d2c61722b948fee9d8d8e10e468dc52b490d3393444fd0".hexToBytes(), + (9223372036854775807, bundle+{*TokenMetadata}), + "3044022065f630061a79b4ae846a705faf6c96dc3f97d90bb9f1dd1dec8d8581d27f50d902207d99a932eee9a9743bef7c7148db938c4ec34bdd4d99b1445db98bc0a61ec851".hexToBytes(), + *uriOut + ) +} diff --git a/casper/src/rust/casper.rs b/casper/src/rust/casper.rs index ec1ba36cb..23b80b152 100755 --- a/casper/src/rust/casper.rs +++ b/casper/src/rust/casper.rs @@ -304,6 +304,12 @@ pub struct CasperShardConf { pub synchrony_finalized_baseline_enabled: bool, pub synchrony_finalized_baseline_max_distance: u64, pub max_user_deploys_per_block: u32, + /// Native token metadata baked into the TokenMetadata contract at genesis. + /// Present on every node (joiner, validator, ceremony master, observer, standalone) + /// so each path can log the effective values at startup. + pub native_token_name: String, + pub native_token_symbol: String, + pub native_token_decimals: u32, } impl CasperShardConf { @@ -336,6 +342,9 @@ impl CasperShardConf { synchrony_finalized_baseline_enabled: true, synchrony_finalized_baseline_max_distance: 2048, max_user_deploys_per_block: 32, + native_token_name: "F1R3CAP".to_string(), + native_token_symbol: "F1R3".to_string(), + native_token_decimals: 8, } } } diff --git a/casper/src/rust/casper_conf.rs b/casper/src/rust/casper_conf.rs index d5181b940..d77e1991a 100644 --- a/casper/src/rust/casper_conf.rs +++ b/casper/src/rust/casper_conf.rs @@ -197,6 +197,63 @@ pub struct GenesisBlockData { #[serde(rename = "pos-multi-sig-quorum")] pub pos_multi_sig_quorum: u32, + + /// Full display name of the native token. Substituted into the + /// TokenMetadata Rholang contract at genesis and registered at + /// `rho:system:tokenMetadata` with nonce `i64::MAX`, making it immutable + /// for the lifetime of the network. Operators MUST set this in config + /// before genesis; a missing value is a config error. + #[serde(rename = "native-token-name")] + pub native_token_name: String, + + /// Ticker symbol of the native token. Immutability rules are identical + /// to `native-token-name`. Operators MUST set this in config before genesis. + #[serde(rename = "native-token-symbol")] + pub native_token_symbol: String, + + /// Number of decimal places used to display the native token + /// (1 token = 10^decimals dust). Immutability rules are identical to + /// `native-token-name`. Operators MUST set this in config before genesis. + #[serde(rename = "native-token-decimals")] + pub native_token_decimals: u32, +} + +/// Maximum decimal places accepted for native token. Matches the de-facto +/// ERC-20 standard (ETH uses 18). Values above 18 exceed IEEE-754 double +/// safe-integer range (2^53), which breaks every JavaScript-based wallet +/// and block explorer. No production blockchain uses more than 18 +/// (BTC=8, SOL=9, ATOM=6, DOT=10, KSM=12, ETH=18). +pub const MAX_NATIVE_TOKEN_DECIMALS: u32 = 18; + +impl GenesisBlockData { + /// Validates native-token-* fields. Called during config load so a + /// misconfigured node fails startup loudly rather than baking bad + /// values into genesis or serving misleading metadata via `/api/status`. + pub fn validate_native_token(&self) -> Result<(), String> { + if self.native_token_name.trim().is_empty() { + return Err(format!( + "native-token-name must be non-empty and non-whitespace; got {:?}", + self.native_token_name + )); + } + if self.native_token_symbol.trim().is_empty() { + return Err(format!( + "native-token-symbol must be non-empty and non-whitespace; got {:?}", + self.native_token_symbol + )); + } + if self.native_token_decimals > MAX_NATIVE_TOKEN_DECIMALS { + return Err(format!( + "native-token-decimals={} exceeds maximum of {} (industry standard; \ + ETH=18, BTC=8, SOL=9, ATOM=6); values above {} exceed IEEE-754 \ + double safe-integer range and break JavaScript clients", + self.native_token_decimals, + MAX_NATIVE_TOKEN_DECIMALS, + MAX_NATIVE_TOKEN_DECIMALS + )); + } + Ok(()) + } } /// Genesis ceremony configuration diff --git a/casper/src/rust/engine/approve_block_protocol.rs b/casper/src/rust/engine/approve_block_protocol.rs index 5db5dd905..106521eb1 100644 --- a/casper/src/rust/engine/approve_block_protocol.rs +++ b/casper/src/rust/engine/approve_block_protocol.rs @@ -136,6 +136,9 @@ impl ApproveBlockProtocolFactory { block_number: i64, pos_multi_sig_public_keys: Vec, pos_multi_sig_quorum: u32, + native_token_name: String, + native_token_symbol: String, + native_token_decimals: u32, runtime_manager: &mut RuntimeManager, last_approved_block: Arc>>, event_log: Option, @@ -187,6 +190,9 @@ impl ApproveBlockProtocolFactory { vaults, supply: i64::MAX, version: 1, + native_token_name, + native_token_symbol, + native_token_decimals, }; let genesis_block = Genesis::create_genesis_block(runtime_manager, &genesis).await?; diff --git a/casper/src/rust/engine/block_approver_protocol.rs b/casper/src/rust/engine/block_approver_protocol.rs index 1c0ab5a12..9d441ddc8 100644 --- a/casper/src/rust/engine/block_approver_protocol.rs +++ b/casper/src/rust/engine/block_approver_protocol.rs @@ -39,6 +39,9 @@ pub struct BlockApproverProtocol { pub required_sigs: i32, pub pos_multi_sig_public_keys: Vec, pub pos_multi_sig_quorum: u32, + pub native_token_name: String, + pub native_token_symbol: String, + pub native_token_decimals: u32, // Infrastructure transport: Arc, @@ -60,6 +63,9 @@ impl BlockApproverProtocol { required_sigs: i32, pos_multi_sig_public_keys: Vec, pos_multi_sig_quorum: u32, + native_token_name: String, + native_token_symbol: String, + native_token_decimals: u32, transport: Arc, conf: Arc, ) -> Result { @@ -94,6 +100,9 @@ impl BlockApproverProtocol { required_sigs, pos_multi_sig_public_keys, pos_multi_sig_quorum, + native_token_name, + native_token_symbol, + native_token_decimals, transport, conf, }) @@ -145,6 +154,9 @@ impl BlockApproverProtocol { shard_id: &str, pos_multi_sig_public_keys: &[String], pos_multi_sig_quorum: u32, + native_token_name: &str, + native_token_symbol: &str, + native_token_decimals: u32, ) -> Result<(), String> { // Basic checks – required sigs, absence of system deploys, bonds equality if candidate.required_sigs < required_sigs { @@ -208,6 +220,9 @@ impl BlockApproverProtocol { vaults, i64::MAX, shard_id, + native_token_name, + native_token_symbol, + native_token_decimals, ); let block_deploys: &Vec = &block.body.deploys; @@ -300,6 +315,9 @@ impl BlockApproverProtocol { shard_id, &self.pos_multi_sig_public_keys, self.pos_multi_sig_quorum, + &self.native_token_name, + &self.native_token_symbol, + self.native_token_decimals, ) .await } diff --git a/casper/src/rust/engine/casper_launch.rs b/casper/src/rust/engine/casper_launch.rs index 165c85180..73d5d9e36 100644 --- a/casper/src/rust/engine/casper_launch.rs +++ b/casper/src/rust/engine/casper_launch.rs @@ -170,6 +170,9 @@ impl CasperLaunchImpl { synchrony_finalized_baseline_max_distance: conf .synchrony_finalized_baseline_max_distance, max_user_deploys_per_block: conf.max_user_deploys_per_block, + native_token_name: conf.genesis_block_data.native_token_name.clone(), + native_token_symbol: conf.genesis_block_data.native_token_symbol.clone(), + native_token_decimals: conf.genesis_block_data.native_token_decimals, }; Self { @@ -332,6 +335,7 @@ impl CasperLaunchImpl { ); let ab = approved_block.candidate.block.clone(); + let genesis_post_state_hash = ab.body.state.post_state_hash.clone(); let casper = self.create_casper(validator_id.clone(), ab)?; let casper_arc = Arc::new(casper); @@ -409,12 +413,41 @@ impl CasperLaunchImpl { ) .await?; + // Guard against config drift: a joiner's local native-token-* values + // must match what this network actually baked into the TokenMetadata + // contract at genesis. If they disagree, the node's /api/status would + // advertise values that contradict on-chain state, which misleads + // block explorers and wallets. + let runtime_manager = self.runtime_manager.lock().await; + crate::rust::util::token_metadata_check::verify_token_metadata_matches_config( + &runtime_manager, + &genesis_post_state_hash, + &self.conf.genesis_block_data.native_token_name, + &self.conf.genesis_block_data.native_token_symbol, + self.conf.genesis_block_data.native_token_decimals, + ) + .await?; + Ok(()) } async fn connect_as_genesis_validator(&self) -> Result<(), CasperError> { println!("connectAsGenesisValidator"); + // As a genesis validator, native-token-* values from local config are + // what will be baked into the TokenMetadata contract at genesis (via + // default_blessed_terms). On-chain state cannot disagree with local + // config here by construction, so no post-genesis verification is + // performed on this path. + tracing::info!( + event = "native_token_metadata_startup", + role = "genesis_validator", + native_token_name = %self.conf.genesis_block_data.native_token_name, + native_token_symbol = %self.conf.genesis_block_data.native_token_symbol, + native_token_decimals = self.conf.genesis_block_data.native_token_decimals, + "Genesis validator: native token metadata will be derived from local config" + ); + let timestamp = self .conf .genesis_block_data @@ -461,6 +494,9 @@ impl CasperLaunchImpl { .pos_multi_sig_public_keys .clone(), self.conf.genesis_block_data.pos_multi_sig_quorum, + self.conf.genesis_block_data.native_token_name.clone(), + self.conf.genesis_block_data.native_token_symbol.clone(), + self.conf.genesis_block_data.native_token_decimals, self.transport_layer.clone(), Arc::new(self.rp_conf_ask.clone()), )?; @@ -501,6 +537,21 @@ impl CasperLaunchImpl { self.conf.validator_private_key.as_deref(), ); + // As ceremony master, native-token-* values from local config will be + // baked into the TokenMetadata contract at genesis (via + // default_blessed_terms). On-chain state matches local config by + // construction on this path, so no post-genesis verification is + // performed. If your chain should use different values, update + // casper.genesis-block-data.native-token-* before genesis. + tracing::info!( + event = "native_token_metadata_startup", + role = "ceremony_master", + native_token_name = %self.conf.genesis_block_data.native_token_name, + native_token_symbol = %self.conf.genesis_block_data.native_token_symbol, + native_token_decimals = self.conf.genesis_block_data.native_token_decimals, + "Ceremony master: native token metadata will be baked into genesis from local config" + ); + tracing::warn!("=== BOOTSTRAP GENESIS INPUT DEBUG START ==="); tracing::warn!(bonds_file = %self.conf.genesis_block_data.bonds_file); @@ -554,6 +605,9 @@ impl CasperLaunchImpl { .pos_multi_sig_public_keys .clone(), self.conf.genesis_block_data.pos_multi_sig_quorum, + self.conf.genesis_block_data.native_token_name.clone(), + self.conf.genesis_block_data.native_token_symbol.clone(), + self.conf.genesis_block_data.native_token_decimals, &mut *self.runtime_manager.lock().await, self.last_approved_block.clone(), Some(self.event_publisher.clone()), diff --git a/casper/src/rust/engine/initializing.rs b/casper/src/rust/engine/initializing.rs index f457b1503..edbc71755 100644 --- a/casper/src/rust/engine/initializing.rs +++ b/casper/src/rust/engine/initializing.rs @@ -899,6 +899,7 @@ impl Initializing { approved_block: &ApprovedBlock, ) -> Result<(), CasperError> { let ab = approved_block.candidate.block.clone(); + let genesis_post_state_hash = ab.body.state.post_state_hash.clone(); // RuntimeManager is now Arc>, so we clone the Arc let runtime_manager = self.runtime_manager.clone(); @@ -969,6 +970,22 @@ impl Initializing { "create_casper_and_transition_to_running: transition_to_running completed successfully" ); + // Guard joiners (first-time connections requesting an approved block from + // peers) against config drift: the node's local native-token-* values + // must match what this network baked into the TokenMetadata contract at + // genesis. See casper/src/rust/util/token_metadata_check.rs for details. + { + let runtime_manager = self.runtime_manager.lock().await; + crate::rust::util::token_metadata_check::verify_token_metadata_matches_config( + &runtime_manager, + &genesis_post_state_hash, + &self.casper_shard_conf.native_token_name, + &self.casper_shard_conf.native_token_symbol, + self.casper_shard_conf.native_token_decimals, + ) + .await?; + } + self.transport_layer .send_fork_choice_tip_request(&self.connections_cell, &self.rp_conf_ask) .await diff --git a/casper/src/rust/genesis/contracts/standard_deploys.rs b/casper/src/rust/genesis/contracts/standard_deploys.rs index b839c744c..73b9eb319 100644 --- a/casper/src/rust/genesis/contracts/standard_deploys.rs +++ b/casper/src/rust/genesis/contracts/standard_deploys.rs @@ -33,6 +33,11 @@ pub const POS_GENERATOR_PK: &str = pub const VAULTS_GENERATOR_PK: &str = "a06959868e39bb3a8502846686a23119716ecd001700baf9e2ecfa0dbf1a3247"; pub const STACK_PK: &str = "c94e647de6876c954ebb7b64c40a220227770f9be003635edfe3336a1a2c8605"; +// Private key, timestamp, pubkey, signature, and URI for TokenMetadata were generated +// via RegistrySigGen. See casper/tests/util/rholang/token_metadata_sig_gen.rs for the +// one-off generator and the derivation table at the top of TokenMetadata.rhox. +pub const TOKEN_METADATA_PK: &str = + "8f9a1c3b2d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a"; // Timestamps for each deploy pub const REGISTRY_TIMESTAMP: i64 = 1559156071321; @@ -45,6 +50,7 @@ pub const SYSTEM_VAULT_TIMESTAMP: i64 = 1559156183943; pub const MULTI_SIG_SYSTEM_VAULT_TIMESTAMP: i64 = 1571408470880; pub const POS_GENERATOR_TIMESTAMP: i64 = 1559156420651; pub const STACK_TIMESTAMP: i64 = 1751539590099; +pub const TOKEN_METADATA_TIMESTAMP: i64 = 1737500000000; lazy_static! { pub static ref REGISTRY_PUB_KEY: PublicKey = to_public(REGISTRY_PK); @@ -58,6 +64,7 @@ lazy_static! { pub static ref POS_GENERATOR_PUB_KEY: PublicKey = to_public(POS_GENERATOR_PK); pub static ref VAULTS_GENERATOR_PUB_KEY: PublicKey = to_public(VAULTS_GENERATOR_PK); pub static ref STACK_PUB_KEY: PublicKey = to_public(STACK_PK); + pub static ref TOKEN_METADATA_PUB_KEY: PublicKey = to_public(TOKEN_METADATA_PK); } pub fn system_public_keys() -> Vec<&'static PublicKey> { @@ -73,6 +80,7 @@ pub fn system_public_keys() -> Vec<&'static PublicKey> { &POS_GENERATOR_PUB_KEY, &VAULTS_GENERATOR_PUB_KEY, &STACK_PUB_KEY, + &TOKEN_METADATA_PUB_KEY, ] } @@ -182,6 +190,32 @@ pub fn stack(shard_id: &str) -> Signed { ) } +/// Deploys the `TokenMetadata` contract that stores the native token's +/// name, symbol, and decimals. Values are substituted into the Rholang +/// source at genesis time and registered at `rho:system:tokenMetadata`. +pub fn token_metadata( + native_token_name: &str, + native_token_symbol: &str, + native_token_decimals: u32, + shard_id: &str, +) -> Signed { + let decimals_str = native_token_decimals.to_string(); + to_deploy( + CompiledRholangTemplate::new( + "TokenMetadata.rhox", + HashMap::new(), + &[ + ("nativeTokenName", native_token_name), + ("nativeTokenSymbol", native_token_symbol), + ("nativeTokenDecimals", &decimals_str), + ], + ), + TOKEN_METADATA_PK, + TOKEN_METADATA_TIMESTAMP, + shard_id, + ) +} + pub fn pos_generator(pos: &ProofOfStake, shard_id: &str) -> Signed { assert!(pos.minimum_bond <= pos.maximum_bond); assert!(pos.validators.len() > 0); diff --git a/casper/src/rust/genesis/genesis.rs b/casper/src/rust/genesis/genesis.rs index 1c140c4db..c8cacee58 100644 --- a/casper/src/rust/genesis/genesis.rs +++ b/casper/src/rust/genesis/genesis.rs @@ -31,6 +31,13 @@ pub struct Genesis { pub vaults: Vec, pub supply: i64, pub version: i64, + /// Full display name of the native token (e.g. "F1R3CAP"). Baked into + /// the `TokenMetadata` Rholang contract at genesis. + pub native_token_name: String, + /// Ticker symbol of the native token (e.g. "F1R3"). + pub native_token_symbol: String, + /// Number of decimal places for native token display (dust per token = 10^decimals). + pub native_token_decimals: u32, } impl Genesis { @@ -56,6 +63,9 @@ impl Genesis { vaults: &Vec, supply: i64, shard_id: &str, + native_token_name: &str, + native_token_symbol: &str, + native_token_decimals: u32, ) -> Vec> { // Splits initial vaults creation in multiple deploys (batches) const BATCH_SIZE: usize = 100; @@ -95,9 +105,15 @@ impl Genesis { let system_vault = standard_deploys::system_vault(shard_id); let multi_sig_system_vault = standard_deploys::multi_sig_system_vault(shard_id); let stack = standard_deploys::stack(shard_id); + let token_metadata = standard_deploys::token_metadata( + native_token_name, + native_token_symbol, + native_token_decimals, + shard_id, + ); let pos_generator = standard_deploys::pos_generator(&pos_params, shard_id); - let mut all_deploys = Vec::with_capacity(10 + vault_deploys.len()); + let mut all_deploys = Vec::with_capacity(11 + vault_deploys.len()); all_deploys.push(registry); all_deploys.push(list_ops); all_deploys.push(either); @@ -107,6 +123,7 @@ impl Genesis { all_deploys.push(system_vault); all_deploys.push(multi_sig_system_vault); all_deploys.push(stack); + all_deploys.push(token_metadata); all_deploys.extend(vault_deploys); all_deploys.push(pos_generator); @@ -118,6 +135,9 @@ impl Genesis { vaults: &Vec, supply: i64, shard_id: &str, + native_token_name: &str, + native_token_symbol: &str, + native_token_decimals: u32, ) -> Vec> { // Use hardcoded timestamp for backwards compatibility const BASE_TIMESTAMP: i64 = 1565818101792; @@ -127,6 +147,9 @@ impl Genesis { vaults, supply, shard_id, + native_token_name, + native_token_symbol, + native_token_decimals, ) } @@ -139,6 +162,9 @@ impl Genesis { &genesis.vaults, genesis.supply, &genesis.shard_id, + &genesis.native_token_name, + &genesis.native_token_symbol, + genesis.native_token_decimals, ); let (start_hash, state_hash, processed_deploys) = runtime_manager diff --git a/casper/src/rust/test_utils/util/genesis_builder.rs b/casper/src/rust/test_utils/util/genesis_builder.rs index 3258f662e..d8fc839ee 100644 --- a/casper/src/rust/test_utils/util/genesis_builder.rs +++ b/casper/src/rust/test_utils/util/genesis_builder.rs @@ -267,6 +267,9 @@ impl GenesisBuilder { supply: i64::MAX, block_number: 0, version: 1, + native_token_name: "F1R3CAP".to_string(), + native_token_symbol: "F1R3".to_string(), + native_token_decimals: 8, }, ) } diff --git a/casper/src/rust/util/mod.rs b/casper/src/rust/util/mod.rs index bcc1c8c00..ebb7df782 100644 --- a/casper/src/rust/util/mod.rs +++ b/casper/src/rust/util/mod.rs @@ -8,4 +8,5 @@ pub mod mergeable_channels_gc; pub mod proto_util; pub mod rholang; pub mod rspace_util; +pub mod token_metadata_check; pub mod vault_parser; diff --git a/casper/src/rust/util/token_metadata_check.rs b/casper/src/rust/util/token_metadata_check.rs new file mode 100644 index 000000000..a6143e18f --- /dev/null +++ b/casper/src/rust/util/token_metadata_check.rs @@ -0,0 +1,167 @@ +// Startup validation that the on-chain TokenMetadata contract values match +// the node's local `native-token-*` configuration. +// +// This guards against the "lying API" scenario where a node joins an existing +// network but has mismatched token metadata in its config: the protocol would +// still work (the values on-chain are the only ones that matter), but the +// node's `/api/status` responses would advertise values that disagree with +// what was baked into genesis state. +// +// Caught here, the node logs a clear error explaining which value(s) disagree +// and refuses to continue. Caught at genesis ceremony time instead, the node +// would fail to sign the UnapprovedBlock and the ceremony would stall without +// a clear reason. + +use models::rhoapi::Par; +use models::rust::block::state_hash::StateHash; +use rholang::rust::interpreter::rho_type::{RhoNumber, RhoString}; + +use crate::rust::errors::CasperError; +use crate::rust::util::rholang::runtime_manager::RuntimeManager; + +const TOKEN_METADATA_QUERY: &str = r#" + new ret, rl(`rho:registry:lookup`), tmCh in { + rl!(`rho:system:tokenMetadata`, *tmCh) | + for (@(_, TokenMetadata) <- tmCh) { + @TokenMetadata!("all", *ret) + } + } +"#; + +/// Queries the on-chain TokenMetadata contract and returns +/// `(name, symbol, decimals)` read from the `rho:system:tokenMetadata` +/// registry entry. +pub async fn read_on_chain_token_metadata( + runtime_manager: &RuntimeManager, + post_state_hash: &StateHash, +) -> Result<(String, String, u32), CasperError> { + let (result, _cost) = runtime_manager + .play_exploratory_deploy(TOKEN_METADATA_QUERY.to_string(), post_state_hash) + .await?; + + // The contract's "all" method returns a single tuple `(name, symbol, decimals)` + // on the exploratory deploy return channel. + let tuple_par = result.first().ok_or_else(|| { + CasperError::RuntimeError( + "TokenMetadata exploratory deploy returned no values".to_string(), + ) + })?; + + parse_all_tuple(tuple_par).ok_or_else(|| { + CasperError::RuntimeError(format!( + "TokenMetadata contract returned an unexpected shape; expected (String, String, Int), got: {:?}", + tuple_par + )) + }) +} + +fn parse_all_tuple(par: &Par) -> Option<(String, String, u32)> { + let expr = par.exprs.first()?; + let etuple = match expr.expr_instance.as_ref()? { + models::rhoapi::expr::ExprInstance::ETupleBody(t) => t, + _ => return None, + }; + + if etuple.ps.len() != 3 { + return None; + } + + let name = RhoString::unapply(&etuple.ps[0])?; + let symbol = RhoString::unapply(&etuple.ps[1])?; + let decimals = RhoNumber::unapply(&etuple.ps[2])?; + + if decimals < 0 || decimals > i64::from(u32::MAX) { + return None; + } + + Some((name, symbol, decimals as u32)) +} + +/// Compares the on-chain token metadata against the node's local config. +/// Returns `Err` with a descriptive message if any field disagrees. +/// +/// This is called once after the node transitions to Running state. A mismatch +/// means the operator's config does not reflect the values baked into this +/// chain's genesis block; the safest behaviour is to abort the node so the +/// API never reports misleading values to clients. +pub async fn verify_token_metadata_matches_config( + runtime_manager: &RuntimeManager, + post_state_hash: &StateHash, + config_name: &str, + config_symbol: &str, + config_decimals: u32, +) -> Result<(), CasperError> { + let (on_chain_name, on_chain_symbol, on_chain_decimals) = + read_on_chain_token_metadata(runtime_manager, post_state_hash).await?; + + // Track mismatches both as a machine-parseable list of field names + // (used by integration tests via structured log fields) and as a + // human-readable description (used in the returned error message). + let mut mismatched_fields: Vec<&'static str> = Vec::new(); + let mut mismatch_descriptions: Vec = Vec::new(); + if on_chain_name != config_name { + mismatched_fields.push("native-token-name"); + mismatch_descriptions.push(format!( + "native-token-name: config={:?}, on-chain={:?}", + config_name, on_chain_name + )); + } + if on_chain_symbol != config_symbol { + mismatched_fields.push("native-token-symbol"); + mismatch_descriptions.push(format!( + "native-token-symbol: config={:?}, on-chain={:?}", + config_symbol, on_chain_symbol + )); + } + if on_chain_decimals != config_decimals { + mismatched_fields.push("native-token-decimals"); + mismatch_descriptions.push(format!( + "native-token-decimals: config={}, on-chain={}", + config_decimals, on_chain_decimals + )); + } + + if !mismatched_fields.is_empty() { + // Emit a structured log event BEFORE returning the error so that + // integration tests (and operators) can grep the JSON-formatted logs + // for a stable event without regex-parsing English error text. + // Field names are stable identifiers matching the HOCON key names. + // + // mismatched_fields is joined into a comma-separated string so that + // the tracing JSON layer serializes it as a plain JSON string that + // consumers (tests, log pipelines) can split on ',' rather than + // parsing Rust Debug-formatted Vec syntax. + let mismatched_fields_joined = mismatched_fields.join(","); + tracing::error!( + event = "native_token_metadata_mismatch", + mismatched_fields = %mismatched_fields_joined, + config_name = %config_name, + on_chain_name = %on_chain_name, + config_symbol = %config_symbol, + on_chain_symbol = %on_chain_symbol, + config_decimals = config_decimals, + on_chain_decimals = on_chain_decimals, + "native token metadata mismatch: configured values do not match \ + values baked into this network's genesis state" + ); + + return Err(CasperError::RuntimeError(format!( + "Configured native token metadata does not match the values baked \ + into this network's genesis state. Mismatches: [{}]. \ + Update casper.genesis-block-data.native-token-* in your config to \ + match the on-chain values, or connect to a network whose genesis \ + was created with your configured values.", + mismatch_descriptions.join("; ") + ))); + } + + tracing::info!( + event = "native_token_metadata_verified", + native_token_name = %config_name, + native_token_symbol = %config_symbol, + native_token_decimals = config_decimals, + "Verified on-chain token metadata matches local configuration" + ); + + Ok(()) +} diff --git a/casper/src/test/resources/TokenMetadataTest.rho b/casper/src/test/resources/TokenMetadataTest.rho new file mode 100644 index 000000000..92ba3777d --- /dev/null +++ b/casper/src/test/resources/TokenMetadataTest.rho @@ -0,0 +1,37 @@ +// Exercises the TokenMetadata contract deployed at genesis. +// All four methods must return the values baked into the contract at +// genesis via the `native-token-*` config fields. + +new + rl(`rho:registry:lookup`), + stdout(`rho:io:stdout`), + tokenMetadataCh, + nameCh, + symbolCh, + decimalsCh, + allCh, + test +in { + rl!(`rho:system:tokenMetadata`, *tokenMetadataCh) | + for (@(_, TokenMetadata) <- tokenMetadataCh) { + @TokenMetadata!("name", *nameCh) | + @TokenMetadata!("symbol", *symbolCh) | + @TokenMetadata!("decimals", *decimalsCh) | + @TokenMetadata!("all", *allCh) | + + for ( + @name <- nameCh & + @symbol <- symbolCh & + @decimals <- decimalsCh & + @(allName, allSymbol, allDecimals) <- allCh + ) { + stdout!(("TokenMetadata.name", name)) | + stdout!(("TokenMetadata.symbol", symbol)) | + stdout!(("TokenMetadata.decimals", decimals)) | + stdout!(("TokenMetadata.all", (allName, allSymbol, allDecimals))) | + + // Consistency: tuple returned by "all" must match the individual getters. + test!((name == allName, symbol == allSymbol, decimals == allDecimals)) + } + } +} diff --git a/casper/tests/block_creator_memory_profile_spec.rs b/casper/tests/block_creator_memory_profile_spec.rs index e8b9553a4..cc8faff63 100644 --- a/casper/tests/block_creator_memory_profile_spec.rs +++ b/casper/tests/block_creator_memory_profile_spec.rs @@ -216,6 +216,9 @@ async fn run_block_creator_create_memory_profile() { vaults: Vec::new(), supply: i64::MAX, version: 1, + native_token_name: "F1R3CAP".to_string(), + native_token_symbol: "F1R3".to_string(), + native_token_decimals: 8, }; let parent = Genesis::create_genesis_block(&mut runtime_manager, &genesis) .await @@ -450,6 +453,9 @@ async fn run_block_creator_phase_split_memory_profile() { vaults: Vec::new(), supply: i64::MAX, version: 1, + native_token_name: "F1R3CAP".to_string(), + native_token_symbol: "F1R3".to_string(), + native_token_decimals: 8, }; let parent = Genesis::create_genesis_block(&mut runtime_manager, &genesis) .await diff --git a/casper/tests/compute_parents_post_state_regression_spec.rs b/casper/tests/compute_parents_post_state_regression_spec.rs index 4fe2a1dd1..fab498cb9 100644 --- a/casper/tests/compute_parents_post_state_regression_spec.rs +++ b/casper/tests/compute_parents_post_state_regression_spec.rs @@ -227,6 +227,9 @@ async fn run_compute_parents_post_state_finalized_skew_regression() { vaults: Vec::new(), supply: i64::MAX, version: 1, + native_token_name: "F1R3CAP".to_string(), + native_token_symbol: "F1R3".to_string(), + native_token_decimals: 8, }; let genesis_block = Genesis::create_genesis_block(&mut runtime_manager, &genesis) @@ -431,6 +434,9 @@ async fn run_compute_parents_post_state_missing_mergeable_regression() { vaults: Vec::new(), supply: i64::MAX, version: 1, + native_token_name: "F1R3CAP".to_string(), + native_token_symbol: "F1R3".to_string(), + native_token_decimals: 8, }; let genesis_block = Genesis::create_genesis_block(&mut runtime_manager, &genesis) @@ -631,6 +637,9 @@ async fn run_visible_blocks_scope_test() { vaults: Vec::new(), supply: i64::MAX, version: 1, + native_token_name: "F1R3CAP".to_string(), + native_token_symbol: "F1R3".to_string(), + native_token_decimals: 8, }; let genesis_block = Genesis::create_genesis_block(&mut runtime_manager, &genesis) diff --git a/casper/tests/engine/block_approver_protocol_test.rs b/casper/tests/engine/block_approver_protocol_test.rs index 812363016..1149a3222 100644 --- a/casper/tests/engine/block_approver_protocol_test.rs +++ b/casper/tests/engine/block_approver_protocol_test.rs @@ -71,6 +71,9 @@ impl TestContext { required_sigs, genesis_params.proof_of_stake.pos_multi_sig_public_keys, genesis_params.proof_of_stake.pos_multi_sig_quorum, + genesis_params.native_token_name.clone(), + genesis_params.native_token_symbol.clone(), + genesis_params.native_token_decimals, node.tle.clone(), Arc::new(node.rp_conf.clone()), )?; @@ -192,6 +195,9 @@ async fn block_approver_protocol_should_successfully_validate_correct_candidate( SHARD_ID, &ctx.protocol.pos_multi_sig_public_keys, ctx.protocol.pos_multi_sig_quorum, + &ctx.protocol.native_token_name, + &ctx.protocol.native_token_symbol, + ctx.protocol.native_token_decimals, ) .await; @@ -222,6 +228,9 @@ async fn block_approver_protocol_should_reject_candidate_with_incorrect_bonds() SHARD_ID, &ctx.protocol.pos_multi_sig_public_keys, ctx.protocol.pos_multi_sig_quorum, + &ctx.protocol.native_token_name, + &ctx.protocol.native_token_symbol, + ctx.protocol.native_token_decimals, ) .await; @@ -252,6 +261,9 @@ async fn block_approver_protocol_should_reject_candidate_with_incorrect_vaults() SHARD_ID, &ctx.protocol.pos_multi_sig_public_keys, ctx.protocol.pos_multi_sig_quorum, + &ctx.protocol.native_token_name, + &ctx.protocol.native_token_symbol, + ctx.protocol.native_token_decimals, ) .await; @@ -286,6 +298,9 @@ async fn block_approver_protocol_should_reject_candidate_with_incorrect_blessed_ SHARD_ID, &ctx.protocol.pos_multi_sig_public_keys, ctx.protocol.pos_multi_sig_quorum, + &ctx.protocol.native_token_name, + &ctx.protocol.native_token_symbol, + ctx.protocol.native_token_decimals, ) .await; diff --git a/casper/tests/engine/setup.rs b/casper/tests/engine/setup.rs index a31b2c8d8..68a58372c 100644 --- a/casper/tests/engine/setup.rs +++ b/casper/tests/engine/setup.rs @@ -427,6 +427,9 @@ impl TestFixture { .pos_multi_sig_public_keys .clone(), genesis_params.proof_of_stake.pos_multi_sig_quorum, + genesis_params.native_token_name.clone(), + genesis_params.native_token_symbol.clone(), + genesis_params.native_token_decimals, transport_layer.clone(), Arc::new(rp_conf.clone()), ) diff --git a/casper/tests/genesis/contracts/mod.rs b/casper/tests/genesis/contracts/mod.rs index da7c07520..ceb31c612 100644 --- a/casper/tests/genesis/contracts/mod.rs +++ b/casper/tests/genesis/contracts/mod.rs @@ -19,6 +19,7 @@ pub mod stack_spec; pub mod standard_deploys_spec; pub mod system_vault_spec; pub mod timeout_result_collector_spec; +pub mod token_metadata_spec; pub mod tree_hash_map_spec; pub mod vault_address_spec; pub mod vault_issuance_test; diff --git a/casper/tests/genesis/contracts/token_metadata_spec.rs b/casper/tests/genesis/contracts/token_metadata_spec.rs new file mode 100644 index 000000000..d828c6e60 --- /dev/null +++ b/casper/tests/genesis/contracts/token_metadata_spec.rs @@ -0,0 +1,23 @@ +use crate::genesis::contracts::GENESIS_TEST_TIMEOUT; +use crate::helper::rho_spec::RhoSpec; +use rholang::rust::build::compile_rholang_source::CompiledRholangSource; +use std::collections::HashMap; + +#[tokio::test] +async fn token_metadata_spec() { + let test_object = CompiledRholangSource::load_source("TokenMetadataTest.rho") + .expect("Failed to load TokenMetadataTest.rho"); + + let compiled = CompiledRholangSource::new( + test_object, + HashMap::new(), + "TokenMetadataTest.rho".to_string(), + ) + .expect("Failed to compile TokenMetadataTest.rho"); + + let spec = RhoSpec::new(compiled, vec![], GENESIS_TEST_TIMEOUT); + + spec.run_tests() + .await + .expect("TokenMetadataSpec tests failed"); +} diff --git a/casper/tests/genesis/genesis_test.rs b/casper/tests/genesis/genesis_test.rs index 07ab6d01e..880d00da6 100644 --- a/casper/tests/genesis/genesis_test.rs +++ b/casper/tests/genesis/genesis_test.rs @@ -244,6 +244,9 @@ async fn from_input_files( supply: i64::MAX, block_number: params.block_number, version: 1, + native_token_name: "F1R3CAP".to_string(), + native_token_symbol: "F1R3".to_string(), + native_token_decimals: 8, }; let genesis_block = Genesis::create_genesis_block(runtime_manager, &genesis).await?; diff --git a/casper/tests/util/genesis_builder.rs b/casper/tests/util/genesis_builder.rs index 502b66bd3..40ca90e12 100644 --- a/casper/tests/util/genesis_builder.rs +++ b/casper/tests/util/genesis_builder.rs @@ -246,6 +246,9 @@ impl GenesisBuilder { supply: i64::MAX, block_number: 0, version: 1, + native_token_name: "F1R3CAP".to_string(), + native_token_symbol: "F1R3".to_string(), + native_token_decimals: 8, }, ) } diff --git a/docker/README.md b/docker/README.md index 8bcb1e0fe..6ca782c3f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -175,8 +175,26 @@ Results are logged to `logs/smoke_test_*.log` with pass/fail counters. ## Genesis Configuration +### Native Token + +The native token's identity is configured in `defaults.conf` (or via CLI flags) and baked into the `TokenMetadata` Rholang contract at genesis. These values are **immutable after genesis** — they cannot be changed without creating a new network. + +| Config Field | CLI Flag | Default | Description | +|---|---|---|---| +| `native-token-name` | `--native-token-name` | `F1R3CAP` | Full display name | +| `native-token-symbol` | `--native-token-symbol` | `F1R3` | Ticker symbol | +| `native-token-decimals` | `--native-token-decimals` | `8` | Decimal places (1 token = 10^decimals dust) | + +Override per-node via environment variables in the compose files (e.g. `NATIVE_TOKEN_NAME=MyToken`), or per-validator via `VALIDATOR1_NATIVE_TOKEN_NAME=MyToken`. + +After genesis, the values are queryable: +- **API**: `GET /api/status` → `nativeTokenName`, `nativeTokenSymbol`, `nativeTokenDecimals` +- **On-chain**: `rho:system:tokenMetadata` contract with methods `name`, `symbol`, `decimals`, `all` + +Joiners verify their config matches the on-chain values at startup. A mismatch causes the node to exit with a clear error. + ### Wallets (genesis/wallets.txt) -- **Bootstrap Node** - Initial REV balance for network operations +- **Bootstrap Node** - Initial balance for network operations - **Validator 1-3** - Funded for transaction fees and operations ### Bonds (genesis/bonds.txt) diff --git a/docker/minikube/setup.md b/docker/minikube/setup.md index cbddab22f..9bad97338 100644 --- a/docker/minikube/setup.md +++ b/docker/minikube/setup.md @@ -74,14 +74,17 @@ The expected result should be like { "version": { "api": "1", - "node": "RChain Node 1.0.0-SNAPSHOT (29aa391c6948c2c1df533b4cd35794d65eab04b5)" + "node": "F1r3node Rust 0.4.10 ()" }, "address": "rnode://c93bc07f3d141459356791f9eaaa05f4cec49c0b@f1r3fly0-0.f1r3fly0?protocol=40400&discovery=40404", - "networkId": "f1r3fly", + "networkId": "testnet", "shardId": "root", "peers": 1, "nodes": 3, - "minPhloPrice": 1 + "minPhloPrice": 1, + "nativeTokenName": "F1R3CAP", + "nativeTokenSymbol": "F1R3", + "nativeTokenDecimals": 8 } ``` diff --git a/docker/observer.yml b/docker/observer.yml index b31b1bdf7..2485d6bf1 100644 --- a/docker/observer.yml +++ b/docker/observer.yml @@ -25,6 +25,9 @@ services: - --no-upnp - --allow-private-addresses - --heartbeat-disabled + - --native-token-name=${NATIVE_TOKEN_NAME:-F1R3CAP} + - --native-token-symbol=${NATIVE_TOKEN_SYMBOL:-F1R3} + - --native-token-decimals=${NATIVE_TOKEN_DECIMALS:-8} ports: - "40450:40400" - "40451:40401" diff --git a/docker/shard.yml b/docker/shard.yml index 1d32c4f26..58d49b400 100644 --- a/docker/shard.yml +++ b/docker/shard.yml @@ -49,6 +49,9 @@ services: - "2" - --ceremony-master-mode - --heartbeat-disabled + - --native-token-name=${BOOTSTRAP_NATIVE_TOKEN_NAME:-${NATIVE_TOKEN_NAME:-F1R3CAP}} + - --native-token-symbol=${BOOTSTRAP_NATIVE_TOKEN_SYMBOL:-${NATIVE_TOKEN_SYMBOL:-F1R3}} + - --native-token-decimals=${BOOTSTRAP_NATIVE_TOKEN_DECIMALS:-${NATIVE_TOKEN_DECIMALS:-8}} ports: - "40400:40400" - "40401:40401" @@ -78,6 +81,9 @@ services: - --validator-public-key=${VALIDATOR1_PUBLIC_KEY:-04fa70d7be5eb750e0915c0f6d19e7085d18bb1c22d030feb2a877ca2cd226d04438aa819359c56c720142fbc66e9da03a5ab960a3d8b75363a226b7c800f60420} - --validator-private-key=${VALIDATOR1_PRIVATE_KEY:-357cdc4201a5650830e0bc5a03299a30038d9934ba4c7ab73ec164ad82471ff9} - --genesis-validator + - --native-token-name=${VALIDATOR1_NATIVE_TOKEN_NAME:-${NATIVE_TOKEN_NAME:-F1R3CAP}} + - --native-token-symbol=${VALIDATOR1_NATIVE_TOKEN_SYMBOL:-${NATIVE_TOKEN_SYMBOL:-F1R3}} + - --native-token-decimals=${VALIDATOR1_NATIVE_TOKEN_DECIMALS:-${NATIVE_TOKEN_DECIMALS:-8}} ports: - "40410:40400" - "40411:40401" @@ -107,6 +113,9 @@ services: - --validator-public-key=${VALIDATOR2_PUBLIC_KEY:-04837a4cff833e3157e3135d7b40b8e1f33c6e6b5a4342b9fc784230ca4c4f9d356f258debef56ad4984726d6ab3e7709e1632ef079b4bcd653db00b68b2df065f} - --validator-private-key=${VALIDATOR2_PRIVATE_KEY:-2c02138097d019d263c1d5383fcaddb1ba6416a0f4e64e3a617fe3af45b7851d} - --genesis-validator + - --native-token-name=${VALIDATOR2_NATIVE_TOKEN_NAME:-${NATIVE_TOKEN_NAME:-F1R3CAP}} + - --native-token-symbol=${VALIDATOR2_NATIVE_TOKEN_SYMBOL:-${NATIVE_TOKEN_SYMBOL:-F1R3}} + - --native-token-decimals=${VALIDATOR2_NATIVE_TOKEN_DECIMALS:-${NATIVE_TOKEN_DECIMALS:-8}} ports: - "40420:40400" - "40421:40401" @@ -136,6 +145,9 @@ services: - --validator-public-key=${VALIDATOR3_PUBLIC_KEY:-0457febafcc25dd34ca5e5c025cd445f60e5ea6918931a54eb8c3a204f51760248090b0c757c2bdad7b8c4dca757e109f8ef64737d90712724c8216c94b4ae661c} - --validator-private-key=${VALIDATOR3_PRIVATE_KEY:-b67533f1f99c0ecaedb7d829e430b1c0e605bda10f339f65d5567cb5bd77cbcb} - --genesis-validator + - --native-token-name=${VALIDATOR3_NATIVE_TOKEN_NAME:-${NATIVE_TOKEN_NAME:-F1R3CAP}} + - --native-token-symbol=${VALIDATOR3_NATIVE_TOKEN_SYMBOL:-${NATIVE_TOKEN_SYMBOL:-F1R3}} + - --native-token-decimals=${VALIDATOR3_NATIVE_TOKEN_DECIMALS:-${NATIVE_TOKEN_DECIMALS:-8}} ports: - "40430:40400" - "40431:40401" @@ -165,6 +177,9 @@ services: - --no-upnp - --allow-private-addresses - --heartbeat-disabled + - --native-token-name=${READONLY_NATIVE_TOKEN_NAME:-${NATIVE_TOKEN_NAME:-F1R3CAP}} + - --native-token-symbol=${READONLY_NATIVE_TOKEN_SYMBOL:-${NATIVE_TOKEN_SYMBOL:-F1R3}} + - --native-token-decimals=${READONLY_NATIVE_TOKEN_DECIMALS:-${NATIVE_TOKEN_DECIMALS:-8}} ports: - "40450:40400" - "40451:40401" diff --git a/docker/standalone.yml b/docker/standalone.yml index 74bcc6170..f710ce2fe 100644 --- a/docker/standalone.yml +++ b/docker/standalone.yml @@ -23,6 +23,9 @@ services: - --host=${STANDALONE_HOST:-rnode.standalone} - --validator-private-key=${STANDALONE_PRIVATE_KEY:-5f668a7ee96d944a4494cc947e4005e172d7ab3461ee5538f1f2a45a835e9657} - --allow-private-addresses + - --native-token-name=${NATIVE_TOKEN_NAME:-F1R3CAP} + - --native-token-symbol=${NATIVE_TOKEN_SYMBOL:-F1R3} + - --native-token-decimals=${NATIVE_TOKEN_DECIMALS:-8} ports: - "40400:40400" - "40401:40401" diff --git a/docs/README.md b/docs/README.md index e8dee6a05..2acb46e1d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -113,6 +113,14 @@ The Cargo workspace contains 11 crates: | [Rholang Language Analysis](./rholang-language-analysis.md) | Language design analysis | | [Features](./features.md) | Feature requirements and status | +### Genesis & Token Identity + +The native token's name, symbol, and decimals are configured before genesis and baked into the on-chain `TokenMetadata` contract at `rho:system:tokenMetadata`. They are immutable after genesis and exposed via `/api/status`. See: + +- [defaults.conf](../node/src/main/resources/defaults.conf) — `native-token-name`, `native-token-symbol`, `native-token-decimals` under `casper.genesis-block-data` +- [Docker Genesis Configuration](../docker/README.md#native-token) — compose env var overrides, API and on-chain query examples +- [Consensus Protocol § Configuration](./casper/CONSENSUS_PROTOCOL.md#10-configuration) — genesis-locked parameters + ### Infrastructure | Document | Description | diff --git a/docs/casper/CONSENSUS_PROTOCOL.md b/docs/casper/CONSENSUS_PROTOCOL.md index 6f1ad6a3a..2a868ae68 100644 --- a/docs/casper/CONSENSUS_PROTOCOL.md +++ b/docs/casper/CONSENSUS_PROTOCOL.md @@ -393,7 +393,13 @@ All consensus parameters are defined in HOCON configuration files: Operator config files are minimal overrides — HOCON's fallback semantics merge them on top of the built-in defaults automatically. -**Genesis-locked parameters** (cannot change after network creation): `fault-tolerance-threshold` and `synchrony-constraint-threshold` are written into the genesis block's on-chain state. Changing them requires a new genesis. +**Genesis-locked parameters** (cannot change after network creation): +- `fault-tolerance-threshold` and `synchrony-constraint-threshold` — written into the genesis block's on-chain state +- `native-token-name`, `native-token-symbol`, `native-token-decimals` — baked into the `TokenMetadata` Rholang contract at `rho:system:tokenMetadata` with nonce `i64::MAX`, making them immutable via the registry's `insertSigned` protocol + +Changing any of these requires a new genesis (new network). + +**Native token metadata** is exposed via `/api/status` (`nativeTokenName`, `nativeTokenSymbol`, `nativeTokenDecimals`) and queryable on-chain by any Rholang contract. Joiners verify their config matches the on-chain values at startup; a mismatch causes the node to exit with a structured error event (`native_token_metadata_mismatch`). See also: [Consensus Configuration Guide](https://github.com/F1R3FLY-io/system-integration/blob/main/docs/consensus-configuration.md) — FTT and synchrony threshold semantics, finalization formula, recommended values per validator set size. diff --git a/docs/node/README.md b/docs/node/README.md index 8e1c0006a..20ba96a6c 100644 --- a/docs/node/README.md +++ b/docs/node/README.md @@ -36,17 +36,27 @@ main() ## Configuration **Config precedence** (highest wins): -1. CLI arguments +1. CLI arguments (`--native-token-name=X`, `--network-id=Y`, etc.) 2. Config file (`rnode.conf` in data directory) -3. Default config (`defaults.conf`) +3. Default config (`defaults.conf` baked into the binary) + +**Config build pipeline** (`configuration/mod.rs::build()`): +1. Load `defaults.conf` via HOCON → `HoconLoader::load_file()` +2. Merge user config on top (if `rnode.conf` exists in data dir) → `default_config.load_file(config_file)` +3. Resolve HOCON substitutions (e.g. `protocol-client.network-id = ${protocol-server.network-id}`) +4. Deserialize merged HOCON into `NodeConf` struct → `merged_config.resolve()` +5. Apply CLI overrides → `node_conf.override_config_values(options)` (via `config_mapper.rs`) +6. Validate → `validate_config(&node_conf)` (e.g. native token non-empty, decimals ≤ 18, quorum ≤ keys) + +**Important**: HOCON substitutions resolve in step 3, before CLI overrides in step 5. A CLI flag like `--network-id` must override both `protocol_server.network_id` AND `protocol_client.network_id` because the substitution `${protocol-server.network-id}` already resolved to the HOCON default. **`NodeConf`** fields: - `protocol_server` -- P2P (port 40400, network-id, TLS) -- `protocol_client` -- Bootstrap peer, timeouts +- `protocol_client` -- Bootstrap peer, network-id (must match server), timeouts - `peers_discovery` -- Kademlia (port 40404) - `api_server` -- gRPC external (40401), internal (40402), HTTP (40403), admin (40405) - `storage` -- Data directory (default `~/.rnode`) -- `casper` -- Validator key, parents, finalization, heartbeat +- `casper` -- Validator key, parents, finalization, heartbeat, genesis block data (bonds, wallets, native token metadata) - `metrics` -- Prometheus, InfluxDB, Zipkin, Sigar toggles - `dev` -- Dev mode, deployer private key - `openai` -- LLM integration settings @@ -62,6 +72,9 @@ The following boolean flags override HOCON configuration at startup. CLI flags a | `--disable-mergeable-channel-gc` | `casper.enable_mergeable_channel_gc = false` | Disable mergeable channel GC (takes precedence over `--enable-mergeable-channel-gc`) | | `--heartbeat-enabled` | `casper.heartbeat_conf.enabled = true` | Enable heartbeat block proposing for liveness | | `--heartbeat-disabled` | `casper.heartbeat_conf.enabled = false` | Disable heartbeat proposing (takes precedence over `--heartbeat-enabled`) | +| `--native-token-name` | `casper.genesis_block_data.native_token_name` | Native token display name (genesis-locked) | +| `--native-token-symbol` | `casper.genesis_block_data.native_token_symbol` | Native token ticker symbol (genesis-locked) | +| `--native-token-decimals` | `casper.genesis_block_data.native_token_decimals` | Native token decimal places, 0-18 (genesis-locked) | **Precedence rules for paired flags**: When both an enable and disable flag are provided for the same setting, the disable flag wins. The config mapper evaluates `--disable-*` after `--enable-*`, so the disable always takes final effect. @@ -88,9 +101,27 @@ CLI flags are applied to the parsed `NodeConf` by `config_mapper.rs`: | Port | Purpose | |------|---------| -| 40403 | Public REST (deploy, blocks, finalization, transactions) via Axum | +| 40403 | Public REST (deploy, blocks, finalization, transactions, status) via Axum | | 40405 | Admin (propose, propose_result) | +**`/api/status`** returns node identity, network membership, and native token metadata: + +```json +{ + "version": {"api": "1", "node": "..."}, + "address": "rnode://...", + "networkId": "testnet", + "shardId": "root", + "peers": 4, + "nodes": 4, + "minPhloPrice": 1, + "nativeTokenName": "F1R3CAP", + "nativeTokenSymbol": "F1R3", + "nativeTokenDecimals": 8, + "peerList": [...] +} +``` + ## WebSocket Events The `/ws/events` endpoint on the HTTP port (40403) streams real-time node events. See [websocket-events.md](websocket-events.md) for full documentation. @@ -99,10 +130,22 @@ The `/ws/events` endpoint on the HTTP port (40403) streams real-time node events Events published during startup are buffered and replayed to clients that connect after the node is running. The buffer is sealed when engine initialization completes. +## Error Handling & Shutdown + +`handle_unrecoverable_errors()` in `node_runtime.rs` is the top-level error boundary. Any `Err` from `NodeRuntime::main()` is caught, logged via `tracing::error!`, and the process exits with code 1. This covers: +- Config validation failures (empty token name, invalid decimals) +- Genesis ceremony failures (required signatures not met) +- Token metadata verification mismatch (joiner config disagrees with on-chain state) +- Any runtime panic or unrecoverable error + +The error chain propagates cleanly: `verify_token_metadata_matches_config → Err(CasperError) → ? in casper_launch.launch() → ? in NodeRuntime::main() → handle_unrecoverable_errors → process::exit(1)`. Destructors fire in order; no mid-async process::exit calls. + ## API Server Startup `bind_tcp_listener_with_retry()` in `servers_instances.rs` handles `AddrInUse` resilience for HTTP/Admin servers: 60 attempts with 500ms delay between retries. +`APIServers::build()` in `api_servers.rs` constructs all gRPC services (Repl, Propose, Deploy, LSP) with shared dependencies (engine cell, block store, connections). `WebApiImpl` in `web_api.rs` handles the HTTP REST layer and caches config-derived values (network-id, shard-id, min-phlo-price, native token metadata) for fast `/api/status` responses without per-request config reads. + ## Transfer Enrichment Pipeline Inline transfer data on `DeployInfo` for `get_block` and `last_finalized_block` responses: diff --git a/models/src/main/protobuf/DeployServiceCommon.proto b/models/src/main/protobuf/DeployServiceCommon.proto index 562e9975b..9b92b3e94 100644 --- a/models/src/main/protobuf/DeployServiceCommon.proto +++ b/models/src/main/protobuf/DeployServiceCommon.proto @@ -247,14 +247,19 @@ message PeerInfo { } message Status { - VersionInfo version = 1 [(scalapb.field).no_box = true]; - string address = 2; - string networkId = 3; - string shardId = 4; - int32 peers = 5; - int32 nodes = 6; - int64 minPhloPrice = 7; - repeated PeerInfo peerList = 8; // Detailed peer list + VersionInfo version = 1 [(scalapb.field).no_box = true]; + string address = 2; + string networkId = 3; + string shardId = 4; + int32 peers = 5; + int32 nodes = 6; + int64 minPhloPrice = 7; + repeated PeerInfo peerList = 8; // Detailed peer list + // Native token metadata baked into genesis state. Queryable on-chain via + // the TokenMetadata contract at `rho:system:tokenMetadata`. + string nativeTokenName = 9; + string nativeTokenSymbol = 10; + uint32 nativeTokenDecimals = 11; } message VersionInfo { diff --git a/node/src/main/resources/defaults.conf b/node/src/main/resources/defaults.conf index bdfc194fc..bfcee20f9 100644 --- a/node/src/main/resources/defaults.conf +++ b/node/src/main/resources/defaults.conf @@ -335,6 +335,27 @@ casper { # How many confirmations are necessary to use multi-sig vault. # The value should be less or equal the number of PoS multi-sig public keys. pos-multi-sig-quorum = 2 + + # Native token metadata written into genesis state and exposed via /api/status. + # + # At genesis these values are substituted into the TokenMetadata Rholang + # contract and registered at `rho:system:tokenMetadata` with nonce + # `i64::MAX` (9223372036854775807). The registry's insertSigned endpoint + # only replaces an entry when `version > oldVersion`, so the genesis + # values are immutable for the lifetime of the network — there is no + # deploy or migration that can change them in-place. To change them you + # must create a new network with a new genesis block. + # + # MUST be set before genesis. If you do not override these values your + # chain will carry the F1R3FLY reference chain's branding; that is almost + # never what operators of other networks want. + # + # native-token-name : full display name (e.g. "F1R3CAP") + # native-token-symbol : ticker symbol (e.g. "F1R3") + # native-token-decimals: number of decimal places (1 token = 10^decimals dust) + native-token-name = "F1R3CAP" + native-token-symbol = "F1R3" + native-token-decimals = 8 } # Genesis ceremony variables diff --git a/node/src/rust/api/deploy_grpc_service_v1.rs b/node/src/rust/api/deploy_grpc_service_v1.rs index b0bbb40de..34f119dda 100644 --- a/node/src/rust/api/deploy_grpc_service_v1.rs +++ b/node/src/rust/api/deploy_grpc_service_v1.rs @@ -73,6 +73,9 @@ pub struct DeployGrpcServiceV1Impl { network_id: String, shard_id: String, min_phlo_price: i64, + native_token_name: String, + native_token_symbol: String, + native_token_decimals: u32, is_node_read_only: bool, engine_cell: EngineCell, block_report_api: BlockReportAPI, @@ -91,6 +94,9 @@ impl DeployGrpcServiceV1Impl { network_id: String, shard_id: String, min_phlo_price: i64, + native_token_name: String, + native_token_symbol: String, + native_token_decimals: u32, is_node_read_only: bool, engine_cell: EngineCell, block_report_api: BlockReportAPI, @@ -107,6 +113,9 @@ impl DeployGrpcServiceV1Impl { network_id, shard_id, min_phlo_price, + native_token_name, + native_token_symbol, + native_token_decimals, is_node_read_only, engine_cell, block_report_api, @@ -885,6 +894,9 @@ impl DeployService for DeployGrpcServiceV1Impl { nodes, min_phlo_price: self.min_phlo_price, peer_list, + native_token_name: self.native_token_name.clone(), + native_token_symbol: self.native_token_symbol.clone(), + native_token_decimals: self.native_token_decimals, }; Ok(tonic::Response::new(StatusResponse { diff --git a/node/src/rust/api/web_api.rs b/node/src/rust/api/web_api.rs index 26cfdfd7d..cdf8d379b 100644 --- a/node/src/rust/api/web_api.rs +++ b/node/src/rust/api/web_api.rs @@ -117,6 +117,9 @@ where network_id: String, shard_id: String, min_phlo_price: i64, + native_token_name: String, + native_token_symbol: String, + native_token_decimals: u32, is_node_read_only: bool, engine_cell: Arc, block_enricher: Arc, @@ -138,6 +141,9 @@ where network_id: String, shard_id: String, min_phlo_price: i64, + native_token_name: String, + native_token_symbol: String, + native_token_decimals: u32, is_node_read_only: bool, block_enricher: Arc, cache_transaction_api: CacheTransactionAPI, @@ -153,6 +159,9 @@ where network_id, shard_id, min_phlo_price, + native_token_name, + native_token_symbol, + native_token_decimals, is_node_read_only, engine_cell, block_enricher, @@ -233,6 +242,9 @@ where nodes, min_phlo_price: self.min_phlo_price, peer_list, + native_token_name: self.native_token_name.clone(), + native_token_symbol: self.native_token_symbol.clone(), + native_token_decimals: self.native_token_decimals, }) } @@ -600,6 +612,16 @@ pub struct ApiStatus { pub min_phlo_price: i64, #[serde(rename = "peerList")] pub peer_list: Vec, + /// Full display name of the native token. Baked into genesis state via + /// the `TokenMetadata` Rholang contract at `rho:system:tokenMetadata`. + #[serde(rename = "nativeTokenName")] + pub native_token_name: String, + /// Ticker symbol of the native token (e.g. "F1R3"). + #[serde(rename = "nativeTokenSymbol")] + pub native_token_symbol: String, + /// Decimal places used to display the native token (dust per token = 10^decimals). + #[serde(rename = "nativeTokenDecimals")] + pub native_token_decimals: u32, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] diff --git a/node/src/rust/configuration/commandline/config_mapper.rs b/node/src/rust/configuration/commandline/config_mapper.rs index 4678eed3e..45057e524 100644 --- a/node/src/rust/configuration/commandline/config_mapper.rs +++ b/node/src/rust/configuration/commandline/config_mapper.rs @@ -31,7 +31,21 @@ impl ConfigMapper for NodeConf { &mut self.protocol_server.disable_state_exporter, run.disable_state_exporter, ); - Self::try_override_value(&mut self.protocol_server.network_id, run.network_id); + // `--network-id` must override BOTH protocol_server.network_id + // (what this node accepts) and protocol_client.network_id (what + // it sends on outbound messages). HOCON uses a substitution + // `protocol-client.network-id = ${protocol-server.network-id}` + // to keep them in sync at load time, but by this point the + // substitution has already resolved, so overriding only + // protocol_server leaves the client stuck on the HOCON default. + Self::try_override_value( + &mut self.protocol_server.network_id, + run.network_id.clone(), + ); + Self::try_override_value( + &mut self.protocol_client.network_id, + run.network_id, + ); Self::try_override_option(&mut self.protocol_server.host, run.host); Self::try_override_bool( &mut self.protocol_server.use_random_ports, @@ -251,6 +265,18 @@ impl ConfigMapper for NodeConf { &mut self.casper.genesis_block_data.number_of_active_validators, run.number_of_active_validators, ); + Self::try_override_value( + &mut self.casper.genesis_block_data.native_token_name, + run.native_token_name, + ); + Self::try_override_value( + &mut self.casper.genesis_block_data.native_token_symbol, + run.native_token_symbol, + ); + Self::try_override_value( + &mut self.casper.genesis_block_data.native_token_decimals, + run.native_token_decimals, + ); Self::try_override_value( &mut self.casper.genesis_block_data.genesis_block_number, run.genesis_block_number, @@ -557,6 +583,9 @@ mod tests { epoch_length: Some(111111), quarantine_length: Some(111111), number_of_active_validators: Some(111111), + native_token_name: Some("F1R3CAP".to_string()), + native_token_symbol: Some("F1R3".to_string()), + native_token_decimals: Some(8), required_signatures: Some(111111), approve_interval: Some(Duration::from_secs(111111)), approve_duration: Some(Duration::from_secs(111111)), @@ -674,6 +703,9 @@ mod tests { pos_multi_sig_public_keys: vec![], pos_multi_sig_quorum: 0, deploy_timestamp: None, + native_token_name: "F1R3CAP".to_string(), + native_token_symbol: "F1R3".to_string(), + native_token_decimals: 8, }, genesis_ceremony: casper::rust::casper_conf::GenesisCeremony { required_signatures: 0, diff --git a/node/src/rust/configuration/commandline/options.rs b/node/src/rust/configuration/commandline/options.rs index 67fa72f7c..e885a8233 100644 --- a/node/src/rust/configuration/commandline/options.rs +++ b/node/src/rust/configuration/commandline/options.rs @@ -380,6 +380,25 @@ pub struct RunOptions { #[arg(long = "number-of-active-validators")] pub number_of_active_validators: Option, + /// Full display name of the native token. Baked into the TokenMetadata + /// Rholang contract at genesis and exposed via /api/status. Must be + /// non-empty, non-whitespace. Immutable after genesis. + #[arg(long = "native-token-name")] + pub native_token_name: Option, + + /// Ticker symbol of the native token (e.g. "F1R3"). Same immutability + /// rules as `--native-token-name`. Must be non-empty, non-whitespace. + #[arg(long = "native-token-symbol")] + pub native_token_symbol: Option, + + /// Number of decimal places used to display the native token + /// (1 token = 10^decimals dust). Accepts 0-18; values above 18 are + /// rejected because they exceed IEEE-754 double safe-integer range + /// (breaks JavaScript-based clients) and are not used by any major + /// blockchain in practice (ETH=18, BTC=8, SOL=9, ATOM=6, DOT=10). + #[arg(long = "native-token-decimals", value_parser = clap::value_parser!(u32).range(0..=18))] + pub native_token_decimals: Option, + /// Number of signatures from bonded validators required for Ceremony Master to approve the genesis block #[arg(long = "required-signatures")] pub required_signatures: Option, diff --git a/node/src/rust/configuration/mod.rs b/node/src/rust/configuration/mod.rs index 584581f25..07b327c3a 100644 --- a/node/src/rust/configuration/mod.rs +++ b/node/src/rust/configuration/mod.rs @@ -140,6 +140,15 @@ pub mod builder { ); } + // Reject empty/whitespace native token name/symbol and out-of-range + // decimals before the node starts. Catches misconfigured shell variable + // expansion, typos, and values outside the industry-standard range. + node_conf + .casper + .genesis_block_data + .validate_native_token() + .map_err(|e| eyre::eyre!("native token config invalid: {}", e))?; + Ok(()) } diff --git a/node/src/rust/diagnostics/tests.rs b/node/src/rust/diagnostics/tests.rs index 6f845c76d..5d1f2369d 100644 --- a/node/src/rust/diagnostics/tests.rs +++ b/node/src/rust/diagnostics/tests.rs @@ -53,6 +53,9 @@ mod tests { genesis_block_number: 0, pos_multi_sig_public_keys: vec![], pos_multi_sig_quorum: 0, + native_token_name: "F1R3CAP".to_string(), + native_token_symbol: "F1R3".to_string(), + native_token_decimals: 8, }, genesis_ceremony: GenesisCeremony { required_signatures: 0, diff --git a/node/src/rust/runtime/api_servers.rs b/node/src/rust/runtime/api_servers.rs index 491a746f1..c38947154 100644 --- a/node/src/rust/runtime/api_servers.rs +++ b/node/src/rust/runtime/api_servers.rs @@ -75,6 +75,9 @@ impl APIServers { network_id: String, shard_id: String, min_phlo_price: i64, + native_token_name: String, + native_token_symbol: String, + native_token_decimals: u32, is_node_read_only: bool, // Shared dependencies engine_cell: EngineCell, @@ -102,6 +105,9 @@ impl APIServers { network_id, shard_id, min_phlo_price, + native_token_name, + native_token_symbol, + native_token_decimals, is_node_read_only, engine_cell, block_report_api, diff --git a/node/src/rust/runtime/setup.rs b/node/src/rust/runtime/setup.rs index 33c96e081..71ca8293f 100644 --- a/node/src/rust/runtime/setup.rs +++ b/node/src/rust/runtime/setup.rs @@ -630,6 +630,9 @@ pub async fn setup_node_program Date: Sat, 18 Apr 2026 14:27:29 -0700 Subject: [PATCH 2/2] comment tweaks --- casper/src/rust/casper_conf.rs | 4 +--- node/src/main/resources/defaults.conf | 13 +------------ 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/casper/src/rust/casper_conf.rs b/casper/src/rust/casper_conf.rs index d77e1991a..0f5e9b16e 100644 --- a/casper/src/rust/casper_conf.rs +++ b/casper/src/rust/casper_conf.rs @@ -200,9 +200,7 @@ pub struct GenesisBlockData { /// Full display name of the native token. Substituted into the /// TokenMetadata Rholang contract at genesis and registered at - /// `rho:system:tokenMetadata` with nonce `i64::MAX`, making it immutable - /// for the lifetime of the network. Operators MUST set this in config - /// before genesis; a missing value is a config error. + /// `rho:system:tokenMetadata`. Immutable after genesis. #[serde(rename = "native-token-name")] pub native_token_name: String, diff --git a/node/src/main/resources/defaults.conf b/node/src/main/resources/defaults.conf index bfcee20f9..613df3ebb 100644 --- a/node/src/main/resources/defaults.conf +++ b/node/src/main/resources/defaults.conf @@ -337,18 +337,7 @@ casper { pos-multi-sig-quorum = 2 # Native token metadata written into genesis state and exposed via /api/status. - # - # At genesis these values are substituted into the TokenMetadata Rholang - # contract and registered at `rho:system:tokenMetadata` with nonce - # `i64::MAX` (9223372036854775807). The registry's insertSigned endpoint - # only replaces an entry when `version > oldVersion`, so the genesis - # values are immutable for the lifetime of the network — there is no - # deploy or migration that can change them in-place. To change them you - # must create a new network with a new genesis block. - # - # MUST be set before genesis. If you do not override these values your - # chain will carry the F1R3FLY reference chain's branding; that is almost - # never what operators of other networks want. + # Immutable after genesis. Must be set before genesis. # # native-token-name : full display name (e.g. "F1R3CAP") # native-token-symbol : ticker symbol (e.g. "F1R3")