Multi-Sig
Secure distributed protocols require mechanisms to authorize high-stakes state transitions without relying on a single point of failure. Distributing authorization prevents a single compromised key from draining an account or executing malicious logic.
MOI handles multi-signature operations at two distinct levels: Account Multi-Sig (managing key weights within a single account) and Interaction Multi-Sig (managing approvals across distinct participants).
The Multi-Sig Authorization Lifecycle
Regardless of the level at which it operates, a multi-signature operation in MOI progresses through four phases:
- Initiation: A participant constructs an interaction (specifying the target, value, and execution payload) and signs it to create a pending state.
- Collection: Signatures are gathered from the required parties. This orchestration happens off-chain (via dApps or wallet SDKs passing the payload between stakeholders) or through coordinating logic accounts.
- Aggregation & Verification: The network evaluates the bundled signatures, confirming they are cryptographically valid, prevent replay attacks, and collectively meet the required execution threshold.
- Execution: Upon successful threshold validation, the network processes the interaction and updates the state.
Account Multi-Sig (Key-Level)
As outlined in the Accounts document, a single MOI account is not bound to one private key. Authorization is managed through a collection of registered keys, each capable of approving state transitions on behalf of that account.
type AccountKey struct {
PublicKey []byte
Algorithm SignatureAlgorithm
Weight uint8
Sequence uint64
}
The core of this model is the Weight parameter. Instead of treating every key equally, keys carry specific voting power. At the protocol level, an interaction is valid only when the cumulative weight of the provided signatures meets or exceeds the account-level threshold.
The default account threshold is 1000.
How it works in practice:
Consider an account where authorization is split among three different stakeholders. To reach the default account threshold of 1000, their keys are assigned weights of 400, 350, and 250.
Stakeholder-1 creates the interaction payload, signs it with their key (Weight:
400), and passes the partially signed payload to Stakeholder-2.Stakeholder-2 adds their signature (Weight:
350) and passes it to Stakeholder-3.Stakeholder-3 adds their signature (Weight:
250). The cumulative weight now successfully hits the network requirement of1000.The final stakeholder broadcasts the interaction to the network.
The core MOI protocol does not orchestrate the collection of these signatures. DApps, SDKs, or wallet providers must implement the abstraction to sequentially or concurrently gather signatures from keyholders before broadcasting the final payload to the network RPC.
Interaction Multi-Sig (Participant-Level)
While Account Multi-Sig manages internal account security, many decentralized workflows, such as escrow releases, DAO governance, or atomic swaps, require approval from multiple independent accounts.
MOI models this using Notary participants and Coco Logics. Instead of a account level threshold, the application logic dictates which participants must explicitly authorize the execution, and the network enforces it via the Notary flag. Concurrently, the execution logic (Coco) is responsible for verifying that any required participant-level thresholds are met for the specific operation.
The Coco Logic Implementation
Here is how an Interaction multi-sig looks inside a Coco contract:
coco CheckSig
state logic:
// Represents the array of Account IDs required to authorize the operation
authorizations []Identifier
// Helper function to check if a specific participant authorized the interaction
endpoint HasSigned(participant Identifier) -> (has_signed Bool):
has_signed = Actor(participant).HasSigned()
dynamic Transfer(amount U256, receiver Identifier) -> (is_transfered bool):
// Assert that all required participants have signed
memory last_index = len(authorizations)
for idx in range(last_index):
if HasSigned(authorizations[idx]) == false:
return false
// Proceed with asset transfer...
Execution Flow
The Transfer function expects a predefined list of required authorizations. Before mutating any state, it iterates through this list and invokes Actor(participant).HasSigned(). This built-in method queries the execution runtime to verify if the specified participant provided a valid signature as a Notary for this specific interaction. If any required signature is missing, the function returns false.
Interaction Payload
The off-chain application must construct an interaction that explicitly instructs the network to expect these additional signatures. This is achieved by adding the required authorizers to the participants array of the interaction payload and setting their "notary" flag to true.
Example JSON payload for a LogicInvoke requiring a Notary signature:
{
"sender": {
"id": "0xSenderAccountID...",
"sequence": 15,
"key_id": 0
},
"fuel_price": 1,
"fuel_limit": 50000,
"ix_operations": [
{
"type": 12, // 12 denotes a LogicInvoke operation
"payload": {
"logic_id": "0xDeployedLogicID...",
"callsite": "Transfer",
"calldata": "0xEncodedArgumentsHere..."
}
}
],
"participants": [
{
"id": "0xYourDeployedLogicID...",
"lock_type": 0,
"notary": false
},
{
"id": "0xRequiredAuthorizerAccountID...",
"lock_type": 2,
"notary": true // Flags this participant as a required signer
}
]
}
Once the dApp has collected all required approvals, the interaction is serialized, signed, packaged into a standard JSON-RPC envelope, and broadcasted to the network.
Under the Hood: The Verification Engine
To prevent bypassing the HasSigned() check, MOI enforces a strict cryptographic boundary between network-level security and application-level logic.
When an interaction is submitted with participants tagged as Notaries, the network processes it in three strict phases:
1. Network-Level Signature Validation
Before our Coco logic is executed, the MOI network inspects the payload. If any participant is tagged as a Notary, the network verifies their digital signatures against their internal account rules and weight thresholds. If the signatures are missing, forged, or carry insufficient weight, the network drops the request entirely. The logic is never invoked.
2. Runtime Initialization
If the request passes the network's signature validation, the system prepares the execution environment. For any Notary who passed Step 1, the runtime securely locks their status as "signed" in the background context.
3. The Coco Logic Check
When the contract calls Actor(participant).HasSigned(), it does not evaluate user input. Instead, it reads the tamper-proof "signed" status generated directly by the runtime in Step 2.
The Security Guarantee
This architecture creates a deterministic security loop for developers:
Omitted Tags: If a user attempts to execute the contract without tagging the required accounts as Notaries,
HasSigned()evaluates tofalseinside the logic.Fraudulent Tags: If a user tags required accounts as Notaries to bypass the contract check but fails to provide valid cryptographic signatures, the network terminates the transaction at Step 1.
By combining internal account weights with Notary tags, developers can enforce complex approval flows without writing gas-heavy cryptographic verification logic into the contracts themselves.
Where Next
- Interactions: how accounts initiate and authorize state transitions.
- Assets: how MOI represents ownership natively at the account level.
- Logics: how executable applications are built and deployed on MOI.
- Accounts: how MOI represents participants through accounts, the participant-centric state model, the Context-Superstate, account types, identifiers, and keys.