Skip to content

Conversation

@mducroux
Copy link
Contributor

@mducroux mducroux commented Jan 5, 2026

DEFI-1010: Add the type MetadataKey to encode ICRC metadata keys as defined here. Replace the usage of String with MetadataKey were applicable.

@github-actions github-actions bot added the feat label Jan 5, 2026
@mducroux mducroux marked this pull request as ready for review January 8, 2026 12:24
@mducroux mducroux requested review from a team as code owners January 8, 2026 12:24
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

This pull request changes code owned by the Governance team. Therefore, make sure that
you have considered the following (for Governance-owned code):

  1. Update unreleased_changelog.md (if there are behavior changes, even if they are
    non-breaking).

  2. Are there BREAKING changes?

  3. Is a data migration needed?

  4. Security review?

How to Satisfy This Automatic Review

  1. Go to the bottom of the pull request page.

  2. Look for where it says this bot is requesting changes.

  3. Click the three dots to the right.

  4. Select "Dismiss review".

  5. In the text entry box, respond to each of the numbered items in the previous
    section, declare one of the following:

  • Done.

  • $REASON_WHY_NO_NEED. E.g. for unreleased_changelog.md, "No
    canister behavior changes.", or for item 2, "Existing APIs
    behave as before.".

Brief Guide to "Externally Visible" Changes

"Externally visible behavior change" is very often due to some NEW canister API.

Changes to EXISTING APIs are more likely to be "breaking".

If these changes are breaking, make sure that clients know how to migrate, how to
maintain their continuity of operations.

If your changes are behind a feature flag, then, do NOT add entrie(s) to
unreleased_changelog.md in this PR! But rather, add entrie(s) later, in the PR
that enables these changes in production.

Reference(s)

For a more comprehensive checklist, see here.

GOVERNANCE_CHECKLIST_REMINDER_DEDUP

@mducroux mducroux dismissed github-actions[bot]’s stale review January 8, 2026 12:28

No canister behavior changes.

Copy link
Contributor

@mbjorkqvist mbjorkqvist left a comment

Choose a reason for hiding this comment

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

Thanks @mducroux, this is a much more comprehensive implementation than what I was expecting! I have some comments/questions regarding the checks and panics.

Comment on lines 130 to 147
/// Parses a metadata key from a string in the format `<namespace>:<key>`.
///
/// # Errors
///
/// Returns an error if the string does not follow the required format.
pub fn parse(s: &str) -> Result<Self, MetadataKeyError> {
let colon_pos = s.find(':').ok_or(MetadataKeyError::MissingColon)?;
let namespace = &s[..colon_pos];
let key = &s[colon_pos + 1..];

if namespace.is_empty() {
return Err(MetadataKeyError::EmptyNamespace);
}
if key.is_empty() {
return Err(MetadataKeyError::EmptyKey);
}
Ok(Self(s.to_string()))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we need a similar check also in the init and post_upgrade of the ledger. Currently, no check on the metadata key is performed there, since the MetadataKey instance is created by deserializing the Candid init/upgrade arguments, This means that if a user e.g., constructs the init/upgrade arguments manually on the command line when installing/upgrading a canister using dfx, then they may still end up setting some metadata where the key doesn't follow the <namespace>:<key> format. In an integration test using StateMachine, you can get the same behavior by creating the metadata key to be included in the init/upgrade args using something like the following:

  let invalid_string = "invalid_key_without_colon";
  let encoded_bytes = Encode!(&invalid_string).unwrap();
  let invalid_metadata_key: MetadataKey = Decode!(&encoded_bytes, MetadataKey).unwrap();

I'm not sure if we'd want to make existing ledgers un-upgradable or forcefully discard currently set metadata that doesn't conform to the <namespace>:<key> format. The fact that you cannot add new metadata, but rather only set all metadata, in an upgrade, presents a bit of a problem. If someone has set some metadata with a key without a namespace, and later wants to add/remove/change any (other) metadata, they need to specify all the metadata fields, including ones that won't be changing. Therefore, if we add a check forbidding invalid metadata keys, ledger maintainers would need to choose between keeping the metadata with the invalid keys, or not changing the other ones. One option could be to only apply a strict check in case the existing metadata has no invalid keys. WDYT?

Comment on lines 22 to 33
/// Create a `(MetadataKey, MetadataValue)` tuple for use in metadata maps.
///
/// The key must be a valid metadata key in the format `<namespace>:<key>`.
/// This is typically used with the predefined constants like `MetadataKey::ICRC1_NAME`.
///
/// # Panics
///
/// Panics if the key is not a valid metadata key format.
pub fn entry(key: &str, val: impl Into<MetadataValue>) -> (MetadataKey, Self) {
let metadata_key =
MetadataKey::parse(key).unwrap_or_else(|e| panic!("invalid metadata key '{key}': {e}"));
(metadata_key, val.into())
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if panicking on bad input here is the best option. I think this is currently only used in tests in our code, but if someone were to e.g., use this in a canister or some other production code for setting metadata in another canister, returning a Result may be more manageable?

Comment on lines 149 to 163
/// Returns the namespace part of the key.
pub fn namespace(&self) -> &str {
self.0
.find(':')
.map(|pos| &self.0[..pos])
.expect("BUG: MetadataKey should always contain a colon")
}

/// Returns the key part (after the namespace).
pub fn key(&self) -> &str {
self.0
.find(':')
.map(|pos| &self.0[pos + 1..])
.expect("BUG: MetadataKey should always contain a colon")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

As mentioned in the comment to parse above, we may have deployments where ledgers have metadata keys that do not conform to the <namespace>:<key> format. These namespace() and key() methods aren't currently used in any of our production code, so for the moment we should be fine, but in other production scenarios panicking here may be sub-optimal. I'm not sure what the best alternative would be though - should we for a MetadataKey without a colon return "", None, or an Error for the namespace?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants