The first piece of Proof of Personhood, the People registry, landed in polkadot-sdk
. While DIMs are still under development and not quite ready to become public, the people registry still packs a ton of new functionality, which we will unpack in this post.
What is it?
The People registry is the source of truth in terms of all recognized people that the chain keeps track of and is implemented in pallet-people
. It itself is not a DIM, as it doesn’t do the verification necessary to prove that an entity is an actual real-life person.
Identification
People are identified on chain through a unique PersonalId
that they are assigned when they prove their personhood. This ID can be registered in advance when applying to be a candidate in a DIM and starting the proving process.
Special keys
People do have keys associated with their personhood, but they are not regular account keys. Instead, people need to create a special key pair which implements the GenerateVerifiable
trait and submit their public key to be associated with their personhood. In practice, this will be the Bandersnatch ring VRF cryptography in ark-vrf
.
Why ring VRF cryptography?
From this point onwards, I will use “ring VRF” to refer to the ring VRF type in ark-vrf
, which implements the GenerateVerifiable
interface in the verifiable
crate.
In short, this special cryptography allows us to hide the identity of a member in a member set in different contexts under deterministic aliases.
How ring VRF works
To further demystify this notion, let’s go through the basics of this mechanism:
- People posses a ring VRF key pair: the public key is the
Member
and the secret key is theSecret
. - Given a context, which is just some arbitrary 32 bytes of data, a member can find their alias within that context using their secret key. We can think of the alias as a hash - it is always the same for the same secret key and the same context, but the inputs cannot be reconstructed from the alias.
- Member sets are organized in rings; each ring can hold a limited number of members, capped at the
RING_SIZE
statically defined in the cryptographic implementation. This number will be 255 in practice. - A member set is uniquely identified by a ring root, the
Members
type. Adding or removing a member from the set will change this ring root. - A member can generate a commitment, the
Commitment
type, within a member set. The commitment is a partial proof valid only for the member generating it, in the specific set it is generated from. - Using their secret key, a member can use their commitment to prove their membership of a set and verify a given message within a context. This process yields a proof as well as the member’s alias in that context. In practice, the proof is similar to a signature, but we don’t know for sure who signed the message; all we know is that it was someone from the member set and their alias, which will be the same for the next proof they generate within the same context.
- The keypair can also be used for regular signatures, without involving a member set.
pallet-people
structure
The pallet keeps track of people using their PersonalId
s as well as a ring VRF public key as their identifiers, both of which are unique to each person. People then use their keys to create proofs and signatures to authorize their activity on chain, like regular users are using their regular keys to authorize their activity under their accounts.
/// Identity of personhood.
///
/// This is a persistent identifier for every individual. Regardless of what else the individual
/// changes within the system (such as identity documents, cryptographic keys, etc...) this does not
/// change. As such, it should never be used in application code.
pub type PersonalId = u64;
Supporting unlimited people
Because the ring size is limited, people are be segregated into different rings. This means that their activity on chain will be linked to the ring that their key is assigned to, as they have to use their ring’s root to create a proof.
Maintaining ring roots
In order to use the ring VRF proof mechanism, we need to have ring roots that reflect the current member set. Whenever the ring root changes, we increment the ring revision. The root building process is incremental, which means that when a new member is added, we can use the previous root to push the new member and compute the new root. However, when a member is removed for whatever reason, The entire ring root needs to be recomputed from scratch, which is considerably more expensive and to be avoided whenever possible.
When a person is suspended, they must lose their privilege of creating valid proofs, so a new ring root must be constructed without their key. To reduce the number of fresh ring root builds, we introduce the concept of people set mutation sessions. When a DIM wants to suspend people it previously recognized, it should start a mutation session, submit its batched suspensions and end the session. During a mutation session, new key onboarding and ring building are not available.
Maintaining privacy
Consider the following scenario: there are 10 people who are part of a ring through their public keys and are all generating proofs under their aliases when a single new member joins the ring. Because aliases are deterministic for all members in a given context, this means that previously we could see the activity of 10 different aliases. Now, in this new revision, we see 11 different aliases, but we have seen 10 of them already. This must mean that the new alias we just spotted does in fact belong to the new member, thus breaking the privacy guarantee of the cryptographic proof - we have successfully linked the alias to its parent member key. We encounter the same problem when a single person is removed from a set, as now the only alias without any activity must belong to the member that was removed, but this is less significant because it concerns a member that lost their privileges.
To mitigate this, the pallet provides an onboarding queue. Newly recognized people are added to the queue and can only be added to a ring when the batch is big enough to not compromise the privacy of the new joiners. In fact, every single key that wants to become part of a ring must go through the onboarding queue, to ensure the privacy of every other key. If a single new person joins, we would know exactly what their alias is as soon as they create a proof. If 2 people join, we have a 1 in 2 chance of guessing their alias. If 100 people join at the same time, the chance drops to 1 in 100.
Once the queue holds enough people, they will be added to the newest ring and the ring root will be rebuilt through unsigned transactions submitted by the offchain worker (to be later moved to the task API when it becomes stable).
In practice, the batch size will probably be around 5-10 as it provides a good enough privacy guarantee without having to wait for lots of people to join before being included in a ring.
Key migration
If a person wants to migrate to a new key for whatever reason (e.g. their previous key is compromised), they can do so by declaring their intent to migrate their key. If the person is not yet included in a ring, the operation is instant. However, if the person is active in a ring, their migration is enqueued and will take effect when the next mutation session happens, along with other suspensions. The new key goes into the onboarding queue along with other new keys.
Key migration also partially mitigates another potential privacy problem, which, going back to the example of a ring with 10 members, happens when 9 out of 10 people are suspended. In this case, the only remaining member would reveal their alias if they tried to create any proof. To avoid this, the member can migrate their key and leave a ring with insufficient privacy guarantees.
However, the downside of a migration is that, because the person migrating loses the privilege of creating proofs with the old key, they also effectively kill all of their activity conducted under alias. Also, from a privacy point of view, key migrations act as a member removal for the old key and its aliases, and carry the same privacy risks. As migration is a voluntary process, we expect users to only do it when the benefits outweigh the risks.
Alias accounts
Regular signatures do not, by themselves, protect against transaction replay. When accounts come into existence, a nonce is stored in the Account
map in frame_system
’s storage. This nonce is part of the signing payload and checked in the CheckNonce
transaction extension. Therefore, transactions authored by regular accounts have native protection against replay through the nonce mechanism in frame_system
and CheckNonce
.
Proofs act somewhat like regular signatures when authorizing transactions, but lack this native replay protection because people origins no not have nonces stored anywhere and bypass the CheckNonce
extension.
To solve this, we introduced alias accounts. An alias account is a regular account that we map to each alias in a context; this works because aliases are unique and deterministic for each ring VRF keypair. Users submit a transaction to set their alias account for a particular context, which is the only transaction that they can submit using a proof. For subsequent transactions under people origins, users will submit them using signatures from their regular account they set as an alias while properly setting up the AsPerson
transaction extension to pick up on their intent to mutate their origin from a traditional signed origin to a custom people origin. Setting an alias account will automatically provide for that account’s nonce, which is then used by the extension to protect against transaction replay.
/// Information required to transform an origin into a personal alias or personal identity.
pub enum AsPersonInfo<T: Config + Send + Sync> {
/// The signed origin will be transformed using account to alias.
AsPersonalAliasWithAccount(T::Nonce),
/// The none origin will be transformed using proof.
///
/// This can only dispatch the call `set_alias_account`.
///
/// Replay is only protected against resetting the same account during the tolerance period
/// after `call_valid_at` parameter.
/// If 2 transaction that set 2 different account are sent for an overlapping validity period,
/// then those 2 transactions can be replayed indefinitely for the duration of the overlapping
/// period.
AsPersonalAliasWithProof(<T::Crypto as GenerateVerifiable>::Proof, RingIndex, Context),
/// The none origin will be transformed using signature.
///
/// This can only dispatch the call `set_personal_id_account`.
///
/// Replay is only protected against resetting the same account during the tolerance period
/// after `call_valid_at` parameter.
/// If 2 transaction that set 2 different account are sent for an overlapping validity period,
/// then those 2 transactions can be replayed indefinitely for the duration of the overlapping
/// period.
AsPersonalIdentityWithProof(<T::Crypto as GenerateVerifiable>::Signature, PersonalId),
/// The signed origin will be transformed using account to personal id.
AsPersonalIdentityWithAccount(T::Nonce),
}
/// Transaction extension to transform an origin into a personal alias or personal identity.
pub struct AsPerson<T: Config + Send + Sync>(Option<AsPersonInfo<T>>);
There is another good reason for using alias accounts instead of proofs - efficiency. Validating a ring VRF proof is about 3 orders of magnitude more expensive in terms of compute time than verifying a regular signature. Creating the proof itself off chain adds another order of magnitude and can take around half a second even on modern CPUs. These numbers get bigger as the maximum ring capacity used by the ring VRF cryptography increases; our current limit of 255 ring members strikes a good compromise between size and efficiency. Regardless, people only have to create and use the proof once and retain all of the privacy benefits by using regular signatures created with alias accounts, which saves compute resources in the long run, both on and off chain.
As people can run transactions using a proof for contextual alias origins as well as a ring VRF regular signature for personal identity origins, we employ the same mechanism for personal identity alias accounts. These are accessory accounts to the personal identity defined by a particular PersonalId
and ring VRF public key and follow the same pattern of setting the alias account once with a signature, then using said account for further transactions under a personal identity.
For further information on the alias account process and origin mutation, refer to the set_alias_account
and set_personal_id_account
calls, as well as the AsPerson
transaction extension.
Spam protection
The chain traditionally protects itself against spam by charging signed origins transaction fees and validating unsigned transactions before even accepting them into the transaction pool. Transactions made by people should be free, in the sense that the submitter should not have to acquire or put up funds in any way in order to include the transaction in the transaction pool and eventually in a block. We cannot make use of funds to address the spam issue as the traditional transaction fee model does because people cannot natively hold any funds.
We propose pallet-origin-restriction
as a general solution to this problem, not limited to the people use case. This pallet essentially introduces weight allowances for configurable origin types and intercepts them in a transaction extension before the call is dispatched to consume the transaction’s weight from the origin’s allowance, similar to how the ChargeTransactionPayment
extension charges a fee, converted from weight. The extension also provides weight refunds just like ChargeTransactionPayment
refunds fees. Unlike with fees, however, the caller’s weight allowance is linearly replenishing itself with each block.
Authenticating people origins
The people pallet offers a range of structures which can synchronously ensure the caller matches a people origin, namely EnsurePersonalIdentity
, EnsurePersonalAlias
, EnsurePersonalAliasInContext
, EnsureRevisedPersonalAlias
, and EnsureRevisedPersonalAliasInContext
. This means that pallets on the same chain as the People registry can integrate people origin checks in their calls. For asynchronous authentication of people on different chains, we plan to introduce a ring root distribution mechanism, which would propagate the latest ring roots to chains which subscribe to this via XCM. This will be particularly interesting when thinking of using people origins on Polkadot Hub, but more on this after we launch the first version of Proof of Personhood.
Lifecycle of a recognized person
Presuming that a DIM has recognized someone’s personhood, let’s go through the steps of what happens next:
- The newly recognized person has a
PersonalId
assigned and must now register their ring VRF public key. - The person enters the onboarding queue and waits to be included in a ring.
- The person submits a transaction with a ring VRF signature to set their personal identity associated account.
- The person waits for a ring root which includes their key to be built.
- The person submits a transaction with a ring VRF proof for each context that they want to interact in to set their alias accounts.
- The person then submits transactions using their alias accounts to use functionality available for people for free, within their weight allowance limit.
What’s next?
Next we plan to reveal some of the functionality uniquely available to recognized people, which will make use of all of the structures we discussed above.