From a7823e8960953a23a98985b30209a04a8c31e609 Mon Sep 17 00:00:00 2001 From: georgedigkas Date: Wed, 30 Jul 2025 17:20:54 +0300 Subject: [PATCH] Enhance README and module documentation for multisig address functionality. --- README.md | 438 +++++++++++++++++++++++++---- move/Move.toml | 32 ++- move/sources/multisig.move | 494 ++++++++++++++++++++++----------- move/tests/multisig_tests.move | 47 ++-- 4 files changed, 770 insertions(+), 241 deletions(-) diff --git a/README.md b/README.md index f8a42af..05640ca 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,404 @@ +# 🔐 Sui Multi-Signature Address Generator & Validator -# Multi-Sig Sui Address Creator / Checker Module +A comprehensive Move module for creating, verifying, and working with multi-signature addresses on the Sui blockchain. This module enables secure collaborative asset management through deterministic address generation and sophisticated threshold-based authorization schemes. -Multi-signature (multi-sig) wallets and accounts allow for enhanced key management by enabling either multiple parties to access shared assets under predefined conditions, or a single user to implement additional security measures. For example, a multi-sig wallet could be used to manage a decentralized autonomous organization’s (DAO) treasury, requiring consent from a certain percentage of members before executing transactions, or it could serve an individual seeking extra protection by distributing access across multiple devices or locations. +## 🌟 Overview -Sui native multi-sig wallets offer a plethora of applications. They can be used to create interactive game elements or commerce platforms that necessitate collective user actions for access. Establishing a user quorum or other conditions for access ensures that digital assets remain protected against unauthorized use by any individual key/member. +Multi-signature (multisig) addresses are cryptographic constructs that require multiple cryptographic signatures to authorize transactions, enabling: -The Sui Move multi-sig smart contract detailed in this article confirms whether a Sui address is multi-sig and accommodates various key combinations, such as 2-of-3 or any M-of-N. Integrating multisig functionality directly into smart contracts, rather than through SDKs, provides distinct benefits. This method grants developers precise control over access and authorization within the contract’s logic, allowing them to stipulate specific conditions—like signatures from a designated subset of addresses—before permitting function execution. Such detailed control bolsters security by guarding against unsanctioned changes to the contract or asset transfers. +- **Enhanced Security**: Distributed key management reduces single points of failure +- **Collaborative Control**: Multiple parties can jointly manage shared assets +- **Flexible Governance**: Customizable voting weights and thresholds +- **Transparent Operations**: On-chain verification of multisig configurations -Incorporating multisig directly into smart contracts is pivotal for creating secure and resilient applications with adaptable governance models. This fosters a foundation of trust and cooperation within decentralized networks. +### Key Benefits -On-chain multisig is crucial because it offers transparency and verifiability in decentralized operations. It ensures that actions, such as executing smart contract functions, are only performed when they meet the agreed-upon criteria among stakeholders. For example, knowing if the caller of a smart contract function is a multi-sig address allows for the implementation of nuanced constraints. If a transaction is signed by 3 out of 5 members, up to 1,000 coins could be moved. Conversely, if only 1 out of 5 members signs, the transaction limit could be set to 100 coins. This flexibility in setting transaction thresholds based on the level of consensus provides a balance between security and functionality, making on-chain multisig an indispensable feature for collective asset management and decision-making in the blockchain space. +- **🛡️ Security**: Protection against unauthorized access and key compromise +- **🤝 Collaboration**: Enable DAO treasuries, joint ventures, and shared accounts +- **⚖️ Flexibility**: Support for various M-of-N configurations with weighted voting +- **🔍 Transparency**: On-chain verification and event emission for auditability -## Module Description -This Move language module, `multisig::multisig` provides a set of functions to generate and verify that a Sui blockchain account is a multi-signature address. The main function, `derive_multisig_address`, takes a set of public keys (pks), corresponding weights, and a threshold to generate a Sui blockchain address. This module is useful for scenarios where identifying the type of sender in Sui transactions is critical, such as verifying multi-signature schemes and their thresholds. +## 🏗️ Architecture -## Features -- **`derive_multisig_address`:** Generates a Sui blockchain address from multi-signature public keys. -- **`ed25519_key_to_address`, `secp256k1_key_to_address`, `secp256r1_key_to_address`:** Functions to create addresses from different types of public keys. -- **`test_address`:** A test function to validate the module in a transaction context. +The module implements a deterministic address derivation algorithm using: -## Usage -### Creating a Multi-Sig Address -1. **Prepare Public Keys and Weights:** - Format your public keys and weights as vectors. Ensure they are of equal length. +1. **BLAKE2b-256 Hashing**: Cryptographically secure address generation +2. **BCS Serialization**: Canonical encoding of threshold parameters +3. **Multi-Scheme Support**: Ed25519, secp256k1, and secp256r1 compatibility +4. **Weight-Based Voting**: Sophisticated authorization beyond simple M-of-N - Example: - ```move - let public_keys: vector> = ...; - let weights: vector = ...; +### Address Derivation Formula + +``` +multisig_address = BLAKE2b-256( + 0x03 || // Multisig flag + BCS(threshold) || // Serialized threshold (u16) + pk₁ || weight₁ || // First signer's key and weight + pk₂ || weight₂ || // Second signer's key and weight + ... // Additional signers + pkₙ || weightₙ +) +``` + +## 🔧 Installation & Setup + +### Prerequisites + +- [Sui CLI](https://docs.sui.io/build/install) installed and configured +- Basic understanding of Move programming language + +### Building the Module + +1. **Clone and navigate to the project:** + + ```bash + git clone + cd multisig-move/move + ``` + +2. **Build the module:** + + ```bash + sui move build + ``` + +3. **Run tests:** + + ```bash + sui move test ``` -2. **Set the Threshold:** - Define the threshold value necessary for the multi-signature. -3. **Call the Function:** - ```move - let multisig_address = multisig::multisig::derive_multisig_address(public_keys, weights, threshold); +4. **Publish to network:** + ```bash + sui client publish --gas-budget 20000000 ``` -## Example +## 📚 API Reference + +### Core Functions + +#### `derive_multisig_address(pks, weights, threshold) -> address` + +Creates a multisig address and emits an event for transparency. + +**Parameters:** + +- `pks: vector>` - Public keys (including signature scheme flags) +- `weights: vector` - Weight for each corresponding public key +- `threshold: u16` - Minimum weight required for authorization + +**Returns:** The derived multisig address + +**Events:** Emits `MultisigAddressEvent` + +#### `derive_multisig_address_quiet(pks, weights, threshold) -> address` + +Creates a multisig address without emitting events (gas-efficient). + +#### `check_multisig_address_eq(pks, weights, threshold, expected) -> bool` + +Validates whether given parameters produce the expected multisig address. + +#### `check_if_sender_is_multisig_address(pks, weights, threshold, ctx) -> bool` + +Verifies if the transaction sender matches the specified multisig configuration. + +### Key Conversion Functions + +#### `ed25519_key_to_address(pk) -> address` + +Converts Ed25519 public key to Sui address. + +- **Format:** `[0x00][32-byte Ed25519 key]` (33 bytes total) + +#### `secp256k1_key_to_address(pk) -> address` + +Converts secp256k1 public key to Sui address. + +- **Format:** `[0x01][33-byte compressed secp256k1 key]` (34 bytes total) + +#### `secp256r1_key_to_address(pk) -> address` + +Converts secp256r1 public key to Sui address. + +- **Format:** `[0x02][33-byte compressed secp256r1 key]` (34 bytes total) + +## 🎯 Usage Examples + +### Basic 2-of-3 Multisig + +```move +module example::basic_multisig { + use multisig::multisig; + + public fun create_2_of_3_multisig(): address { + // Three public keys with equal weights + let pks = vector[ + vector[0x00, /* 32 bytes Ed25519 key */], + vector[0x01, /* 33 bytes secp256k1 key */], + vector[0x02, /* 33 bytes secp256r1 key */] + ]; + let weights = vector[1, 1, 1]; // Equal voting power + let threshold = 2; // Require any 2 signatures + + multisig::derive_multisig_address(pks, weights, threshold) + } +} +``` + +### Weighted Multisig (Corporate Treasury) + ```move -#[test_only] -module multisig::multisig_unit_tests { - use multisig::multisig::{Self as ms}; - - #[test] - fun test_derive_multisig_address() { - // ED25519 - let address_ED25519: address = @0x73a6b3c33e2d63383de5c6786cbaca231ff789f4c853af6d54cb883d8780adc0; - let key_ED25519: vector = vector[0, 13, 125, 171, 53, 140, 141, 173, 170, 78, 250, 0, 73, 167, 91, 7, 67, 101, 85, 177, 10, 54, 130, 25, 187, 104, 15, 112, 87, 19, 73, 215, 117]; - assert!(ms::ed25519_key_to_address(&key_ED25519) == address_ED25519, 0); - - // Secp256k1 - let address_Secp256k1: address = @0xd9607cd03428c904949572b51471e7a9f60019aeb9a3d7ee5e72921cab8e8be7; - let key_Secp256k1: vector = vector[1, 2, 14, 23, 205, 89, 57, 228, 107, 25, 102, 65, 150, 140, 215, 89, 145, 11, 162, 87, 126, 39, 250, 115, 253, 227, 135, 109, 185, 190, 197, 188, 235, 43]; - assert!(ms::secp256k1_key_to_address(&key_Secp256k1) == address_Secp256k1, 0); - - // Secp256r1 - let address_Secp256r1: address = @0x600b1081644fe46f76da3bdc19f8743b9f04458516364374c7d82959e790c19e; - let key_Secp256r1: vector = vector[2, 3, 71, 251, 175, 35, 240, 56, 171, 196, 195, 8, 162, 113, 17, 122, 42, 76, 255, 174, 221, 188, 95, 248, 28, 117, 23, 188, 108, 116, 167, 237, 180, 48]; - assert!(ms::secp256r1_key_to_address(&key_Secp256r1) == address_Secp256r1, 0); - - let pks: vector> = vector[key_ED25519, key_Secp256k1, key_Secp256r1]; - let weights: vector = vector[1, 1, 1]; - let threshold: u16 = 2; - - let expected_multisig_address: address = @0x1c4dac7fb4c01a0c608db993711c451ad655a38b7f0a9571ff099f70090263a8; - let derived_multisig_address: address = ms::derive_multisig_address(pks, weights, threshold); - assert!(derived_multisig_address == expected_multisig_address, 0); +module example::corporate_treasury { + use multisig::multisig; + + public fun create_corporate_multisig(): address { + let pks = vector[ + ceo_key, // CEO + cfo_key, // CFO + board1_key, // Board member 1 + board2_key, // Board member 2 + board3_key // Board member 3 + ]; + let weights = vector[3, 3, 1, 1, 1]; // Executives have more weight + let threshold = 5; // Require 5 weight units + + // Possible authorization combinations: + // - CEO + CFO (3 + 3 = 6 > 5) ✓ + // - CEO + any 2 board members (3 + 1 + 1 = 5) ✓ + // - CFO + any 2 board members (3 + 1 + 1 = 5) ✓ + // - All 3 board members alone (1 + 1 + 1 = 3 < 5) ✗ + + multisig::derive_multisig_address(pks, weights, threshold) } } ``` -## License +### Smart Contract Access Control + +```move +module example::secure_vault { + use sui::tx_context::TxContext; + use multisig::multisig; + + // Error codes + const EUNAUTHORIZED: u64 = 0; + + public fun withdraw_funds( + amount: u64, + admin_pks: vector>, + admin_weights: vector, + ctx: &mut TxContext + ) { + // Require 3-of-5 admin multisig for withdrawals + assert!( + multisig::check_if_sender_is_multisig_address( + admin_pks, + admin_weights, + 3, + ctx + ), + EUNAUTHORIZED + ); + + // Perform withdrawal logic... + } +} +``` + +### Dynamic Authorization Levels + +```move +module example::tiered_access { + use sui::tx_context::TxContext; + use multisig::multisig; + + const EINSUFFICIENT_AUTHORIZATION: u64 = 0; + + public fun execute_transaction( + amount: u64, + pks: vector>, + weights: vector, + ctx: &mut TxContext + ) { + let sender = ctx.sender(); + + if (amount <= 1000) { + // Small amounts: require 1-of-3 + assert!( + multisig::check_multisig_address_eq(pks, weights, 1, sender), + EINSUFFICIENT_AUTHORIZATION + ); + } else if (amount <= 10000) { + // Medium amounts: require 2-of-3 + assert!( + multisig::check_multisig_address_eq(pks, weights, 2, sender), + EINSUFFICIENT_AUTHORIZATION + ); + } else { + // Large amounts: require 3-of-3 + assert!( + multisig::check_multisig_address_eq(pks, weights, 3, sender), + EINSUFFICIENT_AUTHORIZATION + ); + }; + + // Execute transaction... + } +} +``` + +## 🧪 Testing + +The module includes comprehensive tests demonstrating all functionality: + +```bash +# Run all tests +sui move test + +# Run specific test +sui move test --filter test_derive_multisig_address +``` + +### Test Coverage + +- ✅ Ed25519, secp256k1, secp256r1 key conversion +- ✅ Multisig address derivation with mixed key types +- ✅ Weight and threshold validation +- ✅ Event emission verification +- ✅ Error condition handling + +## ⚠️ Error Handling + +The module provides comprehensive error checking: + +| Error Code | Constant | Description | +| ---------- | ------------------------------------------------------ | ------------------------------------------------------ | +| `0` | `ELengthsOfPksAndWeightsAreNotEqual` | Public keys and weights vectors have different lengths | +| `1` | `EThresholdIsPositiveAndNotGreaterThanTheSumOfWeights` | Threshold is 0 or exceeds total weight | +| `2` | `EPublicKeyLength` | Public key length doesn't match expected format | +| `3` | `EPublicKeyFlag` | Public key signature scheme flag is invalid | + +### Common Pitfalls + +1. **Mismatched Vector Lengths**: Ensure `pks` and `weights` have the same length +2. **Invalid Threshold**: Must be `1 ≤ threshold ≤ sum(weights)` +3. **Incorrect Key Format**: Include proper signature scheme flags +4. **Weight Overflow**: Ensure sum of weights fits in `u16` + +## 🎮 Real-World Use Cases + +### 1. DAO Treasury Management + +```move +// 7-of-12 council multisig with equal voting power +let council_pks = vector[/* 12 council member keys */]; +let council_weights = vector[1,1,1,1,1,1,1,1,1,1,1,1]; +let threshold = 7; // Require majority consensus +``` + +### 2. Escrow Services + +```move +// 2-of-3 escrow: buyer, seller, arbitrator +let escrow_pks = vector[buyer_key, seller_key, arbitrator_key]; +let escrow_weights = vector[1, 1, 1]; +let threshold = 2; // Any two parties can release funds +``` + +### 3. Family Trust Fund + +```move +// Weighted family trust with guardian override +let family_pks = vector[parent1_key, parent2_key, guardian_key, child_key]; +let family_weights = vector[2, 2, 5, 1]; // Guardian has veto power +let threshold = 4; // Requires significant consensus +``` + +### 4. Multi-Department Corporate Approval + +```move +// Department heads + CEO approval system +let dept_pks = vector[ceo_key, legal_key, finance_key, tech_key, hr_key]; +let dept_weights = vector[4, 2, 2, 1, 1]; // CEO has significant weight +let threshold = 6; // Requires CEO + others or broad consensus +``` + +## 🔗 Integration Guide + +### Frontend Integration + +```typescript +import { TransactionBlock } from "@mysten/sui.js"; + +// Create multisig address +const tx = new TransactionBlock(); +tx.moveCall({ + target: `${PACKAGE_ID}::multisig::derive_multisig_address`, + arguments: [ + tx.pure(publicKeys), // vector> + tx.pure(weights), // vector + tx.pure(threshold), // u16 + ], +}); +``` + +### Backend Validation + +```typescript +// Verify multisig configuration +async function validateMultisigSender( + provider: SuiClient, + sender: string, + publicKeys: number[][], + weights: number[], + threshold: number +): Promise { + const tx = new TransactionBlock(); + tx.moveCall({ + target: `${PACKAGE_ID}::multisig::check_multisig_address_eq`, + arguments: [ + tx.pure(publicKeys), + tx.pure(weights), + tx.pure(threshold), + tx.pure(sender), + ], + }); + + const result = await provider.dryRunTransactionBlock({ + transactionBlock: tx, + }); + return result.results?.[0]?.returnValues?.[0] === true; +} +``` + +## 🛡️ Security Considerations + +- **Key Management**: Secure storage and distribution of private keys +- **Threshold Selection**: Balance security with operational efficiency +- **Weight Distribution**: Prevent single points of control +- **Regular Audits**: Periodically review multisig configurations +- **Upgrade Paths**: Plan for changing multisig requirements + +## 📄 License + Copyright (c) Mysten Labs, Inc. SPDX-License-Identifier: Apache-2.0 + +## 🤝 Contributing + +Contributions are welcome! Please: + +1. Fork the repository +2. Create a feature branch +3. Add tests for new functionality +4. Ensure all tests pass +5. Submit a pull request + +## 📞 Support + +For questions or issues: + +- Create an issue in the repository +- Join the [Sui Discord](https://discord.gg/sui) +- Check the [Sui Documentation](https://docs.sui.io) diff --git a/move/Move.toml b/move/Move.toml index 8c3e459..72b9174 100644 --- a/move/Move.toml +++ b/move/Move.toml @@ -1,10 +1,36 @@ [package] name = "multisig" -version = "0.0.1" -edition = "2024.beta" +edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move +# license = "" # e.g., "MIT", "GPL", "Apache 2.0" +# authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] [dependencies] -Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } + +# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. +# Revision can be a branch, a tag, and a commit hash. +# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } + +# For local dependencies use `local = path`. Path is relative to the package root +# Local = { local = "../path/to" } + +# To resolve a version conflict and force a specific version for dependency +# override use `override = true` +# Override = { local = "../conflicting/version", override = true } [addresses] multisig = "0x0" + +# Named addresses will be accessible in Move as `@name`. They're also exported: +# for example, `std = "0x1"` is exported by the Standard Library. +# alice = "0xA11CE" + +[dev-dependencies] +# The dev-dependencies section allows overriding dependencies for `--test` and +# `--dev` modes. You can introduce test-only dependencies here. +# Local = { local = "../path/to/dev-build" } + +[dev-addresses] +# The dev-addresses section allows overwriting named addresses for the `--test` +# and `--dev` modes. +# alice = "0xB0B" + diff --git a/move/sources/multisig.move b/move/sources/multisig.move index befc480..d49d87a 100644 --- a/move/sources/multisig.move +++ b/move/sources/multisig.move @@ -1,164 +1,340 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -/// This module contains functions for working with multisig addresses. -module multisig::multisig { - use sui::address; - use sui::bcs; - use sui::event; - use sui::hash::blake2b256; - - /// Error code indicating that the lengths of public keys and weights are not equal. - const ELengthsOfPksAndWeightsAreNotEqual: u64 = 0; - - /// Error code indicating that the threshold is positive and not greater than the sum of weights. - const EThresholdIsPositiveAndNotGreaterThanTheSumOfWeights: u64 = 1; - - const EPublicKeyLength: u64 = 2; - const EPublicKeyFlag: u64 = 3; - - /// Event emitted when a multisig address is created. - public struct MultisigAddressEvent has copy, drop { - pks: vector>, - weights: vector, - threshold: u16, - multisig_address: address, - } - - /// Creates a multisig address based on the provided public keys, weights, and threshold. - /// Returns true if the created multisig address matches the expected address. - public entry fun check_multisig_address_eq( - pks: vector>, - weights: vector, - threshold: u16, - expected_address: address, - ): bool { - let ms_address = derive_multisig_address_quiet(pks, weights, threshold); - return (ms_address == expected_address) - } - - /// Derives a multisig address. No events are emitted. - /// pks - The public keys of the signers. - /// weights - The weights assigned to each signer. - /// threshold - The minimum amount of weight required for a valid approval. - /// Returns The multisig address. - public fun derive_multisig_address_quiet( - pks: vector>, - weights: vector, - threshold: u16, - ): address { - // Define a u8 variable `multiSigFlag` and initialize it with the value 0x03. - let multiSigFlag :u8 = 0x03; // MultiSig: 0x03, - let mut hash_data = vector[]; - - let pks_len = pks.length(); - let weights_len = weights.length(); - - // Check that the lengths of pks and weights are equal - assert!(pks_len == weights_len, ELengthsOfPksAndWeightsAreNotEqual); - - // Check that the threshold is positive and not greater than the sum of weights - let mut sum: u16 = 0; - let mut i = 0; - while (i < weights_len) { - let w = weights[i] as u16; - sum = sum + w; - i = i + 1; - }; - assert!(threshold > 0 && threshold <= sum, EThresholdIsPositiveAndNotGreaterThanTheSumOfWeights); - - // Update the hasher with the MultiSig flag, threshold, and public keys - hash_data.push_back(multiSigFlag); - - // Serialized threshold and append to the hash_data vector. - let threshold_bytes = bcs::to_bytes(&threshold); - hash_data.append(threshold_bytes); - - // Iterate over the `pks` and `weights` vectors and appends the elements to `hash_data`/ - let mut i = 0; - while (i < pks_len) { - hash_data.append(pks[i]); - hash_data.push_back(weights[i]); - i = i + 1; - }; - - let ms_address = address::from_bytes(blake2b256(&hash_data)); - ms_address - } - - /// Derives a multisig address, and emit an event with all the parameters. - /// pks - The public keys of the signers. - /// weights - The weights assigned to each signer. - /// threshold - The minimum amount of weight required for a valid approval. - /// Returns The multisig address. - public fun derive_multisig_address( - pks: vector>, - weights: vector, - threshold: u16, - ): address { - let ms_address = derive_multisig_address_quiet(pks, weights, threshold); - event::emit( - MultisigAddressEvent{ - pks, - weights, - threshold, - multisig_address: ms_address, - }); - let ms_address = derive_multisig_address_quiet(pks, weights, threshold); - ms_address - } - - /// Checks if the sender of the transaction is a multisig address based on the provided public keys, weights, and threshold. - /// Returns true if the sender is a multisig address. - public fun check_if_sender_is_multisig_address( - pks: vector>, - weights: vector, - threshold: u16, - ctx: &mut TxContext - ): bool { - check_multisig_address_eq(pks, weights, threshold, ctx.sender()) - } - - /// Converts an Ed25519 public key to an address. - /// pk - The Ed25519 public key. - /// Returns The address. - public fun ed25519_key_to_address( - pk: &vector, - ): address { - address_from_bytes(pk, 33, 0x00) - } - - /// Converts a secp256k1 public key to an address. - /// pk - The secp256k1 public key. - /// Returns The address. - public fun secp256k1_key_to_address( - pk: &vector, - ): address { - address_from_bytes(pk, 34, 0x01) - } - - /// Converts a secp256r1 public key to an address. - /// pk - The secp256r1 public key. - /// Returns The address. - public fun secp256r1_key_to_address( - pk: &vector, - ): address { - address_from_bytes(pk, 34, 0x02) - } - - /// Converts a public key to an address. - /// pk - The public key. - /// length - The expected length of the public key. - /// flag - The flag indicating the type of public key. - /// Returns The address. - fun address_from_bytes( - pk: &vector, - length: u64, - flag: u8, - ): address { - assert!(pk.length() == length, EPublicKeyLength); - assert!(pk[0] == flag, EPublicKeyFlag); - address::from_bytes(blake2b256(pk)) - } +/// # Multisig Address Generation and Verification Module +/// +/// This module provides comprehensive functionality for working with multisig addresses on the Sui blockchain. +/// It enables the creation, verification, and validation of multi-signature addresses that require multiple +/// cryptographic signatures to authorize transactions. +/// +/// ## Key Concepts: +/// - **Public Keys (pks)**: A vector of public keys from different signers +/// - **Weights**: Individual voting power assigned to each signer's public key +/// - **Threshold**: Minimum cumulative weight required to authorize a transaction +/// - **Multisig Address**: A deterministically derived address based on the above parameters +/// +/// ## Supported Signature Schemes: +/// - Ed25519 (flag: 0x00) +/// - Secp256k1 (flag: 0x01) +/// - Secp256r1 (flag: 0x02) +/// +/// ## Address Derivation Algorithm: +/// The multisig address is derived using BLAKE2b-256 hash of: +/// 1. Multisig flag (0x03) +/// 2. BCS-serialized threshold (u16) +/// 3. For each signer: public_key || weight +module multisig::multisig; +use sui::address; +use sui::bcs; +use sui::event; +use sui::hash::blake2b256; +use sui::tx_context::TxContext; + +// ===== Error Constants ===== + +/// Error: The number of public keys must equal the number of weights. +/// This ensures each signer has exactly one corresponding weight value. +const ELengthsOfPksAndWeightsAreNotEqual: u64 = 0; + +/// Error: The threshold must be positive and not exceed the total sum of all weights. +/// A threshold of 0 would allow unauthorized transactions, while a threshold exceeding +/// the total weight would make the multisig unusable. +const EThresholdIsPositiveAndNotGreaterThanTheSumOfWeights: u64 = 1; + +/// Error: The public key length doesn't match the expected length for its type. +/// Ed25519: 33 bytes, Secp256k1/Secp256r1: 34 bytes (including scheme flag) +const EPublicKeyLength: u64 = 2; + +/// Error: The public key's first byte doesn't match the expected signature scheme flag. +/// Expected flags: Ed25519 (0x00), Secp256k1 (0x01), Secp256r1 (0x02) +const EPublicKeyFlag: u64 = 3; + +// ===== Events ===== + +/// Event emitted when a multisig address is derived using `derive_multisig_address()`. +/// This event provides transparency and allows off-chain services to track multisig creations. +/// +/// # Fields +/// * `pks` - Vector of public keys used in the multisig +/// * `weights` - Corresponding weights for each public key +/// * `threshold` - Minimum weight required for authorization +/// * `multisig_address` - The computed multisig address +public struct MultisigAddressEvent has copy, drop { + pks: vector>, + weights: vector, + threshold: u16, + multisig_address: address, +} + +// ===== Public Functions ===== + +/// Validates whether the given parameters would generate the expected multisig address. +/// This is useful for verifying multisig configurations without emitting events. +/// +/// # Parameters +/// * `pks` - Vector of public keys (each key includes the signature scheme flag) +/// * `weights` - Vector of weights corresponding to each public key +/// * `threshold` - Minimum cumulative weight required for transaction authorization +/// * `expected_address` - The address to compare against +/// +/// # Returns +/// * `bool` - True if the derived multisig address matches the expected address +/// +/// # Aborts +/// * `ELengthsOfPksAndWeightsAreNotEqual` - If pks and weights vectors have different lengths +/// * `EThresholdIsPositiveAndNotGreaterThanTheSumOfWeights` - If threshold is 0 or exceeds total weight +/// +/// # Example +/// ```move +/// let pks = vector[pk1, pk2, pk3]; +/// let weights = vector[1, 2, 1]; // Total weight: 4 +/// let threshold = 3; // Requires at least 3 weight units +/// let expected = @0x1234...; +/// let is_valid = check_multisig_address_eq(pks, weights, threshold, expected); +/// ``` +public entry fun check_multisig_address_eq( + pks: vector>, + weights: vector, + threshold: u16, + expected_address: address, +): bool { + let ms_address = derive_multisig_address_quiet(pks, weights, threshold); + return (ms_address == expected_address) +} + +/// Derives a multisig address without emitting events. +/// This is the core algorithm for generating deterministic multisig addresses on Sui. +/// +/// # Parameters +/// * `pks` - Vector of public keys from all signers (must include signature scheme flag) +/// * `weights` - Vector of weights assigned to each signer (must match pks length) +/// * `threshold` - Minimum cumulative weight required for authorization (1 ≤ threshold ≤ sum of weights) +/// +/// # Returns +/// * `address` - The derived multisig address +/// +/// # Algorithm +/// The address is computed as BLAKE2b-256 hash of the concatenated data: +/// 1. Multisig flag (0x03) +/// 2. BCS-serialized threshold (u16) +/// 3. For each signer: public_key || weight +/// +/// # Aborts +/// * `ELengthsOfPksAndWeightsAreNotEqual` - If pks and weights vectors have different lengths +/// * `EThresholdIsPositiveAndNotGreaterThanTheSumOfWeights` - If threshold is invalid +/// +/// # Example +/// ```move +/// let pks = vector[ed25519_pk, secp256k1_pk]; +/// let weights = vector[2, 3]; // Total weight: 5 +/// let threshold = 4; // Requires both keys (2+3=5 > 4) +/// let address = derive_multisig_address_quiet(pks, weights, threshold); +/// ``` +public fun derive_multisig_address_quiet( + pks: vector>, + weights: vector, + threshold: u16, +): address { + // Multisig scheme identifier - distinguishes from single-sig addresses + let multiSigFlag: u8 = 0x03; + let mut hash_data = vector[]; + + let pks_len = pks.length(); + let weights_len = weights.length(); + + // Validate input parameters + assert!(pks_len == weights_len, ELengthsOfPksAndWeightsAreNotEqual); + + // Calculate total weight and validate threshold + let mut sum: u16 = 0; + let mut i = 0; + while (i < weights_len) { + let w = weights[i] as u16; + sum = sum + w; + i = i + 1; + }; + assert!( + threshold > 0 && threshold <= sum, + EThresholdIsPositiveAndNotGreaterThanTheSumOfWeights, + ); + + // Build hash input: flag + threshold + (public_key + weight) pairs + + // Step 1: Add multisig flag + hash_data.push_back(multiSigFlag); + + // Step 2: Add BCS-serialized threshold + let threshold_bytes = bcs::to_bytes(&threshold); + hash_data.append(threshold_bytes); + + // Step 3: Add each (public_key, weight) pair + let mut i = 0; + while (i < pks_len) { + hash_data.append(pks[i]); + hash_data.push_back(weights[i]); + i = i + 1; + }; + + // Step 4: Compute final address using BLAKE2b-256 + let ms_address = address::from_bytes(blake2b256(&hash_data)); + ms_address +} + +/// Derives a multisig address and emits an event with all parameters. +/// This function is identical to `derive_multisig_address_quiet` but also emits +/// a `MultisigAddressEvent` for transparency and off-chain tracking. +/// +/// # Parameters +/// * `pks` - Vector of public keys from all signers +/// * `weights` - Vector of weights assigned to each signer +/// * `threshold` - Minimum cumulative weight required for authorization +/// +/// # Returns +/// * `address` - The derived multisig address +/// +/// # Events +/// * `MultisigAddressEvent` - Contains all input parameters and the resulting address +/// +/// # Aborts +/// * Same as `derive_multisig_address_quiet` +/// +/// # Usage +/// Use this function when you want to create a multisig address and have the creation +/// logged on-chain for auditing or off-chain indexing purposes. +public fun derive_multisig_address( + pks: vector>, + weights: vector, + threshold: u16, +): address { + let ms_address = derive_multisig_address_quiet(pks, weights, threshold); + event::emit(MultisigAddressEvent { + pks, + weights, + threshold, + multisig_address: ms_address, + }); + ms_address +} + +/// Verifies if the current transaction sender is a multisig address with given parameters. +/// This function is useful in smart contracts that need to verify the sender's multisig status +/// and configuration before allowing certain operations. +/// +/// # Parameters +/// * `pks` - Vector of public keys that should compose the sender's multisig +/// * `weights` - Vector of weights for each public key +/// * `threshold` - Expected threshold for the multisig +/// * `ctx` - Transaction context to get the sender address +/// +/// # Returns +/// * `bool` - True if the sender matches the expected multisig configuration +/// +/// # Example Use Cases +/// - Restricting function access to specific multisig configurations +/// - Implementing different authorization levels based on multisig parameters +/// - Validating that a caller meets minimum security requirements +/// +/// # Example +/// ```move +/// public fun sensitive_operation(pks: vector>, weights: vector, ctx: &mut TxContext) { +/// // Require 3-of-5 multisig with equal weights +/// assert!(check_if_sender_is_multisig_address(pks, vector[1,1,1,1,1], 3, ctx), EUNAUTHORIZED); +/// // ... perform sensitive operation +/// } +/// ``` +public fun check_if_sender_is_multisig_address( + pks: vector>, + weights: vector, + threshold: u16, + ctx: &mut TxContext, +): bool { + check_multisig_address_eq(pks, weights, threshold, ctx.sender()) +} + +// ===== Public Key to Address Conversion Functions ===== + +/// Converts an Ed25519 public key to a Sui address. +/// Ed25519 is a modern elliptic curve signature scheme offering strong security guarantees. +/// +/// # Parameters +/// * `pk` - Ed25519 public key (33 bytes: flag 0x00 + 32-byte key) +/// +/// # Returns +/// * `address` - The corresponding Sui address +/// +/// # Aborts +/// * `EPublicKeyLength` - If the key is not exactly 33 bytes +/// * `EPublicKeyFlag` - If the first byte is not 0x00 (Ed25519 flag) +/// +/// # Format +/// The public key must be: [0x00][32-byte Ed25519 public key] +public fun ed25519_key_to_address(pk: &vector): address { + address_from_bytes(pk, 33, 0x00) +} + +/// Converts a secp256k1 public key to a Sui address. +/// Secp256k1 is the same elliptic curve used by Bitcoin and Ethereum. +/// +/// # Parameters +/// * `pk` - Secp256k1 public key (34 bytes: flag 0x01 + 33-byte compressed key) +/// +/// # Returns +/// * `address` - The corresponding Sui address +/// +/// # Aborts +/// * `EPublicKeyLength` - If the key is not exactly 34 bytes +/// * `EPublicKeyFlag` - If the first byte is not 0x01 (secp256k1 flag) +/// +/// # Format +/// The public key must be: [0x01][33-byte compressed secp256k1 public key] +public fun secp256k1_key_to_address(pk: &vector): address { + address_from_bytes(pk, 34, 0x01) +} + +/// Converts a secp256r1 (P-256) public key to a Sui address. +/// Secp256r1 is a NIST standard curve widely used in enterprise environments. +/// +/// # Parameters +/// * `pk` - Secp256r1 public key (34 bytes: flag 0x02 + 33-byte compressed key) +/// +/// # Returns +/// * `address` - The corresponding Sui address +/// +/// # Aborts +/// * `EPublicKeyLength` - If the key is not exactly 34 bytes +/// * `EPublicKeyFlag` - If the first byte is not 0x02 (secp256r1 flag) +/// +/// # Format +/// The public key must be: [0x02][33-byte compressed secp256r1 public key] +public fun secp256r1_key_to_address(pk: &vector): address { + address_from_bytes(pk, 34, 0x02) +} + +// ===== Private Helper Functions ===== + +/// Internal helper function to convert a public key to a Sui address. +/// This function validates the public key format and computes the address using BLAKE2b-256. +/// +/// # Parameters +/// * `pk` - The public key including its signature scheme flag +/// * `length` - Expected total length of the public key (including flag) +/// * `flag` - Expected signature scheme flag (first byte of the key) +/// +/// # Returns +/// * `address` - The computed Sui address +/// +/// # Aborts +/// * `EPublicKeyLength` - If the public key length doesn't match expected length +/// * `EPublicKeyFlag` - If the signature scheme flag doesn't match expected flag +/// +/// # Algorithm +/// 1. Validate key length matches the expected length for the signature scheme +/// 2. Validate the first byte matches the signature scheme flag +/// 3. Compute BLAKE2b-256 hash of the entire public key (flag + key data) +/// 4. Convert the hash to a Sui address +fun address_from_bytes(pk: &vector, length: u64, flag: u8): address { + assert!(pk.length() == length, EPublicKeyLength); + assert!(pk[0] == flag, EPublicKeyFlag); + address::from_bytes(blake2b256(pk)) } diff --git a/move/tests/multisig_tests.move b/move/tests/multisig_tests.move index 8c05d61..f948202 100644 --- a/move/tests/multisig_tests.move +++ b/move/tests/multisig_tests.move @@ -2,33 +2,32 @@ // SPDX-License-Identifier: Apache-2.0 #[test_only] -module multisig::multisig_unit_tests { - use multisig::multisig::{Self as ms}; +module multisig::multisig_unit_tests; - #[test] - fun test_derive_multisig_address() { - // ED25519 - let address_ED25519: address = @0x73a6b3c33e2d63383de5c6786cbaca231ff789f4c853af6d54cb883d8780adc0; - let key_ED25519: vector = vector[0, 13, 125, 171, 53, 140, 141, 173, 170, 78, 250, 0, 73, 167, 91, 7, 67, 101, 85, 177, 10, 54, 130, 25, 187, 104, 15, 112, 87, 19, 73, 215, 117]; - assert!(ms::ed25519_key_to_address(&key_ED25519) == address_ED25519, 0); +use multisig::multisig as ms; - // Secp256k1 - let address_Secp256k1: address = @0xd9607cd03428c904949572b51471e7a9f60019aeb9a3d7ee5e72921cab8e8be7; - let key_Secp256k1: vector = vector[1, 2, 14, 23, 205, 89, 57, 228, 107, 25, 102, 65, 150, 140, 215, 89, 145, 11, 162, 87, 126, 39, 250, 115, 253, 227, 135, 109, 185, 190, 197, 188, 235, 43]; - assert!(ms::secp256k1_key_to_address(&key_Secp256k1) == address_Secp256k1, 0); +#[test] +fun test_derive_multisig_address() { + // ED25519 + let address_ED25519: address = @0x73a6b3c33e2d63383de5c6786cbaca231ff789f4c853af6d54cb883d8780adc0; + let key_ED25519: vector = vector[0, 13, 125, 171, 53, 140, 141, 173, 170, 78, 250, 0, 73, 167, 91, 7, 67, 101, 85, 177, 10, 54, 130, 25, 187, 104, 15, 112, 87, 19, 73, 215, 117]; + assert!(ms::ed25519_key_to_address(&key_ED25519) == address_ED25519, 0); - // Secp256r1 - let address_Secp256r1: address = @0x600b1081644fe46f76da3bdc19f8743b9f04458516364374c7d82959e790c19e; - let key_Secp256r1: vector = vector[2, 3, 71, 251, 175, 35, 240, 56, 171, 196, 195, 8, 162, 113, 17, 122, 42, 76, 255, 174, 221, 188, 95, 248, 28, 117, 23, 188, 108, 116, 167, 237, 180, 48]; - assert!(ms::secp256r1_key_to_address(&key_Secp256r1) == address_Secp256r1, 0); + // Secp256k1 + let address_Secp256k1: address = @0xd9607cd03428c904949572b51471e7a9f60019aeb9a3d7ee5e72921cab8e8be7; + let key_Secp256k1: vector = vector[1, 2, 14, 23, 205, 89, 57, 228, 107, 25, 102, 65, 150, 140, 215, 89, 145, 11, 162, 87, 126, 39, 250, 115, 253, 227, 135, 109, 185, 190, 197, 188, 235, 43]; + assert!(ms::secp256k1_key_to_address(&key_Secp256k1) == address_Secp256k1, 0); - let pks: vector> = vector[key_ED25519, key_Secp256k1, key_Secp256r1]; - let weights: vector = vector[1, 1, 1]; - let threshold: u16 = 2; + // Secp256r1 + let address_Secp256r1: address = @0x600b1081644fe46f76da3bdc19f8743b9f04458516364374c7d82959e790c19e; + let key_Secp256r1: vector = vector[2, 3, 71, 251, 175, 35, 240, 56, 171, 196, 195, 8, 162, 113, 17, 122, 42, 76, 255, 174, 221, 188, 95, 248, 28, 117, 23, 188, 108, 116, 167, 237, 180, 48]; + assert!(ms::secp256r1_key_to_address(&key_Secp256r1) == address_Secp256r1, 0); - let expected_multisig_address: address = @0x1c4dac7fb4c01a0c608db993711c451ad655a38b7f0a9571ff099f70090263a8; - let derived_multisig_address: address = ms::derive_multisig_address(pks, weights, threshold); - assert!(derived_multisig_address == expected_multisig_address, 0); - } + let pks: vector> = vector[key_ED25519, key_Secp256k1, key_Secp256r1]; + let weights: vector = vector[1, 1, 1]; + let threshold: u16 = 2; -} + let expected_multisig_address: address = @0x1c4dac7fb4c01a0c608db993711c451ad655a38b7f0a9571ff099f70090263a8; + let derived_multisig_address: address = ms::derive_multisig_address(pks, weights, threshold); + assert!(derived_multisig_address == expected_multisig_address, 0); +} \ No newline at end of file