CIS-8: External Key Registry

Created

May 21, 2026

Draft

May 25, 2026

Supported versions

Smart contract version 1 or newer
(Protocol version 4 or newer)

Standard identifier

CIS-8

Requires

CIS-0

Abstract

A standard interface for registering a public link between a Concordium account and an external public key used on another blockchain. A registration asserts that a Concordium account controls a specified external public key, proven by a cryptographic signature produced with that external key.

The standard is chain-agnostic: it supports external blockchains including Ethereum, Solana, Fetch.ai, and Cosmos-SDK chains using CAIP-style chain namespace identifiers, per-chain-family proof scheme identifiers, and chain-specific public key encodings.

Verifier-anchored assertions for non-cryptographic external identifiers such as platform handles or HTTPS endpoints are out of scope.

The interface defines the following:

  • Data structures for external key identifiers, ownership proofs, and registrations.

  • Entrypoints for registering, updating, revoking, and querying external key links.

  • Logged events for each mutating operation.

  • Standard rejection error codes.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

General types and serialization

Note

Integers are encoded in little-endian unless stated otherwise.

String

A variable-length UTF-8 encoded string.

It is serialized as: 2 bytes for the length (n) of the string in little-endian, followed by n bytes for the UTF-8 encoding of the string:

String ::= (n: Byte2) (value: Byten)

Bytestring

A variable-length byte array.

It is serialized as: 2 bytes for the length (n) in little-endian, followed by n bytes:

Bytestring ::= (n: Byte2) (value: Byten)

AccountAddress

An address of a Concordium account.

It is serialized as 32 bytes:

AccountAddress ::= (address: Byte32)

ContractAddress

An address of a Concordium smart contract instance. It consists of an index and a subindex, both unsigned 64-bit integers.

It is serialized as: 8 bytes for the index (index) followed by 8 bytes for the subindex (subindex), both little-endian:

ContractAddress ::= (index: Byte8) (subindex: Byte8)

BlockHash

A 32-byte block hash identifying a Concordium block. Used in the canonical signed message to carry the genesis block hash and prevent cross-network replay.

It is serialized as 32 bytes:

BlockHash ::= (hash: Byte32)

Timestamp

A point in time given in milliseconds since Unix epoch, represented as an unsigned 64-bit integer.

It is serialized as 8 bytes in little-endian:

Timestamp ::= (milliseconds: Byte8)

ExternalKeyId

An identifier for a public key on an external blockchain, consisting of a CAIP-style chain namespace, a key type identifier, and the raw public key bytes.

The namespace field is a CAIP-style chain identifier (e.g., eip155:1, solana:mainnet, cosmos:fetchhub-4).

The key_type field identifies the public key format and encoding. Standard values defined by this specification are:

  • secp256k1-compressed - 33 bytes; used by Ethereum and Cosmos chains.

  • secp256k1-uncompressed - 65 bytes; alternate Ethereum encoding.

  • ed25519 - 32 bytes; used by Solana and Fetch.ai uagents.

The public_key field is the raw encoded public key bytes. Encoding is determined by key_type.

It is serialized as a String (namespace), a String (key_type), and a Bytestring (public_key):

ExternalKeyId ::= (namespace: String) (key_type: String) (public_key: Bytestring)

Proof

A cryptographic ownership proof, produced by signing the canonical signed message with the external key being registered.

The scheme field is a proof scheme identifier (e.g., ethereum-personal-sign, solana-ed25519, cosmos-secp256k1, fetch-ai-ed25519). The signature field contains the signature bytes as produced by that proof scheme.

It is serialized as a String (scheme) followed by a Bytestring (signature):

Proof ::= (scheme: String) (signature: Bytestring)

MetadataEntry

An application-defined key-value string pair associated with a registration.

It is serialized as a String (key) followed by a String (value):

MetadataEntry ::= (key: String) (value: String)

RegistrationStatus

The lifecycle status of a registration.

It is serialized as 1 byte with value 0 for Active and 1 for Revoked:

RegistrationStatus ::= (0: Byte) // Active
                     | (1: Byte) // Revoked

Registration

A complete record of an external key registration, including the linked Concordium account, the external key, the proof scheme used at registration time, application-defined metadata, the current status, and the time of the last status change.

It is serialized as an AccountAddress (owner), an ExternalKeyId (external_key), a String (proof_scheme), 2 bytes for the number of metadata entries (m), followed by m MetadataEntry records (metadata), a RegistrationStatus (status), and a Timestamp (last_updated):

