Proof of Personhood
This page is still work in progress
Proof of Personhood
Decentralized protocols generally rely on voting to make decisions. Anyone can create an identity and cast their vote on a proposal. A naïve implementation of this leads to a sybil attack, where one attacker creates many identities and casts many votes in his favor.
Proof of work and proof of stake are the most commonly implemented sybil attack resistance mechanisms. In proof of work, each identity has to invest some computational power to cast votes. The identity's voting power is proportional to its computational power. In proof of stake, each identity has to purchase some tokens and stake it in the protocol. The identity's voting power is proportional to its stake. In both cases, people with more money gets higher voting power and can alter the decisions of the decentralized protocol.
Proof of personhood is an alternate solution to solve the sybil attack problem. The goal of the system is to make sure each real person gets voting power of (close to) only 1. Even if the person creates many identities, he cannot have a higher voting power. In this system, anyone create an identity but its voting power is 0 by default. After the identity proves that it is controlled by a real person, it is assigned a non-zero voting power. There are many well-known techniques such as CAPTCHA to verify that someone is a real person. However, a motivated person could still manually create many identities, manually prove each of them is controlled by a real person and get a high voting power. To protect against this attack, the system needs to satisfy a second requirement -- If a single person controls many identities, the sum of voting power of all the identities is still (close to) 1. Essentially, an attacker doesn't gain any advantage by creating many identities and proving each of them is controlled by a real person. The second requirement is quite difficult to satisfy.
This article describes the people parties project currently under development at Dfinity Foundation.
People Parties
People parties[1] are a mechanism for obtaining proof of personhood (also referred to as proof of humanity) built on and for the Internet Computer.
Each people party takes place at one specific point in time. Prior to that time, each prospective participant commits to a location that they will visit for the time of the party. At the beginning of the party, participants are assigned to small, random subgroups, and meet in a real-time video call with other participants assigned to the same group. The video call, however, does not show the participants’ faces; instead, participants show their surroundings, proving that they are at the place they committed to. As locations chosen by different participants must have a certain minimum distance, and no participant can be physically present at two locations simultaneously, the proof of personhood even guarantees the uniqueness of the validated persons.
The main purpose of people parties is democratization. Each validated participant can designate a neuron in the Network Nervous System that receives increased voting power; this improves the relative voting power of the “many” vs. the “heavy”. It also provides additional voting rewards to all validated participants, which further motivates people to participate in the parties. The validated personhood also benefits the Internet Computer ecosystem more broadly: Open Internet Systems, which are dapps that are controlled by a decentralized governance system, will be able to profit from the improved decentralization similarly to the Internet Computer itself. And any dapp will be able to use the validation information in order to, e.g., differentiate between bots and actual human users.
Architecture
The architecture of the people parties project involves 3 components.
- Client
- People party canister
- Signaling server
Client
The client is a device that is used by the participants who want to validate their identity. The client can be a desktop, laptop or a mobile device with a web browser.
People party canister
People party canister is a canister (software program) hosted on the Internet Computer. The canister maintains the information related to all the people parties and identities. This includes
- The longitude boundaries of the participants allowed for the party
- List of participants in the party
- The state of each call in the party
- Validation results of each participant
- Personhood score (voting power) of each identity
The client queries people party canister to obtain the list of all the upcoming parties. The client can then register or deregister for any party by calling the people party canister. During the registration, the client specifies the latitude and longitude coordinates of the location he will be during the party.
A few minutes before a party commences, the client calls people party canister to join the party. Throughout the party, the client repeatedly calls the people party canister to obtain the state of the call. During the call, each participant streams a video that they are in the promised location. Other participants verify this by simultaneously glancing at the google street view of the location, and send an approve/decline vote to the people party canister. After the call ends, the client queries the people party canister to know if the client was approved/declined by others. The client later calls the people party canister to withdraw 1 ICP it deposited to register for the party.
Signaling server
Signaling server is used to facilitate video calls amongst the participants.
People Party Canister Interface
We describe the interface supported by the people party canister here.
get_parties: () -> (vec record { PartyId; PartySpecification; }) query; register: (id: PartyId, place: Location) -> (RegistrationResponse); deregister: (id: PartyId) -> (DeregistrationResponse); withdraw: (destination: AccountIdentifier) -> (WithdrawResult); join: (id: PartyId, key: blob) -> (JoinResponse); vote: (id: PartyId, vote: record { Vote; ParticipantName; }) -> (VoteResponse); http_request: (request: HttpRequest) -> (HttpResponse) query; get_person_profile: () -> (opt PersonProfile) query; get_call_state: (id: PartyId) -> (PublicCallState) query; get_call_result: (id: PartyId) -> (CallResult) query;
When a user enters the people party website, the frontend calls get_parties
endpoint to display the information of all the parties. The PartySpecification
for each people party is described as follows.
type PartySpecification = record { registration_start: Time; registration_end: Time; call_start : Time; longitude_min : float64; longitude_max : float64; };
The user can then then register or deregister for a party by calling register
and deregister
endpoints. The canister responds with the status of registration. When the user registers for a party, he specifies the the Location (latitude and longitude) of where the user will be when the party starts.
A few minutes before the party starts, the user joins the party by calling join
endpoint. After the user joins a call, the user sends one vote (approve/decline) for each of the other participants by calling vote
endpoint.
At any point in time, one can call get_person_profile
, get_call_state
and get_call_result
endpoints to obtain the information about the list of parties, a person's profile, state of a call and result of a call respectively.
The profile of a person is described by
type PersonProfile = record { upcoming_parties: vec record { PartyId; Location; }; validation_score: nat64; past_parties: vec PartyId; };
When the user registers for a party, he needs to deposit 1 ICP. After the party ends, the user can withdraw this amount by calling withdraw
endpoint. The user can only withdraw if he hasn't registered for any upcoming party. Once the user withdraws the amount, he cannot register again until he deposits 1 ICP.
Party Flow
The below flow describing a call. Say the call is scheduled to start at 10am. Then the timeline should be as follows:
- Until 9:55am, users can enroll with the register endpoint. Since the assignment to groups has not been made, calling
get_call_state
will return what is in case 0 below. - At 9:55am, the canister closes the registration phase and calling the
register
endpoint for the current party is not allowed anymore. - From 9:55am to 10am, users are allowed to join. For this, there is a join endpoint that accepts a key – a blob. Calling
get_call_state
will return the result described in case 1 below. - At 10am, the canister generates the random setup of all calls. From this time onward, calling the join endpoint is not allowed anymore, and clients are supposed to set up the WebRTC connections.
- Between 10am and 10:01am, the canister returns the call setup state as described in step 1.5 below.
- At 10:01am, the actual calls start.
- From 10:01am to 10:11am, the actual party is supposed to take place and users can submit votes. Calling
get_call_state
will return the result described in case 2 below. - At 10:11am, the canister closes all calls and makes the tally. From this point onward, calling vote is not allowed anymore.
- From 10:11am,
get_call_state
will return the result described in case 3 below.
Call State at Various Stages
The people parties canister stores the state of each call, and updates the state during each phase of the party. The canister exposes the get_call_state
method that returns the state of the call.
Before Registration
This is returned for parties that do not exist:
call_state = record { public_call_state = variant{not_created}; my_votes = vec{}; };
Joining – Before call starts
This is returned from the point in time where the groups are assigned but before the point in time when the call is scheduled to start. The state transition from 1 to 1.5 is defined by the canister computing the random assignment.
call_state = record { public_call_state = variant{not_started = record { joined = true; ← so the UI can show that the user is in the waiting room starts_in_seconds = 30; }}; my_votes = vec{}; };
Starting the call
call_state = record { public_call_state = variant{starting = record{ myself = “adhesive bread”; participants = vec{ record{ name = “adhesive bread”; location = … key = [30, 57, 30, …]; }; record{ Name = “stinky tofu”; Location = … key = [30, 57, 30, …]; ... }; starts_in_seconds = 30; }}; my_votes = vec{}; };
After call starts – before call ends
If the user hasn’t successfully joined the call, then the following is returned:
call_state = record { public_call_state = variant{not_joined}; };
If the user has successfully joined the call, then the following is returned:
call_state = record { public_call_state = variant{active = record{ myself = “adhesive bread”; participants = vec{ record{ name = “adhesive bread”; location = … key = [30, 57, 30, …]; }; ... }; round = 0; remaining_seconds = 42; voters_in_round = vec{“chilly mustard”; “angry fox”;}; }} };
State in later round:
call_state = record { public_call_state = variant{active = record{ myself = “adhesive bread”; participants = vec{ ... }; round = 1; remaining_seconds = 22; voters_in_round = vec{“adhesive bread”; “angry fox”;}; }} };
After call ends
call_state = record { public_call_state = variant{ended}; };
Personhood Score
Each identity in the Internet Computer is assigned a personhood score. If an identity hasn’t participated in any people party, its personhood score is 0. As the identity participates in people parties and is successfully validated to be a person, its personhood score increases.
Any canister will be able to query and obtain the personhood score of an identity. One can potentially use the personhood score to determine the voting power of an identity in an election/voting program.
Requirement #1
An Internet Computer user is naturally inclined to increase his voting power. He is therefore motivated to create many identities and attend a people party with each identity. This gives him control of many identities with a positive personhood score, and thereby a higher voting power. We would like to avoid this. We would like to design the personhood score in such a way that an IC user is motivated to attend each people party with the same identity rather than creating a new identity for each people party.
Requirement #2
Suppose Alice attended many parties with her identity, and Bob attended only one recent party. Alice and Bob’s personhood scores should still be close. Alice should not have a significant advantage by attending many parties.
Failed Attempt #1
Suppose personhood score of an identity = 1 if the identity is successfully validated in at least one party. This personhood score violates our Requirement #1.
Failed Attempt #2
Suppose personhood score of an identity = number of parties in which the identity is successfully validated. This personhood score violates our Requirement #2.
Successful Attempt
After a party ends, the personhood score of an identity is composed of three components:
- Recent party result (RPR): This value is 1 if the identity is accepted to be a person in the recent party. The value is 0 if the identity did not participate or is rejected in the recent party.
- Decay function of previous personhood score: The decay function is a monotonically non-increasing function. It is computed on the old personhood score (the score before the recent party started). The decay lets us give more weightage to the validation in the recent party over the previous parties.
- Bonus reward function: If the identity is accepted in the most recent party and is successfully accepted in some parties before, a bonus reward is given to the identity. This bonus component is added to incentivize IC users to participate in all the parties using the same identity rather than creating a different identity for each party.
Suppose the personhood score of an identity is PSold before a party starts. The trust score PSnew of the identity after the party ends is calculated as follows.
PSnew = RPR * (1 + bonus(PSold)) + decay(PSold)
Where Recent Party Result (RPR) is 1 iff the identity is accepted in the party.
Through empirical evidence, we found that
- bonus(x) = log10(1 + 0.25 * x) and
- decay(x) = log10(1 + x)
work pretty well. That is,
PSnew = RPR * (1 + log10(1 + 0.25 * PSold)) + log10(1 + PSold)
Example
Consider 2 scenarios.
- Scenario A: The user enters each party with the same identity, and the user is accepted in each of the parties.
- Scenario B: The user enters each party with a new identity, and the user is accepted in each of the parties.
Let us compare the personhood scores of the user in each of the scenarios. In Scenario B, we sum up the personhood scores of all the identities controlled by the user.
Scenario A | Scenario B | |
---|---|---|
Initial Score | 0 | 0 |
After 1st party | 1 | 1 |
After 2nd party | 1.398 | 1.301 |
After 3rd party | 1.510 | 1.415 |
After 4th party | 1.539 | 1.462 |
After 5th party | 1.546 | 1.482 |
After 6th party | 1.548 | 1.491 |
After 7th party | 1.548 | 1.494 |
After 8th party | 1.548 | 1.496 |