├── README.md ├── contracts └── Identity.sol └── sybil.py /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This repository is an experimental anti-sybil system named Odin. 4 | 5 | The name Odin comes from the chief god of Norse mythology is referred to by 6 | more than 200 names, which seems to be a fitting name for a system which 7 | attempts to solve the problem of knowing whether the entity you are interacting 8 | with is already known to you by another name. 9 | 10 | See https://github.com/ethereum/wiki/wiki/Problems#14-anti-sybil-systems for a 11 | detailed write-up of the problem. 12 | 13 | ## Goals 14 | 15 | The goals of this system are as follows. 16 | 17 | - Decentralization: No dependency on a central authority for the system to work. 18 | - Cost to individuals to obtain an identity is low. 19 | - Cost to individuals to obtaining multiple identities is high. 20 | - Cost to automated systems to obtaining multiple identities is high. 21 | 22 | ## Definitions 23 | 24 | - **Odin** 25 | 26 | The name of this anti-sybil system. 27 | 28 | - **entity** 29 | 30 | The term entity is used to refer to one of more individuals who are operating 31 | as a single unit. 32 | 33 | - **individual** 34 | 35 | The term individual is used to refer to a singular person or address. 36 | 37 | - **identity pool** 38 | 39 | A pool is a set of identities. Normally referred to using the shorthand **pool** 40 | 41 | - **issuer** 42 | 43 | Each pool is run by an issuer who is the sole distributor or identities for 44 | that pool. The term **operator** is sometimes used to refer to the entity that 45 | issues identities for a pool. 46 | 47 | - **operator** 48 | 49 | Alias for the **issuer** of identities for a pool. 50 | 51 | - **identity** 52 | 53 | A member of a pool, identified by an identifier which is unique for that pool 54 | and owned by an ethereum address. 55 | 56 | - **identity fee** 57 | 58 | The cost of getting an identity issued. This value can differ from pool to pool. 59 | 60 | - **sybil fund** 61 | 62 | A monitary fund within each pool that is paid for with identity fees. 63 | 64 | - **sybil proof** 65 | 66 | The act of multiple identities with a pool revealing that they are operated by 67 | the same entity. 68 | 69 | - **proof reward** 70 | 71 | A monitary reward for successful sybil proofs. 72 | 73 | - **proof secret** 74 | 75 | A secret value that is committed to at the initiation of a sybil proof, and 76 | revealed at the end of the proof. 77 | 78 | - **secret hash** 79 | 80 | The `sha3` hash of a secret, possibly combined with other data which commits 81 | the submitter to a value without revealing that value. 82 | 83 | 84 | # Overview 85 | 86 | Odin consists of a set of identity pools. Each pool is run by an entity who 87 | acts as the sole issuer of identities for that pool. Anyone may create and 88 | operate a pool. 89 | 90 | ## How pools issue identities. 91 | 92 | The issuer for each pool may issue identities as they see fit. Each identity 93 | issued must be assigned a unique identifier which is chosen by the issuer and 94 | is required to be unique within the given pool. 95 | 96 | * Some pools may decide to enact very strict rules for identity issuance such 97 | as requiring verification of a passport 98 | * while others may choose to enact no rules and issue identities with 99 | no verification. 100 | 101 | The **facebook** pool for example, may choose to issue a single identity for 102 | each facebook account. In this pool, the rules for getting an identity issued 103 | are simply that you must have a facebook account. 104 | 105 | A company may create a pool and issue identies to each of it's employees. 106 | Under this model, each new employee would be issued an identity. 107 | 108 | The government of a country running a pool may choose to issue identities based 109 | on passport or drivers licence numbers where each identity corresponds to one 110 | of these document that has been verified by some government department. 111 | 112 | ## Issuance Fee 113 | 114 | When an entity requests an identity from a pool, they must include a fee with 115 | their request. A portion of this fee is given to the pool operator as a fee, 116 | and a portion is placed into the sybil fund for the pool. 117 | 118 | ## Identities 119 | 120 | Each identity may choose to associate additional addresses with their identity. 121 | This allows them to *link* and identity from one pool with an identity from 122 | another pool. 123 | 124 | ## Sybil Proofs 125 | 126 | At any time, two or more identities within a pool can choose to participate in 127 | a sybil proof, revealing that they are being operated by the same entity. A 128 | successful proof pays the proof reward to the addresses of the participating 129 | identities. A sybil proof is considered successful if 2 or more identities 130 | participate. Participation in a proof destroys the participating identity. 131 | 132 | ### Maximum Proof Size 133 | 134 | Any proof may only have a maximum of `max(2, floor(sqrt(num_members)))` 135 | participants where `num_members` is the number of members currently in the 136 | pool. 137 | 138 | This mechanism adds an upper bound to the number of identities any entity will 139 | try to include in a given proof. The payout amount for a proof increases with 140 | each additional participant, so it is useful to have this value be bounded as 141 | it ensures that sybil proofs will remain profitable as long as a pool continues 142 | to grow in size. 143 | 144 | ### Proof Stages 145 | 146 | A sybil proof occurs in 4 stages. 147 | 148 | #### Stage 1. - Initiation 149 | 150 | Any account may initiate a sybil proof at any time. Initiation requires the following: 151 | 152 | - a deposit, which will be refunded if the proof is successful. 153 | - a proof-secret hash which is the `sha3(proof_secret)` of a secret that will 154 | be revealed during stage 3. 155 | 156 | #### Stage 2. - Enrollment 157 | 158 | During stage 2 any other identity may enroll as a participant of this proof by 159 | providing the following. 160 | 161 | - a deposit, which will be refunded if the proof is successful. 162 | - an enrollment-secret hash which is the `sha3(identity_id, secret)` where 163 | `secret` is the proof-secret submitted during the initiation phase. 164 | 165 | #### Stage 3. - Proof 166 | 167 | During stage 3 any address related to one of the participating identities who 168 | can *prove* they know the proof-secret can initiate a payment claim for the 169 | proof. A proof is considered successful at this point if two or more identities 170 | participated in the proof. 171 | 172 | The payment is computed as the sum of all deposits submitted for the proof, the 173 | issuance fees for all participating identities, and the proof-reward value. 174 | 175 | #### Stage 4. - Payment 176 | 177 | After a short waiting period, if no other claims are submitted, a claimer may 178 | initiate payment which transfers the proof payment to their address and 179 | finalizes the proof. 180 | 181 | If at any time during the waiting period a new claim comes in from another 182 | participating identity that has not already initiated a claim, the initial 183 | claim is cancelled and replaced by the new claim. 184 | 185 | #### Preventing collusion on proofs 186 | 187 | The goal of sybil proofs is to expose weaknesses where independent individuals 188 | are able to acquire more than one identity in a given pool. In order to 189 | de-incentivize any collusion by separate individuals, two mechanisms are in 190 | place to make it difficult for disparate individuals to trust each other. 191 | 192 | #### Secret Sharing 193 | 194 | In order for a proof to be successful, the individual who initiates the proof 195 | must share their secret with any other identity that is going to participate in 196 | the proof so that they can calculatethe enrollment-secret. 197 | 198 | If the participating identities, trust the initiatior of the proof to compute 199 | this hash for them, then it is possible for the initiator to provide them with 200 | an incorrect hash which will allow the initiator to take their deposit since 201 | their hash will not verify in stage 3. 202 | 203 | Additionally, during stage 2 anyone who can *prove* they know the proof-secret can claim all 204 | of the submitted deposits, which allows any participant to steal all of the 205 | deposits. 206 | 207 | #### Payment Claiming 208 | 209 | If at any time during the waiting period a new claim comes in from another 210 | participating identity that has not already initiated a claim, the initial 211 | claim is cancelled and replaced by the new claim. Each time this occurs, the 212 | overall payment amount adjusted by a multiplier defined as `2 / (N + 1)` whe N 213 | is the total number of claims for this proof. 214 | 215 | This exposes another mechanism through which participants can steal from each 216 | other. Each successive claim allows the individual to claim more of the 217 | payment than they would have received were it to have been split evenly, while 218 | simultaneously reducing the overall payout. 219 | 220 | 221 | ### Proof Reward Schedule 222 | 223 | > The exact formulas 224 | 225 | The reward for a sybil proof dynamically computed based on various properties 226 | of the pool. The formula is: 227 | 228 | `BaseReward * ProofSizeMultiplier * PoolStateMultiplier` 229 | 230 | This formula is designed to have the following economical incentives. 231 | 232 | - As long as a pool is growing sybil proofs will be profitable. 233 | - For each proof size, successive sybil-proofs become linearly less profitable, 234 | eventually reaching zero. 235 | - Pools with very few sybil proofs (difficult to get multiple identities) will 236 | have larger proof rewards. 237 | - Pools with very many sybil proofs (easy to get multiple identities) will have 238 | smaller or zero reward for proofs. 239 | 240 | This system effectively sets up a marketplace where there is an encouraged 241 | financial incentive for entities to expose weaknesses in an issuer's identity 242 | verification scheme. 243 | 244 | #### BaseReward 245 | 246 | The `BaseReward` variable is computed as `sybil_fund / member_count` where: 247 | 248 | - `sybil_fund`: the amount available in the sybil-fund to pay for sybil proofs 249 | - `member_count`: the number of members in the pool. 250 | 251 | For a pool which has had zero sybil proofs, this value is equal to the portion 252 | of the issuance fee that goes into the sybil fund. As more sybil-proofs occur, 253 | this value drops towards a lower bound of zero. 254 | 255 | #### Proof Size Multiplier 256 | 257 | The `ProofSizeMultiplier` variable is computed as `1 - 1 / proof_size` where 258 | `proof_size` is the number of identities participating in the proof. 259 | 260 | This multiplier starts at `0.5` and approaches `1` asymptotically as the number 261 | of participants increases. This incentivises larger proof sizes while making 262 | the incremental value of each successive proof size drop quickly. 263 | 264 | #### Pool State Multiplier 265 | 266 | The `PoolStateMultiplier` variable is computed as 267 | 268 | `1 - min(total_sybil_accounts, member_count) / member_count` 269 | 270 | where: 271 | 272 | - `total_sybil_accounts` is the number of accounts that have been destroyed in 273 | sybil proofs who's number of participants was greater than or equal to the 274 | number of participants in this proof. 275 | - `member_count` is the number of members currently in the pool. 276 | 277 | This multiplier starts at `1` for the first sybil-proof for each number of 278 | participants, and then drops linearly towards zero for each successive proof of 279 | the same size. 280 | 281 | Each time someone successfully submits a sybil-proof of a new higher value, 282 | they are again rewarded 100% of the bonus. 283 | 284 | ## Deriving Uniqueness 285 | 286 | TODO: this needs to be worked out. 287 | 288 | Odin does not provide a single source of identity uniqueness verification, but 289 | rather provides a network through which anyone wishing to verify the uniqueness 290 | for an identity can query. 291 | 292 | Each entity that wishes to know about the uniqueness of an individual will 293 | likely select a set of pools from which they choose to trust and not allow 294 | registration from identities which are not registered with one or more of the 295 | pools. 296 | 297 | ## Bad Behavior 298 | 299 | ### Puppet Pools 300 | 301 | An entity could set up a pool, and register many accounts with it, while in 302 | reality, the entire pool and the identities registered with it are really 303 | controlled by a single individual or entity. 304 | 305 | Odin provides direct no protection against this sort of attack since Odin is 306 | not in the business of trust. For this puppet pool to be useful in any way, it 307 | would need to convince others to use it as a source of trust which while not 308 | impossible, is very unlikely. 309 | -------------------------------------------------------------------------------- /contracts/Identity.sol: -------------------------------------------------------------------------------- 1 | contract IdentityHub { 2 | } 3 | 4 | 5 | contract Pool { 6 | uint public identityFee; 7 | 8 | function Pool(uint identityFee) { 9 | if (identityFee < 1 ether) { 10 | // The minimum fee needs to be sufficiently large 11 | // to motivate attackers to exploit weaknesses, but 12 | // still small enough that it is easy for any 13 | // individual to pay. This should probably be it's own 14 | // sub-currency (SybilCoin?) 15 | throw; 16 | } 17 | identityFee = identityFee; 18 | } 19 | 20 | // counter for giving identity requests id's 21 | uint requestCounter; 22 | 23 | struct IdentityRequest { 24 | // Unique Identifier 25 | uint id; 26 | 27 | // Owner 28 | address owner; 29 | 30 | // Fee 31 | uint fee; 32 | 33 | // Timestamps 34 | uint requestedAt; 35 | uint acceptedAt; 36 | uint rejectedAt; 37 | 38 | // Identity contract 39 | Identity identity; 40 | } 41 | 42 | // Running total of the number of identities in this pool. 43 | uint public identityCount; 44 | 45 | struct Identity { 46 | // Unique Identifier 47 | bytes32 id; 48 | 49 | // The id of the IdentityRequest which resulted in the creation 50 | // of this identity. 51 | uint requestId; 52 | 53 | // Identity owner 54 | address owner; 55 | 56 | // Timestamps 57 | uint createdAt; 58 | 59 | // The id of the proof which destroys this identity. 60 | uint proofId; 61 | } 62 | 63 | // Stores mapping of requestId to IdentityRequest 64 | mapping (uint => IdentityRequest) requests; 65 | 66 | // Stores whether an identity id has already been issued. 67 | mapping (bytes32 => bool) issued_ids; 68 | 69 | // Mapping from identity id to Identity contract. 70 | mapping (bytes32 => Identity) identities; 71 | 72 | // Mapping of addresses to identities. 73 | mapping (address => bytes32) addr_to_identity; 74 | 75 | function requestIdentity() { 76 | if (msg.value < identityFee) { 77 | msg.sender.send(msg.value); 78 | return; 79 | } 80 | 81 | var request = requests[requestCounter]; 82 | 83 | request.id = requestCounter; 84 | request.owner = msg.sender; 85 | request.fee = msg.value; 86 | request.requestedAt = now; 87 | 88 | requestCounter++; 89 | } 90 | 91 | function acceptRequest(uint requestId, bytes32 identityId) { 92 | var request = requests[requestId]; 93 | 94 | // Validation 95 | if (request.id != requestId) { 96 | return; 97 | } 98 | if (request.rejectedAt > 0) { 99 | // Already rejected. 100 | return; 101 | } 102 | if (request.acceptedAt > 0) { 103 | // Already accepted 104 | return; 105 | } 106 | if (issued_ids[identityId]) { 107 | // An identity already exists for this pool with the 108 | // provided identityId. 109 | return; 110 | } 111 | 112 | issued_ids[identityId] = true; 113 | 114 | addr_to_identity[request.owner] = identityId; 115 | 116 | request.acceptedAt = now; 117 | request.identity = identities[identityId]; 118 | 119 | request.identity.id = identityId; 120 | request.identity.owner = request.owner 121 | request.identity.requestId = requestId; 122 | request.identity.createdAt = now; 123 | } 124 | 125 | function rejectRequest(uint requestId) { 126 | var request = request[requestId]; 127 | 128 | // Validation 129 | if (request.id != requestId) { 130 | // Invalid request id. 131 | return; 132 | } 133 | if (request.acceptedAt > 0) { 134 | // Already accepted 135 | return; 136 | } 137 | if (request.rejectedAt > 0) { 138 | // Already rejected. 139 | return; 140 | } 141 | // Send back their ether. 142 | request.rejectedAt = now; 143 | 144 | // TODO: the requester should not get all of their money back 145 | // (maybe half). The other half should potentially be 146 | // distributed back to the pool members as re-imbursment for 147 | // their fee. This reimbursment should be accounted for in the 148 | // event that the member participates in a proof.. 149 | request.owner.gas(msg.gas)(value) 150 | } 151 | 152 | /* 153 | * Sybil proofs 154 | */ 155 | uint proofCounter; 156 | 157 | // Window of time for each proof that secondary identities are allowed 158 | // to join the proof. 159 | uint constant PROOF_ENROLLMENT_WINDOW = 60 minutes; 160 | uint constant PROOF_CLAIM_WINDOW = 60 minutes; 161 | uint constant PROOF_DEPOSIT = 1 ether; 162 | 163 | struct Proof { 164 | // Identifier 165 | uint id; 166 | 167 | // The identity id that initiated this proof 168 | bytes32 primaryIdentityId; 169 | 170 | // The additional identities that are participating in the 171 | // proof. 172 | bytes32[] secondaryIdentityIds; 173 | 174 | // Each participating identity must put down a deposit so that 175 | // they have something at stake. 176 | uint deposit; 177 | 178 | // Timestamps 179 | uint createdAt; 180 | 181 | // Identities that have claimed the proof reward 182 | bytes32[] claims; 183 | } 184 | 185 | mapping (uint => Proof) proofs; 186 | mapping (bytes32 => uint) identity_to_proof; 187 | 188 | function initiateProof() { 189 | bytes32 identityId = addr_to_identity[msg.sender]; 190 | if (identityId == 0x0) { 191 | // This address does not have any identity assiciated 192 | // with it. 193 | return; 194 | } 195 | 196 | if (msg.value < PROOF_DEPOSIT) { 197 | // Insufficient deposit so send it back. 198 | msg.sender.gas(msg.gas)(value); 199 | return; 200 | } 201 | 202 | var identity = identities[identityId]; 203 | 204 | if (identity.proofId != 0) { 205 | // This identity has already participated in a proof 206 | // and thus cannot participate in any other proofs. 207 | return; 208 | } 209 | 210 | var proof = identity_to_proof[identityId]; 211 | 212 | if (proof.id != 0) { 213 | // Once an identity has added itself to a proof, it 214 | // cannot participate in any other proof. 215 | return; 216 | } 217 | 218 | // Increment the proof counter 219 | proofCounter += 1; 220 | 221 | // Initialize the proof. 222 | proof.id = proofCounter; 223 | proof.primaryIdentityId = identityId; 224 | proof.deposit = msg.value; 225 | proof.createdAt = now; 226 | } 227 | 228 | function joinProof(uint proofId) { 229 | bytes32 identityId = addr_to_identity[msg.sender]; 230 | if (identityId == 0x0) { 231 | // This address does not have any identity assiciated 232 | // with it. 233 | return; 234 | } 235 | 236 | if (msg.value < PROOF_DEPOSIT) { 237 | // Insufficient deposit so send it back. 238 | msg.sender.gas(msg.gas)(value); 239 | return; 240 | } 241 | 242 | var identity = identities[identityId]; 243 | 244 | if (identity.proofId != 0) { 245 | // This identity has already participated in a proof 246 | // and thus cannot participate in any other proofs. 247 | return; 248 | } 249 | 250 | var proof = identity_to_proof[identityId]; 251 | 252 | if (proof.id == 0) { 253 | // Invalid proof id 254 | return; 255 | } 256 | 257 | if (proof.createdAt + PROOF_ENROLLMENT_WINDOW < now) { 258 | // Enrollment window for proof has expired. 259 | return; 260 | } 261 | 262 | // Add the identity to the proof 263 | proof.secondaryIdentityIds.length += 1; 264 | proof.secondaryIdentityIds[proof.secondaryIdentityIds.length - 1] = identityId; 265 | } 266 | 267 | function claimReward(uint proofId) { 268 | bytes32 identityId = addr_to_identity[msg.sender]; 269 | if (identityId == 0x0) { 270 | // This address does not have any identity assiciated 271 | // with it. 272 | return; 273 | } 274 | 275 | var identity = identities[identityId]; 276 | 277 | if (identity.proofId != 0) { 278 | // This identity has already participated in a proof 279 | // and thus cannot participate in any other proofs. 280 | return; 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /sybil.py: -------------------------------------------------------------------------------- 1 | import math 2 | import decimal 3 | import collections 4 | import random 5 | 6 | 7 | p = { 8 | 'member_count': 1000, 9 | 'deposit_size': 1, 10 | 'account_balance': 1000, 11 | 'proofs': collections.defaultdict(int), 12 | } 13 | 14 | 15 | class Pool(object): 16 | def __init__(self, member_count, deposit_size=1, account_balance=None, proofs=None): 17 | if account_balance is None: 18 | account_balance = member_count * deposit_size 19 | 20 | if proofs is None: 21 | proofs = collections.defaultdict(int) 22 | 23 | self.member_count = member_count 24 | self.deposit_size = deposit_size 25 | self.account_balance = account_balance 26 | self.proofs = proofs 27 | 28 | def get_effective_deposit(self): 29 | """ 30 | The deposit amount as derived from the account balance. As a pool gets 31 | more sybil proofs, the effective deposit goes down, thus reducing the 32 | reward for attacking. 33 | """ 34 | return self.account_balance * 1.0 / self.member_count 35 | 36 | def get_max_proof_size(self): 37 | """ 38 | When a pool is really small, it isn't interesting to see that people 39 | can get 1000's of accounts. Hence we keep this number small with 40 | respect to the total pool size. 41 | 42 | 10 : 3 43 | 100 : 10 44 | 1,000 : 31 45 | 10,000 : 100 46 | 100,000 : 316 47 | 1,000,000 : 1000 48 | """ 49 | return max(2, int(math.sqrt(self.member_count))) 50 | 51 | def get_base_bonus(self, proof_size): 52 | """ 53 | Profit goes up as proof 54 | """ 55 | ps = int(min(proof_size, self.get_max_proof_size())) 56 | return self.get_effective_deposit() * (ps - 1) ** 2 / (ps) 57 | 58 | def get_total_proofs(self, proof_size): 59 | return sum(v for k, v in self.proofs.items() if k >= proof_size) 60 | 61 | def get_bonus_multiplier(self, proof_size): 62 | """ 63 | As a pool's number of sybil proofs goes up with respect to it's member 64 | size, the sybil proof bonus needs to go down, eventually hitting zero 65 | at a certain point. 66 | 67 | """ 68 | tp = self.get_total_proofs(proof_size) 69 | return 1 - min(tp, self.member_count) / self.member_count 70 | 71 | def get_sybil_proof_value(self, proof_size): 72 | return self.get_base_bonus(proof_size) * self.get_bonus_multiplier(proof_size) 73 | 74 | def apply_sybil_proof(self, proof_size): 75 | if self.member_count <= 0: 76 | raise ValueError 77 | value = self.get_sybil_proof_value(proof_size) 78 | if value <= 0: 79 | raise ValueError 80 | 81 | self.account_balance -= value + (proof_size * self.deposit_size) 82 | self.proofs[proof_size] += 1 83 | self.member_count -= proof_size 84 | self.member_count = max(0, self.member_count) 85 | return value 86 | 87 | def join(self): 88 | self.member_count += 1 89 | self.account_balance += self.deposit_size 90 | 91 | 92 | def simulate_pool_growth(to_size=1000000, difficulty_rating=1.01): 93 | """ 94 | Choices 95 | - attack pool (prob based on value) (success based on difficulty_rating) 96 | - join pool 97 | """ 98 | pool = Pool(0, 1) 99 | generation = 0 100 | width = int(math.ceil(math.log10(to_size))) 101 | 102 | while pool.member_count < to_size: 103 | print "Generation: {0}".format(generation) 104 | generation += 1 105 | 106 | ps_average = max([1] + pool.proofs.keys()) 107 | ps = int(random.triangular(2, min(pool.get_max_proof_size(), ps_average + 1))) 108 | 109 | if pool.member_count: 110 | attack_value = pool.get_base_bonus(ps) 111 | else: 112 | attack_value = 0 113 | p_attack = 1 - 1 / random.uniform(1, 1 + 10 * float(attack_value)) 114 | p_join = 1 - p_attack 115 | 116 | c = random.uniform(0, 1) 117 | if c < p_join: 118 | pool.join() 119 | print "{0} > member joined".format(str(pool.member_count).ljust(width)) 120 | else: 121 | success = 1 / random.uniform(1, difficulty_rating) 122 | if random.uniform(0, 1) < success: 123 | try: 124 | for _ in range(int(ps)): 125 | pool.join() 126 | value = pool.apply_sybil_proof(ps) 127 | except ValueError: 128 | continue 129 | print "{0} > sybil proof success: {1}".format( 130 | str(pool.member_count).ljust(width), 131 | decimal.Decimal(value).quantize(decimal.Decimal('1.000')), 132 | ) 133 | continue 134 | print "{0} > sybil proof failure".format(str(pool.member_count).ljust(width)) 135 | return pool 136 | --------------------------------------------------------------------------------