Skip to content
Open
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
42 changes: 27 additions & 15 deletions casper-name-contracts/src/contracts/registrar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,19 +265,21 @@ impl Registrar {
fn prolong(&mut self, tokens: Vec<TokenRenewalInfo>) {
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
let mut metadata = self.wrapped_metadata(token_id);
// 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());
Expand All @@ -287,16 +289,18 @@ impl Registrar {
fn register(&mut self, names: Vec<NameMintInfo>) {
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(),
);
Expand Down Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -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(),
);
Expand Down
45 changes: 31 additions & 14 deletions casper-name-contracts/src/data_structures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand All @@ -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),
}
Expand Down Expand Up @@ -75,23 +75,29 @@ 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;
}
}

impl TryFrom<String> for NameTokenMetadata {
type Error = NameTokenError;

fn try_from(value: String) -> Result<Self, Self::Error> {
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)
}
}

Expand All @@ -112,7 +118,6 @@ impl TryFrom<Vec<(String, String)>> for NameTokenMetadata {
.ok_or(NameTokenError::DeserializationError)?
.1
.parse()
.map(trim_microseconds_to_milliseconds_if_needed)
.map_err(|_| NameTokenError::DeserializationError)?;

let resolver = value
Expand Down Expand Up @@ -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(&micros_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(&micros_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(&micros_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(&micros_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]
Expand Down