Skip to content

Commit

Permalink
added initial comments, fixed up story (#25)
Browse files Browse the repository at this point in the history
* added initial comments, fixed up story

* added emojis to functions

* revert signed stats since this is handled in a seperate pr

* small additions

* remove mention of dynamic UI

* changed redeem to get

* tpp to ttp

---------

Co-authored-by: filip <[email protected]>
  • Loading branch information
venoivankovic and FilipHarald authored Sep 4, 2023
1 parent 54f33f8 commit 5672487
Show file tree
Hide file tree
Showing 9 changed files with 60 additions and 42 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Scaffold-ETH Noir

Sandbox project for testing age-restricted contracts using [Noir](https://noir-lang.org/) for writing ZKP-circuits. Also has *basic* dynamic UI for experimenting with circuits and proof-generation. This was built using Scaffold-ETH 2, [refer to SE2 README for set-up](https://github.com/scaffold-eth/scaffold-eth-2#readme).
This is a sandbox educational project for testing age-restricted contracts using [Noir](https://noir-lang.org/), a Domain Specific Language for writing ZKP-circuits. This project was built using Scaffold-ETH 2, [refer to SE2 README for set-up](https://github.com/scaffold-eth/scaffold-eth-2#readme).

# Prerequisites
* requires [nargo](https://noir-lang.org/dev/getting_started/nargo_installation) (tested with v0.10.1)
* requires [node] (https://nodejs.org/en) (tested with v18.8.0)
* requires [yarn] (https://yarnpkg.com/getting-started/install) (tested with 3.2.3)

# Inspiration
- Age proof circuit from "[noir by example](https://noir-by-example.org/gadgets/zk-age-verification/)"
Expand All @@ -14,4 +17,4 @@ Sandbox project for testing age-restricted contracts using [Noir](https://noir-l
- If we instead wanted to make an age restricted contract, but checking "older than", what would we need to change?
- How would you change the contract so that it's possible for the balloon store to have multiple trusted third parties?
- Try to create the proof manually using `nargo prove` and use that proof to call the contract. Does it work?
- What happens if Alice, instead of redeeming her balloon, shares her privateKey with Charlie who is 14 y/o, is there something stopping him from getting a free balloon? What could we change or add to this implementation to prevent that from happening?
- What happens if Alice, instead of getting her balloon token, shares her privateKey with Charlie who is 14 y/o, is there something stopping him from getting a free balloon? What could we change or add to this implementation to prevent that from happening?
3 changes: 2 additions & 1 deletion packages/hardhat/contracts/BalloonVendor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ contract BalloonVendor is Ownable {
emit BuyTokens(msg.sender, msg.value, amountOfTokens);
}

function redeemFreeToken(bytes calldata proof) public onlyKids(proof) {
// This function gets the actual balloon 🎈 NFT
function getFreeToken(bytes calldata proof) public onlyKids(proof) {
// TODO: only allow once per address
balloonToken.transfer(msg.sender, 1);
emit FreeToken(msg.sender, 1);
Expand Down
8 changes: 4 additions & 4 deletions packages/hardhat/test/BalloonVendor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ describe("BalloonVendor", function () {
expect(await balloonToken.balanceOf(addr1.address)).to.equal(100);
});
});
describe("Redeeming tokens", function () {
it("should be able to redeem for free as a kid", async function () {
await balloonVendor.connect(addr1).redeemFreeToken(validProof);
describe("Getting tokens", function () {
it("should be able to get for free as a kid", async function () {
await balloonVendor.connect(addr1).getFreeToken(validProof);
expect(await balloonToken.balanceOf(addr1.address)).to.equal(1);
});

it("should not work with invalid proof", async function () {
try {
await balloonVendor.connect(addr1).redeemFreeToken(invalidProof);
await balloonVendor.connect(addr1).getFreeToken(invalidProof);
} catch (e: any) {
expect(e.message).to.contain("PROOF_FAILURE");
}
Expand Down
34 changes: 17 additions & 17 deletions packages/nextjs/generated/deployedContracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const contracts = {
name: "localhost",
contracts: {
BalloonToken: {
address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0",
address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
abi: [
{
inputs: [],
Expand Down Expand Up @@ -285,7 +285,7 @@ const contracts = {
],
},
BalloonVendor: {
address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
address: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
abi: [
{
inputs: [
Expand Down Expand Up @@ -455,6 +455,19 @@ const contracts = {
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "bytes",
name: "proof",
type: "bytes",
},
],
name: "getFreeToken",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "owner",
Expand All @@ -468,19 +481,6 @@ const contracts = {
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "bytes",
name: "proof",
type: "bytes",
},
],
name: "redeemFreeToken",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "renounceOwnership",
Expand Down Expand Up @@ -563,7 +563,7 @@ const contracts = {
],
},
VerifierLessThanSignedAge: {
address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6",
abi: [
{
inputs: [],
Expand Down Expand Up @@ -646,7 +646,7 @@ const contracts = {
],
},
YourContract: {
address: "0x5FbDB2315678afecb367f032d93F642f64180aa3",
address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
abi: [
{
inputs: [
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/hooks/noir/useProofGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function getPublicInputsLength(parameters: CircuitAbiParameters) {
}, 0);
}

// This function generates the proof ✅
export const generateProof = async (circuitName: CircuitName, parsedArgs: ParsedArgs) => {
isGeneratingProof = true;
const noir = new NoirBrowser();
Expand Down
18 changes: 13 additions & 5 deletions packages/nextjs/pages/example-zk/AgeRestrictedContractExecutor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const AgeRestrictedContractExecutor = () => {

const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "BalloonVendor",
functionName: "redeemFreeToken",
functionName: "getFreeToken",
args: [proof],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
Expand All @@ -18,22 +18,30 @@ export const AgeRestrictedContractExecutor = () => {
return (
<div className="grid grid-cols-2 gap-6 max-w-7xl">
<div>
<h1 className="text-3xl font-bold">Step 3: Getting the balloon🎈 NFT</h1>
<p>
The ballon store is using the same <CodeText text="TokenVendor.sol" /> as the{" "}
The ballon store is using the same <CodeText text="TokenVendor.sol" /> contract as the{" "}
<a className="link" href="https://speedrunethereum.com/challenge/token-vendor">
Speedrun Ethereum challange
</a>
, with some additions. They&apos;ve added a function <CodeText text="redeemFreeToken" />, with the{" "}
, with some additions. They&apos;ve added a function <CodeText text="getFreeToken" />, with the{" "}
<CodeText text="onlyKids" />
-modifier. The modifier constructs the public inputs and calls the proof-verifier (
-modifier. This implementation can be found in{" "}
<a href="https://github.com/Kryha/scaffold-eth-2-noir/blob/main/packages/hardhat/contracts/BalloonVendor.sol">
<CodeText text="packages/hardhat/contracts/BalloonVendor.sol" />
</a>{" "}
in our project.
<br />
The modifier constructs the public inputs and calls the proof-verifier in (
<a href="https://github.com/Kryha/scaffold-eth-2-noir/blob/main/packages/hardhat/contracts/verifiers/LessThanSignedAge.sol">
<CodeText text="packages/hardhat/contracts/verifiers/LessThanSignedAge.sol" />
</a>
). The public inputs is part of the information that was used to generate the proof. They are needed to show
what we are actually proving.
</p>
<p>
Now Alice gets a balloon🎈 <strong>token</strong>, that she can redeem at the store to get an actual ballloon.
Now that Alice has received a balloon <strong>token</strong>, she can redeem that digital token at the store
to get the actual ballloon.
</p>
</div>
<div>
Expand Down
17 changes: 10 additions & 7 deletions packages/nextjs/pages/example-zk/BirthDateSignature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AddressInput } from "~~/components/scaffold-eth/Input/AddressInput";
import { useBirthYearProofsStore } from "~~/services/store/birth-year-proofs";
import { notification } from "~~/utils/scaffold-eth";

// Hardcoded Trusted Third Party(TTP) private key
const THIRD_PARTY_PRIVATE_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";

type TForm = {
Expand All @@ -20,6 +21,7 @@ const getInitialFormState = (aliceDefaultAge: number): TForm => ({
thirdPartyPrivateKey: THIRD_PARTY_PRIVATE_KEY,
});

// This function is called when the TTP 🏛 generates the signature 📜
export const signBirthYear = async (form: TForm) => {
const { personEthereumAddress, birthYear, thirdPartyPrivateKey } = form;
const claimHash = ethers.utils.solidityKeccak256(["address", "uint16"], [personEthereumAddress, birthYear]);
Expand Down Expand Up @@ -62,15 +64,15 @@ export const BirthDateSignature = ({ aliceDefaultAge }: { aliceDefaultAge: numbe
return (
<div className="grid grid-cols-2 gap-6 max-w-7xl">
<div>
<h1 className="text-3xl font-bold">Step 1: Town Hall 🏛 generates the signature 📜</h1>
<p>
Alice recognizes that, in order for her to not having to share her age with the balloon store, she at least
has to share her age with a third party that the balloon store also can trust. In this case, the balloon store
has selected the Town Hall to be the trusted third party🏛. Alice accepts that she has to share her age with
the Town Hall.
Alice recognizes that, in order for her to not have to share her age with the balloon store, she at least has
to share her age with a trusted third party. In this case, the balloon store has selected the Town Hall to be
the trusted third party🏛. Alice accepts the fact that she has to share her age with the Town Hall.
</p>
<p>
When the balloon store implemented their zero knowledge proof solution they made sure that they are using the
same format as the Town Hall for constructing the claim that is being signed📜. In this project the claim
same format as the Town Hall for constructing the claim that is being signed. In this project the claim
construction can be found in{" "}
<a href="https://github.com/Kryha/scaffold-eth-2-noir/blob/main/packages/nextjs/pages/example-zk/BirthDateSignature.tsx">
<CodeText text="packages/nextjs/pages/example-zk/BirthDateSignature.tsx" />
Expand All @@ -84,10 +86,11 @@ export const BirthDateSignature = ({ aliceDefaultAge }: { aliceDefaultAge: numbe
<CodeText text="construct_claim_payload" />) when implemented as a Noir circuit.
</p>
<p>
What the Town Hall actually signs is that they confirm that Alice is born on a certain year AND that she has
What the Town Hall actually signs is that they confirm that Alice is born in a certain year AND that she has
control over a certain Ethereum address. The check of Alice&apos;s Ethereum address is not done in this
example. The code for producing the signature currently includes the Town Hall&apos;s hardcoded private key.
This can be improved in many ways, but at a minium it should be provided to the UI by a Town Hall employee.
This can be improved in many ways, but at a minimum it should be the Towna Hall eployee that provides it to
the UI.
</p>
</div>
<div>
Expand Down
10 changes: 6 additions & 4 deletions packages/nextjs/pages/example-zk/GenerateProof.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,21 @@ export const GenerateProof = ({ requiredBirthYear }: { requiredBirthYear: number
return (
<div className="grid grid-cols-2 gap-6 max-w-7xl">
<div>
<h1 className="text-3xl font-bold">Step 2: Generating the proof ✅</h1>
<p>
One of the reasons that Alice knows that she is not sharing her birth year with anyone is that the proof
generation is open source, and she herself can double check the code. Furthermore she can even generate the
proof herself locally. This is actually what we are doing in this implementation.
proof herself locally. This is actually what we are doing in this implementation.
</p>
<p>
The proof is generated in the browser in the file{" "}
<a href="https://github.com/Kryha/scaffold-eth-2-noir/blob/main/packages/nextjs/utils/noir/noirBrowser.ts">
<CodeText text="packages/nextjs/utils/noir/noirBrowser.ts" />
</a>
. In this file you can see that the proof is generated by importing the <CodeText text="aztec/bb.js" /> and{" "}
<CodeText text="noir-lang/acvm_js" /> libraries. The proof can also be generated locally, with calling{" "}
<CodeText text="nargo prove" />. The predefined circuit ABI code used to generate the proof can be found in{" "}
. In this file you can see that the proof is generated by using the imported libraries{" "}
<CodeText text="aztec/bb.js" /> and <CodeText text="noir-lang/acvm_js" />. The proof can also be generated
locally, with calling <CodeText text="nargo prove" />. The predefined circuit ABI code used to generate the
proof can be found in{" "}
<a href="https://github.com/Kryha/scaffold-eth-2-noir/blob/main/packages/nextjs/generated/circuits.json">
<CodeText text="packages/nextjs/generated/circuits.json" />
</a>
Expand Down
4 changes: 2 additions & 2 deletions packages/nextjs/pages/failing-zk/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const ExampleUI: NextPage = () => {
const [proof, setProof] = useState<string>("");
const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "BalloonVendor",
functionName: "redeemFreeToken",
functionName: "getFreeToken",
args: [proof as `0x${string}`],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
Expand All @@ -25,7 +25,7 @@ const ExampleUI: NextPage = () => {

const { writeAsync: writeAsyncHardcoded, isLoading: isLoadingHardcoded } = useScaffoldContractWrite({
contractName: "BalloonVendor",
functionName: "redeemFreeToken",
functionName: "getFreeToken",
args: [WORKING_PROOF as `0x${string}`],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
Expand Down

0 comments on commit 5672487

Please sign in to comment.