diff --git a/packages/contracts/src/ProofOfTwitter.sol b/packages/contracts/src/ProofOfTwitter.sol index 9d5372b..8e10a7e 100644 --- a/packages/contracts/src/ProofOfTwitter.sol +++ b/packages/contracts/src/ProofOfTwitter.sol @@ -26,25 +26,38 @@ contract ProofOfTwitter is ERC721Enumerable { Verifier public immutable verifier; mapping(uint256 => string) public tokenIDToName; + mapping(string => uint256) public nameToTokenID; + mapping(bytes32 => uint8) public publishedProofs; constructor(Verifier v, DKIMRegistry d) ERC721("VerifiedEmail", "VerifiedEmail") { verifier = v; dkimRegistry = d; } + function tokenActive(uint256 tokenId) public view returns(bool) { + if(tokenId == 0) return false; + return nameToTokenID[tokenIDToName[tokenId]] == tokenId; + } + function tokenDesc(uint256 tokenId) public view returns (string memory) { string memory twitter_username = tokenIDToName[tokenId]; address address_owner = ownerOf(tokenId); - string memory result = string( - abi.encodePacked("Twitter username", twitter_username, "is owned by", StringUtils.toString(address_owner)) - ); + bool active = tokenActive(tokenId); + string memory result = string(abi.encodePacked( + "Twitter username ", + twitter_username, + " is owned by ", + StringUtils.toString(address_owner), + active ? " (active)" : " (inactive)" + )); return result; } function tokenURI(uint256 tokenId) public view override returns (string memory) { string memory username = tokenIDToName[tokenId]; address owner = ownerOf(tokenId); - return NFTSVG.constructAndReturnSVG(username, tokenId, owner); + bool active = tokenActive(tokenId); + return NFTSVG.constructAndReturnSVG(username, tokenId, owner, active); } function _domainCheck(uint256[] memory headerSignals) public pure returns (bool) { @@ -77,7 +90,12 @@ contract ProofOfTwitter is ERC721Enumerable { bytes32 dkimPublicKeyHashInCircuit = bytes32(signals[pubKeyHashIndexInSignals]); require(dkimRegistry.isDKIMPublicKeyHashValid(domain, dkimPublicKeyHashInCircuit), "invalid dkim signature"); - // Veiry RSA and proof + // Ensure every email is unique + bytes32 proofHash = keccak256(abi.encodePacked(proof)); + require(publishedProofs[proofHash] == 0, "duplicate proof hash"); + publishedProofs[proofHash] = 1; + + // Verify RSA and proof require( verifier.verifyProof( [proof[0], proof[1]], @@ -106,6 +124,8 @@ contract ProofOfTwitter is ERC721Enumerable { bytesInPackedBytes ); tokenIDToName[tokenId] = messageBytes; + // Latest mint for this username + nameToTokenID[messageBytes] = tokenId; _mint(msg.sender, tokenId); tokenCounter = tokenCounter + 1; } diff --git a/packages/contracts/src/Verifier.sol b/packages/contracts/src/Verifier.sol index e052349..fa41f1d 100644 --- a/packages/contracts/src/Verifier.sol +++ b/packages/contracts/src/Verifier.sol @@ -194,31 +194,31 @@ contract Verifier { 8495653923123431417604973247489272438418190587263600148770280649306958101930] ); vk.delta2 = Pairing.G2Point( - [15689642542677967497134067368563531820480674004982824717576658572626718466391, - 16652529577953158133724874987569713515723562698809526747487443883069189795186], - [19578730506967077591463930942667211516774284300936444243947842127193250517416, - 4536552593120443892406677780742987401638542379716848088897788244621510231909] + [19900105261107285259861926748615364823776209902130541753343670525116816827504, + 16871687104510690421087365581937594117753817545276340064727970298257710735832], + [2686163927303010603966877155034843015757997914642430723035480608695298916263, + 17605625178543043083326720077778234031995299983790929374626868288625263534687] ); vk.IC = new Pairing.G1Point[](4); vk.IC[0] = Pairing.G1Point( - 464672216472717123175377621582035874032903571141497045787672358740099702636, - 20700013873146247057744585053994320915957604504495190567552889970740327423450 + 4166669233397695515944494590165634902078260599849840640489629222612849307491, + 3292201350172082119664217774896570865578162752250558652161288549649774233258 ); vk.IC[1] = Pairing.G1Point( - 13824374510819511408860558084354215108767589941171681109671363643434597826875, - 15893411466783354082371940868796318103641892426261514820087952808965898692618 + 17129283500326630952836428737211591031180924811058018660180138013723053718173, + 18184689325155826029727301333419936323694568085647753330355942133799391435336 ); vk.IC[2] = Pairing.G1Point( - 10354183383754243095032574416437126834039711592942176439470816356391149733442, - 8280441816900579725617590923875608072852937903891725502052259255852267454502 + 21718565147698610966456336174729990144088765439767663935672312532730009013150, + 9225301756330698176793237129606080622214629819256186354857656311466006516664 ); vk.IC[3] = Pairing.G1Point( - 5120551512096972305685873367094652128645340059150503559752829122995980189160, - 11532774489310909699279135714435020872521488641996532910371632179306654530330 + 6264516697026139371207665779745221041583355410231373690453627960130582044858, + 3595306340455641100361287360760270183261207227330892439864772362872545861864 ); } diff --git a/packages/contracts/src/utils/NFTSVG.sol b/packages/contracts/src/utils/NFTSVG.sol index 732a6e7..0631c90 100644 --- a/packages/contracts/src/utils/NFTSVG.sol +++ b/packages/contracts/src/utils/NFTSVG.sol @@ -15,6 +15,7 @@ library NFTSVG { struct SVGParams { string username; + bool active; uint256 tokenId; string color0; string color1; @@ -33,7 +34,7 @@ library NFTSVG { abi.encodePacked( generateSVGDefs(params), generateSVGBorderText(params.username), - generateSVGCardMantle(params.username), + generateSVGCardMantle(params.username, params.active), generateSVGLogo(), "" ) @@ -139,17 +140,19 @@ library NFTSVG { ); } - function generateSVGCardMantle(string memory username) private pure returns (string memory svg) { + function generateSVGCardMantle(string memory username, bool active) private pure returns (string memory svg) { svg = string( abi.encodePacked( - ' ', + ' ', "My verified", - '', + '', "Twitter account", - '', + '', "is", - '', + '', username, + '', + active ? ' (active)' : ' (inactive)', "", '' ) @@ -187,13 +190,14 @@ library NFTSVG { return string(StringUtils.toHexStringNoPrefix(token >> offset, 3)); } - function constructAndReturnSVG(string memory username, uint256 tokenId, address owner) + function constructAndReturnSVG(string memory username, uint256 tokenId, address owner, bool active) internal pure returns (string memory svg) { SVGParams memory svgParams = SVGParams({ username: username, + active: active, tokenId: tokenId, color0: tokenToColorHex(uint256(uint160(owner)), 136), color1: tokenToColorHex(uint256(uint160(owner)), 136), @@ -216,6 +220,9 @@ library NFTSVG { '{"trait_type": "Name",', '"value": "', username, + '"}, {"trait_type": "Active",', + '"value": "', + active ? "true" : "false", '"}, {"trait_type": "Owner",', '"value": "', StringUtils.toHexString(uint256(uint160(owner)), 42), diff --git a/packages/contracts/test/TestTwitter.t.sol b/packages/contracts/test/TestTwitter.t.sol index d3e2e6b..6958370 100644 --- a/packages/contracts/test/TestTwitter.t.sol +++ b/packages/contracts/test/TestTwitter.t.sol @@ -9,8 +9,6 @@ import "../src/Verifier.sol"; contract TwitterUtilsTest is Test { using StringUtils for *; - address constant VM_ADDR = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; // Hardcoded address of the VM from foundry - Verifier proofVerifier; DKIMRegistry dkimRegistry; ProofOfTwitter testVerifier; @@ -90,35 +88,35 @@ contract TwitterUtilsTest is Test { // These proof and public input values are generated using scripts in packages/circuits/scripts/generate-proof.ts // The sample email in `/emls` is used as the input, but you will have different values if you generated your own zkeys - function testVerifyTestEmail() public { - uint256[3] memory publicSignals; - publicSignals[ - 0 - ] = 1983664618407009423875829639306275185491946247764487749439145140682408188330; - publicSignals[1] = 131061634216091175196322682; - publicSignals[2] = 1163446621798851219159656704542204983322218017645; + function proofTestData() internal view returns ( + uint256[3] memory publicSignals, + uint256[8] memory proof + ) { + publicSignals[0] = 1983664618407009423875829639306275185491946247764487749439145140682408188330; + publicSignals[1] = 60688095039584876602025332; + publicSignals[2] = 939406481697058082851001177880059329846108047162; uint256[2] memory proof_a = [ - 5797457318420687771988333280962152259257379892303951979169813170317326477434, - 14189472520472776516417665921077060465051105690711006171274266938697420566951 + 2009445536733820940614696809993322277245951542303198989655358849969062470372, + 8816577960801104870014601849299786208491980694496377614657829475846590189044 ]; // Note: you need to swap the order of the two elements in each subarray uint256[2][2] memory proof_b = [ [ - 18921035250897022958148917928657494416170154529165080398233299677407236026846, - 7543904973418857428529380479194238699124092071535155780217645796569464525390 + 14855789773713162959395469568085738009479688945606671646123078952384187715749, + 5537551629211653307129736243267704849501872155474305257425810771375879194045 ], [ - 16835983125386052464761616884519063200215669738277458297351574243466146108017, - 16210421528119385263780767241818749780020239542889025688358560426656253630309 + 1132186781256405271827663020181423082108968049637269513640889795929913374755, + 20283187854758064375389662408752458008801719101365442241161112666095010479777 ] ]; uint256[2] memory proof_c = [ - 19160114768014303520076125815800143167812482606052748549955911430674608929788, - 18614452123455216414192085875877133967969502306927521502651735939542857695693 + 5044973743340357316712989815484977055865277059347265143314644500926851858180, + 11111706666208986243818708247898127453788585043016564128696584275645826038016 ]; - uint256[8] memory proof = [ + proof = [ proof_a[0], proof_a[1], proof_b[0][0], @@ -137,9 +135,65 @@ contract TwitterUtilsTest is Test { publicSignals ); assertEq(verified, true); + } + + // Need two proofs for the same account to test inactivity + function proofTestData2() internal view returns ( + uint256[3] memory publicSignals, + uint256[8] memory proof + ) { + publicSignals[0] = 1983664618407009423875829639306275185491946247764487749439145140682408188330; + publicSignals[1] = 60688095039584876602025332; + publicSignals[2] = 939406481697058082851001177880059329846108047162; + + uint256[2] memory proof_a = [ + 7799039678913605710259821229942352464082220364020014946144130184928336196865, + 12130394184898533762274334952424785094770793233409037588953920933439195731442 + ]; + // Note: you need to swap the order of the two elements in each subarray + uint256[2][2] memory proof_b = [ + [ + 20482212462660099379624491309707025656117065161929921230598395801723983742613, + 6090631549061757191387350641326752562101523174229830619865248275166199071666 + ], + [ + 7614549401779143625809358345392036412871867684423898593516000263403520566181, + 9130965690958350324179373283450055322497354099003455526448543990473093311817 + ] + ]; + uint256[2] memory proof_c = [ + 1537938571189380464959355373094939980865238915155273757911400383684934917, + 17243504316948413489100276038947070591642744340336274718832513702927574205343 + ]; + proof = [ + proof_a[0], + proof_a[1], + proof_b[0][0], + proof_b[0][1], + proof_b[1][0], + proof_b[1][1], + proof_c[0], + proof_c[1] + ]; + + // Test proof verification + bool verified = proofVerifier.verifyProof( + proof_a, + proof_b, + proof_c, + publicSignals + ); + assertEq(verified, true); + } + + function proofTestUsername() public pure returns (string memory) { + return "test_zk9432"; + } + + function testVerifyTestEmail() public { + (uint256[3] memory publicSignals, uint256[8] memory proof) = proofTestData(); // Test mint after spoofing msg.sender - Vm vm = Vm(VM_ADDR); vm.startPrank(0x0000000000000000000000000000000000000001); testVerifier.mint(proof, publicSignals); vm.stopPrank(); @@ -152,6 +206,62 @@ contract TwitterUtilsTest is Test { assert(bytes(svgValue).length > 0); } + function testDuplicateProofHash() public { + (uint256[3] memory publicSignals, uint256[8] memory proof) = proofTestData(); + // Test mint after spoofing msg.sender + vm.startPrank(0x0000000000000000000000000000000000000001); + + testVerifier.mint(proof, publicSignals); + + vm.expectRevert("duplicate proof hash"); + testVerifier.mint(proof, publicSignals); + + vm.stopPrank(); + } + + function testInactive() public { + (uint256[3] memory publicSignals1, uint256[8] memory proof1) = proofTestData(); + (uint256[3] memory publicSignals2, uint256[8] memory proof2) = proofTestData2(); + string memory username = proofTestUsername(); + // Test mint after spoofing msg.sender + vm.startPrank(0x0000000000000000000000000000000000000001); + + // Mint first NFT + testVerifier.mint(proof1, publicSignals1); + + // TokenID 0 does not exist, will always be inactive + assertEq(testVerifier.tokenActive(0), false); + // First NFT is active + assertEq(testVerifier.tokenActive(1), true); + // Username resolves to first NFT + assertEq(testVerifier.nameToTokenID(username), 1); + // NFT resolves to username + assertEq(testVerifier.tokenIDToName(1), username); + + // Mint second NFT for the same username + testVerifier.mint(proof2, publicSignals2); + + // Both NFTs resolve to username + assertEq(testVerifier.tokenIDToName(1), username); + assertEq(testVerifier.tokenIDToName(2), username); + // Username now resolves to second NFT + assertEq(testVerifier.nameToTokenID(username), 2); + // Second NFT is now active + assertEq(testVerifier.tokenActive(2), true); + // First NFT is now inactive + assertEq(testVerifier.tokenActive(1), false); + + vm.stopPrank(); + + // TokenID 0 does not exist + vm.expectRevert(); + testVerifier.tokenDesc(0); + + // For manual verification + console.log(testVerifier.tokenDesc(1)); + console.log(testVerifier.tokenDesc(2)); + } + function testChainID() public view { uint256 chainId; assembly {