The People Registry

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 the Secret.
  • 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 PersonalIds 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.

10 Likes

Some emphasize for app devs:

App/pallet devs can use the API from this pallet to ensure the personhood of the caller, using EnsurePersonalAliasInContext or other Ensure.. provided.
You need to define a context (a 32 bytes identifier, e.g. b"mysocialnetwork101..."), then in this context, all proven people have a unique 32 bytes identifier, their alias in the context

fn my_call(origin) {
  let caller_alias =
    EnsurePersonalAliasInContext::ensure_origin(origin, b"mycontext...")?;
}

Users will use the transaction extension AsPerson to send extrinsics with the origin that satisfies this check.

I also want to emphasize the multi-chain future:

So every chain subscribing will be able to get the ring roots and perform those personhood checks. Including smart contracts.

2 Likes

What if one has to assign not just person id, but also location, specialization or some other tags after validation?

After you are recognized as a person, you get a PersonalId and you register a ring VRF public key as well. These are your identifiers as far as the people registry is concerned.

If other entities on chain want to register additional information for people, they are free to do so in their own implementations. For example, the identity pallet will allow people to register personal usernames. Personal usernames are a special type of username allocated by the system itself, not by an authority. To do this, the identity pallet authorizes the people origin and then performs its own validation to eventually grant this username, which is kept in its own storage.

In short, additional information can and will be attributed to recognized people, but it will be done in other pallets which accept people origins.

1 Like

Where is the People Registry pallet (pallet-people)? I couldn’t find it in the Polkadot SDK repository yet.

So is that true that you will be in an anonymity set that has maximum size of 255? And you can sign Actions, which will be tied to that Ring, but not to your ID.

Yes exactly, rings of 255 member, any action from an alias is tied to one ring.
Apps should choose a context and authenticate people from their aliases. I expect that only few operational action will require the ID, like migrating your ring-vrf key is done from your personal ID.

1 Like

It is not merged at the moment, you can find it in this PR: [PoP] Add personhood tracking pallets by georgepisaltu · Pull Request #8164 · paritytech/polkadot-sdk · GitHub

Yes exactly, rings contains maximum 255 member, and any action from an alias is tied to one ring.
Services should choose a context and authenticate people from their aliases.
Only few operational action will require the ID, like migrating your ring-vrf key is done from your personal ID.