Add guardians protocol on-chain (only need a new ProxyType)
I thought about a way to define guardians on-chain, and actually it’s already possible with the existing proxy and multisig pallets; we just need to add one new ProxyType.
TL;DR: It only requires adding one ProxyType:
ProxyType::Guardians => {
matches!(
c,
RuntimeCall::Utility(pallet_utility::Call::batch_all { calls }) if {
matches!(calls.as_slice(), [
RuntimeCall::UniversalDividend(pallet_universal_dividend::Call::claim_uds { .. }),
RuntimeCall::Identity(pallet_identity::Call::change_owner_key { .. }),
RuntimeCall::Balances(pallet_balances::Call::transfer_all { .. })
])
}
)
}
We can use the existing proxy and multisig pallets to create guardians that can restore an account if the user loses his private key.
Example scenario
- The user defines his guardians as a multisig account. For example, to allow 2/3 of guardians A, B, and C, the account ID is computed as:
blake2_256(b"modlpy/utilisuba" | A | B | C | 3)
.
See the multisig account ID computation here: Multisig account ID computation - The user creates a proxy of type
ProxyType::Guardians
to allow his guardians to change the owner key of his member account and transfer funds (and claim UDs).
Call: Proxy.add_proxy(guardiansMultisigAccountId, ProxyType::Guardians, DELAY_IN_BLOCKS) - Some time later (potentially years later), the user loses his private key and doesn’t have his revocation document. In that case, he creates a new account and asks one guardian to call:
Multisig.as_multi(threshold, otherGuardiansAccountIds, None, Proxy.annonce(oldUserAccountId, batchCallHash), MAX_WEIGHT)
batchCallHash
is ablake2_256
hash of the SCALE-encodedbatchCall
defined as:Utility.batch_all([ UniversalDividend.claim_uds(), Identity.change_owner_key(newUserKey, newUserKeySig), Balances.transfer_all(newUserKey, false) ])
- An indexer indexes the
NewMultisig
events and computes their related timepoint (block number and extrinsic position in the block). - The remaining (threshold - 1) guardians each call:
Multisig.approve_as_multi(threshold, otherGuardiansAccountIds, Some(timepoint), batchCallHash, MAX_WEIGHT)
timepoint
is a structTimepoint { height: BlockNumber, index: u32 }
that points to the on-chain extrinsic submitted in step 3 (Multisig.as_multi
). - After
DELAY_IN_BLOCKS
blocks, anyone (including a bot, since it’s permissionless) can call:Proxy.proxy_announced(guardiansMultisigAccountId, oldUserAccountId, batchCall)
batchCall
with originoldUserAccountId
. - The user regains his identity, certifications, and funds under his new public key.
Important Notes
-
MAX_WEIGHT
must be high enough for the execution of the wholebatchCall
. The best approach is probably to compute a value based on weight calculations and expose it as a constant in the metadata (for example, as an identity constant with a name likeGUARDIANS_BATCH_CALL_MAX_WEIGHT
). - The user can also create a proxy of type
CancelProxy
to allow a third party to reject the guardians’ announcement before theDELAY_IN_BLOCKS
period expires. -
DELAY_IN_BLOCKS
should be long enough to allow the user (or a third party) to detect a malicious announcement from a subset of guardians. - This protocol is useful only if the user loses his private key; it is not useful if the user’s private key is stolen, since only the revocation mechanism would help in that case.
- Guardians cannot revoke the user’s identity even if we allow calling
identity.revoke_identity
, because a multisig account is just a hash and therefore cannot create a valid signature. If we want to allow guardians to revoke the identity, we must create a new callIdentity.revoke_identity_instant
that does not require a signature (see [issue to be created]).