Skip to main content

Pre-Authorize Multiple Transactions with a Single Signature

Account Abstraction Beta

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 authorize a set of transactions upfront with a single signature, so that each transaction in the set can be submitted independently without requiring a new signature.

Recommended reading

This guide builds on the Authenticate an Account with a Public Key how-to. Reviewing that one first will help you understand the public key verification used here.

Why is this possible?

The authenticator receives ctx.digest() from TxContext, which is the unique digest of the current transaction. This allows the on-chain authenticator to verify that the transaction being submitted was part of the pre-approved set, without requiring another signature.

Relation to the OneSig protocol

This example is inspired by the OneSig protocol, which proposes authorizing multiple transactions using a single signature over a Merkle root.

The implementation shown here demonstrates the core concept of verifying a transaction against a signed Merkle root. However, it is simplified for educational purposes and does not fully implement all details of the OneSig protocol.

Example Code

  1. Define the authenticator. It runs two checks in sequence: first verifies the signature over the Merkle root, then verifies that the current transaction's digest is a leaf in that tree.
public fun onesig_authenticator(
account: &OneSigAccount,
merkle_root: vector<u8>,
merkle_proof: vector<vector<u8>>,
signature: vector<u8>,
_: &AuthContext,
ctx: &TxContext,
) {
verify_merkle_root(account, &merkle_root, &signature);

verify_merkle_proof(&merkle_root, &merkle_proof, ctx);
}
  1. The two verification helpers called by the authenticator. verify_merkle_root checks the Ed25519 signature against the stored public key. verify_merkle_proof hashes the transaction digest and walks up the tree using the provided sibling hashes to reconstruct the root.
/// Verify the Merkle root against the provided signature.
/// Ed25519 is used for simplicity. It can be extended to include a set of public keys to verify the signature.
fun verify_merkle_root(self: &OneSigAccount, root: &vector<u8>, signature: &vector<u8>) {
assert!(
ed25519::ed25519_verify(signature, self.public_key(), root),
EEd25519VerificationFailed,
);
}

/// Verify the Merkle proof for the transaction digest.
fun verify_merkle_proof(
merkle_root: &vector<u8>,
merkle_proof: &vector<vector<u8>>,
ctx: &TxContext,
) {
let leaf_raw = ctx.digest();

assert!(
merkle::verify_sorted_keccak_from_leaf_bytes(leaf_raw, merkle_root, merkle_proof),
EInvalidMerkleProof,
);
}
note

The example also creates a simple Account with a public key attached which you can check in the full example code if needed.

Expected Behavior

  • A single signature over the Merkle root authorizes all transactions in the pre-approved set.
  • Each transaction must be submitted with its Merkle proof; transactions not in the tree are rejected.
  • Once the root is signed, any party with the transaction and its proof can submit it — no further interaction with the key is needed.

Full Example Code

// Copyright (c) 2026 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

/// The OneSigAccount module defines an account abstraction that allows executing multiple transactions
/// with a single signature using a Merkle tree structure for transaction authorization. It includes
/// functionality for account creation, authentication, and Merkle proof verification.
///
/// The account is created with a public key and an authenticator function. To authenticate the account,
/// the authenticator verifies the provided signature against the Merkle root, which represents the set of
/// authorized transactions. It also verifies that the transaction digest is part of the authorized set
/// using the Merkle proof.
///
/// The implementation of this module is based on the OneSig protocol (https://github.com/LayerZero-Labs/OneSig)
/// and is designed for demonstration purposes only. It can be extended to support more complex authentication
/// schemes, such as multiple signatures or different types of authenticators.
module onesig::account;

use iota::account;
use iota::authenticator_function::AuthenticatorFunctionRefV1;
use iota::ed25519;
use onesig::merkle;
use public_key_authentication::public_key_authentication;

// === Errors ===

#[error(code = 0)]
const EEd25519VerificationFailed: vector<u8> = b"Ed25519 authenticator verification failed.";
#[error(code = 1)]
const EInvalidMerkleProof: vector<u8> = b"Invalid Merkle proof.";

// === Structs ===

/// This struct represents an account which allows to execute several transactions using a single signature.
public struct OneSigAccount has key {
id: UID,
}

// === OneSigAccount Handling ===

/// Creates a new `OneSigAccount` instance as a shared object with the given public key and authenticator.
public fun create(
public_key: vector<u8>,
authenticator: AuthenticatorFunctionRefV1<OneSigAccount>,
ctx: &mut TxContext,
) {
// Create the OneSig account object.
let mut account = OneSigAccount { id: object::new(ctx) };
let id = &mut account.id;

// Attach public key using the public_key_authentication module.
public_key_authentication::attach_public_key(id, public_key);

// Finalize account creation.
account::create_account_v1(account, authenticator);
}

// === Authenticators ===

/// Authenticates a transaction.
/// The signature is verified against the Merkle root, which represents the set of transactions authorized by the account.
/// The Merkle proof is verified against the transaction digest in the transaction context, ensuring that the transaction is part of the authorized set.
#[authenticator]
public fun onesig_authenticator(
account: &OneSigAccount,
merkle_root: vector<u8>,
merkle_proof: vector<vector<u8>>,
signature: vector<u8>,
_: &AuthContext,
ctx: &TxContext,
) {
verify_merkle_root(account, &merkle_root, &signature);

verify_merkle_proof(&merkle_root, &merkle_proof, ctx);
}

// === View Functions ===

/// Returns the address of the account.
public fun account_address(self: &OneSigAccount): address {
self.id.to_address()
}

/// Helper function to borrow the owner public key from the account.
public fun public_key(account: &OneSigAccount): &vector<u8> {
public_key_authentication::borrow_public_key(&account.id)
}

// === Private Functions ===

/// Verify the Merkle root against the provided signature.
/// Ed25519 is used for simplicity. It can be extended to include a set of public keys to verify the signature.
fun verify_merkle_root(self: &OneSigAccount, root: &vector<u8>, signature: &vector<u8>) {
assert!(
ed25519::ed25519_verify(signature, self.public_key(), root),
EEd25519VerificationFailed,
);
}

/// Verify the Merkle proof for the transaction digest.
fun verify_merkle_proof(
merkle_root: &vector<u8>,
merkle_proof: &vector<vector<u8>>,
ctx: &TxContext,
) {
let leaf_raw = ctx.digest();

assert!(
merkle::verify_sorted_keccak_from_leaf_bytes(leaf_raw, merkle_root, merkle_proof),
EInvalidMerkleProof,
);
}