Registration ::= (owner: AccountAddress) (external_key: ExternalKeyId) (proof_scheme: String)
                 (m: Byte2) (metadata: MetadataEntrym) (status: RegistrationStatus)
                 (last_updated: Timestamp)

Canonical signed message

The canonical signed message is the message that MUST be signed with the external private key as the ownership proof in a registerExternalKey call.

The message MUST NOT include a nonce or expiry timestamp. Replay protection is provided by the Concordium transaction itself, which is bound to the sender account, a sequential nonce, and an expiry time at the protocol level. Cross-contract and cross-network replay is prevented by the inclusion of the contract_address and concordium_genesis_hash fields.

The message bytes MUST be prefixed with the 18-byte ASCII domain separation tag CIS-8/v1/canonical.

It is serialized as the 18 raw ASCII bytes of the domain separation tag, followed by an AccountAddress (concordium_account), a ContractAddress (contract_address), a BlockHash (concordium_genesis_hash), a String (external_namespace), an ExternalKeyId (external_key), and a String (proof_scheme):

CanonicalSignedMessage ::= ("CIS-8/v1/canonical": Byte18)
                            (concordium_account: AccountAddress)
                            (contract_address: ContractAddress)
                            (concordium_genesis_hash: BlockHash)
                            (external_namespace: String)
                            (external_key: ExternalKeyId)
                            (proof_scheme: String)

Proof verification

This section specifies the proof schemes that a conformant CIS-8 contract SHOULD support. A contract MUST reject with UnsupportedProofScheme for any scheme identifier it does not implement. A contract MAY support additional proof schemes beyond those listed here.

ethereum-personal-sign

  • Algorithm: ECDSA over secp256k1.

  • Hash: Keccak-256.

  • Public key encoding: secp256k1 compressed (33 bytes) or uncompressed (65 bytes).

  • Message construction: Let m be the canonical signed message bytes. Construct "\x19Ethereum Signed Message:\n" + ascii(len(m)) + m and hash with Keccak-256.

  • Signature: 65 bytes (r || s || v). Verify by public-key recovery and comparison against the supplied public_key.

solana-ed25519

  • Algorithm: Ed25519.

  • Public key encoding: 32 bytes.

  • Signature: 64 bytes (R || S).

  • Message construction: Sign the canonical signed message bytes directly (the CIS-8/v1/canonical domain prefix is included as part of the message).

cosmos-secp256k1

  • Algorithm: ECDSA over secp256k1.

  • Hash: SHA-256.

  • Public key encoding: secp256k1 compressed (33 bytes).

  • Message construction: Wrap the canonical signed message bytes in an ADR-036 off-chain envelope, then hash with SHA-256.

  • Signature: Standard ECDSA (r || s). Verify against the supplied compressed public key.

fetch-ai-ed25519

  • Algorithm: Ed25519.

  • Public key encoding: 32 bytes (Fetch.ai uagent ed25519 public key).

  • Signature: 64 bytes (R || S).

  • Message construction: Identical to solana-ed25519.

Logged events

The events defined by this specification are serialized using one byte to discriminate the different events. A custom event SHOULD NOT have a first byte colliding with any of the events defined by this specification.

If account B replaces account A as the active owner of an external key, the contract MUST emit ExternalKeyRevoked for account A before emitting ExternalKeyRegistered for account B.

ExternalKeyRegistered

An ExternalKeyRegistered event MUST be logged whenever an external key is successfully registered or re-registered.

It is serialized as: first a byte with the value of 231, followed by the AccountAddress (owner) and the ExternalKeyId (external_key):

ExternalKeyRegistered ::= (231: Byte) (owner: AccountAddress) (external_key: ExternalKeyId)

ExternalKeyRevoked

An ExternalKeyRevoked event MUST be logged whenever an active registration is revoked, whether by an explicit revoke call or by displacement through a new registerExternalKey call.

It is serialized as: first a byte with the value of 232, followed by the AccountAddress (owner) and the ExternalKeyId (external_key):

ExternalKeyRevoked ::= (232: Byte) (owner: AccountAddress) (external_key: ExternalKeyId)

UpdateMetadata

An UpdateMetadata event MUST be logged whenever the metadata of an active registration is successfully updated.

It is serialized as: first a byte with the value of 233, followed by the AccountAddress (owner), the ExternalKeyId (external_key), 2 bytes for the number of metadata entries (m), and then m MetadataEntry records (metadata):

