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
457 changes: 301 additions & 156 deletions Cargo.lock

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ members = [
resolver = "2"

[workspace.dependencies]
odra = { version = "2.4.0" }
odra-cli = { version = "2.4.0" }
odra-modules = { version = "2.4.0" }
odra-test = { version = "2.4.0" }
odra-build = { version = "2.4.0" }
odra-bdd = { version = "2.4.0" }
odra-casper-livenet-env = { version = "2.4.0" }
odra = { version = "2.8.1" }
odra-cli = { version = "2.8.1" }
odra-modules = { version = "2.8.1" }
odra-test = { version = "2.8.1" }
odra-build = { version = "2.8.1" }
odra-bdd = { version = "2.8.1" }
odra-casper-livenet-env = { version = "2.8.1" }

[profile.release]
codegen-units = 1
Expand Down
3 changes: 3 additions & 0 deletions casper-name-cli/src/odra_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use deploy::DeployScript;
use odra_cli::OdraCli;
use scenario::{CalculateSignature, CalculateTokenHash, RegisterTokenScenario, SetConfigScript, UpgradeReverseResolver, UpgradeNameToken};

use crate::odra_cli::scenario::UpgradeRegistrar;

mod deploy;
mod scenario;

Expand All @@ -24,6 +26,7 @@ pub fn cli() {
.scenario(CalculateSignature)
.scenario(UpgradeReverseResolver)
.scenario(UpgradeNameToken)
.scenario(UpgradeRegistrar)
.build()
.run();
}
5 changes: 5 additions & 0 deletions casper-name-cli/src/odra_cli/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ impl odra_cli::deploy::DeployScript for DeployScript {

let token = NameToken::load_or_deploy_with_cfg(
&env,
None,
NameTokenInitArgs {
name: "CSPR.name".to_string(),
symbol: "NAME".to_string(),
Expand All @@ -33,6 +34,7 @@ impl odra_cli::deploy::DeployScript for DeployScript {

let _resolver = DefaultResolver::load_or_deploy_with_cfg(
&env,
None,
DefaultResolverInitArgs {
name_token: token.address(),
},
Expand All @@ -43,6 +45,7 @@ impl odra_cli::deploy::DeployScript for DeployScript {

let registrar = Registrar::load_or_deploy_with_cfg(
&env,
None,
RegistrarInitArgs {
name_token: token.address(),
},
Expand All @@ -53,6 +56,7 @@ impl odra_cli::deploy::DeployScript for DeployScript {

let _controller = Controller::load_or_deploy_with_cfg(
&env,
None,
ControllerInitArgs {
registrar: registrar.address(),
treasury: admin,
Expand All @@ -65,6 +69,7 @@ impl odra_cli::deploy::DeployScript for DeployScript {

let _reverse_resolver = ReverseResolver::load_or_deploy_with_cfg(
&env,
None,
ReverseResolverInitArgs {
name_token: token.address(),
},
Expand Down
40 changes: 37 additions & 3 deletions casper-name-cli/src/odra_cli/scenario.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,16 +264,50 @@ impl Scenario for UpgradeNameToken {
.unwrap();

env.set_gas(cspr!(400));
let _result = NameToken::try_upgrade_with_cfg(
NameToken::try_upgrade_with_cfg(
env,
name_token_addr,
NoArgs,
UpgradeConfig {
package_named_key: String::from("NameToken_contract_package"),
package_named_key: NameToken::ident(),
force_create_upgrade_group: false,
allow_key_override: true,
},
)?;

Ok(())
}
}

pub struct UpgradeRegistrar;

impl ScenarioMetadata for UpgradeRegistrar {
const NAME: &'static str = "upgrade-registrar";
const DESCRIPTION: &'static str = "Upgrade the Registrar contract.";
}

impl Scenario for UpgradeRegistrar {
fn run(
&self,
env: &HostEnv,
container: &DeployedContractsContainer,
_args: Args,
) -> Result<(), ScenarioError> {
let registrar_addr = container
.address_by_name(&Registrar::ident())
.unwrap();

env.set_gas(cspr!(400));
Registrar::try_upgrade_with_cfg(
env,
registrar_addr,
NoArgs,
UpgradeConfig {
package_named_key: Registrar::ident(),
force_create_upgrade_group: true,
allow_key_override: true,
},
);
)?;

Ok(())
}
Expand Down
6 changes: 1 addition & 5 deletions casper-name-contracts/src/contracts/name_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,7 @@ impl NameToken {
self.revert(NameTokenError::ExpiredTokenTransfer);
}

let owner = self
.token
.owner_of(token_id)
.unwrap_or_revert_with(self, Cep95Error::ValueNotSet);
self.token.raw_transfer_from(owner, recipient, token_id);
let owner = self.token.raw_transfer(recipient, token_id);
// if called by an operator
if caller != owner {
self.cleanup(token_id);
Expand Down
98 changes: 94 additions & 4 deletions casper-name-contracts/src/contracts/registrar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,16 +265,19 @@ 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 =
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.token_expiration, block_time);
self.assert_token_expires_in_future(token_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.token_expiration);
metadata.set_expiration(token_expiration);

self.name_token
.set_token_metadata(token_id, metadata.to_vec());
Expand All @@ -284,13 +287,16 @@ impl Registrar {
fn register(&mut self, names: Vec<NameMintInfo>) {
let block_time = self.env().get_block_time();
for info in names {
self.assert_token_expires_in_future(info.token_expiration, block_time);
// The offchain component may supply microsecond timestamps.
let token_expiration =

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove trim_microseconds_to_milliseconds_if_needed(...) from all metadata write/storage paths.

The off-chain CSPR.cloud code still sends expiration in microseconds and also reads token metadata assuming microseconds. After the recent contract changes, the contract trims the value before saving metadata, so newly created tokens are stored with expiration in milliseconds. That creates an inconsistent state between old and new tokens and breaks the expiration job.

Current problem:

  • off-chain sends voucher/token expiration in microseconds
  • contract trims it to milliseconds before storing metadata
  • the job later reads metadata and parses expiration as microseconds
  • for newly created tokens, this makes the job think the token expired long ago
  • the job keeps calling expire
  • but on-chain, the contract compares against the milliseconds value, so the token is not burnable yet
  • result: no Burn event is emitted and the job retries the same tokens

We should preserve metadata exactly as provided by off-chain. Please keep the normalization only in the expire path, where the contract reads existing token metadata and checks whether it can be burned. That way we support legacy microsecond metadata without mutating stored metadata units.

In short:

  • do not trim when creating token metadata
  • do not trim when updating token metadata
  • do not trim in NameTokenMetadata::with_resolver
  • do not trim in NameTokenMetadata::with_no_resolver
  • do not trim in NameTokenMetadata::set_expiration
  • do not trim in deserialization paths if the deserialized metadata may later be written back to storage
  • normalize only when reading/checking expiration in the expire logic

сс @zie1ony

utils::trim_microseconds_to_milliseconds_if_needed(info.token_expiration);
self.assert_token_expires_in_future(token_expiration, block_time);
if !utils::is_label_valid(&info.label) {
self.revert(RegistrarError::TokenNameIsNotValid);
}
let metadata = NameTokenMetadata::with_resolver(
&info.label,
info.token_expiration,
token_expiration,
&info.asset_uri,
self.name_token.get_default_resolver(),
);
Expand Down Expand Up @@ -510,6 +516,90 @@ mod tests {
ctx.expect_name_is_registered(alice, TOKEN_NAME);
}

#[test]
fn register_with_microsecond_timestamps_stores_milliseconds() {
let mut ctx = TestContext::install_and_setup();
let (admin, alice) = (ctx.admin, ctx.alice);

// When Admin registers with microsecond timestamps.
let token_expiration = ctx.token_expiration_time();
let voucher_expiration = ctx.voucher_expiration_time();
ctx.try_name_register(
admin,
alice,
TOKEN_NAME,
token_expiration * 1000,
voucher_expiration * 1000,
)
.unwrap();

// Then the token is minted with a millisecond expiration.
ctx.expect_name_is_registered(alice, TOKEN_NAME);

// And the token is valid and not burned prematurely.
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() {
let mut ctx = TestContext::install_and_setup();
let (admin, alice) = (ctx.admin, ctx.alice);

// Given Alice has a token.
ctx.with_name_registered(admin, alice, TOKEN_NAME);

// When Admin renews with microsecond timestamps.
let token_expiration = INIT_TIME + 2 * TOKEN_EXPIRATION;
let voucher_expiration = INIT_TIME + TOKEN_EXPIRATION + GRACE_PERIOD;
let tokens = vec![TokenRenewalInfo::new(
generate_token_id(TOKEN_NAME),
token_expiration * 1000,
)];
let voucher = RenewalVoucher::new(tokens, voucher_expiration * 1000);
ctx.advance_block_time(TOKEN_EXPIRATION + GRACE_PERIOD - 1);
ctx.set_caller(admin);
ctx.registrar.controller_prolong(voucher);

// Then the token expiration is stored in milliseconds.
let metadata = ctx.token.token_metadata(generate_token_id(TOKEN_NAME));
let expected = NameTokenMetadata::with_resolver(
TOKEN_NAME,
token_expiration,
"",
ctx.default_resolver.address(),
);
assert_eq!(metadata, expected.to_vec());
}

#[test]
fn legacy_microsecond_metadata_expires_correctly() {
let mut ctx = TestContext::install_and_setup();
let (admin, alice) = (ctx.admin, ctx.alice);

// Given Alice has a token with a legacy microsecond expiration stored on-chain.
ctx.with_name_registered(admin, alice, TOKEN_NAME);
let token_id = generate_token_id(TOKEN_NAME);
let expiration = ctx.token_expiration_time();
ctx.set_caller(admin);
ctx.token.set_token_metadata(
token_id,
vec![
("asset_uri".to_string(), String::new()),
("expiration".to_string(), (expiration * 1000).to_string()),
("name".to_string(), TOKEN_NAME.to_string()),
],
);

// When the grace period is over.
ctx.advance_block_time(TOKEN_EXPIRATION + GRACE_PERIOD + PENDING_DELETE_PERIOD + 1);

// Then the token can be expired despite the microsecond value.
ctx.with_name_expired(TOKEN_NAME);
assert_eq!(ctx.token.balance_of(alice), U256::zero());
}

#[test]
fn register_the_same_name_before_expiration() {
let mut ctx = TestContext::install_and_setup();
Expand Down
23 changes: 23 additions & 0 deletions casper-name-contracts/src/contracts/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ pub fn is_label_valid(label: &str) -> bool {
is_valid_dns_label(label) && label != "cspr" && !label.contains('.')
}

/// Convert microsecond timestamps to milliseconds if they exceed the threshold.
///
/// The offchain component may supply timestamps in microseconds instead of
/// milliseconds. This helper is applied at every timestamp entry point
/// ([NameTokenMetadata](crate::data_structures::NameTokenMetadata) construction
/// and deserialization, voucher expiration reads, and registrar inputs) so all
/// time comparisons work in milliseconds, matching the block time.
pub fn trim_microseconds_to_milliseconds_if_needed(expiration: u64) -> u64 {
// Values above ~10^14 are microseconds: 10^14 ms is the year 5138,
// while any microsecond timestamp after 1973 exceeds it.
if expiration > 99_999_999_999_999 {
expiration / 1000
} else {
expiration
}
}

#[cfg(test)]
mod t {
#[test]
Expand Down Expand Up @@ -141,4 +158,10 @@ mod t {
assert!(!super::is_label_valid("invalid.label"));
assert!(super::is_label_valid("valid123"));
}

#[test]
fn test_trim_microseconds_to_milliseconds_if_needed() {
assert_eq!(super::trim_microseconds_to_milliseconds_if_needed(100_000_000_000_000), 100_000_000_000);
assert_eq!(super::trim_microseconds_to_milliseconds_if_needed(99_999_999_999_999), 99_999_999_999_999);
}
}
Loading
Loading