From 08f5277c34e8271d5ff73b4d803c77c054051aaa Mon Sep 17 00:00:00 2001 From: Maciej Zielinski Date: Mon, 15 Jun 2026 16:13:40 +0200 Subject: [PATCH] Fixes to expiration time handling --- .../src/contracts/registrar.rs | 42 ++++++++++------- casper-name-contracts/src/data_structures.rs | 45 +++++++++++++------ 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/casper-name-contracts/src/contracts/registrar.rs b/casper-name-contracts/src/contracts/registrar.rs index 2ba6339..c53a273 100644 --- a/casper-name-contracts/src/contracts/registrar.rs +++ b/casper-name-contracts/src/contracts/registrar.rs @@ -265,11 +265,13 @@ impl Registrar { fn prolong(&mut self, tokens: Vec) { let block_time = self.env().get_block_time(); for token in tokens { - // The offchain component may supply microsecond timestamps. - let token_expiration = + // The offchain component may supply microsecond timestamps. Normalize + // only for the block-time comparison; the metadata stores the value + // verbatim (normalization happens on read in `metadata.expiration()`). + let normalized_expiration = utils::trim_microseconds_to_milliseconds_if_needed(token.token_expiration); // verify the new expiration date is in the future - self.assert_token_expires_in_future(token_expiration, block_time); + self.assert_token_expires_in_future(normalized_expiration, block_time); // Compute token hash. let token_id = token.token_id; // get the token metadata @@ -277,7 +279,7 @@ impl Registrar { // check if the time for the renewal does not elapsed let expiration = metadata.expiration(); self.assert_in_renewal_period(expiration); - metadata.set_expiration(token_expiration); + metadata.set_expiration(token.token_expiration); self.name_token .set_token_metadata(token_id, metadata.to_vec()); @@ -287,16 +289,18 @@ impl Registrar { fn register(&mut self, names: Vec) { let block_time = self.env().get_block_time(); for info in names { - // The offchain component may supply microsecond timestamps. - let token_expiration = + // The offchain component may supply microsecond timestamps. Normalize + // only for the block-time comparison; the metadata stores the value + // verbatim (normalization happens on read in `metadata.expiration()`). + let normalized_expiration = utils::trim_microseconds_to_milliseconds_if_needed(info.token_expiration); - self.assert_token_expires_in_future(token_expiration, block_time); + self.assert_token_expires_in_future(normalized_expiration, block_time); if !utils::is_label_valid(&info.label) { self.revert(RegistrarError::TokenNameIsNotValid); } let metadata = NameTokenMetadata::with_resolver( &info.label, - token_expiration, + info.token_expiration, &info.asset_uri, self.name_token.get_default_resolver(), ); @@ -517,7 +521,7 @@ mod tests { } #[test] - fn register_with_microsecond_timestamps_stores_milliseconds() { + fn register_with_microsecond_timestamps_preserves_metadata() { let mut ctx = TestContext::install_and_setup(); let (admin, alice) = (ctx.admin, ctx.alice); @@ -533,17 +537,25 @@ mod tests { ) .unwrap(); - // Then the token is minted with a millisecond expiration. - ctx.expect_name_is_registered(alice, TOKEN_NAME); + // Then the token is minted and the metadata preserves the raw microsecond value. + let token_id = generate_token_id(TOKEN_NAME); + assert_eq!(ctx.token.owner_of(token_id), Some(alice)); + let metadata = ctx.token.token_metadata(token_id); + let stored_expiration = metadata + .into_iter() + .find(|(key, _)| key == "expiration") + .unwrap() + .1; + assert_eq!(stored_expiration, (token_expiration * 1000).to_string()); - // And the token is valid and not burned prematurely. + // And the token is valid and not burned prematurely (on-chain reads normalize). assert!(ctx.token.is_token_valid(generate_token_id(TOKEN_NAME))); ctx.with_name_expired(TOKEN_NAME); assert_eq!(ctx.token.balance_of(alice), U256::one()); } #[test] - fn renew_with_microsecond_timestamps_stores_milliseconds() { + fn renew_with_microsecond_timestamps_preserves_metadata() { let mut ctx = TestContext::install_and_setup(); let (admin, alice) = (ctx.admin, ctx.alice); @@ -562,11 +574,11 @@ mod tests { ctx.set_caller(admin); ctx.registrar.controller_prolong(voucher); - // Then the token expiration is stored in milliseconds. + // Then the token metadata preserves the raw microsecond value. let metadata = ctx.token.token_metadata(generate_token_id(TOKEN_NAME)); let expected = NameTokenMetadata::with_resolver( TOKEN_NAME, - token_expiration, + token_expiration * 1000, "", ctx.default_resolver.address(), ); diff --git a/casper-name-contracts/src/data_structures.rs b/casper-name-contracts/src/data_structures.rs index 8bdb210..0f05509 100644 --- a/casper-name-contracts/src/data_structures.rs +++ b/casper-name-contracts/src/data_structures.rs @@ -33,7 +33,7 @@ impl NameTokenMetadata { pub fn with_resolver(name: &str, expiration: u64, asset_uri: &str, resolver: Address) -> Self { Self { name: String::from(name), - expiration: trim_microseconds_to_milliseconds_if_needed(expiration), + expiration, resolver: Some(resolver), asset_uri: String::from(asset_uri), } @@ -42,7 +42,7 @@ impl NameTokenMetadata { pub fn with_no_resolver(name: &str, expiration: u64, asset_uri: &str) -> Self { Self { name: String::from(name), - expiration: trim_microseconds_to_milliseconds_if_needed(expiration), + expiration, resolver: None, asset_uri: String::from(asset_uri), } @@ -75,12 +75,19 @@ impl NameTokenMetadata { vec } + /// Returns the expiration normalized to milliseconds. + /// + /// The stored value is kept verbatim as the off-chain component provides it + /// (which may be microseconds), so this getter normalizes on read. All + /// on-chain comparisons against block time (also in milliseconds) go + /// through here. The raw stored value is only ever serialized back via + /// [`to_vec`](Self::to_vec) / [`json`](Self::json), preserving metadata units. pub fn expiration(&self) -> u64 { - self.expiration + trim_microseconds_to_milliseconds_if_needed(self.expiration) } pub fn set_expiration(&mut self, expiration: u64) { - self.expiration = trim_microseconds_to_milliseconds_if_needed(expiration); + self.expiration = expiration; } } @@ -88,10 +95,9 @@ impl TryFrom for NameTokenMetadata { type Error = NameTokenError; fn try_from(value: String) -> Result { - let mut metadata: NameTokenMetadata = - serde_json_wasm::from_str(&value).map_err(|_| NameTokenError::DeserializationError)?; - metadata.expiration = trim_microseconds_to_milliseconds_if_needed(metadata.expiration); - Ok(metadata) + // Metadata is preserved verbatim (off-chain may store microseconds); + // normalization to milliseconds happens on read in `expiration()`. + serde_json_wasm::from_str(&value).map_err(|_| NameTokenError::DeserializationError) } } @@ -112,7 +118,6 @@ impl TryFrom> for NameTokenMetadata { .ok_or(NameTokenError::DeserializationError)? .1 .parse() - .map(trim_microseconds_to_milliseconds_if_needed) .map_err(|_| NameTokenError::DeserializationError)?; let resolver = value @@ -393,40 +398,52 @@ mod tests { } #[test] - fn test_metadata_normalizes_microsecond_expiration() { + fn test_metadata_normalizes_microsecond_expiration_on_read() { // 2024-01-01T10:00:00Z in microseconds. let micros: u64 = 1_704_103_200_000_000; let millis: u64 = 1_704_103_200_000; + let micros_str = micros.to_string(); - // Constructors. + // Constructors normalize on read but keep storage verbatim. let metadata = NameTokenMetadata::with_no_resolver("test-label", micros, ""); assert_eq!(metadata.expiration(), millis); + assert!(metadata.json().contains(µs_str)); let resolver = Address::new("hash-7ba9daac84bebee8111c186588f21ebca35550b6cf1244e71768bd871938be6a") .unwrap(); let metadata = NameTokenMetadata::with_resolver("test-label", micros, "", resolver); assert_eq!(metadata.expiration(), millis); + assert!(metadata.json().contains(µs_str)); - // Setter. + // Setter keeps storage verbatim. let mut metadata = NameTokenMetadata::with_no_resolver("test-label", millis, ""); metadata.set_expiration(micros); assert_eq!(metadata.expiration(), millis); + assert!(metadata.json().contains(µs_str)); - // Deserialization from JSON (legacy on-chain data). + // Deserialization from JSON (legacy on-chain data) is not mutated. let json = format!( r#"{{"name":"test-label","expiration":{},"resolver":null,"asset_uri":""}}"#, micros ); let metadata: NameTokenMetadata = json.try_into().unwrap(); assert_eq!(metadata.expiration(), millis); + assert!(metadata.json().contains(µs_str)); - // Deserialization from key-value pairs (legacy on-chain data). + // Deserialization from key-value pairs (legacy on-chain data) is not mutated. let pairs = vec![ ("name".to_string(), "test-label".to_string()), ("expiration".to_string(), micros.to_string()), ]; let metadata: NameTokenMetadata = pairs.try_into().unwrap(); assert_eq!(metadata.expiration(), millis); + // `to_vec` round-trips the raw stored value. + let expiration_pair = metadata + .to_vec() + .into_iter() + .find(|(key, _)| key == "expiration") + .unwrap(); + assert_eq!(expiration_pair.1, micros_str); } #[test]