UpdateMetadata ::= (233: Byte) (owner: AccountAddress) (external_key: ExternalKeyId)
                   (m: Byte2) (metadata: MetadataEntrym)

Contract functions

A smart contract implementing this standard MUST export the following functions:

supports

As specified in CIS-0.

registerExternalKey

Register or replace the active owner of an external key with a cryptographic ownership proof. The proof MUST be signed with the external private key over the canonical signed message constructed from the transaction context and the supplied parameters.

If an active registration already exists for the supplied external key under a different owner, the previous registration is displaced: the contract MUST emit ExternalKeyRevoked for the displaced owner before emitting ExternalKeyRegistered for the new owner.

Parameter

The parameter consists of an ExternalKeyId (external_key), a Proof (proof), 2 bytes for the number of metadata entries (m), and then m MetadataEntry records (metadata):

RegisterExternalKeyParam ::= (external_key: ExternalKeyId) (proof: Proof) (m: Byte²) (metadata: MetadataEntryᵐ)
Requirements
  • The proof.scheme MUST be supported by the contract; otherwise the contract MUST reject with UnsupportedProofScheme.

  • The external_key.key_type MUST be supported by the contract; otherwise the contract MUST reject with UnsupportedKeyType.

  • The external_key MUST be well-formed for its declared key_type; otherwise the contract MUST reject with MalformedExternalKey.

  • The proof MUST verify against the canonical signed message reconstructed from the transaction context and the supplied parameters; otherwise the contract MUST reject with InvalidProof.

  • If the caller is already the active owner of the supplied external key the contract MUST reject with AlreadyRegistered.

  • If metadata violates implementation-defined limits the contract MUST reject with InvalidMetadata.

  • On success the contract MUST emit ExternalKeyRegistered.

updateMetadata

Update the metadata of an existing active registration. The caller MUST be the current active owner of the registration.

Parameter

The parameter consists of an ExternalKeyId (external_key), 2 bytes for the number of metadata entries (m), and then m MetadataEntry records (metadata):

UpdateMetadataParam ::= (external_key: ExternalKeyId) (m: Byte2) (metadata: MetadataEntrym)
Requirements
  • The contract MUST reject with NotRegistered if no active registration exists for the supplied external key.

  • The contract MUST reject with Unauthorized if the caller is not the active owner of the registration.

  • If metadata violates implementation-defined limits the contract MUST reject with InvalidMetadata.

  • On success the contract MUST emit UpdateMetadata.

revoke

Revoke an active registration. The caller MUST be the current active owner of the registration.

Parameter

The parameter consists of the ExternalKeyId (external_key) to revoke:

RevokeParam ::= (external_key: ExternalKeyId)
Requirements
  • The contract MUST reject with NotRegistered if no active registration exists for the supplied external key.

  • The contract MUST reject with Unauthorized if the caller is not the active owner.

  • On success the contract MUST emit ExternalKeyRevoked.

ownerOfKey

Query the active registration for a given external key identifier.

Parameter

The parameter consists of the ExternalKeyId (external_key) to query:

OwnerOfKeyParam ::= (external_key: ExternalKeyId)
Response

The response is 1 byte indicating whether an active registration was found. If the value is 0, no active registration exists and no further bytes follow. If the value is 1, the full Registration (registration) follows:

OwnerOfKeyResponse ::= (0: Byte)                                 // not found
                     | (1: Byte) (registration: Registration)    // found

Rejection errors

A smart contract following this specification MUST use the following error codes to reject under the described conditions:

Name

Error code

Description

InvalidProof

-7100

The supplied proof does not verify against the reconstructed canonical signed message.

UnsupportedProofScheme

-7101

The proof scheme identifier is not supported by this contract.

MalformedExternalKey

-7102

The external key identifier is malformed or its encoding does not match the declared key_type.

Unauthorized

-7103

The sender is not the active owner of the registration.

AlreadyRegistered

-7104

The sender is already the active owner of the supplied external key.

NotRegistered

-7105

No active registration exists for the supplied identifier.

AmbiguousIdentifier

-7106

The supplied identifier resolves ambiguously.

UnsupportedKeyType

-7107

The supplied key_type is not supported by this contract.

InvalidMetadata

-7108

The metadata violates implementation-defined limits or encoding requirements.

Rejecting using an error code from the table above MUST only occur in a situation as described in the corresponding error description.

The smart contract implementing this specification MAY introduce custom error codes other than the ones specified in the table above.