Authenticate an Account with a Public Key
Account Abstraction is currently in beta and therefore only available on Devnet.
If you test it out, please share your feedback with us in the IOTA Builders Discord.
This how-to demonstrates how to authenticate an account using public key verification. The supported schemes are Ed25519, Secp256k1, and Secp256r1. If you want to learn more about the concepts of Account Abstraction, check out the Account Abstraction Introduction.
IOTA natively supports Ed25519, Secp256k1, and Secp256r1 signatures. Account Abstraction gives you two additional capabilities:
- Key rotation without address change — the account address is a stable on-chain object ID, independent of the signing key. You can rotate to a new key at any time without changing the account address.
- Custom signature schemes — because the authenticator is just a Move function, you are not limited to the natively supported schemes. You can implement any verification logic, including post-quantum algorithms or any other scheme expressible in Move.
This guide builds on the Create an Account Using the Builder Pattern how-to. Reviewing that one first will help you understand the account creation used here.
Example Code
- Define authenticator functions for the desired cryptographic scheme. Each verifies the transaction signature against the public key stored on the account.
#[authenticator]
public fun ed25519_authenticator(
account: &IOTAccount,
signature: vector<u8>,
_: &AuthContext,
ctx: &TxContext,
) {
public_key_authentication::authenticate_ed25519(account.borrow_uid(), signature, ctx);
}
/// Secp256k1 signature authenticator for `IOTAccount`.
#[authenticator]
public fun secp256k1_authenticator(
account: &IOTAccount,
signature: vector<u8>,
_: &AuthContext,
ctx: &TxContext,
) {
public_key_authentication::authenticate_secp256k1(account.borrow_uid(), signature, ctx);
}
/// Secp256r1 signature authenticator for `IOTAccount`.
#[authenticator]
public fun secp256r1_authenticator(
account: &IOTAccount,
signature: vector<u8>,
_: &AuthContext,
ctx: &TxContext,
) {
public_key_authentication::authenticate_secp256r1(account.borrow_uid(), signature, ctx);
}
- The authenticators rely on standard IOTA Move cryptographic functions. For example, the Ed25519 helper verifies the signature against the stored public key and the transaction digest.
/// Ed25519 signature authenticator helper.
public fun authenticate_ed25519(account_id: &UID, signature: vector<u8>, ctx: &TxContext) {
assert!(has_public_key(account_id), EPublicKeyMissing);
assert!(
ed25519::ed25519_verify(&signature, borrow_public_key(account_id), ctx.digest()),
EEd25519VerificationFailed,
);
}
- Create the account with a public key attached and the authenticator function reference set.
public fun create(
public_key: vector<u8>,
authenticator: AuthenticatorFunctionRefV1<IOTAccount>,
ctx: &mut TxContext,
) {
iotaccount::builder(authenticator, ctx).with_field(public_key_field_name(), public_key).build();
}
Additional helper functions for rotating or adding a public key to an existing account are available in the full example. These are standard dynamic field mutations — see Manage Account Dynamic Fields for the underlying pattern.
Expected Behavior
- The account validates each transaction by verifying the provided signature against the stored public key using the configured scheme.
- Transactions with an invalid or missing signature are rejected during authentication.
Full Example Code
- Public key authentication helpers
- IOTAccount public key authentication
// Copyright (c) 2025 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
/// This module defines a set of authenticators helpers that use public key cryptography to implement
/// authenticators.
///
/// It allows to set a public key as field of an account, such that any authenticator can authenticate
/// the account by verifying a signature against the public key.
/// The public key schemes implemented in this module are:
/// - ed25519
/// - secp256k1
/// - secp256r1
module public_key_authentication::public_key_authentication;
use iota::dynamic_field as df;
use iota::ecdsa_k1;
use iota::ecdsa_r1;
use iota::ed25519;
// === Errors ===
#[error(code = 0)]
const EPublicKeyAlreadyAttached: vector<u8> = b"Public key already attached.";
#[error(code = 1)]
const EPublicKeyMissing: vector<u8> = b"Public key missing.";
#[error(code = 2)]
const EEd25519VerificationFailed: vector<u8> = b"Ed25519 authenticator verification failed.";
#[error(code = 3)]
const ESecp256k1VerificationFailed: vector<u8> = b"Secp256k1 authenticator verification failed.";
#[error(code = 4)]
const ESecp256r1VerificationFailed: vector<u8> = b"Secp256r1 authenticator verification failed.";
// === Constants ===
// === Structs ===
/// A dynamic field name for the account owner public key.
public struct PublicKeyFieldName has copy, drop, store {}
// === Public Functions ===
/// Attach public key data to the account with the provided `public_key`.
public fun attach_public_key(account_id: &mut UID, public_key: vector<u8>) {
assert!(!has_public_key(account_id), EPublicKeyAlreadyAttached);
df::add(account_id, PublicKeyFieldName {}, public_key)
}
/// Detach public key data from the account.
public fun detach_public_key(account_id: &mut UID): vector<u8> {
assert!(has_public_key(account_id), EPublicKeyMissing);
df::remove(account_id, PublicKeyFieldName {})
}
/// Update the public key attached to the account.
public fun rotate_public_key(account_id: &mut UID, public_key: vector<u8>): vector<u8> {
assert!(has_public_key(account_id), EPublicKeyMissing);
let prev_public_key = df::remove(account_id, PublicKeyFieldName {});
df::add(account_id, PublicKeyFieldName {}, public_key);
prev_public_key
}
// === Public Authenticators Helpers ===
/// Ed25519 signature authenticator helper.
public fun authenticate_ed25519(account_id: &UID, signature: vector<u8>, ctx: &TxContext) {
assert!(has_public_key(account_id), EPublicKeyMissing);
assert!(
ed25519::ed25519_verify(&signature, borrow_public_key(account_id), ctx.digest()),
EEd25519VerificationFailed,
);
}
/// Secp256k1 signature authenticator helper.
public fun authenticate_secp256k1(account_id: &UID, signature: vector<u8>, ctx: &TxContext) {
assert!(has_public_key(account_id), EPublicKeyMissing);
assert!(
ecdsa_k1::secp256k1_verify(&signature, borrow_public_key(account_id), ctx.digest(), 0),
ESecp256k1VerificationFailed,
);
}
/// Secp256r1 signature authenticator helper.
public fun authenticate_secp256r1(account_id: &UID, signature: vector<u8>, ctx: &TxContext) {
assert!(has_public_key(account_id), EPublicKeyMissing);
assert!(
ecdsa_r1::secp256r1_verify(&signature, borrow_public_key(account_id), ctx.digest(), 0),
ESecp256r1VerificationFailed,
);
}
// === View Functions ===
/// An utility function to check if the account has a public key set.
public fun has_public_key(account_id: &UID): bool {
df::exists_(account_id, PublicKeyFieldName {})
}
/// An utility function to borrow the account-related public key.
public fun borrow_public_key(account_id: &UID): &vector<u8> {
df::borrow(account_id, PublicKeyFieldName {})
}
// === Admin Functions ===
// === Package Functions ===
/// An utility function to construct the dynamic field name for the public key field.
public(package) fun public_key_field_name(): PublicKeyFieldName {
PublicKeyFieldName {}
}
// === Private Functions ===
// === Test Functions ===
// Copyright (c) 2025 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
/// This module defines a set of authenticators for `IOTAccount` that use public key cryptography to verify
/// the authenticity of the transaction sender.
///
/// It allows to set a public key as field of the account, such that the authenticators of this module can
/// authenticate the account by verifying a signature.
/// The public key schemes implemented in this module are:
/// - ed25519 -> `ed25519_authenticator`
/// - secp256k1 -> `secp256k1_authenticator`
/// - secp256r1 -> `secp256r1_authenticator`
module public_key_authentication::public_key_iotaccount;
use iota::authenticator_function::AuthenticatorFunctionRefV1;
use iotaccount::iotaccount::{Self, IOTAccount, IOTAccountBuilder};
use public_key_authentication::public_key_authentication::{Self, public_key_field_name};
// === Errors ===
// === Constants ===
// === Structs ===
// === Account Helpers ===
/// Creates a new `IOTAccount` as a shared object with the given authenticator.
///
/// It sets a public key as field of the account and the given authenticator, which will be used to
/// authenticate the account.
public fun create(
public_key: vector<u8>,
authenticator: AuthenticatorFunctionRefV1<IOTAccount>,
ctx: &mut TxContext,
) {
iotaccount::builder(authenticator, ctx).with_field(public_key_field_name(), public_key).build();
}
/// Creates a new `IOTAccount` as a shared object with the given authenticator.
///
/// It sets a public key as field of the account, the given authenticator which will be used to
/// authenticate the account and the admin address.
public fun create_with_admin(
public_key: vector<u8>,
admin: address,
authenticator: AuthenticatorFunctionRefV1<IOTAccount>,
ctx: &mut TxContext,
) {
iotaccount::builder(authenticator, ctx)
.with_field(public_key_field_name(), public_key)
.with_admin(admin)
.build();
}
/// Attach a PublicKey as a dynamic field to the account being built.
public fun with_public_key(self: IOTAccountBuilder, public_key: vector<u8>): IOTAccountBuilder {
self.with_field(public_key_field_name(), public_key)
}
/// Rotates the account owner public key to a new one as well as the authenticator.
/// Once this function is called, the previous public key and authenticator are no longer valid.
/// Only the account itself can call this function.
public fun rotate_public_key(
account: &mut IOTAccount,
public_key: vector<u8>,
authenticator: AuthenticatorFunctionRefV1<IOTAccount>,
ctx: &TxContext,
) {
// Update the account owner public key dynamic field. It is expected that the field already exists.
account.rotate_field(public_key_field_name(), public_key, ctx);
// Update the account authenticator dynamic field. It is expected that the field already exists.
account.rotate_auth_function_ref_v1(authenticator, ctx);
}
/// Attach a public key to the account with the provided `public_key`.
/// It fails if the account already has a public key attached.
/// Only the account itself can call this function.
public fun add_public_key(
account: &mut IOTAccount,
public_key: vector<u8>,
authenticator: AuthenticatorFunctionRefV1<IOTAccount>,
ctx: &TxContext,
) {
// Update the account owner public key dynamic field. It is expected that the field does not exist.
account.add_field(public_key_field_name(), public_key, ctx);
// Update the account authenticator dynamic field. It is expected that the field already exists.
account.rotate_auth_function_ref_v1(authenticator, ctx);
}
// === Authenticators ===
/// Ed25519 signature authenticator for `IOTAccount`.
#[authenticator]
public fun ed25519_authenticator(
account: &IOTAccount,
signature: vector<u8>,
_: &AuthContext,
ctx: &TxContext,
) {
public_key_authentication::authenticate_ed25519(account.borrow_uid(), signature, ctx);
}
/// Secp256k1 signature authenticator for `IOTAccount`.
#[authenticator]
public fun secp256k1_authenticator(
account: &IOTAccount,
signature: vector<u8>,
_: &AuthContext,
ctx: &TxContext,
) {
public_key_authentication::authenticate_secp256k1(account.borrow_uid(), signature, ctx);
}
/// Secp256r1 signature authenticator for `IOTAccount`.
#[authenticator]
public fun secp256r1_authenticator(
account: &IOTAccount,
signature: vector<u8>,
_: &AuthContext,
ctx: &TxContext,
) {
public_key_authentication::authenticate_secp256r1(account.borrow_uid(), signature, ctx);
}
// === View Functions ===
/// An utility function to check if the account has a public key set.
public fun has_public_key(account: &IOTAccount): bool {
account.has_field(public_key_field_name())
}
/// An utility function to borrow the account-related public key.
public fun borrow_public_key(account: &IOTAccount): &vector<u8> {
account.borrow_field(public_key_field_name())
}
// === Admin Functions ===
// === Package Functions ===
// === Private Functions ===
// === Test Functions ===