diff --git a/.circleci/config.yml b/.circleci/config.yml index e4afb837e09..a68aec2bd9f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -712,6 +712,19 @@ jobs: command: ./scripts/cond_run_script end-to-end $JOB_NAME ./scripts/run_tests_local e2e_lending_contract.test.ts working_directory: yarn-project/end-to-end + e2e-token-contract: + machine: + image: ubuntu-2004:202010-01 + resource_class: large + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: ./scripts/cond_run_script end-to-end $JOB_NAME ./scripts/run_tests_local e2e_token_contract.test.ts + working_directory: yarn-project/end-to-end + + e2e-private-token-contract: machine: image: ubuntu-2004:202010-01 @@ -1427,6 +1440,7 @@ workflows: - e2e-2-rpc-servers: *e2e_test - e2e-deploy-contract: *e2e_test - e2e-lending-contract: *e2e_test + - e2e-token-contract: *e2e_test - e2e-private-token-contract: *e2e_test - e2e-sandbox-example: *e2e_test - e2e-multi-transfer-contract: *e2e_test @@ -1461,6 +1475,7 @@ workflows: - e2e-2-rpc-servers - e2e-deploy-contract - e2e-lending-contract + - e2e-token-contract - e2e-private-token-contract - e2e-sandbox-example - e2e-multi-transfer-contract diff --git a/yarn-project/acir-simulator/src/client/private_execution.ts b/yarn-project/acir-simulator/src/client/private_execution.ts index 79b5afc1f19..2e058c86852 100644 --- a/yarn-project/acir-simulator/src/client/private_execution.ts +++ b/yarn-project/acir-simulator/src/client/private_execution.ts @@ -135,15 +135,16 @@ export class PrivateFunctionExecution { return Promise.resolve(ZERO_ACVM_FIELD); }, enqueuePublicFunctionCall: async ([acvmContractAddress], [acvmFunctionSelector], [acvmArgsHash]) => { + const selector = FunctionSelector.fromField(fromACVMField(acvmFunctionSelector)); const enqueuedRequest = await this.enqueuePublicFunctionCall( frToAztecAddress(fromACVMField(acvmContractAddress)), - FunctionSelector.fromField(fromACVMField(acvmFunctionSelector)), + selector, this.context.packedArgsCache.unpack(fromACVMField(acvmArgsHash)), this.callContext, ); this.log( - `Enqueued call to public function (with side-effect counter #${enqueuedRequest.sideEffectCounter}) ${acvmContractAddress}:${acvmFunctionSelector}`, + `Enqueued call to public function (with side-effect counter #${enqueuedRequest.sideEffectCounter}) ${acvmContractAddress}:${selector}`, ); enqueuedPublicFunctionCalls.push(enqueuedRequest); return toAcvmEnqueuePublicFunctionResult(enqueuedRequest); diff --git a/yarn-project/acir-simulator/src/public/executor.ts b/yarn-project/acir-simulator/src/public/executor.ts index 7c7ce906dd3..4d8fe6da158 100644 --- a/yarn-project/acir-simulator/src/public/executor.ts +++ b/yarn-project/acir-simulator/src/public/executor.ts @@ -124,10 +124,11 @@ export class PublicExecutor { }, callPublicFunction: async ([address], [functionSelector], [argsHash]) => { const args = packedArgs.unpack(fromACVMField(argsHash)); - this.log(`Public function call: addr=${address} selector=${functionSelector} args=${args.join(',')}`); + const selector = FunctionSelector.fromField(fromACVMField(functionSelector)); + this.log(`Public function call: addr=${address} selector=${selector} args=${args.join(',')}`); const childExecutionResult = await this.callPublicFunction( frToAztecAddress(fromACVMField(address)), - FunctionSelector.fromField(fromACVMField(functionSelector)), + selector, args, execution.callContext, globalVariables, diff --git a/yarn-project/aztec-rpc/src/simulator_oracle/index.ts b/yarn-project/aztec-rpc/src/simulator_oracle/index.ts index cfdeccff424..52737d91a42 100644 --- a/yarn-project/aztec-rpc/src/simulator_oracle/index.ts +++ b/yarn-project/aztec-rpc/src/simulator_oracle/index.ts @@ -48,7 +48,7 @@ export class SimulatorOracle implements DBOracle { async getAuthWitness(messageHash: Fr): Promise { const witness = await this.db.getAuthWitness(messageHash); - if (!witness) throw new Error(`Unknown auth witness for message hash ${messageHash.toString()}`); + if (!witness) throw new Error(`Unknown auth witness for message hash ${messageHash.toString(true)}`); return witness; } diff --git a/yarn-project/aztec.js/src/abis/schnorr_auth_witness_account_contract.json b/yarn-project/aztec.js/src/abis/schnorr_auth_witness_account_contract.json index 1339fd6d3ad..c39c83865de 100644 --- a/yarn-project/aztec.js/src/abis/schnorr_auth_witness_account_contract.json +++ b/yarn-project/aztec.js/src/abis/schnorr_auth_witness_account_contract.json @@ -15,7 +15,268 @@ } ], "returnTypes": [], - "bytecode": "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", + "bytecode": "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", + "verificationKey": "0000000200000800000000740000000f00000003515f3109623eb3c25aa5b16a1a79fd558bac7a7ce62c4560a8c537c77ce80dd339128d1d37b6582ee9e6df9567efb64313471dfa18f520f9ce53161b50dbf7731bc5f900000003515f322bc4cce83a486a92c92fd59bd84e0f92595baa639fc2ed86b00ffa0dfded2a092a669a3bdb7a273a015eda494457cc7ed5236f26cee330c290d45a33b9daa94800000003515f332729426c008c085a81bd34d8ef12dd31e80130339ef99d50013a89e4558eee6d0fa4ffe2ee7b7b62eb92608b2251ac31396a718f9b34978888789042b790a30100000003515f342be6b6824a913eb7a57b03cb1ee7bfb4de02f2f65fe8a4e97baa7766ddb353a82a8a25c49dc63778cd9fe96173f12a2bc77f3682f4c4448f98f1df82c75234a100000003515f351f85760d6ab567465aadc2f180af9eae3800e6958fec96aef53fd8a7b195d7c000c6267a0dd5cfc22b3fe804f53e266069c0e36f51885baec1e7e67650c62e170000000c515f41524954484d455449430d9d0f8ece2aa12012fa21e6e5c859e97bd5704e5c122064a66051294bc5e04213f61f54a0ebdf6fee4d4a6ecf693478191de0c2899bcd8e86a636c8d3eff43400000003515f43224a99d02c86336737c8dd5b746c40d2be6aead8393889a76a18d664029096e90f7fe81adcc92a74350eada9622ac453f49ebac24a066a1f83b394df54dfa0130000000c515f46495845445f42415345060e8a013ed289c2f9fd7473b04f6594b138ddb4b4cf6b901622a14088f04b8d2c83ff74fce56e3d5573b99c7b26d85d5046ce0c6559506acb7a675e7713eb3a00000007515f4c4f4749430721a91cb8da4b917e054f72147e1760cfe0ef3d45090ac0f4961d84ec1996961a25e787b26bd8b50b1a99450f77a424a83513c2b33af268cd253b0587ff50c700000003515f4d05dbd8623b8652511e1eb38d38887a69eceb082f807514f09e127237c5213b401b9325b48c6c225968002318095f89d0ef9cf629b2b7f0172e03bc39aacf6ed800000007515f52414e474504b57a3805e41df328f5ca9aefa40fad5917391543b7b65c6476e60b8f72e9ad07c92f3b3e11c8feae96dedc4b14a6226ef3201244f37cfc1ee5b96781f48d2b000000075349474d415f3125001d1954a18571eaa007144c5a567bb0d2be4def08a8be918b8c05e3b27d312c59ed41e09e144eab5de77ca89a2fd783be702a47c951d3112e3de02ce6e47c000000075349474d415f3223994e6a23618e60fa01c449a7ab88378709197e186d48d604bfb6931ffb15ad11c5ec7a0700570f80088fd5198ab5d5c227f2ad2a455a6edeec024156bb7beb000000075349474d415f3300cda5845f23468a13275d18bddae27c6bb189cf9aa95b6a03a0cb6688c7e8d829639b45cf8607c525cc400b55ebf90205f2f378626dc3406cc59b2d1b474fba000000075349474d415f342d299e7928496ea2d37f10b43afd6a80c90a33b483090d18069ffa275eedb2fc2f82121e8de43dc036d99b478b6227ceef34248939987a19011f065d8b5cef5c0000000010000000000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b0000000c0000000d0000000e0000000f" + }, + { + "name": "_set_is_valid_storage", + "functionType": "open", + "isInternal": true, + "parameters": [ + { + "name": "message_hash", + "type": { + "kind": "field" + }, + "visibility": "private" + }, + { + "name": "value", + "type": { + "kind": "field" + }, + "visibility": "private" + } + ], + "returnTypes": [ + { + "kind": "struct", + "path": "aztec::abi::PublicCircuitPublicInputs", + "fields": [ + { + "name": "call_context", + "type": { + "kind": "struct", + "path": "aztec::abi::CallContext", + "fields": [ + { + "name": "msg_sender", + "type": { + "kind": "field" + } + }, + { + "name": "storage_contract_address", + "type": { + "kind": "field" + } + }, + { + "name": "portal_contract_address", + "type": { + "kind": "field" + } + }, + { + "name": "is_delegate_call", + "type": { + "kind": "boolean" + } + }, + { + "name": "is_static_call", + "type": { + "kind": "boolean" + } + }, + { + "name": "is_contract_deployment", + "type": { + "kind": "boolean" + } + } + ] + } + }, + { + "name": "args_hash", + "type": { + "kind": "field" + } + }, + { + "name": "return_values", + "type": { + "kind": "array", + "length": 4, + "type": { + "kind": "field" + } + } + }, + { + "name": "contract_storage_update_requests", + "type": { + "kind": "array", + "length": 16, + "type": { + "kind": "struct", + "path": "aztec::abi::ContractStorageUpdateRequest", + "fields": [ + { + "name": "storage_slot", + "type": { + "kind": "field" + } + }, + { + "name": "old_value", + "type": { + "kind": "field" + } + }, + { + "name": "new_value", + "type": { + "kind": "field" + } + } + ] + } + } + }, + { + "name": "contract_storage_read", + "type": { + "kind": "array", + "length": 16, + "type": { + "kind": "struct", + "path": "aztec::abi::ContractStorageRead", + "fields": [ + { + "name": "storage_slot", + "type": { + "kind": "field" + } + }, + { + "name": "value", + "type": { + "kind": "field" + } + } + ] + } + } + }, + { + "name": "public_call_stack", + "type": { + "kind": "array", + "length": 4, + "type": { + "kind": "field" + } + } + }, + { + "name": "new_commitments", + "type": { + "kind": "array", + "length": 16, + "type": { + "kind": "field" + } + } + }, + { + "name": "new_nullifiers", + "type": { + "kind": "array", + "length": 16, + "type": { + "kind": "field" + } + } + }, + { + "name": "new_l2_to_l1_msgs", + "type": { + "kind": "array", + "length": 2, + "type": { + "kind": "field" + } + } + }, + { + "name": "unencrypted_logs_hash", + "type": { + "kind": "array", + "length": 2, + "type": { + "kind": "field" + } + } + }, + { + "name": "unencrypted_log_preimages_length", + "type": { + "kind": "field" + } + }, + { + "name": "block_data", + "type": { + "kind": "struct", + "path": "aztec::abi::HistoricBlockData", + "fields": [ + { + "name": "private_data_tree_root", + "type": { + "kind": "field" + } + }, + { + "name": "nullifier_tree_root", + "type": { + "kind": "field" + } + }, + { + "name": "contract_tree_root", + "type": { + "kind": "field" + } + }, + { + "name": "l1_to_l2_messages_tree_root", + "type": { + "kind": "field" + } + }, + { + "name": "blocks_tree_root", + "type": { + "kind": "field" + } + }, + { + "name": "public_data_tree_root", + "type": { + "kind": "field" + } + }, + { + "name": "global_variables_hash", + "type": { + "kind": "field" + } + } + ] + } + }, + { + "name": "prover_address", + "type": { + "kind": "field" + } + } + ] + } + ], + "bytecode": "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", "verificationKey": "0000000200000800000000740000000f00000003515f3109623eb3c25aa5b16a1a79fd558bac7a7ce62c4560a8c537c77ce80dd339128d1d37b6582ee9e6df9567efb64313471dfa18f520f9ce53161b50dbf7731bc5f900000003515f322bc4cce83a486a92c92fd59bd84e0f92595baa639fc2ed86b00ffa0dfded2a092a669a3bdb7a273a015eda494457cc7ed5236f26cee330c290d45a33b9daa94800000003515f332729426c008c085a81bd34d8ef12dd31e80130339ef99d50013a89e4558eee6d0fa4ffe2ee7b7b62eb92608b2251ac31396a718f9b34978888789042b790a30100000003515f342be6b6824a913eb7a57b03cb1ee7bfb4de02f2f65fe8a4e97baa7766ddb353a82a8a25c49dc63778cd9fe96173f12a2bc77f3682f4c4448f98f1df82c75234a100000003515f351f85760d6ab567465aadc2f180af9eae3800e6958fec96aef53fd8a7b195d7c000c6267a0dd5cfc22b3fe804f53e266069c0e36f51885baec1e7e67650c62e170000000c515f41524954484d455449430d9d0f8ece2aa12012fa21e6e5c859e97bd5704e5c122064a66051294bc5e04213f61f54a0ebdf6fee4d4a6ecf693478191de0c2899bcd8e86a636c8d3eff43400000003515f43224a99d02c86336737c8dd5b746c40d2be6aead8393889a76a18d664029096e90f7fe81adcc92a74350eada9622ac453f49ebac24a066a1f83b394df54dfa0130000000c515f46495845445f42415345060e8a013ed289c2f9fd7473b04f6594b138ddb4b4cf6b901622a14088f04b8d2c83ff74fce56e3d5573b99c7b26d85d5046ce0c6559506acb7a675e7713eb3a00000007515f4c4f4749430721a91cb8da4b917e054f72147e1760cfe0ef3d45090ac0f4961d84ec1996961a25e787b26bd8b50b1a99450f77a424a83513c2b33af268cd253b0587ff50c700000003515f4d05dbd8623b8652511e1eb38d38887a69eceb082f807514f09e127237c5213b401b9325b48c6c225968002318095f89d0ef9cf629b2b7f0172e03bc39aacf6ed800000007515f52414e474504b57a3805e41df328f5ca9aefa40fad5917391543b7b65c6476e60b8f72e9ad07c92f3b3e11c8feae96dedc4b14a6226ef3201244f37cfc1ee5b96781f48d2b000000075349474d415f3125001d1954a18571eaa007144c5a567bb0d2be4def08a8be918b8c05e3b27d312c59ed41e09e144eab5de77ca89a2fd783be702a47c951d3112e3de02ce6e47c000000075349474d415f3223994e6a23618e60fa01c449a7ab88378709197e186d48d604bfb6931ffb15ad11c5ec7a0700570f80088fd5198ab5d5c227f2ad2a455a6edeec024156bb7beb000000075349474d415f3300cda5845f23468a13275d18bddae27c6bb189cf9aa95b6a03a0cb6688c7e8d829639b45cf8607c525cc400b55ebf90205f2f378626dc3406cc59b2d1b474fba000000075349474d415f342d299e7928496ea2d37f10b43afd6a80c90a33b483090d18069ffa275eedb2fc2f82121e8de43dc036d99b478b6227ceef34248939987a19011f065d8b5cef5c0000000010000000000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b0000000c0000000d0000000e0000000f" }, { @@ -97,7 +358,285 @@ } ], "returnTypes": [], - "bytecode": "H4sIAAAAAAAA/+2dB5gUVRaFXw05ioBkEIwY6ZrAzKjoEIwYMWIkzYAJA2ACRMUESDTnnPOqq66ucY1rXOMa17jGNa5xhX1XbsvrolGgz2vqfPPq++53q4rh1Tn3Vb2uv7q66vYSYz61IVNkQ2Yb6nx2uVFiubHOu1N2uUZzWaZveXltZWltXBYPz5RWj6iqyJRXjOhbFVfFFVUVo0qryspqq8qrKqtHVFdmquPystq4rqK6rC6zcOrotJUpcPKpsxOJzs4kOruQ6OxKorMbic7uJDp7kOhclURnTxKdvUh0rkaic3USnWuQ6FyTROdaJDrXJtHZm0TnOiQ61yXRuR6JzvVJdG5AonNDEp19SHRmSHTGJDpLSXSWkegsJ9FZAdQp2uQaY09tr4ON+TY6au6kubPmLpq7au6mubvmHppX1dxTcy/Nq2leXfMamtfUvJbmtTX31ryO5nU1r6d5fc0baN5Qcx/NGc2x5lLNZZrLNVc47fW1UWkWXouVSS/Z/rbeZ99WGY59sJpE50YkOjcm0bkJic5+JDo3JdG5GYnOGhKd/Ul0DiDROZBE5yASnZuT6NzC4M+F22h7cr4n54RVmqs1b6R5Y82baO6neVPNm2mu0dxf8wDNAzUP0ry55i3MonPRLW1sZRbdF5A9F82ub2AWvz8gX50zhU1xB1xbmYaOzq01b6Oas9sYbGNbG9vZ2N7GDjZ2tLGTjSE2draxi41dbexmY3cbe9gYamNPG3vZ2NvGPjb2tbGfjWE2htsYYWOkjVE2am3U2RhtY4yN/W0cYONAGwfZONjGWBuH2DjUxmE2DrcxzsZ4GxNsHGHjSBtH2TjaxjE2JtqYZGOyjWNtTLFxnI3jbZxgY6qNE22cZONkG6fYONXGNBvTtQYznDqdpx3azCzaB7JTY2e+RnOmwMm97wTVZiPVb5xsHD/NnG02MIv7beisy/57ltFaax0aQjWXl7nbyk7J463Gmc9uv6lqkml07fj+E8aP2X3/8WNrx42LnFayLQ/M03LWdWOzqIdrIK4ypc2cyi2Nq8ZObgrVUp4RLU2WQUtTR0szqJaFe31zcJvSRgtHf9ZrVnsL59+bO95aYHXEkck96moSGrLbDf6h2w3+TfAf/Af/wX/wH/wH/8F/8B/8B//Bf/Af/Af/wX/wH/wH/8F/8B/8B//Bf/Af/Af/wX/wH/wH/zXBf/Af/Af/wX/wH/wH/8F/8B/8B//Bf/Af/Af/wX/wH/wH/8F/8B/8B//Bf/Af/Af/wf8y+2/mrGu0grW0cDQ08aalPNPC5N8PwJ4zzfJsR56WsU+0aJutwN6kzTZgHyI3+2QPmaY4vto49fOx3ZUS222V2K78TUtHwxRHa/b/NnD+5qhoUT/UOv2A7ntpY2VHe/a5J+5xn/WwsqO/LVaHPATUtMujo62jI7v9ds667Lw7JqwC1VYuDyb99emrSW2rODra63z275o5822cdR0TtZR1nRK+ZF1nnW/vrCvJs42slo7OuuyTbjo567L1ybbbRHVnl936Zduq0ZwpbCp1tWT1uJpl6uLMN0job+547uLo7IrV+etx7Opo6my3q7PdbtDtLnzOTleTO0WJ5RpnvpujpQdUy8IarOq0X+Nsw91uT+x2Y3e7kUZ2G9n1DZz5WU6Bei6a/W1/zmqWfad7nr9z57sm/k8L59+7e/bcw9FR4yxntyXH8lTHa/c8ut3P7uy/u8eJj7G6m6Mju/22znJWhzuugPfVX+vXPVG/7LLbl40S9cJrWXiulty2e16VrZd7XpU8xmGFEcMlTnuDTf1+ZF9JothpfgRjT23nNBszbcyyMdvGHBtzbcyzcbqNM2ycaeMsG2fbOMfGuTbOs3G+jQtsXGjjIhsX27jExqU2LrNxuY0rbFxp4yobV9u4xsa1Nq6zcb0WKftYS9HS1CxanplYnpVYnp1YnpNYnptYnpdYPj2xfEZi+czE8lmJ5bMTy+ckls9NLJ+XWD4/sXxBYvnCxPJFieWLE8uXJJYvTSxflli+PLF8RWL5ysTyVYnlqxPL1ySWr00sX5dYvt4sephfdsqelNVozhQ25RwzhT5q9jRgW0eWYOFqSfVbXp21dTJl4pmgtqQvZgHrd1Tq6/dr0/HswtsqVc/xHGD9jk5z/cp/0xnPLaytjOM5nges3zFprV9pjs749OVvK5PwHJ8BrN/EFNavb91iOuMzl6+tqjye47OA9ZuUtvpV5dUZn73sbVUuwXN8DrB+k9NUv8ol6ozPXba2Sn/Hc3wesH7HpqV+lb+rMz5/6dsa+Qee4wuA9ZuShvpV/qHO+MKlayuzFJ7ji4D1O25F1y+zVDrji/+4rYql9BxfAqzf8SuyfuVLrTO+9HfbKq9bBs/xZcD6nbCi6le5TDrjy5fcVtUyeo6vANZv6gqoX3XdMuuMr8zfVmY5PMdXAet3YrHrl1kunfHVi7cVL6fn+Bpg/U4qZv1GLbfO+NrctsoK8BxfB6zfyUWqX2ldQTrj6w3uWqJ7za7Q+p1SpPplCpti4HW2+Ghg/U4lqR/wOlE8EVi/aST1A17niCcD6zedpH5ATo+nAOs3g6R+QM6MjwfW7zSS+gE5KZ4KrN9MkvoBz/Pjk4D1m0VSP+B5anwKsH6zSeoHPM+KpwHrN4ekfsDzhHgGsH5zSeoH/JyLZwLrN4+kfsBxOp4NrN/pJPUDjjPxXGD9ziCpH/A4iYH7TIysXyOtW09tT645ybU2uXYn1yzlGqhc+5VryXINXa7JX2YW3p93iVl4H598Nybftcl3jPKdpXxXK9/9ynfe8h263Dsg9yLIPRhyT4fcyyL3xsg9QXKPkdxbJfdqnWZyp+Q9qIXudzcsf1uL3U9SrFeX34BrK+PqvdGZz/4YpsRZlz2WGnvwZBLbSdaxtfF447evTrrRQ7s3GdzO78v3Tfg+yhnUfdY0U+DUwCz+fnX8PluaAbYdJ1f4052J3QHlZs23OOuyv/woMblvjJcpcmorA9QC5/9FTo6cNhY4/yff30RLaMf9NXP2/7d2tBhcTTIeBtSM1wEze8e7dOB9ZtEd8Lc42zBOJ7jbLnTQuhnY1q2/U5NlbbtYn/63Gj+f/n9y5sOnf4Ft3qoFRbd7m0n3p7/4vg3fR3m1Fur/FtWKbve2lKLsYjqBnm83wH28BDvAySAm+vqb3KnAD6bF8A/5wXQLrJ6lv3vmXKjOO4D1K9YH6B3Gzwfon5358AFaYJt3aEHR7d5p0v0BKr7vxPeRV3y+E6hzSfjs48N/OTX7xOUVhuJ3ab7bWbcsKD7ALN5XSRQfYP4YxfO1E1B8ydNvKH6XU0xZvtssjuKoHwPnO4gK/fS/C6jrbuPnAEQPQneZ4gzwher8C1BnQ5N/gEfXAf0hh6yBL433GD/7E3yHutfgBo5iYcO9uLZysOGvznzAhgLbvFcLim73PpNubBDf9+H7yCs23AfUSYgNcR65dNhwv+YHnHUBGzBtFgUb7je52PCA4cKG+4G6HjB+Dm70IHS/Kc4AX6jOB4H7Kys2PEig8SHjZ3+C71APG9zAUSxseBjXVg42/M2ZD9hQYJsPa0HR7T5i0o0N4vsRfB95xYZHgDoJsaE0j1w6bHhU82POuoANmDaLgg2PmlxseMxwYcOjQF2PGT8HN3oQetQUZ4AvVOfjOJ2lrNjwOIHGJ4yf/Qm+Qz1pcANHsbDhSVxbOdjwd2c+YEOBbT6pBUW3+5RJNzaI76fwfeQVG54C6iTEhrI8cumw4WnNzzjrAjZg2iwKNjxtcrHhGcOFDU8DdT1j/Bzc6EHoaVOcAb5Qnc/idJaxYsOzBBqfM372J/gO9bzBDRzFwobncW3lYMM/nPmADQW2+bwWFN3uCybd2CC+X8D3kVdseAGokxAbyvPIpcOGFzW/5KwL2IBpsyjY8KLJxYaXDBc2vAjU9ZLxc3CjB6EXTXEG+EJ1vozTWc6KDS8TaHzF+Nmf4DvUqwY3cBQLG17FtZWDDf905gM2FNjmq1pQdLuvmXRjg/h+Dd9HXrHhNaBOQmyoyCOXDhte1/yGsy5gA6bNomDD6yYXG94wXNjwOlDXG8bPwY0ehF43xRngC9X5Jk5nBSs2vEmg8S3jZ3+C71BvG9zAUSxseBvXVg42/MuZD9hQYJtva0HR7b5j0o0N4vsdfB95xYZ3gDoJsaFvHrl02PCu5vecdQEbMG0WBRveNbnY8J7hwoZ3gbreM34ObvQg9K4pzgBfqM73cTr7smLD+wQaPzB+9if4DvWhwQ0cxcKGD3Ft5WDDv535gA0FtvmhFhTd7kcm3dggvj/C95FXbPgIqJMQGyrzyKXDho81f+KsC9iAabMo2PCxycWGTwwXNnwM1PWJ8XNwowehj01xBvhCdX6K01nJig2fEmj8zPjZn+A71OcGN3AUCxs+x7WVgw3/ceYDNhTY5udaUHS7X5h0Y4P4/gLfR16x4QugTkJsqMojlw4bvtT8lbMuYAOmzaJgw5cmFxu+MlzY8CVQ11fGz8GNHoS+NMUZ4AvV+TVOZxUrNnxNoPEb42d/gu9Q3xrcwFEsbPgW11YONvzXmQ/YUGCb32pB0e1+Z9KNDeL7O3wfecWG74A6CbGhOo9cOmz4XvMPzrqADZg2i4IN35tcbPjBcGHD90BdPxg/Bzd6EPreFGeAL1Tnjzid1azY8COBxp+Mn/0JvkP9bHADR7Gw4WdcWznY8D9nPmBDgW3+rAVFt/uLSTc2iO9f8H3kFRt+AeokxIbheeTSYcN8zQucdQEbMG0WBRvmm1xsWGC4sGE+UNcC4+fgRg9C801xBviCP+gimM7hrNgArIE3jVHkZ3+C71AlER82lACL6+pt4CwEbCiwTekkKSi63YYRcDT15LthBO8jr9jQEHhAEWLDiDxy6bChkRa4sbPvBWzAtFkUbGgU5WJD44gLGxoBB+bGkZ+DGz0INYqKM8AXqrMJTucIVmxoEqVfY1MWbGhGiA3NPGFD84AN2E5q7gEbWqQcG8R3CzJsaFG/sWFkHrl02NBSC9wqYAMnNrRMYEMrMmxoCRyYW0V+Dm70INSSBBta43SOZMWG1lH6Na7Egg1tCLGhjSdsWDlgA7aTVvaADW1Tjg3iuy0ZNrSt39gwKo9cOmxopwVuH7CBExvaJbChPRk2tAMOzO0jPwc3ehBqR4INq+B0jmLFhlWi9GvswIINHQmxoaMnbOgUsAHbSZ08YEPnlGOD+O5Mhg2d6zc21OaRS4cNXbTAXQM2cGJDlwQ2dCXDhi7Agblr5OfgRg9CXUiwoRtOZy0rNnSL0q+xOws29CDEhh6esGHVgA3YTlrVAzb0TDk2iO+eZNjQs35jQ10euXTY0EsLvFrABk5s6JXAhtXIsKEXcGBeLfJzcKMHoV4k2LA6TmcdKzasHqVf4xos2LAmITas6Qkb1grYgO2ktTxgw9opxwbxvTYZNqxdr7EhRp7arzBs6K0FXidgAyc29E5gwzpk2NAbODCvE/k5uNGDUG8SbFgXpjPOsGLDulH6Na7Hgg3rE2LD+p6wYYOADdhO2sADNmyYcmwQ3xuSYcOG9Rsb4jxy6bChjxY4E7CBExv6JLAhQ4YNfYADcybyc3CjB6E+JNgQ47AhZsWGOEq/xlIWbCgjxIYyT9hQHrAB20nlHrChIuXYIL4ryLChon5jQ2keuXTY0FcLXBmwgRMb+iawoZIMG/oCB+bKyM/BjR6E+pJgQxUOG0pZsaEqSr/GahZs2IgQGzbyhA0bB2zAdtLGHrBhk5Rjg/jehAwbNqnf2FCWRy4dNvTTAm8asIETG/olsGFTMmzoBxyYN438HNzoQagfCTZshsOGMlZs2CxKv8YaFmzoT4gN/T1hw4CADdhOGuABGwamHBvE90AybBhYv7GhPI9cOmwYpAXePGADJzYMSmDD5mTYMAg4MG8e+Tm40YPQIBJs2AKHDeWs2LBFlH6NW7Jgw1aE2LCVJ2zYOmADtpO29oAN26QcG8T3NmTYsE39xoaKPHLpsGGwFnjbgA2c2DA4gQ3bkmHDYODAvG3k5+BGD0KDSbBhOxw2VLBiw3ZR+jVuz4INOxBiww6esGHHgA3YTtrRAzbslHJsEN87kWHDTvUbG/rmkUuHDUO0wDsHbODEhiEJbNiZDBuGAAfmnSM/Bzd6EBpCgg274LChLys27BKlX+OuLNiwGyE27OYJG3YP2IDtpN09YMMeKccG8b0HGTbsUb+xoTKPXDpsGKoF3jNgAyc2DE1gw55k2DAUODDvGfk5uNGD0FASbNgLhw2VrNiwV5R+jXuzYMM+hNiwjyds2DdgA7aT9vWADfulHBvE935k2LBf/caGqjxy6bBhmBZ4eMAGTmwYlsCG4WTYMAw4MA+P/Bzc6EFoGAk2jMBhQxUrNoyI0q9xJAs2jCLEhlGesKE2YAO2k2o9YENdyrFBfNeRYUNd/caG6jxy6bBhtBZ4TMAGTmwYncCGMWTYMBo4MI+J/Bzc6EFoNAk27I/DhmpWbNg/Sr/GA1iw4UBCbDjQEzYcFLAB20kHecCGg1OODeL7YDJsOLh+Y8PwPHLpsGGsFviQgA2c2DA2gQ2HkGHDWODAfEjk5+BGD0JjSbDhUBw2DGfFhkOj9Gs8jAUbDifEhsM9YcO4gA3YThrnARvGpxwbxPd4MmwYX7+xYUQeuXTYMEELfETABk5smJDAhiPIsGECcGA+IvJzcKMHoQkk2HAkDhtGsGLDkVH6NR7Fgg1HE2LD0Z6w4ZiADdhOOsYDNkxMOTaI74lk2DCxfmPDyDxy6bBhkhZ4csAGTmyYlMCGyWTYMAk4ME+O/Bzc6EFoEgk2HIvDhpGs2HBslH6NU1iw4ThCbDjOEzYcH7AB20nHe8CGE1KODeL7BDJsOKF+Y8OoPHLpsGGqFvjEgA2c2DA1gQ0nkmHDVODAfGLk5+BGD0JTSbDhJBw2jGLFhpOi9Gs8mQUbTiHEhlM8YcOpARuwnXSqB2yYlnJsEN/TyLBhWv3Ghto8cumwYboWeEbABk5smJ7Ahhlk2DAdODDPiPwc3OhBaDoJNpyGw4ZaVmw4LUq/xpks2DCLEBtmecKG2QEbsJ002wM2zEk5NojvOWTYMKd+Y0NdHrl02DBXCzwvYAMnNsxNYMM8MmyYCxyY50V+Dm70IDSXBBtOx2FDHSs2nB6lX+MZSI0irpGNrW3Mt7GN5uwGBttoamNbzdtp3l7zDpp31LyT5iGad9a8i+ZdNe+meXfNe2geqnlPzXtp3lvzPpr31byf5mGah2seoXmk5lGaazXXaR6teYzm/TUfoPlAzQdpPljzWM2HaD5U82GaD9c8TvN4zRM0H6H5SM1HaT5a8zGaJ2qepHmy5mM1T9F8nObjNZ+gearmEzWfpPlkzadoPlXzNM3TNfc0C6d7dPkhzU9ofk7zK5rf0vyB5s80f6P5J81RtDA31byS5g6au2teQ/N6mks1V2uu0byl5u0176p5b80jNR+g+TDNR2meovlkzTM1n6H5zMRHO/qgPhP4wSja5AwnOwAnj+0ZmleycZb947OjhWOADNrZM8DsencqSXguVGcJsH5nkXzIngPQWVvVd/iI8jqvJy3nktTzPBKd55PovIBE54UkOi8i0Xkxic5LSHReSqLzMhKdl5PovIJE55UkOq8i0Xk1ic5rSHReS6LzOhKd15PovIFE540kOm8i0Xkzic5bSHTeSqLzTyQ6byPReTuJzjtIdP6ZROedJDrvItF5N4nOv5DovIdE570kOv9KovM+Ep33k+h8gETngyQ6HyLR+TCJzr+R6HyEROejJDofI9H5OInOJ0h0Pkmi8+8kOp8i0fk0ic5nSHQ+S6LzORKdz5Po/AeJzhdIdL5IovMlEp0vk+h8hUTnqyQ6/0mi8zUSna+T6HyDROebJDrfItH5NonOf5HofIdE57skOt8j0fk+ic4PSHR+SKLz3yQ6PyLR+TGJzk9IdH5KovMzEp2fk+j8D4nOL0h0fkmi8ysSnV+T6PyGROe3JDr/S6LzOxKd35Po/IFE548kOn8i0fmzJ50lCZ2ZwqZfH7SF8vw/Es8lQM+/kHhuAPQ8n8RzQ6DnBSSeGwE9izgGz42BniMSz1sCPZeQeHafJVSo5wYkns8Fem5I4vk8oOdGJJ7PB3puTOL5AqDnJiSeLwR6bkri+SKg52Ykni8Gem5O4vkSoOcWJJ4vBXpuSeL5MqDnViSeLwd6bk3i+Qqg55VIPF8J9NyGxPNVQM8rk3i+Gui5LYnna4Ce25F4vhbouT2J5+uAnlch8Xw90HMHEs83AD13JPF8I9BzJxLPNwE9dybxfDPQcxcSz7cAPXcl8Xwr0HM3Es9/AnruTuL5NqDnHiSebwd6XpXE8x1Azz1JPP8Z6LkXiec7gZ5XI/F8F9Dz6iSe7wZ6XoPE81+Antck8XwP0PNaJJ7vBXpem8TzX4Gee5N4vg/oeR0Sz/cDPa9L4vkBoOf1SDw/CPS8Ponnh4CeNyDx/DDQ84Yknv8G9NyHxPMjQM8ZEs+PAj3HJJ4fA3ouJfH8ONBzGYnnJ4Cey0k8Pwn0XEHi+e9Az31JPD8F9FxJ4vlpoOcqEs/PAD1Xk3h+Fuh5IxLPzwE9b0zi+Xmg501IPP8D6LkfiecXgJ43JfH8ItDzZiSeXwJ6riHx/DLQc38Sz68APQ8g8fwq0PNAEs//BHoeROL5NaDnzUk8vw70vAWJ5zeAnrck8fwm0PNWJJ7fAnremsTz20DP25B4/hfQ82ASz+8APW9L4vldoOftSDy/B/S8PYnn94GedyDx/AHQ844knj8Eet6JxPO/gZ6HkHj+COh5ZxLPHwM970Li+ROg511JPH8K9LwbiefPgJ53J/H8OdDzHiSe/wP0PJTE8xdAz3uSeP4S6HkvEs9fAT3vTeL5a6DnfUg8fwP0vC+J52+Bnvcj8fxfoOdhJJ6/A3oeTuL5e6DnESSefwB6Hkni+Ueg51Eknn8Ceq4l8fwz0HMdiecmBud5NInnpkDPY0g8NwN63p/Ec3Og5wNIPLcAej6QxHNLoOeDSDy3Ano+mMRza6DnsSSeVwJ6PoTEcxug50NJPK8M9HwYiee2QM+Hk3huB/Q8jsRze6Dn8SSeVwF6ngD03EHbidSzvBNS3pEo7wyUd+gJDwofCS/I+bOcT8r5lZxvyOevfB7J+CzjlRy/sj9L/4rfDk49j9Es7wOV92PK+yIX6Ebl/Xryvjl5/5q8j0zezyXvq5L3N8n7jOT9PvK+G3n/i7wPRd4PIu/LkPdHyPsU5P0C8rx9ef68PI9dnk8uz+uW51fL85zl+cbyvF95/q08D1aejyrPC5XnZ/a0Ic9XlOcNyvP35Hl08nw2eV6ZPL9Lnmclz3eS5x3J83/keTjyfBh5Xoo8P0SepyHPl5DnLcjzB+T3+PL7dPm9tvx+WX7PK79vld97yu8f5feA8vs4+b3Yr7+fsiG/r5Hfm8jvL+T3CHJ/vtyvLvdvy/3Mcn+v3O8q93/K/ZByf6DcLyf3j8n9VHJ/kdxvI/efyP0Ycn+CfF8v31/L97ny/aZ83yfff8n3QfL9iHxfINfPh9mQ66tyvVGuv8n1KLk+I9crhN+FZ4XvhHfk/F/Oh+X8UM6X5PxBPk/l80XGWxl/5HicULKo3/8Pd3aZ6uLvAgA=", + "bytecode": "H4sIAAAAAAAA/+2dB3QV1RaGz6RAQugdQiBgASxwJ7lpChiKFSsWREVpCWDBAigqiooVFQE7NsTesWMDFQEbKgL293zWZ33WZ33CO5vsMecOFwXuPpf5V86stdeemSRn/n/PzLnzTeae+S1Dqe910OTpoNksng+Ws0PL9XjenILlSs7FsdJ4vKqsqMov9ofHiipGlJfE4iUjSsv9cr+kvGRUUXlxcVV5vLysYkRFWazCjxdX+dUlFcXVsZqpjdFWLMXJps62IDrbgehsD6IzH0RnBxCdBSA6O4Lo7ASisxBEZ2cQnV1AdG4BonNLEJ1bgejcGkRnVxCd3UB0dgfRuQ2Izm1BdG4HonN7EJ09QHT2BNEZA9Hpg+gsAtFZDKIzDqKzRFAnaaN7jIXcXmsdq3W04dyWczvO7Tnnc+7AuYBzR86dOBdy7sy5C+ctOG/JeSvOW3Puyrkb5+6ct+G8LeftOG/PuQfnnpxjnH3ORZyLOcc5lxjtleooUzX3YmniW7Z/rre5b8sVxjFYAaJzBxCdO4Lo7AWiszeIzj4gOncC0VkJorMviM5+IDr7g+gcAKJzZxCduyj5a+Gm3B5d79E1YTnnCs47cN6Rcy/OvTn34bwT50rOfTn349yf8wDOO3PeRdVei+6qYzdV+1xAcC0arM9U6z4fkKzOsdQmv7VcW7EsQ+funPdgzcE2BurYU8deOvbWsY+OfXXsp2OQjv11HKDjQB0H6Ris42AdQ3QcouNQHYfpGKrjcB1H6BimY7iOETpG6hilo0pHtY7ROsboGKvjSB1H6ThaxzE6xuk4VsdxOo7XcYKO8Tom6Jio40QdJ+mYpONkHafoOFXHZB2n6ThdxxQdZ+g4U8dZOqbqOFvHOTrO1XGejvN1XKBjGtfgQqNOs3mH5qraYyCY6hnzlZxjKU7mcydSbWazfmVkZfjJNbaZqdb1m2WsC34eMFpjrkOWqOZ4sbmtYAqfb5XGfLD9HNZE0+iqCX0nThgzeOyEcVXjx3tGK0HL/ZO0HLiup2r3cKWIq1hRrlG5DXFVz8g5olriMdJSfyO05BhackW11Bz1DYTbpDbyDP2B10B7nvHzBoa3PFkdvqcSz7rKkIZgu86/6Hadf+X8O//Ov/Pv/Dv/zr/z7/w7/86/8+/8O//Ov/Pv/Dv/zr/z7/w7/86/8+/8O//Ov/Pv/Dv/zn+l8+/8O//Ov/Pv/Dv/zr/z7/w7/86/8+/8O//Ov/Pv/Dv/zr/z7/w7/86/8+/8O//Ov/O/0f5zjXXZm1lLnqGhvjUt8VieSn4cCHuO5SbZDo2WMdSr3WYjYW/UZlNhHyQ3GNmDpimGr6ZG/Wxst0lou41C26XfaWhomGJoDf420/idSV7tfqgy9oP0vqc2mhnag3FPzPM+8NDM0N9cVgcNAqpaJNHR3NARbL+FsS6YN/uEVqLa4jQw6drRV8PaWhk6WvJ88Hu5xnxTY12bUC1pXduQL1rXjudbGusykmwj0NLGWBeMdNPWWBfUJ2i3PusOls36BW1Vco6lNhWZWgI9pmaa2hvzmSH9DQzP7Q2d+bI6157Hpo4cY7v5xnY7iG63ZpydfJU4eaHlSmO+g6Glo6iWmhp0MtqvNLZhbrdQdru+uV2PI9hGsD7TmJ9uFKiwdvbP4znQTMdOQZLfM+fzQ3+TZ/y8wLLnjoaOSmM52Bady1MNrwVJdJuf3cHPzfPERl/dwdARbL+5sRzoMPsV4WN1bf0KQvULls19mR2ql7yWmmu18LbN66qgXuZ1VfgcFysMGc4w2huo6vaQfRmhYkd5CMZCbuciHRfrmK7jEh0zdMzUMUvHpTou03G5jit0XKnjKh1X65it4xod1+q4Tsf1Om7QMUfHjTrm6rhJx806btFxq47bdNyu4w4dd+q4i4sUDGtJWnJU7fLFoeXpoeVLQsszQsszQ8uzQsuXhpYvCy1fHlq+IrR8ZWj5qtDy1aHl2aHla0LL14aWrwstXx9aviG0PCe0fGNoeW5o+abQ8s2h5VtCy7eGlm8LLd8eWr4jtHxnaPkuVTuYXzAFF2WVnGOpTQnnTKpDzV4k2NZTGbJwtb76barOqmqaYv7FQm3RvpguWL8Fka/f2qb9S1Jvq4g9+zME67cwyvWL/6nTn5laWzHDsz9LsH5PR7V+RQk6/Us3va1YyLN/mWD9nolg/Uqr19HpX75pbZUn8exfIVi/Z6NWv/KkOv0rN76tsvV49q8SrN+iKNWvbL06/as3rq2iv/Dszxas33NRqV/ZX+r0r9nwtkb+jWf/WsH6LY5C/cr+Vqd/3Ya1FdsAz/71gvVbsrnrF9sgnf4Nf99WyQZ69ucI1m/p5qxffIN1+jf+ZVvx6o3w7M8VrN/zm6t+ZRul079p/W2Vb6Rn/2bB+r2wGepXUb3ROv1bkrcV2wTP/q2C9Xsx3fWLbZJO/7Z12/I30bN/u2D9Xkpn/UZtsk7/jsS2ilPw7N8pWL+X01S/ouqUdPp3Kbl7ieY9u1TrtyxN9YulNvmC99n8hYL1ewWkfoL3ifxnBOv3Kkj9BO9z+IsE6/caSP0EOd1fLFi/5SD1E+RMf6lg/V4HqZ8gJ/kvCNZvBUj9BK/z/ZcE67cSpH6C16n+MsH6rQKpn+B1lv+qYP3eAKmf4HWCv1ywfm+C1E/wc85fIVi/t0DqJ9hP+6sE6/c2SP0E+xn/TcH6vQNSP8HzxBc8ZnzJ+mVz3Qq5PbrnRPfa6N4d3bOke6B075fuJdM9dLonP1fVPJ83R9U8x0f/G6P/tdH/GOl/lvS/WvrfL/3Pm/6HTs8O0LMI9AwGPdNBz7LQszH0TBA9Y0TPVtGzWhepxCn8DGqqx93dm97WOs+TpOvV5XfLtRUz9d5jzAdfhskw1gXnUj0LnlRoO+E6NlYWH/y2tZPusdDuvUru4Lfl+175fZTQqdusaSzFKVOt+351+WO2KCbYth9eYU93zDc7lPs4zzPWBd/8yFCJb4ynyTNqSx3UGuPvPCN7RhtrjL9J9jveetoxv80c/H1jQ4uSq0nMQocas9phBk+80w5coGqfgJ9nbEMZO8Hcdqqd1n2Cbd3/FzXZ2LbT9el/v7Lz6f+AMe8+/VNs834uqHS7D6pof/qT7wfl91FSran6n8dapdv9NaIoG9YpeSw9JLevfcn6BR9MpK+vSpxS/GBaB/8kP5jmidWz6C+vnFPV+bBg/dL1AfqwsvMB+ogx7z5AU2zzYS6odLuPqmh/gJLvR+X3kVV8flRQ5/rw2caH/yZqtonLmw3F53N+zFi3MSjeT627r8Io3k/9PYona8eh+PqnP1F8vlFMWn5MrYviUl8GTnYSpfrpP19Q12PKzgko3QnNV+np4FPV+bigziyVvIOXroP0h5xkDWxpfELZOZ7ED6gnlVzHkS5seFKurQRseMqYd9iQYptPckGl212goo0N5HuB/D6yig0LBHUCYoOfRC4cNizk/LSxzmGDTJtpwYaFKhEbnlZY2LBQUNfTys7JLd0JLVTp6eBTHhNH8HhFxYZnADQ+q+wcT+IH1CIl13GkCxsWybWVgA3PGfMOG1JscxEXVLrdxSra2EC+F8vvI6vYsFhQJyA2FCWRC4cNSzgvNdY5bJBpMy3YsEQlYsNShYUNSwR1LVV2Tm7pTmiJSk8Hn/JQVHI6i1Cx4XkAjS8oO8eT+AH1opLrONKFDS/KtZWADS8Z8w4bUmzzRS6odLsvq2hjA/l+WX4fWcWGlwV1AmJDcRK5cNiwjPMrxjqHDTJtpgUblqlEbHhFYWHDMkFdryg7J7d0J7RMpaeDT3kEODmdxajY8CqAxteUneNJ/IBaruQ6jnRhw3K5thKw4XVj3mFDim0u54JKt7tCRRsbyPcK+X1kFRtWCOoExIZ4Erlw2LCS8ypjncMGmTbTgg0rVSI2rFJY2LBSUNcqZefklu6EVqr0dPApD7wopzOOig1vAGh8U9k5nsQPqLeUXMeRLmx4S66tBGx425h32JBim29xQaXbfUdFGxvI9zvy+8gqNrwjqBMQG0qSyIXDhnc5v2esc9gg02ZasOFdlYgN7yksbHhXUNd7ys7JLd0JvavS08GnqvMfcjpLULHhHwAa/6nsHE/iB9T7Sq7jSBc2vC/XVgI2/MuYd9iQYpvvc0Gl2/1ARRsbyPcH8vvIKjZ8IKgTEBtKk8iFw4YPOX9krHPYINNmWrDhQ5WIDR8pLGz4UFDXR8rOyS3dCX2o0tPBp6rzYzmdpajY8DGAxk+UneNJ/ID6VMl1HOnChk/l2krAhn8b8w4bUmzzUy6odLufqWhjA/n+TH4fWcWGzwR1AmJDWRK5cNjwOecvjHUOG2TaTAs2fK4SseELhYUNnwvq+kLZObmlO6HPVXo6+FR1fimnswwVG74E0PiVsnM8iR9QXyu5jiNd2PC1XFsJ2PAfY95hQ4ptfs0FlW73GxVtbCDf38jvI6vY8I2gTkBsKE8iFw4bvuX8nbHOYYNMm2nBhm9VIjZ8p7Cw4VtBXd8pOye3dCf0rUpPB5+qzu/ldJajYsP3ABp/UHaOJ/ED6kcl13GkCxt+lGsrARv+a8w7bEixzR+5oNLt/qSijQ3k+yf5fWQVG34S1AmIDRVJ5MJhw8+cfzHWOWyQaTMt2PCzSsSGXxQWNvwsqOsXZefklu6Eflbp6eBTfoG1nM4KVGz4FUDjb8rO8SR+QP2u5DqOdGHD73JtJWDD/4x5hw0ptvk7F1S63T9UtLGBfP8hv4+sYsMfgjoBsWF4Erlw2LCa8xpjncMGmTbTgg2rVSI2rFFY2LBaUNcaZefklu6EVqv0dPApf9B5YjqHo2KDYA2safQ8O8eT+AGV4eFhQ4ZgcU29mcaCw4YU26SdRAWVbjfLE+xNLfnO8sT3kVVsyBI8oQCxYUQSuXDYkM0Frmccew4bZNpMCzZke4nYUM/DwoZswY65nmfn5JbuhLK99HTwqeqsL6dzBCo21PeirzEHBRtyAbEh1xI2NHDYILuTGljAhryIYwP5zgPDhry6jQ0jk8iFw4aGXOBGDhswsaFhCBsagWFDQ8GOuZFn5+SW7oQagmBDYzmdI1GxobEXfY1NULChKSA2NLWEDc0cNsjupGYWsKF5xLGBfDcHw4bmdRsbRiWRC4cNLbjALR02YGJDixA2tATDhhaCHXNLz87JLd0JtQDBhlZyOkehYkMrL/oaW6NgQxtAbGhjCRvaOmyQ3UltLWBDu4hjA/luB4YN7eo2NlQlkQuHDe25wPkOGzCxoX0IG/LBsKG9YMec79k5uaU7ofYg2NBBTmcVKjZ08KKvsQAFGzoCYkNHS9jQyWGD7E7qZAEbCiOODeS7EAwbCus2NlQnkQuHDZ25wF0cNmBiQ+cQNnQBw4bOgh1zF8/OyS3dCXUGwYYt5HRWo2LDFl70NW6Jgg1bAWLDVpawYWuHDbI7aWsL2NA14thAvruCYUPXOo0NvuSl/WbDhm5c4O4OGzCxoVsIG7qDYUM3wY65u2fn5JbuhLqBYMM2Yjr9GCo2bONFX+O2KNiwHSA2bGcJG7Z32CC7k7a3gA09Io4N5LsHGDb0qNvY4CeRC4cNPbnAMYcNmNjQM4QNMTBs6CnYMcc8Oye3dCfUEwQbfDls8FGxwfeir7EIBRuKAbGh2BI2xB02yO6kuAVsKIk4NpDvEjBsKKnb2FCURC4cNpRygcscNmBiQ2kIG8rAsKFUsGMu8+yc3NKdUCkINpTLYUMRKjaUe9HXWIGCDTsAYsMOlrBhR4cNsjtpRwvY0Cvi2EC+e4FhQ6+6jQ3FSeTCYUNvLnAfhw2Y2NA7hA19wLCht2DH3Mezc3JLd0K9QbBhJzlsKEbFhp286GusRMGGvoDY0NcSNvRz2CC7k/pZwIb+EccG8t0fDBv6121siCeRC4cNA7jAOztswMSGASFs2BkMGwYIdsw7e3ZObulOaAAINuwihw1xVGzYxYu+xl1RsGE3QGzYzRI27O6wQXYn7W4BG/aIODaQ7z3AsGGPuo0NJUnkwmHDQC7wng4bMLFhYAgb9gTDhoGCHfOenp2TW7oTGgiCDXvJYUMJKjbs5UVf494o2LAPIDbsYwkb9nXYILuT9rWADftFHBvI935g2LBf3caG0iRy4bBhEBd4f4cNmNgwKIQN+4NhwyDBjnl/z87JLd0JDQLBhgPksKEUFRsO8KKv8UAUbDgIEBsOsoQNgx02yO6kwRaw4eCIYwP5PhgMGw6u29hQlkQuHDYM4QIf4rABExuGhLDhEDBsGCLYMR/i2Tm5pTuhISDYcKgcNpShYsOhXvQ1HoaCDUMBsWGoJWw43GGD7E463AI2HBFxbCDfR4BhwxF1GxvKk8iFw4ZhXODhDhswsWFYCBuGg2HDMMGOebhn5+SW7oSGgWDDCDlsKEfFhhFe9DWORMGGUYDYMMoSNlQ5bJDdSVUWsKE64thAvqvBsKG6bmNDRRK5cNgwmgs8xmEDJjaMDmHDGDBsGC3YMY/x7Jzc0p3QaBBsGCuHDRWo2DDWi77GI1Gw4ShAbDjKEjYc7bBBdicdbQEbjok4NpDvY8Cw4Zi6jQ3Dk8iFw4ZxXOBjHTZgYsO4EDYcC4YN4wQ75mM9Oye3dCc0DgQbjpPDhuGo2HCcF32Nx6NgwwmA2HCCJWwY77BBdieNt4ANEyKODeR7Ahg2TKjb2DAiiVw4bJjIBT7RYQMmNkwMYcOJYNgwUbBjPtGzc3JLd0ITQbDhJDlsGIGKDSd50dc4CQUbTgbEhpMtYcMpDhtkd9IpFrDh1IhjA/k+FQwbTq3b2DAyiVw4bJjMBT7NYQMmNkwOYcNpYNgwWbBjPs2zc3JLd0KTQbDhdDlsGImKDad70dc4BQUbzgDEhjMsYcOZDhtkd9KZFrDhrIhjA/k+Cwwbzqrb2DAqiVw4bJjKBT7bYQMmNkwNYcPZYNgwVbBjPtuzc3JLd0JTQbDhHDlsGIWKDed40dd4Lgo2nAeIDedZwobzHTbI7qTzLWDDBRHHBvJ9ARg2XFC3saEqiVw4bJjGBb7QYQMmNkwLYcOFYNgwTbBjvtCzc3JLd0LTQLDhIjlsqELFhou86Gu8GAUbpgNiw3RL2HCJwwbZnXSJBWyYEXFsIN8zwLBhRt3GhuokcuGwYSYXeJbDBkxsmBnChllg2DBTsGOe5dk5uaU7oZkg2HCpHDZUo2LDpV70NV4mqZHEZevYXcdqHXtwDjYwUEeOjj0578V5b877cN6X836cB3Hen/MBnA/kfBDnwZwP5jyE8yGcD+V8GOehnA/nfATnYZyHcx7BeSTnUZyrOFdzHs15DOexnI/kfBTnozkfw3kc52M5H8f5eM4ncB7PeQLniZxP5HwS50mcT+Z8CudTOU/mfBrn0zlP4XwG5zM5n8V5KuezOZ/D+VzO53E+n/MFnKdxLlQ10xO8/CznFzi/xvlNzv/k/Annrzj/wPk3zp5Xk3M4N+HcmnMB5y05b8u5iHMF50rOu3Lem/OBnA/jPJLzkZyP5zyJ8xTO53K+mPNlnC8PfbRLn9SXC34wkja6wgk64PC5fSHnJjqu0L98pVfTB1CnHVwBBuvNKSPkOVWdGYL1u0KYTtJxi6K1XFuxLEPnVSz0ar5YDLYxWy9co+NaHdfpuF7HDTrm6LhRx1wdN+m4WcctOm7VcZuO23XcoeNOHXfpuFvHPTru1XGfjnk67tfxgI4HdTyk42Edj+h4VMd8HY/peFzHEzqe1PGUjgU6Fup4WsczOp7VsUjHczoW61iiY6mO53W8oONFHS/peFnHMh2v6HhVx2s6lut4XccKHSt1rNLxho43dbyl422uyTvGjp3N87kq8RYOTSjUka1qyckkqAxjXbDNTLWu3yxjXfDzbM4BfWWJao4Xm9sKpvD5VmnMB9vPYU00ja6a0HfihDGDx04YVzX+z/FGPKPl/klaNvm2nqirWFGuUbkNcVXPyDmiWuIx0lJ/I7TkGFpyRbXUHPUNhNukNvIM/YHXQHue8fMGhrc8WR1rsTw3VFNTQ7Bd5190u86/cv6df+ff+Xf+nX/n3/l3/p1/59/5d/6df+ff+Xf+nX/n3/l3/p1/59/5d/6df+ff+Xf+nX/nv9L5d/6df+ff+Xf+nX/n3/l3/p1/59/5d/6df+ff+Xf+nX/n3/l3/p1/59/5d/6df+ff+d9o/7nGuuzNrCXP0FDfmpZ4LE8lPw6EPcdyk2yHRssY6tVus5GwN2qzqbAPkhuM7EHTFMNXU6N+NrbbJLTdRqHt0u80NDRMMbQGf5tp/M4kr3Y/VBn7QXrfUxvNDO3BuCfmeR94aGboby6rY+24wC2S6Ghu6Ai238JYF8ybfUIrUW3xImqjdRJtrQwdLXk++L1cY76psa6NSqwlrWsb8kXr2vF8S2NdRpJtBFraGOuCkW7aGuuC+gTt1mfdwbJZv6CtSs6x1KYiU0ugx9RMU3tjPjOkv4Hhub2hM19W59rz2NSRY2w339huB9Ht1oyzk68SJy+0XGnMdzC0dBTVUlODTkb7lcY2zO0Wym7XN7frcQTbCNZnGvPTjQIV1s7+eTwHmunYKUjye+Z8fuhv8oyfF1j23NHQUWksB9uic3mq4bUgiW7zszv4uXme2OirOxg6gu03N5YDHWa/Inysrq1fQah+wbK5L7ND9ZLXUnOtFt62eV0V1Mu8rgqf42KFyWbTQXt1fcg+j+tBQxrSkJVXe4nD01J9qLO/hvO1nK/jfD3nGzjP4Xwj57mcb+J8M+dbON/K+TbOt3O+g/OdnO/ifDfnezjfy/k+zvM438/5Ac4Pcn6I88OcH+H8KOf5nB/j/DjnJzg/yfkpzgs4L+T8NOdnOD/LeRHn5zgv5ryE81LOz3N+gfOLnF/i/DLnZZxf4fwq59c4L+f8OucVnFdyXsX5Dc5vcn6L89ueG57WHJ72XU8lTMFiJedYapNP7QdtSQ9PGz633/Fqh6d9T8//w6vpA+hvggvpYL05ZYQ8R2l42vc8WfBb335O1fM/BXRWlZcOHxGvrrap832Qev4LROcHIDo/BNH5EYjOj0F0fgKi81MQnf8G0fkZiM7PQXR+AaLzSxCdX4Ho/BpE539AdH4DovNbEJ3fgej8HkTnDyA6fwTR+V8QnT+B6PwZROcvIDp/BdH5G4jO30F0/g9E5x8gOleD6FwDopNuyiPo9EB0ZoDozATRmQWiMxtEZz0QnfVBdOaA6MwF0dkARGceiM6GIDobgehsDKKzCYjOpiA6m4HobA6iswWIzpYgOluB6GwNorMNiM62IDrbgehsD6IzH0RnBxCdBSA6O4Lo7ASisxBEZ2cQnV1AdG4BonNLEJ1bgejcGkRnVxCd3UB0dgfRuQ2Izm1BdG4HonN7EJ09QHT2BNEZA9Hpg+gsAtFZDKIzDqKzBERnKYjOMhCd5SA6K0B07gCic0cQnb1AdPYG0dkHROdOIDorQXT2taQzI6Qzltq0duAkKc/9QDxnCHruD+I5U9DzABDPWYKedwbxnC3oeRcQz/UEPe8K4nlXQc+7gXg2xxJK1fPuIJ7fF/S8B4jnfwl6Hgji+QNBz3uCeP5Q0PNeIJ4/EvS8N4jnjwU97wPi+RNBz/uCeP5U0PN+IJ7/Leh5EIjnzwQ97w/i+XNBzweAeP5C0POBIJ6/FPR8EIjnrwQ9Dwbx/LWg54NBPP9H0PMQEM/fCHo+BMTzt4KeDwXx/J2g58NAPH8v6HkoiOcfBD0fDuL5R0HPR4B4/q+g52Egnn8S9DwcxPPPgp5HgHj+RdDzSBDPvwp6HgXi+TdBz1Ugnn8X9FwN4vl/gp5Hg3j+Q9DzGBDPqwU9jwXxvEbQ85EgnlWGnOejQDx7gp6PBvGcIej5GBDPmYKex4F4zhL0fCyI52xBz8eBeK4n6Pl4EM/1BT2fAOI5R9DzeBDPuYKeJ4B4biDoeSKI5zxBzyeCeG4o6PkkEM+NBD1PAvHcWNDzySCemwh6PgXEc1NBz6eCeG4m6HkyiOfmgp5PA/HcQtDz6SCeWwp6ngLiuZWg5zNAPLcW9HwmiOc2gp7PAvHcVtDzVBDP7QQ9nw3iub2g53NAPOcLej4XxHMHQc/ngXguEPR8PojnjoKeLwDx3EnQ8zQQz4WCni8E8dxZ0PNFIJ67CHq+GMTzFoKep4N43lLQ8yUgnrcS9DwDxPPWgp5ngnjuKuh5FojnboKeLwXx3F3Q82UgnrcR9Hw5iOdtBT1fAeJ5O0HPV4J43l7Q81UgnnsIer4axHNPQc+zQTzHBD1fA+LZF/R8LYjnIkHP14F4Lhb0fD2I57ig5xtAPJcIep4D4rlU0PONIJ7LBD3PBfFcLuj5JhDPFYKebwbxvIOg51tAPO8o6PlWEM+9BD3fBuK5t6Dn20E89xH0fAeI550EPd8J4rlS0PNdIJ77Cnq+G8RzfSXn+R4QzzmCnu8F8Zwr6Pk+EM8NBD3PA/GcJ+j5fhDPDQU9PwDiuZGg5wdBPDcW9PwQiOcmgp4fBvHcVNDzIyCemwl6fhTEc3NBz/NBPLcQ9PwYiOeWgp4fB/HcStDzE4KeW3M7Hnumd0LSOxLpnYH0Dj3iQeIj4gW6fqbrSbq+ousN+vylzyPqn6m/ovOXjmfav+S3tVHPUzjT+0Dp/Zj0vkh6fyK9T5Der0fvm6P3r9H7yOj9XPS+Knp/E73PiN7vQ++7ofe/0PtQ6P0g9L4Men8EvU+B3i9A4+3T+PM0HjuNT07jddP41TSeM41vPEwHjX9L48HS+Kg0XiiNn0njSdL4ijTeII2/R+PR0fhsNF4Zjd9F41nR+E403hGN/0Pj4dD4MDReCo0fQuNp0PgSNN4CjT9A38en76fT97Xp+8v0fV76fusZOuj7j/R9QPp+HH1fjL4/Rd8nou/X0PdN6PsX9H0Eej6fnlen57fpeWZ6vpeed6XnP+l5SHo+kJ6Xo+fH6Hkqer6Inreh50/oeQx6PoH+X0//v6b/59L/N+fooP9/0f+D6P8j9P8Cun9O95Pp/irdb6T7b3Q/iu7P0P0K4nfiWeI74h26/qfrYbo+pOslun6gz1P6fKH+lvofOh/p+Aym/wP8vQ8FgUYDAA==", + "verificationKey": "0000000200000800000000740000000f00000003515f3109623eb3c25aa5b16a1a79fd558bac7a7ce62c4560a8c537c77ce80dd339128d1d37b6582ee9e6df9567efb64313471dfa18f520f9ce53161b50dbf7731bc5f900000003515f322bc4cce83a486a92c92fd59bd84e0f92595baa639fc2ed86b00ffa0dfded2a092a669a3bdb7a273a015eda494457cc7ed5236f26cee330c290d45a33b9daa94800000003515f332729426c008c085a81bd34d8ef12dd31e80130339ef99d50013a89e4558eee6d0fa4ffe2ee7b7b62eb92608b2251ac31396a718f9b34978888789042b790a30100000003515f342be6b6824a913eb7a57b03cb1ee7bfb4de02f2f65fe8a4e97baa7766ddb353a82a8a25c49dc63778cd9fe96173f12a2bc77f3682f4c4448f98f1df82c75234a100000003515f351f85760d6ab567465aadc2f180af9eae3800e6958fec96aef53fd8a7b195d7c000c6267a0dd5cfc22b3fe804f53e266069c0e36f51885baec1e7e67650c62e170000000c515f41524954484d455449430d9d0f8ece2aa12012fa21e6e5c859e97bd5704e5c122064a66051294bc5e04213f61f54a0ebdf6fee4d4a6ecf693478191de0c2899bcd8e86a636c8d3eff43400000003515f43224a99d02c86336737c8dd5b746c40d2be6aead8393889a76a18d664029096e90f7fe81adcc92a74350eada9622ac453f49ebac24a066a1f83b394df54dfa0130000000c515f46495845445f42415345060e8a013ed289c2f9fd7473b04f6594b138ddb4b4cf6b901622a14088f04b8d2c83ff74fce56e3d5573b99c7b26d85d5046ce0c6559506acb7a675e7713eb3a00000007515f4c4f4749430721a91cb8da4b917e054f72147e1760cfe0ef3d45090ac0f4961d84ec1996961a25e787b26bd8b50b1a99450f77a424a83513c2b33af268cd253b0587ff50c700000003515f4d05dbd8623b8652511e1eb38d38887a69eceb082f807514f09e127237c5213b401b9325b48c6c225968002318095f89d0ef9cf629b2b7f0172e03bc39aacf6ed800000007515f52414e474504b57a3805e41df328f5ca9aefa40fad5917391543b7b65c6476e60b8f72e9ad07c92f3b3e11c8feae96dedc4b14a6226ef3201244f37cfc1ee5b96781f48d2b000000075349474d415f3125001d1954a18571eaa007144c5a567bb0d2be4def08a8be918b8c05e3b27d312c59ed41e09e144eab5de77ca89a2fd783be702a47c951d3112e3de02ce6e47c000000075349474d415f3223994e6a23618e60fa01c449a7ab88378709197e186d48d604bfb6931ffb15ad11c5ec7a0700570f80088fd5198ab5d5c227f2ad2a455a6edeec024156bb7beb000000075349474d415f3300cda5845f23468a13275d18bddae27c6bb189cf9aa95b6a03a0cb6688c7e8d829639b45cf8607c525cc400b55ebf90205f2f378626dc3406cc59b2d1b474fba000000075349474d415f342d299e7928496ea2d37f10b43afd6a80c90a33b483090d18069ffa275eedb2fc2f82121e8de43dc036d99b478b6227ceef34248939987a19011f065d8b5cef5c0000000010000000000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b0000000c0000000d0000000e0000000f" + }, + { + "name": "is_valid_public", + "functionType": "open", + "isInternal": false, + "parameters": [ + { + "name": "message_hash", + "type": { + "kind": "field" + }, + "visibility": "private" + } + ], + "returnTypes": [ + { + "kind": "struct", + "path": "aztec::abi::PublicCircuitPublicInputs", + "fields": [ + { + "name": "call_context", + "type": { + "kind": "struct", + "path": "aztec::abi::CallContext", + "fields": [ + { + "name": "msg_sender", + "type": { + "kind": "field" + } + }, + { + "name": "storage_contract_address", + "type": { + "kind": "field" + } + }, + { + "name": "portal_contract_address", + "type": { + "kind": "field" + } + }, + { + "name": "is_delegate_call", + "type": { + "kind": "boolean" + } + }, + { + "name": "is_static_call", + "type": { + "kind": "boolean" + } + }, + { + "name": "is_contract_deployment", + "type": { + "kind": "boolean" + } + } + ] + } + }, + { + "name": "args_hash", + "type": { + "kind": "field" + } + }, + { + "name": "return_values", + "type": { + "kind": "array", + "length": 4, + "type": { + "kind": "field" + } + } + }, + { + "name": "contract_storage_update_requests", + "type": { + "kind": "array", + "length": 16, + "type": { + "kind": "struct", + "path": "aztec::abi::ContractStorageUpdateRequest", + "fields": [ + { + "name": "storage_slot", + "type": { + "kind": "field" + } + }, + { + "name": "old_value", + "type": { + "kind": "field" + } + }, + { + "name": "new_value", + "type": { + "kind": "field" + } + } + ] + } + } + }, + { + "name": "contract_storage_read", + "type": { + "kind": "array", + "length": 16, + "type": { + "kind": "struct", + "path": "aztec::abi::ContractStorageRead", + "fields": [ + { + "name": "storage_slot", + "type": { + "kind": "field" + } + }, + { + "name": "value", + "type": { + "kind": "field" + } + } + ] + } + } + }, + { + "name": "public_call_stack", + "type": { + "kind": "array", + "length": 4, + "type": { + "kind": "field" + } + } + }, + { + "name": "new_commitments", + "type": { + "kind": "array", + "length": 16, + "type": { + "kind": "field" + } + } + }, + { + "name": "new_nullifiers", + "type": { + "kind": "array", + "length": 16, + "type": { + "kind": "field" + } + } + }, + { + "name": "new_l2_to_l1_msgs", + "type": { + "kind": "array", + "length": 2, + "type": { + "kind": "field" + } + } + }, + { + "name": "unencrypted_logs_hash", + "type": { + "kind": "array", + "length": 2, + "type": { + "kind": "field" + } + } + }, + { + "name": "unencrypted_log_preimages_length", + "type": { + "kind": "field" + } + }, + { + "name": "block_data", + "type": { + "kind": "struct", + "path": "aztec::abi::HistoricBlockData", + "fields": [ + { + "name": "private_data_tree_root", + "type": { + "kind": "field" + } + }, + { + "name": "nullifier_tree_root", + "type": { + "kind": "field" + } + }, + { + "name": "contract_tree_root", + "type": { + "kind": "field" + } + }, + { + "name": "l1_to_l2_messages_tree_root", + "type": { + "kind": "field" + } + }, + { + "name": "blocks_tree_root", + "type": { + "kind": "field" + } + }, + { + "name": "public_data_tree_root", + "type": { + "kind": "field" + } + }, + { + "name": "global_variables_hash", + "type": { + "kind": "field" + } + } + ] + } + }, + { + "name": "prover_address", + "type": { + "kind": "field" + } + } + ] + } + ], + "bytecode": "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", + "verificationKey": "0000000200000800000000740000000f00000003515f3109623eb3c25aa5b16a1a79fd558bac7a7ce62c4560a8c537c77ce80dd339128d1d37b6582ee9e6df9567efb64313471dfa18f520f9ce53161b50dbf7731bc5f900000003515f322bc4cce83a486a92c92fd59bd84e0f92595baa639fc2ed86b00ffa0dfded2a092a669a3bdb7a273a015eda494457cc7ed5236f26cee330c290d45a33b9daa94800000003515f332729426c008c085a81bd34d8ef12dd31e80130339ef99d50013a89e4558eee6d0fa4ffe2ee7b7b62eb92608b2251ac31396a718f9b34978888789042b790a30100000003515f342be6b6824a913eb7a57b03cb1ee7bfb4de02f2f65fe8a4e97baa7766ddb353a82a8a25c49dc63778cd9fe96173f12a2bc77f3682f4c4448f98f1df82c75234a100000003515f351f85760d6ab567465aadc2f180af9eae3800e6958fec96aef53fd8a7b195d7c000c6267a0dd5cfc22b3fe804f53e266069c0e36f51885baec1e7e67650c62e170000000c515f41524954484d455449430d9d0f8ece2aa12012fa21e6e5c859e97bd5704e5c122064a66051294bc5e04213f61f54a0ebdf6fee4d4a6ecf693478191de0c2899bcd8e86a636c8d3eff43400000003515f43224a99d02c86336737c8dd5b746c40d2be6aead8393889a76a18d664029096e90f7fe81adcc92a74350eada9622ac453f49ebac24a066a1f83b394df54dfa0130000000c515f46495845445f42415345060e8a013ed289c2f9fd7473b04f6594b138ddb4b4cf6b901622a14088f04b8d2c83ff74fce56e3d5573b99c7b26d85d5046ce0c6559506acb7a675e7713eb3a00000007515f4c4f4749430721a91cb8da4b917e054f72147e1760cfe0ef3d45090ac0f4961d84ec1996961a25e787b26bd8b50b1a99450f77a424a83513c2b33af268cd253b0587ff50c700000003515f4d05dbd8623b8652511e1eb38d38887a69eceb082f807514f09e127237c5213b401b9325b48c6c225968002318095f89d0ef9cf629b2b7f0172e03bc39aacf6ed800000007515f52414e474504b57a3805e41df328f5ca9aefa40fad5917391543b7b65c6476e60b8f72e9ad07c92f3b3e11c8feae96dedc4b14a6226ef3201244f37cfc1ee5b96781f48d2b000000075349474d415f3125001d1954a18571eaa007144c5a567bb0d2be4def08a8be918b8c05e3b27d312c59ed41e09e144eab5de77ca89a2fd783be702a47c951d3112e3de02ce6e47c000000075349474d415f3223994e6a23618e60fa01c449a7ab88378709197e186d48d604bfb6931ffb15ad11c5ec7a0700570f80088fd5198ab5d5c227f2ad2a455a6edeec024156bb7beb000000075349474d415f3300cda5845f23468a13275d18bddae27c6bb189cf9aa95b6a03a0cb6688c7e8d829639b45cf8607c525cc400b55ebf90205f2f378626dc3406cc59b2d1b474fba000000075349474d415f342d299e7928496ea2d37f10b43afd6a80c90a33b483090d18069ffa275eedb2fc2f82121e8de43dc036d99b478b6227ceef34248939987a19011f065d8b5cef5c0000000010000000000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b0000000c0000000d0000000e0000000f" + }, + { + "name": "set_is_valid_storage", + "functionType": "secret", + "isInternal": false, + "parameters": [ + { + "name": "message_hash", + "type": { + "kind": "field" + }, + "visibility": "private" + }, + { + "name": "value", + "type": { + "kind": "field" + }, + "visibility": "private" + } + ], + "returnTypes": [], + "bytecode": "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", "verificationKey": "0000000200000800000000740000000f00000003515f3109623eb3c25aa5b16a1a79fd558bac7a7ce62c4560a8c537c77ce80dd339128d1d37b6582ee9e6df9567efb64313471dfa18f520f9ce53161b50dbf7731bc5f900000003515f322bc4cce83a486a92c92fd59bd84e0f92595baa639fc2ed86b00ffa0dfded2a092a669a3bdb7a273a015eda494457cc7ed5236f26cee330c290d45a33b9daa94800000003515f332729426c008c085a81bd34d8ef12dd31e80130339ef99d50013a89e4558eee6d0fa4ffe2ee7b7b62eb92608b2251ac31396a718f9b34978888789042b790a30100000003515f342be6b6824a913eb7a57b03cb1ee7bfb4de02f2f65fe8a4e97baa7766ddb353a82a8a25c49dc63778cd9fe96173f12a2bc77f3682f4c4448f98f1df82c75234a100000003515f351f85760d6ab567465aadc2f180af9eae3800e6958fec96aef53fd8a7b195d7c000c6267a0dd5cfc22b3fe804f53e266069c0e36f51885baec1e7e67650c62e170000000c515f41524954484d455449430d9d0f8ece2aa12012fa21e6e5c859e97bd5704e5c122064a66051294bc5e04213f61f54a0ebdf6fee4d4a6ecf693478191de0c2899bcd8e86a636c8d3eff43400000003515f43224a99d02c86336737c8dd5b746c40d2be6aead8393889a76a18d664029096e90f7fe81adcc92a74350eada9622ac453f49ebac24a066a1f83b394df54dfa0130000000c515f46495845445f42415345060e8a013ed289c2f9fd7473b04f6594b138ddb4b4cf6b901622a14088f04b8d2c83ff74fce56e3d5573b99c7b26d85d5046ce0c6559506acb7a675e7713eb3a00000007515f4c4f4749430721a91cb8da4b917e054f72147e1760cfe0ef3d45090ac0f4961d84ec1996961a25e787b26bd8b50b1a99450f77a424a83513c2b33af268cd253b0587ff50c700000003515f4d05dbd8623b8652511e1eb38d38887a69eceb082f807514f09e127237c5213b401b9325b48c6c225968002318095f89d0ef9cf629b2b7f0172e03bc39aacf6ed800000007515f52414e474504b57a3805e41df328f5ca9aefa40fad5917391543b7b65c6476e60b8f72e9ad07c92f3b3e11c8feae96dedc4b14a6226ef3201244f37cfc1ee5b96781f48d2b000000075349474d415f3125001d1954a18571eaa007144c5a567bb0d2be4def08a8be918b8c05e3b27d312c59ed41e09e144eab5de77ca89a2fd783be702a47c951d3112e3de02ce6e47c000000075349474d415f3223994e6a23618e60fa01c449a7ab88378709197e186d48d604bfb6931ffb15ad11c5ec7a0700570f80088fd5198ab5d5c227f2ad2a455a6edeec024156bb7beb000000075349474d415f3300cda5845f23468a13275d18bddae27c6bb189cf9aa95b6a03a0cb6688c7e8d829639b45cf8607c525cc400b55ebf90205f2f378626dc3406cc59b2d1b474fba000000075349474d415f342d299e7928496ea2d37f10b43afd6a80c90a33b483090d18069ffa275eedb2fc2f82121e8de43dc036d99b478b6227ceef34248939987a19011f065d8b5cef5c0000000010000000000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b0000000c0000000d0000000e0000000f" } ] diff --git a/yarn-project/aztec.js/src/account/entrypoint/auth_witness_account_entrypoint.ts b/yarn-project/aztec.js/src/account/entrypoint/auth_witness_account_entrypoint.ts index e8067d57585..4ba3dc80993 100644 --- a/yarn-project/aztec.js/src/account/entrypoint/auth_witness_account_entrypoint.ts +++ b/yarn-project/aztec.js/src/account/entrypoint/auth_witness_account_entrypoint.ts @@ -9,13 +9,50 @@ import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from '../../utils/defaults.js'; import { buildPayload, hashPayload } from './entrypoint_payload.js'; import { Entrypoint } from './index.js'; +/** + * An extended interface for entrypoints that support signing and adding auth witnesses. + */ +export interface IAuthWitnessAccountEntrypoint extends Entrypoint { + /** + * Sign a message hash with the private key. + * @param message - The message hash to sign. + * @returns The signature as a Buffer. + */ + sign(message: Buffer): Buffer; + + /** + * Creates an AuthWitness witness for the given message. In this case, witness is the public key, the signature + * and the partial address, to be used for verification. + * @param message - The message hash to sign. + * @param opts - Options. + * @returns [publicKey, signature, partialAddress] as Fr[]. + */ + createAuthWitness(message: Buffer): Promise; + + /** + * Returns the transaction request and the auth witness for the given function calls. + * Returning the witness here as a nonce is generated in the buildPayload action. + * @param executions - The function calls to execute + * @param opts - The options + * @returns The TxRequest, the auth witness to insert in db and the message signed + */ + createTxExecutionRequestWithWitness(executions: FunctionCall[]): Promise<{ + /** The transaction request */ + txRequest: TxExecutionRequest; + /** The auth witness */ + witness: Fr[]; + /** The message signed */ + message: Buffer; + }>; +} + /** * Account contract implementation that uses a single key for signing and encryption. This public key is not * stored in the contract, but rather verified against the contract address. Note that this approach is not * secure and should not be used in real use cases. * The entrypoint is extended to support signing and creating eip1271-like witnesses. */ -export class AuthWitnessAccountEntrypoint implements Entrypoint { +export class AuthWitnessAccountEntrypoint implements IAuthWitnessAccountEntrypoint { constructor( private address: AztecAddress, private partialAddress: PartialAddress, @@ -25,21 +62,10 @@ export class AuthWitnessAccountEntrypoint implements Entrypoint { private version: number = DEFAULT_VERSION, ) {} - /** - * Sign a message hash with the private key. - * @param message - The message hash to sign. - * @returns The signature as a Buffer. - */ public sign(message: Buffer): Buffer { return this.signer.constructSignature(message, this.privateKey).toBuffer(); } - /** - * Creates an AuthWitness witness for the given message. In this case, witness is the public key, the signature - * and the partial address, to be used for verification. - * @param message - The message hash to sign. - * @returns [publicKey, signature, partialAddress] as Fr[]. - */ async createAuthWitness(message: Buffer): Promise { const signature = this.sign(message); const publicKey = await generatePublicKey(this.privateKey); diff --git a/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts b/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts index 83f49162064..40e22a4640b 100644 --- a/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts +++ b/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts @@ -16,7 +16,7 @@ import { TxReceipt, } from '@aztec/types'; -import { AuthWitnessAccountEntrypoint, Entrypoint } from '../account/entrypoint/index.js'; +import { Entrypoint, IAuthWitnessAccountEntrypoint } from '../account/entrypoint/index.js'; import { CompleteAddress } from '../index.js'; /** @@ -121,7 +121,7 @@ export class EntrypointWallet extends BaseWallet { * to provide authentication data to the contract during execution. */ export class AuthWitnessEntrypointWallet extends BaseWallet { - constructor(rpc: AztecRPC, protected accountImpl: AuthWitnessAccountEntrypoint) { + constructor(rpc: AztecRPC, protected accountImpl: IAuthWitnessAccountEntrypoint, protected address: CompleteAddress) { super(rpc); } @@ -151,12 +151,33 @@ export class AuthWitnessEntrypointWallet extends BaseWallet { * This is useful for signing messages that are not directly part of the transaction payload, such as * approvals . * @param messageHash - The message hash to sign + * @param opts - The options. */ async signAndAddAuthWitness(messageHash: Buffer): Promise { const witness = await this.accountImpl.createAuthWitness(messageHash); await this.rpc.addAuthWitness(Fr.fromBuffer(messageHash), witness); return Promise.resolve(); } + + /** + * Signs the `messageHash` and adds the witness to the RPC. + * This is useful for signing messages that are not directly part of the transaction payload, such as + * approvals . + * @param messageHash - The message hash to sign + */ + async signAndGetAuthWitness(messageHash: Buffer): Promise { + return await this.accountImpl.createAuthWitness(messageHash); + } + + /** Returns the complete address of the account that implements this wallet. */ + public getCompleteAddress() { + return this.address; + } + + /** Returns the address of the account that implements this wallet. */ + public getAddress() { + return this.address.address; + } } /** diff --git a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts index c77869a4c5e..204e163fa53 100644 --- a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts +++ b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts @@ -104,6 +104,7 @@ SchnorrAuthWitnessAccountContractAbi SchnorrHardcodedAccountContractAbi SchnorrSingleKeyAccountContractAbi TestContractAbi +TokenContractAbi UniswapContractAbi // docs:end:example-contracts `; diff --git a/yarn-project/end-to-end/src/e2e_account_contracts.test.ts b/yarn-project/end-to-end/src/e2e_account_contracts.test.ts index 760afc1637d..43283fc8d2e 100644 --- a/yarn-project/end-to-end/src/e2e_account_contracts.test.ts +++ b/yarn-project/end-to-end/src/e2e_account_contracts.test.ts @@ -128,7 +128,7 @@ describe('e2e_account_contracts', () => { await tx.wait(); } const entryPoint = (await account.getEntrypoint()) as unknown as AuthWitnessAccountEntrypoint; - const wallet = new AuthWitnessEntrypointWallet(rpc, entryPoint); + const wallet = new AuthWitnessEntrypointWallet(rpc, entryPoint, await account.getCompleteAddress()); return { account, wallet }; }, ); diff --git a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts index 0f6d538290b..efc5b8843d4 100644 --- a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts @@ -3,11 +3,11 @@ import { AztecRPCServer } from '@aztec/aztec-rpc'; import { Account, AuthWitnessAccountContract, - AuthWitnessAccountEntrypoint, AuthWitnessEntrypointWallet, AztecAddress, CheatCodes, Fr, + IAuthWitnessAccountEntrypoint, computeMessageSecretHash, } from '@aztec/aztec.js'; import { CircuitsWasm, CompleteAddress, FunctionSelector, GeneratorIndex, GrumpkinScalar } from '@aztec/circuits.js'; @@ -82,15 +82,18 @@ describe('e2e_lending_contract', () => { beforeEach(async () => { ({ aztecNode, aztecRpcServer, logger, cheatCodes: cc } = await setup(0)); - const privateKey = GrumpkinScalar.random(); - const account = new Account(aztecRpcServer, privateKey, new AuthWitnessAccountContract(privateKey)); - const entryPoint = (await account.getEntrypoint()) as unknown as AuthWitnessAccountEntrypoint; - - const deployTx = await account.deploy(); - await deployTx.wait({ interval: 0.1 }); - - wallet = new AuthWitnessEntrypointWallet(aztecRpcServer, entryPoint); - accounts = await wallet.getAccounts(); + { + const privateKey = GrumpkinScalar.random(); + const account = new Account(aztecRpcServer, privateKey, new AuthWitnessAccountContract(privateKey)); + const deployTx = await account.deploy(); + await deployTx.wait({ interval: 0.1 }); + wallet = new AuthWitnessEntrypointWallet( + aztecRpcServer, + (await account.getEntrypoint()) as unknown as IAuthWitnessAccountEntrypoint, + await account.getCompleteAddress(), + ); + accounts = await wallet.getAccounts(); + } }, 100_000); afterEach(async () => { diff --git a/yarn-project/end-to-end/src/e2e_token_contract.test.ts b/yarn-project/end-to-end/src/e2e_token_contract.test.ts new file mode 100644 index 00000000000..649a53cdee6 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract.test.ts @@ -0,0 +1,1391 @@ +import { AztecNodeService } from '@aztec/aztec-node'; +import { AztecRPCServer } from '@aztec/aztec-rpc'; +import { + Account, + AuthWitnessAccountContract, + AuthWitnessEntrypointWallet, + AztecAddress, + IAuthWitnessAccountEntrypoint, + computeMessageSecretHash, +} from '@aztec/aztec.js'; +import { + CircuitsWasm, + CompleteAddress, + Fr, + FunctionSelector, + GeneratorIndex, + GrumpkinScalar, +} from '@aztec/circuits.js'; +import { pedersenPlookupCompressWithHashIndex } from '@aztec/circuits.js/barretenberg'; +import { DebugLogger } from '@aztec/foundation/log'; +import { SchnorrAuthWitnessAccountContract, TokenContract } from '@aztec/noir-contracts/types'; +import { AztecRPC, TxStatus } from '@aztec/types'; + +import { jest } from '@jest/globals'; + +import { setup } from './fixtures/utils.js'; + +const hashPayload = async (payload: Fr[]) => { + return pedersenPlookupCompressWithHashIndex( + await CircuitsWasm.get(), + payload.map(fr => fr.toBuffer()), + GeneratorIndex.SIGNATURE_PAYLOAD, + ); +}; + +const TIMEOUT = 60_000; + +class TokenSimulator { + private balancesPrivate: Map = new Map(); + private balancePublic: Map = new Map(); + public totalSupply: bigint = 0n; + + constructor(protected token: TokenContract, protected logger: DebugLogger, protected accounts: CompleteAddress[]) {} + + public mintPrivate(to: AztecAddress, amount: bigint) { + this.totalSupply += amount; + } + + public mintPublic(to: AztecAddress, amount: bigint) { + this.totalSupply += amount; + const value = this.balancePublic.get(to) || 0n; + this.balancePublic.set(to, value + amount); + } + + public transferPublic(from: AztecAddress, to: AztecAddress, amount: bigint) { + const fromBalance = this.balancePublic.get(from) || 0n; + this.balancePublic.set(from, fromBalance - amount); + expect(fromBalance).toBeGreaterThanOrEqual(amount); + + const toBalance = this.balancePublic.get(to) || 0n; + this.balancePublic.set(to, toBalance + amount); + } + + public transferPrivate(from: AztecAddress, to: AztecAddress, amount: bigint) { + const fromBalance = this.balancesPrivate.get(from) || 0n; + expect(fromBalance).toBeGreaterThanOrEqual(amount); + this.balancesPrivate.set(from, fromBalance - amount); + + const toBalance = this.balancesPrivate.get(to) || 0n; + this.balancesPrivate.set(to, toBalance + amount); + } + + public shield(from: AztecAddress, amount: bigint) { + const fromBalance = this.balancePublic.get(from) || 0n; + expect(fromBalance).toBeGreaterThanOrEqual(amount); + this.balancePublic.set(from, fromBalance - amount); + } + + public redeemShield(to: AztecAddress, amount: bigint) { + const toBalance = this.balancesPrivate.get(to) || 0n; + this.balancesPrivate.set(to, toBalance + amount); + } + + public unshield(from: AztecAddress, to: AztecAddress, amount: bigint) { + const fromBalance = this.balancesPrivate.get(from) || 0n; + const toBalance = this.balancePublic.get(to) || 0n; + expect(fromBalance).toBeGreaterThanOrEqual(amount); + this.balancesPrivate.set(from, fromBalance - amount); + this.balancePublic.set(to, toBalance + amount); + } + + public burnPrivate(from: AztecAddress, amount: bigint) { + const fromBalance = this.balancesPrivate.get(from) || 0n; + expect(fromBalance).toBeGreaterThanOrEqual(amount); + this.balancesPrivate.set(from, fromBalance - amount); + + this.totalSupply -= amount; + } + + public burnPublic(from: AztecAddress, amount: bigint) { + const fromBalance = this.balancePublic.get(from) || 0n; + expect(fromBalance).toBeGreaterThanOrEqual(amount); + this.balancePublic.set(from, fromBalance - amount); + + this.totalSupply -= amount; + } + + public balanceOfPublic(address: AztecAddress) { + return this.balancePublic.get(address) || 0n; + } + + public balanceOfPrivate(address: AztecAddress) { + return this.balancesPrivate.get(address) || 0n; + } + + public async check() { + expect(await this.token.methods.total_supply().view()).toEqual(this.totalSupply); + + // Check that all our public matches + for (const { address } of this.accounts) { + expect(await this.token.methods.balance_of_public({ address }).view()).toEqual(this.balanceOfPublic(address)); + expect(await this.token.methods.balance_of_private({ address }).view()).toEqual(this.balanceOfPrivate(address)); + } + } +} + +describe('e2e_token_contract', () => { + jest.setTimeout(TIMEOUT); + + let aztecNode: AztecNodeService | undefined; + let aztecRpcServer: AztecRPC; + let wallets: AuthWitnessEntrypointWallet[]; + let accounts: CompleteAddress[]; + let logger: DebugLogger; + + let asset: TokenContract; + + let tokenSim: TokenSimulator; + + beforeAll(async () => { + ({ aztecNode, aztecRpcServer, logger } = await setup(0)); + + { + const _accounts = []; + for (let i = 0; i < 3; i++) { + const privateKey = GrumpkinScalar.random(); + const account = new Account(aztecRpcServer, privateKey, new AuthWitnessAccountContract(privateKey)); + const deployTx = await account.deploy(); + await deployTx.wait({ interval: 0.1 }); + _accounts.push(account); + } + wallets = await Promise.all( + _accounts.map( + async account => + new AuthWitnessEntrypointWallet( + aztecRpcServer, + (await account.getEntrypoint()) as unknown as IAuthWitnessAccountEntrypoint, + await account.getCompleteAddress(), + ), + ), + ); + //wallet = new AuthWitnessEntrypointWallet(aztecRpcServer, await AuthEntrypointCollection.fromAccounts(_accounts)); + accounts = await wallets[0].getAccounts(); + } + + { + logger(`Deploying token contract`); + const tx = TokenContract.deploy(wallets[0]).send(); + logger(`Tx sent with hash ${await tx.getTxHash()}`); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + logger(`Token deployed to ${receipt.contractAddress}`); + asset = await TokenContract.at(receipt.contractAddress!, wallets[0]); + } + + tokenSim = new TokenSimulator(asset, logger, accounts); + + { + const initializeTx = asset.methods._initialize({ address: accounts[0].address }).send(); + const receipt = await initializeTx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + expect(await asset.methods.admin().view()).toBe(accounts[0].address.toBigInt()); + } + + asset.abi.functions.forEach(fn => { + logger( + `Function ${fn.name} has ${fn.bytecode.length} bytes and the selector: ${FunctionSelector.fromNameAndParameters( + fn.name, + fn.parameters, + )}`, + ); + }); + }, 100_000); + + afterAll(async () => { + await aztecNode?.stop(); + if (aztecRpcServer instanceof AztecRPCServer) { + await aztecRpcServer?.stop(); + } + }); + + afterEach(async () => { + await tokenSim.check(); + }, TIMEOUT); + + describe('Access controlled functions', () => { + it('Set admin', async () => { + const tx = asset.methods.set_admin({ address: accounts[1].address }).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + expect(await asset.methods.admin().view()).toBe(accounts[1].address.toBigInt()); + }); + + it('Add minter as admin', async () => { + const tx = asset.withWallet(wallets[1]).methods.set_minter({ address: accounts[1].address }, 1).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + expect(await asset.methods.is_minter({ address: accounts[1].address }).view()).toBe(true); + }); + + it('Revoke minter as admin', async () => { + const tx = asset.withWallet(wallets[1]).methods.set_minter({ address: accounts[1].address }, 0).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + expect(await asset.methods.is_minter({ address: accounts[1].address }).view()).toBe(false); + }); + + describe('failure cases', () => { + it('Set admin (not admin)', async () => { + await expect(asset.methods.set_admin({ address: accounts[0].address }).simulate()).rejects.toThrowError( + 'Assertion failed: caller is not admin', + ); + }); + it('Revoke minter not as admin', async () => { + await expect(asset.methods.set_minter({ address: accounts[0].address }, 0).simulate()).rejects.toThrowError( + 'Assertion failed: caller is not admin', + ); + }); + }); + }); + + describe('Minting', () => { + describe('Public', () => { + it('as minter', async () => { + const amount = 10000n; + const tx = asset.methods.mint_public({ address: accounts[0].address }, amount).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + + tokenSim.mintPublic(accounts[0].address, amount); + expect(await asset.methods.balance_of_public({ address: accounts[0].address }).view()).toEqual( + tokenSim.balanceOfPublic(accounts[0].address), + ); + expect(await asset.methods.total_supply().view()).toEqual(tokenSim.totalSupply); + }); + + describe('failure cases', () => { + it('as non-minter', async () => { + const amount = 10000n; + await expect( + asset.withWallet(wallets[1]).methods.mint_public({ address: accounts[0].address }, amount).simulate(), + ).rejects.toThrowError('Assertion failed: caller is not minter'); + }); + + it('mint >u120 tokens to overflow', async () => { + const amount = 2n ** 120n; // SafeU120::max() + 1; + await expect( + asset.methods.mint_public({ address: accounts[0].address }, amount).simulate(), + ).rejects.toThrowError('Assertion failed: Value too large for SafeU120'); + }); + + it('mint u120', async () => { + const amount = 2n ** 120n - tokenSim.balanceOfPublic(accounts[0].address); + await expect( + asset.methods.mint_public({ address: accounts[0].address }, amount).simulate(), + ).rejects.toThrowError('Assertion failed: Overflow'); + }); + + it('mint u120', async () => { + const amount = 2n ** 120n - tokenSim.balanceOfPublic(accounts[0].address); + await expect( + asset.methods.mint_public({ address: accounts[1].address }, amount).simulate(), + ).rejects.toThrowError('Assertion failed: Overflow'); + }); + }); + }); + + describe('Private', () => { + const secret = Fr.random(); + const amount = 10000n; + let secretHash: Fr; + + beforeAll(async () => { + secretHash = await computeMessageSecretHash(secret); + }); + + describe('Mint flow', () => { + it('mint_private as minter', async () => { + const tx = asset.methods.mint_private(amount, secretHash).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + tokenSim.mintPrivate(accounts[0].address, amount); + }); + + it('redeem as recipient', async () => { + const txClaim = asset.methods.redeem_shield({ address: accounts[0].address }, amount, secret).send(); + const receiptClaim = await txClaim.wait(); + expect(receiptClaim.status).toBe(TxStatus.MINED); + tokenSim.redeemShield(accounts[0].address, amount); + }); + }); + + describe('failure cases', () => { + it('try to redeem as recipient (double-spend) [REVERTS]', async () => { + const txClaim = asset.methods.redeem_shield({ address: accounts[0].address }, amount, secret).send(); + await txClaim.isMined(); + const receipt = await txClaim.getReceipt(); + expect(receipt.status).toBe(TxStatus.DROPPED); + }); + + it('mint_private as non-minter', async () => { + await expect( + asset.withWallet(wallets[1]).methods.mint_private(amount, secretHash).simulate(), + ).rejects.toThrowError('Assertion failed: caller is not minter'); + }); + + it('mint >u120 tokens to overflow', async () => { + const amount = 2n ** 120n; // SafeU120::max() + 1; + await expect(asset.methods.mint_private(amount, secretHash).simulate()).rejects.toThrowError( + 'Assertion failed: Value too large for SafeU120', + ); + }); + + it('mint u120', async () => { + const amount = 2n ** 120n - tokenSim.balanceOfPrivate(accounts[0].address); + expect(amount).toBeLessThan(2n ** 120n); + await expect(asset.methods.mint_private(amount, secretHash).simulate()).rejects.toThrowError( + 'Assertion failed: Overflow', + ); + }); + + it('mint u120', async () => { + const amount = 2n ** 120n - tokenSim.totalSupply; + await expect(asset.methods.mint_private(amount, secretHash).simulate()).rejects.toThrowError( + 'Assertion failed: Overflow', + ); + }); + }); + }); + }); + + describe('Transfer', () => { + describe('public', () => { + const transferMessageHash = async ( + caller: CompleteAddress, + from: CompleteAddress, + to: CompleteAddress, + amount: bigint, + nonce: Fr, + ) => { + return await hashPayload([ + caller.address.toField(), + asset.address.toField(), + FunctionSelector.fromSignature('transfer_public((Field),(Field),Field,Field)').toField(), + from.address.toField(), + to.address.toField(), + new Fr(amount), + nonce, + ]); + }; + + it('transfer less than balance', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const tx = asset.methods + .transfer_public({ address: accounts[0].address }, { address: accounts[1].address }, amount, 0) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + + tokenSim.transferPublic(accounts[0].address, accounts[1].address, amount); + }); + + it('transfer to self', async () => { + const balance = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance / 2n; + expect(amount).toBeGreaterThan(0n); + const tx = asset.methods + .transfer_public({ address: accounts[0].address }, { address: accounts[0].address }, amount, 0) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + + tokenSim.transferPublic(accounts[0].address, accounts[0].address, amount); + }); + + it('transfer on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + // We need to compute the message we want to sign. + const messageHash = await transferMessageHash(accounts[1], accounts[0], accounts[1], amount, nonce); + + // Add it to the wallet as approved + const me = await SchnorrAuthWitnessAccountContract.at(accounts[0].address, wallets[0]); + const setValidTx = me.methods.set_is_valid_storage(messageHash, 1).send(); + const validTxReceipt = await setValidTx.wait(); + expect(validTxReceipt.status).toBe(TxStatus.MINED); + + // Perform the transfer + const tx = asset + .withWallet(wallets[1]) + .methods.transfer_public({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + + tokenSim.transferPublic(accounts[0].address, accounts[1].address, amount); + + // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txReplay = asset + .withWallet(wallets[1]) + .methods.transfer_public({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .send(); + await txReplay.isMined(); + const receiptReplay = await txReplay.getReceipt(); + expect(receiptReplay.status).toBe(TxStatus.DROPPED); + }); + + describe('failure cases', () => { + it('transfer more than balance', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance0 + 1n; + const nonce = 0; + await expect( + asset.methods + .transfer_public({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .simulate(), + ).rejects.toThrowError('Assertion failed: Underflow'); + }); + + it('transfer on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance0 - 1n; + const nonce = 1; + await expect( + asset.methods + .transfer_public({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .simulate(), + ).rejects.toThrowError('Assertion failed: invalid nonce'); + }); + + it('transfer on behalf of other without "approval"', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + await expect( + asset + .withWallet(wallets[1]) + .methods.transfer_public( + { address: accounts[0].address }, + { address: accounts[1].address }, + amount, + nonce, + ) + .simulate(), + ).rejects.toThrowError('Assertion failed: invalid call'); + }); + + it('transfer more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const balance1 = await asset.methods.balance_of_public({ address: accounts[1].address }).view(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await transferMessageHash(accounts[1], accounts[0], accounts[1], amount, nonce); + + // Add it to the wallet as approved + const me = await SchnorrAuthWitnessAccountContract.at(accounts[0].address, wallets[0]); + const setValidTx = me.methods.set_is_valid_storage(messageHash, 1).send(); + const validTxReceipt = await setValidTx.wait(); + expect(validTxReceipt.status).toBe(TxStatus.MINED); + + // Perform the transfer + await expect( + asset + .withWallet(wallets[1]) + .methods.transfer_public( + { address: accounts[0].address }, + { address: accounts[1].address }, + amount, + nonce, + ) + .simulate(), + ).rejects.toThrowError('Assertion failed: Underflow'); + + expect(await asset.methods.balance_of_public({ address: accounts[0].address }).view()).toEqual(balance0); + expect(await asset.methods.balance_of_public({ address: accounts[1].address }).view()).toEqual(balance1); + }); + + it('transfer on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const balance1 = await asset.methods.balance_of_public({ address: accounts[1].address }).view(); + const amount = balance0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await transferMessageHash(accounts[0], accounts[0], accounts[1], amount, nonce); + + // Add it to the wallet as approved + const me = await SchnorrAuthWitnessAccountContract.at(accounts[0].address, wallets[0]); + const setValidTx = me.methods.set_is_valid_storage(messageHash, 1).send(); + const validTxReceipt = await setValidTx.wait(); + expect(validTxReceipt.status).toBe(TxStatus.MINED); + + // Perform the transfer + await expect( + asset + .withWallet(wallets[1]) + .methods.transfer_public( + { address: accounts[0].address }, + { address: accounts[1].address }, + amount, + nonce, + ) + .simulate(), + ).rejects.toThrowError('Assertion failed: invalid call'); + + expect(await asset.methods.balance_of_public({ address: accounts[0].address }).view()).toEqual(balance0); + expect(await asset.methods.balance_of_public({ address: accounts[1].address }).view()).toEqual(balance1); + }); + + it('transfer on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const balance1 = await asset.methods.balance_of_public({ address: accounts[1].address }).view(); + const amount = balance0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await transferMessageHash(accounts[0], accounts[0], accounts[1], amount, nonce); + + // Add it to the wallet as approved + const me = await SchnorrAuthWitnessAccountContract.at(accounts[0].address, wallets[0]); + const setValidTx = me.methods.set_is_valid_storage(messageHash, 1).send(); + const validTxReceipt = await setValidTx.wait(); + expect(validTxReceipt.status).toBe(TxStatus.MINED); + + // Perform the transfer + await expect( + asset + .withWallet(wallets[1]) + .methods.transfer_public( + { address: accounts[0].address }, + { address: accounts[1].address }, + amount, + nonce, + ) + .simulate(), + ).rejects.toThrowError('Assertion failed: invalid call'); + + expect(await asset.methods.balance_of_public({ address: accounts[0].address }).view()).toEqual(balance0); + expect(await asset.methods.balance_of_public({ address: accounts[1].address }).view()).toEqual(balance1); + }); + + it.skip('transfer into account to overflow', () => { + // This should already be covered by the mint case earlier. e.g., since we cannot mint to overflow, there is not + // a way to get funds enough to overflow. + // Require direct storage manipulation for us to perform a nice explicit case though. + // See https://github.com/AztecProtocol/aztec-packages/issues/1259 + }); + }); + }); + + describe('private', () => { + const transferMessageHash = async ( + caller: CompleteAddress, + from: CompleteAddress, + to: CompleteAddress, + amount: bigint, + nonce: Fr, + ) => { + return await hashPayload([ + caller.address.toField(), + asset.address.toField(), + FunctionSelector.fromSignature('transfer((Field),(Field),Field,Field)').toField(), + from.address.toField(), + to.address.toField(), + new Fr(amount), + nonce, + ]); + }; + + it('transfer less than balance', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const tx = asset.methods + .transfer({ address: accounts[0].address }, { address: accounts[1].address }, amount, 0) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + tokenSim.transferPrivate(accounts[0].address, accounts[1].address, amount); + }); + + it('transfer to self', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const tx = asset.methods + .transfer({ address: accounts[0].address }, { address: accounts[0].address }, amount, 0) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + tokenSim.transferPrivate(accounts[0].address, accounts[0].address, amount); + }); + + it('transfer on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await transferMessageHash(accounts[1], accounts[0], accounts[1], amount, nonce); + + // Both wallets are connected to same node and rpc so we could just insert directly using + // await wallet.signAndAddAuthWitness(messageHash, ); + // But doing it in two actions to show the flow. + const witness = await wallets[0].signAndGetAuthWitness(messageHash); + await wallets[1].addAuthWitness(Fr.fromBuffer(messageHash), witness); + + // Perform the transfer + const tx = asset + .withWallet(wallets[1]) + .methods.transfer({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + tokenSim.transferPrivate(accounts[0].address, accounts[1].address, amount); + + // Perform the transfer again, should fail + const txReplay = asset + .withWallet(wallets[1]) + .methods.transfer({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .send(); + await txReplay.isMined(); + const receiptReplay = await txReplay.getReceipt(); + expect(receiptReplay.status).toBe(TxStatus.DROPPED); + }); + + describe('failure cases', () => { + it('transfer more than balance', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 + 1n; + expect(amount).toBeGreaterThan(0n); + await expect( + asset.methods + .transfer({ address: accounts[0].address }, { address: accounts[1].address }, amount, 0) + .simulate(), + ).rejects.toThrowError('Assertion failed: Balance too low'); + }); + + it('transfer on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 - 1n; + expect(amount).toBeGreaterThan(0n); + await expect( + asset.methods + .transfer({ address: accounts[0].address }, { address: accounts[1].address }, amount, 1) + .simulate(), + ).rejects.toThrowError('Assertion failed: invalid nonce'); + }); + + it('transfer more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const balance1 = await asset.methods.balance_of_private({ address: accounts[1].address }).view(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await transferMessageHash(accounts[1], accounts[0], accounts[1], amount, nonce); + + // Both wallets are connected to same node and rpc so we could just insert directly using + // await wallet.signAndAddAuthWitness(messageHash, ); + // But doing it in two actions to show the flow. + const witness = await wallets[0].signAndGetAuthWitness(messageHash); + await wallets[1].addAuthWitness(Fr.fromBuffer(messageHash), witness); + + // Perform the transfer + await expect( + asset + .withWallet(wallets[1]) + .methods.transfer({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .simulate(), + ).rejects.toThrowError('Assertion failed: Balance too low'); + expect(await asset.methods.balance_of_private({ address: accounts[0].address }).view()).toEqual(balance0); + expect(await asset.methods.balance_of_private({ address: accounts[1].address }).view()).toEqual(balance1); + }); + + it.skip('transfer into account to overflow', () => { + // This should already be covered by the mint case earlier. e.g., since we cannot mint to overflow, there is not + // a way to get funds enough to overflow. + // Require direct storage manipulation for us to perform a nice explicit case though. + // See https://github.com/AztecProtocol/aztec-packages/issues/1259 + }); + + it('transfer on behalf of other without approval', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await transferMessageHash(accounts[1], accounts[0], accounts[1], amount, nonce); + + await expect( + asset + .withWallet(wallets[1]) + .methods.transfer({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .simulate(), + ).rejects.toThrowError(`Unknown auth witness for message hash 0x${messageHash.toString('hex')}`); + }); + + it('transfer on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await transferMessageHash(accounts[1], accounts[0], accounts[1], amount, nonce); + const expectedMessageHash = await transferMessageHash(accounts[2], accounts[0], accounts[1], amount, nonce); + + const witness = await wallets[0].signAndGetAuthWitness(messageHash); + await wallets[2].addAuthWitness(Fr.fromBuffer(messageHash), witness); + + await expect( + asset + .withWallet(wallets[2]) + .methods.transfer({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .simulate(), + ).rejects.toThrowError(`Unknown auth witness for message hash 0x${expectedMessageHash.toString('hex')}`); + expect(await asset.methods.balance_of_private({ address: accounts[0].address }).view()).toEqual(balance0); + }); + }); + }); + }); + + describe('Shielding (shield + redeem_shield)', () => { + const secret = Fr.random(); + let secretHash: Fr; + + beforeAll(async () => { + secretHash = await computeMessageSecretHash(secret); + }); + + const shieldMessageHash = async ( + caller: CompleteAddress, + from: CompleteAddress, + amount: bigint, + secretHash: Fr, + nonce: Fr, + ) => { + return await hashPayload([ + caller.address.toField(), + asset.address.toField(), + FunctionSelector.fromSignature('shield((Field),Field,Field,Field)').toField(), + from.address.toField(), + new Fr(amount), + secretHash, + nonce, + ]); + }; + + it('on behalf of self', async () => { + const balancePub = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balancePub / 2n; + expect(amount).toBeGreaterThan(0n); + + const tx = asset.methods.shield({ address: accounts[0].address }, amount, secretHash, 0).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + + tokenSim.shield(accounts[0].address, amount); + await tokenSim.check(); + + // Redeem it + const txClaim = asset.methods.redeem_shield({ address: accounts[0].address }, amount, secret).send(); + const receiptClaim = await txClaim.wait(); + expect(receiptClaim.status).toBe(TxStatus.MINED); + + tokenSim.redeemShield(accounts[0].address, amount); + + // Check that claiming again will hit a double-spend and fail due to pending note already consumed. + const txClaimDoubleSpend = asset.methods.redeem_shield({ address: accounts[0].address }, amount, secret).send(); + await txClaimDoubleSpend.isMined(); + const receiptDoubleSpend = await txClaimDoubleSpend.getReceipt(); + expect(receiptDoubleSpend.status).toBe(TxStatus.DROPPED); + }); + + it('on behalf of other', async () => { + const balancePub = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balancePub / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await shieldMessageHash(accounts[1], accounts[0], amount, secretHash, nonce); + + // Add it to the wallet as approved + const me = await SchnorrAuthWitnessAccountContract.at(accounts[0].address, wallets[0]); + const setValidTx = me.methods.set_is_valid_storage(messageHash, 1).send(); + const validTxReceipt = await setValidTx.wait(); + expect(validTxReceipt.status).toBe(TxStatus.MINED); + + const tx = asset + .withWallet(wallets[1]) + .methods.shield({ address: accounts[0].address }, amount, secretHash, nonce) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + + tokenSim.shield(accounts[0].address, amount); + await tokenSim.check(); + + // Check that replaying the shield should fail! + const txReplay = asset + .withWallet(wallets[1]) + .methods.shield({ address: accounts[0].address }, amount, secretHash, nonce) + .send(); + await txReplay.isMined(); + const receiptReplay = await txReplay.getReceipt(); + expect(receiptReplay.status).toBe(TxStatus.DROPPED); + + // Redeem it + const txClaim = asset.methods.redeem_shield({ address: accounts[0].address }, amount, secret).send(); + const receiptClaim = await txClaim.wait(); + expect(receiptClaim.status).toBe(TxStatus.MINED); + + tokenSim.redeemShield(accounts[0].address, amount); + + // Check that claiming again will hit a double-spend and fail due to pending note already consumed. + const txClaimDoubleSpend = asset.methods.redeem_shield({ address: accounts[0].address }, amount, secret).send(); + await txClaimDoubleSpend.isMined(); + const receiptDoubleSpend = await txClaimDoubleSpend.getReceipt(); + expect(receiptDoubleSpend.status).toBe(TxStatus.DROPPED); + }); + + describe('failure cases', () => { + it('on behalf of self (more than balance)', async () => { + const balancePub = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balancePub + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.methods.shield({ address: accounts[0].address }, amount, secretHash, 0).simulate(), + ).rejects.toThrowError('Assertion failed: Underflow'); + }); + + it('on behalf of self (invalid nonce)', async () => { + const balancePub = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balancePub + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.methods.shield({ address: accounts[0].address }, amount, secretHash, 1).simulate(), + ).rejects.toThrowError('Assertion failed: invalid nonce'); + }); + + it('on behalf of other (more than balance)', async () => { + const balancePub = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balancePub + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await shieldMessageHash(accounts[1], accounts[0], amount, secretHash, nonce); + + // Add it to the wallet as approved + const me = await SchnorrAuthWitnessAccountContract.at(accounts[0].address, wallets[0]); + const setValidTx = me.methods.set_is_valid_storage(messageHash, 1).send(); + const validTxReceipt = await setValidTx.wait(); + expect(validTxReceipt.status).toBe(TxStatus.MINED); + + await expect( + asset + .withWallet(wallets[1]) + .methods.shield({ address: accounts[0].address }, amount, secretHash, nonce) + .simulate(), + ).rejects.toThrowError('Assertion failed: Underflow'); + }); + + it('on behalf of other (wrong designated caller)', async () => { + const balancePub = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balancePub + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await shieldMessageHash(accounts[1], accounts[0], amount, secretHash, nonce); + + // Add it to the wallet as approved + const me = await SchnorrAuthWitnessAccountContract.at(accounts[0].address, wallets[0]); + const setValidTx = me.methods.set_is_valid_storage(messageHash, 1).send(); + const validTxReceipt = await setValidTx.wait(); + expect(validTxReceipt.status).toBe(TxStatus.MINED); + + await expect( + asset + .withWallet(wallets[2]) + .methods.shield({ address: accounts[0].address }, amount, secretHash, nonce) + .simulate(), + ).rejects.toThrowError('Assertion failed: invalid call'); + }); + + it('on behalf of other (wrong designated caller)', async () => { + const balancePub = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const balancePriv = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balancePub + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await shieldMessageHash(accounts[1], accounts[0], amount, secretHash, nonce); + + // Add it to the wallet as approved + const me = await SchnorrAuthWitnessAccountContract.at(accounts[0].address, wallets[0]); + const setValidTx = me.methods.set_is_valid_storage(messageHash, 1).send(); + const validTxReceipt = await setValidTx.wait(); + expect(validTxReceipt.status).toBe(TxStatus.MINED); + + await expect( + asset + .withWallet(wallets[2]) + .methods.shield({ address: accounts[0].address }, amount, secretHash, nonce) + .simulate(), + ).rejects.toThrowError('Assertion failed: invalid call'); + + expect(await asset.methods.balance_of_public({ address: accounts[0].address }).view()).toEqual(balancePub); + expect(await asset.methods.balance_of_private({ address: accounts[0].address }).view()).toEqual(balancePriv); + }); + + it('on behalf of other (without approval)', async () => { + const balance = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + await expect( + asset + .withWallet(wallets[1]) + .methods.shield({ address: accounts[0].address }, amount, secretHash, nonce) + .simulate(), + ).rejects.toThrowError(`Assertion failed: invalid call`); + }); + }); + }); + + describe('Unshielding', () => { + const unshieldMessageHash = async ( + caller: CompleteAddress, + from: CompleteAddress, + to: CompleteAddress, + amount: bigint, + nonce: Fr, + ) => { + return await hashPayload([ + caller.address.toField(), + asset.address.toField(), + FunctionSelector.fromSignature('unshield((Field),(Field),Field,Field)').toField(), + accounts[0].address.toField(), + accounts[1].address.toField(), + new Fr(amount), + nonce, + ]); + }; + + it('on behalf of self', async () => { + const balancePriv = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balancePriv / 2n; + expect(amount).toBeGreaterThan(0n); + + const tx = asset.methods + .unshield({ address: accounts[0].address }, { address: accounts[0].address }, amount, 0) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + + tokenSim.unshield(accounts[0].address, accounts[0].address, amount); + }); + + it('on behalf of other', async () => { + const balancePriv0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balancePriv0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await unshieldMessageHash(accounts[1], accounts[0], accounts[1], amount, nonce); + + // Both wallets are connected to same node and rpc so we could just insert directly using + // await wallet.signAndAddAuthWitness(messageHash, ); + // But doing it in two actions to show the flow. + const witness = await wallets[0].signAndGetAuthWitness(messageHash); + await wallets[1].addAuthWitness(Fr.fromBuffer(messageHash), witness); + + const tx = asset + .withWallet(wallets[1]) + .methods.unshield({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + tokenSim.unshield(accounts[0].address, accounts[1].address, amount); + + // Perform the transfer again, should fail + const txReplay = asset + .withWallet(wallets[1]) + .methods.unshield({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .send(); + await txReplay.isMined(); + const receiptReplay = await txReplay.getReceipt(); + expect(receiptReplay.status).toBe(TxStatus.DROPPED); + }); + + describe('failure cases', () => { + it('on behalf of self (more than balance)', async () => { + const balancePriv = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balancePriv + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.methods + .unshield({ address: accounts[0].address }, { address: accounts[0].address }, amount, 0) + .simulate(), + ).rejects.toThrowError('Assertion failed: Balance too low'); + }); + + it('on behalf of self (invalid nonce)', async () => { + const balancePriv = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balancePriv + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.methods + .unshield({ address: accounts[0].address }, { address: accounts[0].address }, amount, 1) + .simulate(), + ).rejects.toThrowError('Assertion failed: invalid nonce'); + }); + + it('on behalf of other (more than balance)', async () => { + const balancePriv0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balancePriv0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await unshieldMessageHash(accounts[1], accounts[0], accounts[1], amount, nonce); + + // Both wallets are connected to same node and rpc so we could just insert directly using + // await wallet.signAndAddAuthWitness(messageHash, ); + // But doing it in two actions to show the flow. + const witness = await wallets[0].signAndGetAuthWitness(messageHash); + await wallets[1].addAuthWitness(Fr.fromBuffer(messageHash), witness); + + await expect( + asset + .withWallet(wallets[1]) + .methods.unshield({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .simulate(), + ).rejects.toThrowError('Assertion failed: Balance too low'); + }); + + it('on behalf of other (invalid designated caller)', async () => { + const balancePriv0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balancePriv0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await unshieldMessageHash(accounts[1], accounts[0], accounts[1], amount, nonce); + const expectedMessageHash = await unshieldMessageHash(accounts[2], accounts[0], accounts[1], amount, nonce); + + // Both wallets are connected to same node and rpc so we could just insert directly using + // await wallet.signAndAddAuthWitness(messageHash, ); + // But doing it in two actions to show the flow. + const witness = await wallets[0].signAndGetAuthWitness(messageHash); + await wallets[2].addAuthWitness(Fr.fromBuffer(messageHash), witness); + + await expect( + asset + .withWallet(wallets[2]) + .methods.unshield({ address: accounts[0].address }, { address: accounts[1].address }, amount, nonce) + .simulate(), + ).rejects.toThrowError(`Unknown auth witness for message hash 0x${expectedMessageHash.toString('hex')}`); + }); + }); + }); + + describe('Burn', () => { + describe('public', () => { + const burnMessageHash = async (caller: CompleteAddress, from: CompleteAddress, amount: bigint, nonce: Fr) => { + return await hashPayload([ + caller.address.toField(), + asset.address.toField(), + FunctionSelector.fromSignature('burn_public((Field),Field,Field)').toField(), + from.address.toField(), + new Fr(amount), + nonce, + ]); + }; + + it('burn less than balance', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const tx = asset.methods.burn_public({ address: accounts[0].address }, amount, 0).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + + tokenSim.burnPublic(accounts[0].address, amount); + }); + + it('burn on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + // We need to compute the message we want to sign. + const messageHash = await burnMessageHash(accounts[1], accounts[0], amount, nonce); + + // Add it to the wallet as approved + const me = await SchnorrAuthWitnessAccountContract.at(accounts[0].address, wallets[0]); + const setValidTx = me.methods.set_is_valid_storage(messageHash, 1).send(); + const validTxReceipt = await setValidTx.wait(); + expect(validTxReceipt.status).toBe(TxStatus.MINED); + + const tx = asset + .withWallet(wallets[1]) + .methods.burn_public({ address: accounts[0].address }, amount, nonce) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + + tokenSim.burnPublic(accounts[0].address, amount); + + // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txReplay = asset + .withWallet(wallets[1]) + .methods.burn_public({ address: accounts[0].address }, amount, nonce) + .send(); + await txReplay.isMined(); + const receiptReplay = await txReplay.getReceipt(); + expect(receiptReplay.status).toBe(TxStatus.DROPPED); + }); + + describe('failure cases', () => { + it('burn more than balance', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance0 + 1n; + const nonce = 0; + await expect( + asset.methods.burn_public({ address: accounts[0].address }, amount, nonce).simulate(), + ).rejects.toThrowError('Assertion failed: Underflow'); + }); + + it('burn on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance0 - 1n; + expect(amount).toBeGreaterThan(0n); + const nonce = 1; + await expect( + asset.methods.burn_public({ address: accounts[0].address }, amount, nonce).simulate(), + ).rejects.toThrowError('Assertion failed: invalid nonce'); + }); + + it('burn on behalf of other without "approval"', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + await expect( + asset + .withWallet(wallets[1]) + .methods.burn_public({ address: accounts[0].address }, amount, nonce) + .simulate(), + ).rejects.toThrowError('Assertion failed: invalid call'); + }); + + it('burn more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await burnMessageHash(accounts[1], accounts[0], amount, nonce); + + // Add it to the wallet as approved + const me = await SchnorrAuthWitnessAccountContract.at(accounts[0].address, wallets[0]); + const setValidTx = me.methods.set_is_valid_storage(messageHash, 1).send(); + const validTxReceipt = await setValidTx.wait(); + expect(validTxReceipt.status).toBe(TxStatus.MINED); + + await expect( + asset + .withWallet(wallets[1]) + .methods.burn_public({ address: accounts[0].address }, amount, nonce) + .simulate(), + ).rejects.toThrowError('Assertion failed: Underflow'); + }); + + it('burn on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_public({ address: accounts[0].address }).view(); + const amount = balance0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await burnMessageHash(accounts[0], accounts[0], amount, nonce); + + // Add it to the wallet as approved + const me = await SchnorrAuthWitnessAccountContract.at(accounts[0].address, wallets[0]); + const setValidTx = me.methods.set_is_valid_storage(messageHash, 1).send(); + const validTxReceipt = await setValidTx.wait(); + expect(validTxReceipt.status).toBe(TxStatus.MINED); + + await expect( + asset + .withWallet(wallets[1]) + .methods.burn_public({ address: accounts[0].address }, amount, nonce) + .simulate(), + ).rejects.toThrowError('Assertion failed: invalid call'); + }); + }); + }); + + describe('private', () => { + const burnMessageHash = async (caller: CompleteAddress, from: CompleteAddress, amount: bigint, nonce: Fr) => { + return await hashPayload([ + caller.address.toField(), + asset.address.toField(), + FunctionSelector.fromSignature('burn((Field),Field,Field)').toField(), + from.address.toField(), + new Fr(amount), + nonce, + ]); + }; + + it('burn less than balance', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const tx = asset.methods.burn({ address: accounts[0].address }, amount, 0).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + tokenSim.burnPrivate(accounts[0].address, amount); + }); + + it('burn on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await burnMessageHash(accounts[1], accounts[0], amount, nonce); + + // Both wallets are connected to same node and rpc so we could just insert directly using + // await wallet.signAndAddAuthWitness(messageHash, ); + // But doing it in two actions to show the flow. + const witness = await wallets[0].signAndGetAuthWitness(messageHash); + await wallets[1].addAuthWitness(Fr.fromBuffer(messageHash), witness); + + const tx = asset.withWallet(wallets[1]).methods.burn({ address: accounts[0].address }, amount, nonce).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + tokenSim.burnPrivate(accounts[0].address, amount); + + // Perform the transfer again, should fail + const txReplay = asset + .withWallet(wallets[1]) + .methods.burn({ address: accounts[0].address }, amount, nonce) + .send(); + await txReplay.isMined(); + const receiptReplay = await txReplay.getReceipt(); + expect(receiptReplay.status).toBe(TxStatus.DROPPED); + }); + + describe('failure cases', () => { + it('burn more than balance', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 + 1n; + expect(amount).toBeGreaterThan(0n); + await expect(asset.methods.burn({ address: accounts[0].address }, amount, 0).simulate()).rejects.toThrowError( + 'Assertion failed: Balance too low', + ); + }); + + it('burn on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 - 1n; + expect(amount).toBeGreaterThan(0n); + await expect(asset.methods.burn({ address: accounts[0].address }, amount, 1).simulate()).rejects.toThrowError( + 'Assertion failed: invalid nonce', + ); + }); + + it('burn more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await burnMessageHash(accounts[1], accounts[0], amount, nonce); + + // Both wallets are connected to same node and rpc so we could just insert directly using + // await wallet.signAndAddAuthWitness(messageHash, ); + // But doing it in two actions to show the flow. + const witness = await wallets[0].signAndGetAuthWitness(messageHash); + await wallets[1].addAuthWitness(Fr.fromBuffer(messageHash), witness); + + await expect( + asset.withWallet(wallets[1]).methods.burn({ address: accounts[0].address }, amount, nonce).simulate(), + ).rejects.toThrowError('Assertion failed: Balance too low'); + }); + + it('burn on behalf of other without approval', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await burnMessageHash(accounts[1], accounts[0], amount, nonce); + + await expect( + asset.withWallet(wallets[1]).methods.burn({ address: accounts[0].address }, amount, nonce).simulate(), + ).rejects.toThrowError(`Unknown auth witness for message hash 0x${messageHash.toString('hex')}`); + }); + + it('burn on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await burnMessageHash(accounts[1], accounts[0], amount, nonce); + const expectedMessageHash = await burnMessageHash(accounts[2], accounts[0], amount, nonce); + + const witness = await wallets[0].signAndGetAuthWitness(messageHash); + await wallets[2].addAuthWitness(Fr.fromBuffer(messageHash), witness); + + await expect( + asset.withWallet(wallets[2]).methods.burn({ address: accounts[0].address }, amount, nonce).simulate(), + ).rejects.toThrowError(`Unknown auth witness for message hash 0x${expectedMessageHash.toString('hex')}`); + }); + }); + + it('on behalf of other (invalid designated caller)', async () => { + const balancePriv0 = await asset.methods.balance_of_private({ address: accounts[0].address }).view(); + const amount = balancePriv0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign. + const messageHash = await burnMessageHash(accounts[1], accounts[0], amount, nonce); + const expectedMessageHash = await burnMessageHash(accounts[2], accounts[0], amount, nonce); + + // Both wallets are connected to same node and rpc so we could just insert directly using + // await wallet.signAndAddAuthWitness(messageHash, { origin: accounts[0].address }); + // But doing it in two actions to show the flow. + const witness = await wallets[0].signAndGetAuthWitness(messageHash); + await wallets[2].addAuthWitness(Fr.fromBuffer(messageHash), witness); + + await expect( + asset.withWallet(wallets[2]).methods.burn({ address: accounts[0].address }, amount, nonce).simulate(), + ).rejects.toThrowError(`Unknown auth witness for message hash 0x${expectedMessageHash.toString('hex')}`); + }); + }); + }); +}); diff --git a/yarn-project/noir-contracts/Nargo.toml b/yarn-project/noir-contracts/Nargo.toml index d3300102923..9f6306f3b9a 100644 --- a/yarn-project/noir-contracts/Nargo.toml +++ b/yarn-project/noir-contracts/Nargo.toml @@ -23,5 +23,6 @@ members = [ "src/contracts/schnorr_hardcoded_account_contract", "src/contracts/schnorr_single_key_account_contract", "src/contracts/test_contract", + "src/contracts/token_contract", "src/contracts/uniswap_contract", ] diff --git a/yarn-project/noir-contracts/src/contracts/schnorr_auth_witness_account_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/schnorr_auth_witness_account_contract/src/main.nr index 749c1b04276..335ac5b9e98 100644 --- a/yarn-project/noir-contracts/src/contracts/schnorr_auth_witness_account_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/schnorr_auth_witness_account_contract/src/main.nr @@ -2,12 +2,57 @@ mod util; mod auth_oracle; contract SchnorrAuthWitnessAccount { - use dep::std::hash::pedersen_with_separator; - use dep::aztec::entrypoint::EntrypointPayload; - use dep::aztec::constants_gen::GENERATOR_INDEX__SIGNATURE_PAYLOAD; + use dep::std::{ + hash::pedersen_with_separator, + option::Option, + }; + + use dep::aztec::{ + entrypoint::EntrypointPayload, + constants_gen::GENERATOR_INDEX__SIGNATURE_PAYLOAD, + oracle::compute_selector::compute_selector, + context::{ + PrivateContext, + PublicContext, + Context, + }, + state_vars::{ + map::Map, + public_state::PublicState, + }, + types::type_serialisation::{ + field_serialisation::{ + FieldSerialisationMethods, + FIELD_SERIALISED_LEN, + } + } + }; + use crate::util::recover_address; use crate::auth_oracle::get_auth_witness; + struct Storage { + approved_action: Map>, + } + + impl Storage { + fn init(context: Context) -> pub Self { + Storage { + approved_action: Map::new( + context, + 1, + |context, slot| { + PublicState::new( + context, + slot, + FieldSerialisationMethods, + ) + }, + ), + } + } + } + #[aztec(private)] fn constructor() {} @@ -19,7 +64,7 @@ contract SchnorrAuthWitnessAccount { payload.serialize(), GENERATOR_INDEX__SIGNATURE_PAYLOAD )[0]; - _inner_is_valid(message_hash, context.this_address()); + assert(_inner_is_valid(message_hash, context.this_address())); payload.execute_calls(&mut context); } @@ -27,15 +72,54 @@ contract SchnorrAuthWitnessAccount { fn is_valid( message_hash: Field ) -> Field { - _inner_is_valid(message_hash, context.this_address()); - 0xe86ab4ff + if (_inner_is_valid(message_hash, context.this_address())){ + 0xe86ab4ff + } else { + 0 + } + } + + #[aztec(public)] + fn is_valid_public( + message_hash: Field, + ) -> Field { + let storage = Storage::init(Context::public(&mut context)); + let value = storage.approved_action.at(message_hash).read(); + if (value == 1){ + 0xe86ab4ff + } else { + 0 + } + } + + #[aztec(private)] + fn set_is_valid_storage( + message_hash: Field, + value: Field, + ) { + assert((value == 0) | (value == 1), "value must be a boolean"); + assert(context.msg_sender() == context.this_address(), "only the owner can set the storage"); + // assert(_inner_is_valid(message_hash, context.this_address()), "only the owner can set the storage"); + + let selector = compute_selector("_set_is_valid_storage(Field,Field)"); + let _void = context.call_public_function(context.this_address(), selector, [message_hash, value]); + } + + #[aztec(public)] + internal fn _set_is_valid_storage( + message_hash: Field, + value: Field, + ) { + let storage = Storage::init(Context::public(&mut context)); + storage.approved_action.at(message_hash).write(value); } fn _inner_is_valid( message_hash: Field, address: Field, - ) { + ) -> pub bool{ let witness = get_auth_witness(message_hash); assert(recover_address(message_hash, witness) == address); + true } } \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/Nargo.toml b/yarn-project/noir-contracts/src/contracts/token_contract/Nargo.toml new file mode 100644 index 00000000000..cece7de949c --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_contract/Nargo.toml @@ -0,0 +1,10 @@ +[package] +name = "token_contract" +authors = [""] +compiler_version = "0.1" +type = "contract" + +[dependencies] +aztec = { path = "../../../../noir-libs/aztec-noir" } +value_note = { path = "../../../../noir-libs/value-note"} +safe_math = { path = "../../../../noir-libs/safe-math" } \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/account_interface.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/account_interface.nr new file mode 100644 index 00000000000..7ee25a9eaf9 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/account_interface.nr @@ -0,0 +1,52 @@ + +use dep::std::option::Option; + +use dep::aztec::context::{ + PrivateContext, + PublicContext, + Context, +}; + +struct AccountContract { + address: Field, +} + +impl AccountContract { + fn at(address: Field) -> Self { + Self { + address, + } + } + + // Will call is_valid(_public) on the account contract + // Will insert the nullifier into the context + // Will revert if the authorization fails + fn is_valid( + self: Self, + context: Context, + message_hash: Field + ) { + let return_value = if context.private.is_some() { + let context = context.private.unwrap(); + context.push_new_nullifier(message_hash, 0); + // @todo @lherskind Call should be static but unsupported atm + context.call_private_function( + self.address, + 0xe86ab4ff, + [message_hash] + )[0] + } else if context.public.is_some() { + let context = context.public.unwrap(); + context.push_new_nullifier(message_hash, 0); + // @todo @lherskind Call should be static but unsupported atm + (*context).call_public_function( + self.address, + 0xf3661153, + [message_hash] + )[0] + } else { + 0 + }; + assert(return_value == 0xe86ab4ff, "invalid call"); + } +} \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr new file mode 100644 index 00000000000..677998e7d1e --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr @@ -0,0 +1,419 @@ +mod types; +mod account_interface; +mod util; + +// Minimal token implementation that supports `AuthWit` accounts. +// The auth message follows a similar pattern to the cross-chain message and includes a designated caller. +// The designated caller is ALWAYS used here, and not based on a flag as cross-chain. +// message hash = H([caller, contract, selector, ...args]) +// To be read as `caller` calls function at `contract` defined by `selector` with `args` +// Including a nonce in the message hash ensures that the message can only be used once. + +contract Token { + // Libs + use dep::std::option::Option; + + use dep::safe_math::SafeU120; + + use dep::value_note::{ + balance_utils, + utils::{increment, decrement}, + value_note::{VALUE_NOTE_LEN, ValueNoteMethods, ValueNote}, + }; + + use dep::aztec::{ + note::{ + note_header::NoteHeader, + utils as note_utils, + }, + context::{PrivateContext, PublicContext, Context}, + state_vars::{map::Map, public_state::PublicState, set::Set}, + types::type_serialisation::field_serialisation::{ + FieldSerialisationMethods, FIELD_SERIALISED_LEN, + }, + oracle::compute_selector::compute_selector, + }; + + use crate::types::{AztecAddress, TransparentNote, TransparentNoteMethods, TRANSPARENT_NOTE_LEN}; + use crate::account_interface::AccountContract; + use crate::util::{compute_message_hash}; + + struct Storage { + admin: PublicState, + minters: Map>, + balances: Map>, + total_supply: PublicState, + pending_shields: Set, + public_balances: Map>, + } + + impl Storage { + fn init(context: Context) -> pub Self { + Storage { + admin: PublicState::new( + context, + 1, + FieldSerialisationMethods, + ), + minters: Map::new( + context, + 2, + |context, slot| { + PublicState::new( + context, + slot, + FieldSerialisationMethods, + ) + }, + ), + balances: Map::new( + context, + 3, + |context, slot| { + Set::new(context, slot, ValueNoteMethods) + }, + ), + total_supply: PublicState::new( + context, + 4, + FieldSerialisationMethods, + ), + pending_shields: Set::new(context, 5, TransparentNoteMethods), + public_balances: Map::new( + context, + 6, + |context, slot| { + PublicState::new( + context, + slot, + FieldSerialisationMethods, + ) + }, + ), + } + } + } + + #[aztec(private)] + fn constructor() { + // Currently not possible to execute public calls from constructor as code not yet available to sequencer. + // let selector = compute_selector("_initialize((Field))"); + // let _callStackItem = context.call_public_function(context.this_address(), selector, [context.msg_sender()]); + } + + #[aztec(public)] + fn set_admin( + new_admin: AztecAddress, + ) { + let storage = Storage::init(Context::public(&mut context)); + assert(storage.admin.read() == context.msg_sender(), "caller is not admin"); + storage.admin.write(new_admin.address); + } + + #[aztec(public)] + fn set_minter( + minter: AztecAddress, + approve: Field, + ) { + assert((approve == 1) | (approve == 0), "not providing boolean"); + let storage = Storage::init(Context::public(&mut context)); + assert(storage.admin.read() == context.msg_sender(), "caller is not admin"); + storage.minters.at(minter.address).write(approve as Field); + } + + #[aztec(public)] + fn mint_public( + to: AztecAddress, + amount: Field, + ) -> Field { + let storage = Storage::init(Context::public(&mut context)); + assert(storage.minters.at(context.msg_sender()).read() == 1, "caller is not minter"); + let amount = SafeU120::new(amount); + let new_balance = SafeU120::new(storage.public_balances.at(to.address).read()).add(amount); + let supply = SafeU120::new(storage.total_supply.read()).add(amount); + + storage.public_balances.at(to.address).write(new_balance.value as Field); + storage.total_supply.write(supply.value as Field); + 1 + } + + #[aztec(public)] + fn mint_private( + amount: Field, + secret_hash: Field, + ) -> Field { + let storage = Storage::init(Context::public(&mut context)); + assert(storage.minters.at(context.msg_sender()).read() == 1, "caller is not minter"); + let pending_shields = storage.pending_shields; + let mut note = TransparentNote::new(amount, secret_hash); + let supply = SafeU120::new(storage.total_supply.read()).add(SafeU120::new(amount)); + + storage.total_supply.write(supply.value as Field); + pending_shields.insert_from_public(&mut note); + 1 + } + + #[aztec(public)] + fn shield( + from: AztecAddress, + amount: Field, + secret_hash: Field, + nonce: Field, + ) -> Field { + let storage = Storage::init(Context::public(&mut context)); + + if (from.address != context.msg_sender()) { + // The redeem is only spendable once, so we need to ensure that you cannot insert multiple shields from the same message. + let selector = compute_selector("shield((Field),Field,Field,Field)"); + let message_field = compute_message_hash([context.msg_sender(), context.this_address(), selector, from.address, amount, secret_hash, nonce]); + AccountContract::at(from.address).is_valid(Context::public(&mut context), message_field); + } else { + assert(nonce == 0, "invalid nonce"); + } + + let amount = SafeU120::new(amount); + let from_balance = SafeU120::new(storage.public_balances.at(from.address).read()).sub(amount); + + let pending_shields = storage.pending_shields; + let mut note = TransparentNote::new(amount.value as Field, secret_hash); + + storage.public_balances.at(from.address).write(from_balance.value as Field); + pending_shields.insert_from_public(&mut note); + 1 + } + + #[aztec(public)] + fn transfer_public( + from: AztecAddress, + to: AztecAddress, + amount: Field, + nonce: Field, + ) -> Field { + let storage = Storage::init(Context::public(&mut context)); + + if (from.address != context.msg_sender()) { + let selector = compute_selector("transfer_public((Field),(Field),Field,Field)"); + let message_field = compute_message_hash([context.msg_sender(), context.this_address(), selector, from.address, to.address, amount, nonce]); + AccountContract::at(from.address).is_valid(Context::public(&mut context), message_field); + } else { + assert(nonce == 0, "invalid nonce"); + } + + let amount = SafeU120::new(amount); + let from_balance = SafeU120::new(storage.public_balances.at(from.address).read()).sub(amount); + storage.public_balances.at(from.address).write(from_balance.value as Field); + + let to_balance = SafeU120::new(storage.public_balances.at(to.address).read()).add(amount); + storage.public_balances.at(to.address).write(to_balance.value as Field); + + 1 + } + + #[aztec(public)] + fn burn_public( + from: AztecAddress, + amount: Field, + nonce: Field, + ) -> Field { + let storage = Storage::init(Context::public(&mut context)); + + if (from.address != context.msg_sender()) { + let selector = compute_selector("burn_public((Field),Field,Field)"); + let message_field = compute_message_hash([context.msg_sender(), context.this_address(), selector, from.address, amount, nonce]); + AccountContract::at(from.address).is_valid(Context::public(&mut context), message_field); + } else { + assert(nonce == 0, "invalid nonce"); + } + + let amount = SafeU120::new(amount); + let from_balance = SafeU120::new(storage.public_balances.at(from.address).read()).sub(amount); + storage.public_balances.at(from.address).write(from_balance.value as Field); + + let new_supply = SafeU120::new(storage.total_supply.read()).sub(amount); + storage.total_supply.write(new_supply.value as Field); + + 1 + } + + #[aztec(private)] + fn redeem_shield( + to: AztecAddress, + amount: Field, + secret: Field, + ) -> Field { + let storage = Storage::init(Context::private(&mut context)); + let pending_shields = storage.pending_shields; + let balance = storage.balances.at(to.address); + let public_note = TransparentNote::new_from_secret(amount, secret); + + pending_shields.assert_contains_and_remove_publicly_created(public_note); + increment(balance, amount, to.address); + + 1 + } + + #[aztec(private)] + fn unshield( + from: AztecAddress, + to: AztecAddress, + amount: Field, + nonce: Field, + ) -> Field { + let storage = Storage::init(Context::private(&mut context)); + + if (from.address != context.msg_sender()) { + let selector = compute_selector("unshield((Field),(Field),Field,Field)"); + let message_field = compute_message_hash([context.msg_sender(), context.this_address(), selector, from.address, to.address, amount, nonce]); + AccountContract::at(from.address).is_valid(Context::private(&mut context), message_field); + } else { + assert(nonce == 0, "invalid nonce"); + } + + let from_balance = storage.balances.at(from.address); + decrement(from_balance, amount, from.address); + + let selector = compute_selector("_increase_public_balance((Field),Field)"); + let _void = context.call_public_function(context.this_address(), selector, [to.address, amount]); + + 1 + } + + #[aztec(private)] + fn transfer( + from: AztecAddress, + to: AztecAddress, + amount: Field, + nonce: Field, + ) -> Field { + let storage = Storage::init(Context::private(&mut context)); + + if (from.address != context.msg_sender()) { + let selector = compute_selector("transfer((Field),(Field),Field,Field)"); + let message_field = compute_message_hash([context.msg_sender(), context.this_address(), selector, from.address, to.address, amount, nonce]); + AccountContract::at(from.address).is_valid(Context::private(&mut context), message_field); + } else { + assert(nonce == 0, "invalid nonce"); + } + + let from_balance = storage.balances.at(from.address); + let to_balance = storage.balances.at(to.address); + + decrement(from_balance, amount, from.address); + increment(to_balance, amount, to.address); + + 1 + } + + #[aztec(private)] + fn burn( + from: AztecAddress, + amount: Field, + nonce: Field, + ) -> Field { + let storage = Storage::init(Context::private(&mut context)); + + if (from.address != context.msg_sender()) { + let selector = compute_selector("burn((Field),Field,Field)"); + let message_field = compute_message_hash([context.msg_sender(), context.this_address(), selector, from.address, amount, nonce]); + AccountContract::at(from.address).is_valid(Context::private(&mut context), message_field); + } else { + assert(nonce == 0, "invalid nonce"); + } + + let from_balance = storage.balances.at(from.address); + + decrement(from_balance, amount, from.address); + + let selector = compute_selector("_reduce_total_supply(Field)"); + let _void = context.call_public_function(context.this_address(), selector, [amount]); + + 1 + } + + /// SHOULD BE Internal /// + + // We cannot do this from the constructor currently + // Since this should be internal, for now, we ignore the safety checks of it, as they are + // enforced by it being internal and only called from the constructor. + #[aztec(public)] + fn _initialize( + new_admin: AztecAddress, + ) { + let storage = Storage::init(Context::public(&mut context)); + storage.admin.write(new_admin.address); + storage.minters.at(new_admin.address).write(1); + } + + /// Internal /// + + #[aztec(public)] + internal fn _increase_public_balance( + to: AztecAddress, + amount: Field, + ) { + let storage = Storage::init(Context::public(&mut context)); + let new_balance = SafeU120::new(storage.public_balances.at(to.address).read()).add(SafeU120::new(amount)); + storage.public_balances.at(to.address).write(new_balance.value as Field); + } + + #[aztec(public)] + internal fn _reduce_total_supply( + amount: Field, + ) { + // Only to be called from burn. + let storage = Storage::init(Context::public(&mut context)); + let new_supply = SafeU120::new(storage.total_supply.read()).sub(SafeU120::new(amount)); + storage.total_supply.write(new_supply.value as Field); + } + + /// Unconstrained /// + + unconstrained fn admin() -> Field { + let storage = Storage::init(Context::none()); + storage.admin.read() + } + + unconstrained fn is_minter( + minter: AztecAddress, + ) -> bool { + let storage = Storage::init(Context::none()); + storage.minters.at(minter.address).read() as bool + } + + unconstrained fn total_supply() -> Field { + let storage = Storage::init(Context::none()); + storage.total_supply.read() + } + + unconstrained fn balance_of_private( + owner: AztecAddress, + ) -> Field { + let storage = Storage::init(Context::none()); + let owner_balance = storage.balances.at(owner.address); + + balance_utils::get_balance(owner_balance) + } + + unconstrained fn balance_of_public( + owner: AztecAddress, + ) -> Field { + let storage = Storage::init(Context::none()); + storage.public_balances.at(owner.address).read() + } + + // Below this point is the stuff of nightmares. + // This should ideally not be required. What do we do if vastly different types of preimages? + + // Computes note hash and nullifier. + // Note 1: Needs to be defined by every contract producing logs. + // Note 2: Having it in all the contracts gives us the ability to compute the note hash and nullifier differently for different kind of notes. + unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, preimage: [Field; VALUE_NOTE_LEN]) -> [Field; 4] { + let note_header = NoteHeader { contract_address, nonce, storage_slot }; + if (storage_slot == 5) { + note_utils::compute_note_hash_and_nullifier(TransparentNoteMethods, note_header, preimage) + } else { + note_utils::compute_note_hash_and_nullifier(ValueNoteMethods, note_header, preimage) + } + } + +} diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/types.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/types.nr new file mode 100644 index 00000000000..9161fba1018 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/types.nr @@ -0,0 +1,176 @@ +use dep::std::hash::pedersen; +use dep::std::hash::pedersen_with_separator; +use dep::aztec::note::{ + note_header::NoteHeader, + note_interface::NoteInterface, + utils::compute_siloed_note_hash, +}; +use dep::aztec::constants_gen::GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET; + +global TRANSPARENT_NOTE_LEN: Field = 2; + + + +struct AztecAddress { + address: Field +} + +impl AztecAddress { + fn new(address: Field) -> Self { + Self { + address + } + } + + fn serialize(self: Self) -> [Field; 1] { + [self.address] + } + + fn deserialize(fields: [Field; 1]) -> Self { + Self { + address: fields[0] + } + } +} + +struct EthereumAddress { + address: Field +} + +impl EthereumAddress { + fn new(address: Field) -> Self { + Self { + address + } + } + + + fn serialize(self: Self) -> [Field; 1] { + [self.address] + } + + fn deserialize(fields: [Field; 1]) -> Self { + Self { + address: fields[0] + } + } +} + + + +// Transparent note represents a note that is created in the clear (public execution), +// but can only be spent by those that know the preimage of the "secret_hash" +struct TransparentNote { + amount: Field, + secret_hash: Field, + // the fields below are not serialised/deserialised + secret: Field, + header: NoteHeader, +} + +impl TransparentNote { + + // CONSTRUCTORS + + fn new(amount: Field, secret_hash: Field) -> Self { + TransparentNote { + amount: amount, + secret_hash: secret_hash, + secret: 0, + header: NoteHeader::empty(), + } + } + + // new oracle call primitive + // get me the secret corresponding to this hash + fn new_from_secret(amount: Field, secret: Field) -> Self { + TransparentNote { + amount: amount, + secret_hash: TransparentNote::compute_secret_hash(secret), + secret: secret, + header: NoteHeader::empty(), + } + } + + + // STANDARD NOTE_INTERFACE FUNCTIONS + + fn serialise(self) -> [Field; TRANSPARENT_NOTE_LEN] { + [self.amount, self.secret_hash] + } + + fn deserialise(preimage: [Field; TRANSPARENT_NOTE_LEN]) -> Self { + TransparentNote { + amount: preimage[0], + secret_hash: preimage[1], + secret: 0, + header: NoteHeader::empty(), + } + } + + fn compute_note_hash(self) -> Field { + // TODO(#1205) Should use a non-zero generator index. + dep::std::hash::pedersen([ + self.amount, + self.secret_hash, + ])[0] + } + + fn compute_nullifier(self) -> Field { + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/1386): should use + // `compute_note_hash_for_read_or_nullify` once public functions inject nonce! + let siloed_note_hash = compute_siloed_note_hash(TransparentNoteMethods, self); + // TODO(#1205) Should use a non-zero generator index. + pedersen([self.secret, siloed_note_hash])[0] + } + + fn set_header(&mut self, header: NoteHeader) { + self.header = header; + } + + + // CUSTOM FUNCTIONS FOR THIS NOTE TYPE + + fn compute_secret_hash(secret: Field) -> Field { + // TODO(#1205) This is probably not the right index to use + pedersen_with_separator([secret], GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET)[0] + } + + fn knows_secret(self, secret: Field) { + let hash = TransparentNote::compute_secret_hash(secret); + assert(self.secret_hash == hash); + } +} + +fn deserialise(preimage: [Field; TRANSPARENT_NOTE_LEN]) -> TransparentNote { + TransparentNote::deserialise(preimage) +} + +fn serialise(note: TransparentNote) -> [Field; TRANSPARENT_NOTE_LEN] { + note.serialise() +} + +fn compute_note_hash(note: TransparentNote) -> Field { + note.compute_note_hash() +} + +fn compute_nullifier(note: TransparentNote) -> Field { + note.compute_nullifier() +} + +fn get_header(note: TransparentNote) -> NoteHeader { + note.header +} + +fn set_header(note: &mut TransparentNote, header: NoteHeader) { + note.set_header(header) +} + +global TransparentNoteMethods = NoteInterface { + deserialise, + serialise, + compute_note_hash, + compute_nullifier, + get_header, + set_header, +}; \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/util.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/util.nr new file mode 100644 index 00000000000..09d030e3318 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/util.nr @@ -0,0 +1,8 @@ +use dep::std::hash::{pedersen_with_separator}; +use dep::aztec::constants_gen::GENERATOR_INDEX__SIGNATURE_PAYLOAD; + +fn compute_message_hash(args: [Field; N]) -> Field { + // @todo @lherskind We should probably use a separate generator for this, + // to avoid any potential collisions with payloads. + pedersen_with_separator(args, GENERATOR_INDEX__SIGNATURE_PAYLOAD)[0] +} \ No newline at end of file diff --git a/yarn-project/noir-libs/safe-math/src/safe_u120.nr b/yarn-project/noir-libs/safe-math/src/safe_u120.nr index 47f28f72169..ccb6ab906a8 100644 --- a/yarn-project/noir-libs/safe-math/src/safe_u120.nr +++ b/yarn-project/noir-libs/safe-math/src/safe_u120.nr @@ -21,7 +21,7 @@ impl SafeU120 { // Check that it actually will fit. Spending a lot of constraints here :grimacing: let bytes = value.to_be_bytes(32); for i in 0..17 { - assert(bytes[i] == 0); + assert(bytes[i] == 0, "Value too large for SafeU120"); } Self { value: value as u120 @@ -45,7 +45,7 @@ impl SafeU120 { self: Self, b: Self, ) -> Self { - assert(self.value >= b.value); + assert(self.value >= b.value, "Underflow"); Self { value: self.value - b.value } @@ -56,7 +56,7 @@ impl SafeU120 { b: Self, ) -> Self { let c: u120 = self.value + b.value; - assert(c >= self.value); + assert(c >= self.value, "Overflow"); Self { value: c } @@ -68,7 +68,7 @@ impl SafeU120 { ) -> Self { let c: u120 = self.value * b.value; if !b.is_zero() { - assert(c / b.value == self.value); + assert(c / b.value == self.value, "Overflow"); } Self { value: c @@ -79,7 +79,7 @@ impl SafeU120 { self: Self, b: Self, ) -> Self { - assert(!b.is_zero()); + assert(!b.is_zero(), "Divide by zero"); Self { value: self.value / b.value } @@ -99,7 +99,7 @@ impl SafeU120 { divisor: Self ) -> Self { let c = self.mul(b); - assert(!divisor.is_zero()); + assert(!divisor.is_zero(), "Divide by zero"); let adder = ((self.value * b.value % divisor.value) as u120 > 0) as u120; c.div(divisor).add(Self {value: adder}) } diff --git a/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts b/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts index f5c1be30b53..d9e112e8d0a 100644 --- a/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts +++ b/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts @@ -1,7 +1,7 @@ import { createEthereumChain } from '@aztec/ethereum'; import { createDebugLogger } from '@aztec/foundation/log'; import { ContractDeploymentEmitterAbi, RollupAbi } from '@aztec/l1-artifacts'; -import { ExtendedContractData } from '@aztec/types'; +import { BLOB_SIZE_IN_BYTES, ExtendedContractData } from '@aztec/types'; import { GetContractReturnType, @@ -141,6 +141,9 @@ export class ViemTxSender implements L1PublisherTxSender { `0x${extendedContractData.bytecode.toString('hex')}`, ] as const; + const codeSize = extendedContractData.bytecode.length; + this.log(`Bytecode is ${codeSize} bytes and require ${codeSize / BLOB_SIZE_IN_BYTES} blobs`); + const gas = await this.contractDeploymentEmitterContract.estimateGas.emitContractDeployment(args, { account: this.account, }); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 319c58b1fa5..083df1ac5a2 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -147,7 +147,11 @@ export class Sequencer { await this.p2pClient.deleteTxs(await Tx.getHashes(failedTxData)); } - if (processedTxs.length === 0) { + // Only accept processed transactions that are not double-spends + // public functions emitting nullifiers would pass earlier check but fail here + const processedValidTxs = await this.takeValidProcessedTxs(processedTxs); + + if (processedValidTxs.length === 0) { this.log('No txs processed correctly to build block. Exiting'); return; } @@ -158,16 +162,16 @@ export class Sequencer { this.log('Successfully retrieved L1 to L2 messages from contract'); // Build the new block by running the rollup circuits - this.log(`Assembling block with txs ${processedTxs.map(tx => tx.hash).join(', ')}`); + this.log(`Assembling block with txs ${processedValidTxs.map(tx => tx.hash).join(', ')}`); const emptyTx = await processor.makeEmptyProcessedTx(); - const block = await this.buildBlock(processedTxs, l1ToL2Messages, emptyTx, newGlobalVariables); + const block = await this.buildBlock(processedValidTxs, l1ToL2Messages, emptyTx, newGlobalVariables); this.log(`Assembled block ${block.number}`); await this.publishExtendedContractData(validTxs, block); await this.publishL2Block(block); - this.log.info(`Submitted rollup block ${block.number} with ${processedTxs.length} transactions`); + this.log.info(`Submitted rollup block ${block.number} with ${processedValidTxs.length} transactions`); } catch (err) { this.log.error(err); this.log.error(`Rolling back world state DB`); @@ -249,6 +253,13 @@ export class Sequencer { return validTxs; } + protected async takeValidProcessedTxs(txs: ProcessedTx[]) { + const isDoubleSpends = await Promise.all(txs.map(async tx => await this.isTxDoubleSpend(tx as unknown as Tx))); + const doubleSpends = txs.filter((tx, index) => isDoubleSpends[index]).map(tx => tx.hash); + await this.p2pClient.deleteTxs(doubleSpends); + return txs.filter((tx, index) => !isDoubleSpends[index]); + } + /** * Returns whether the previous block sent has been mined, and all dependencies have caught up with it. * @returns Boolean indicating if our dependencies are synced to the latest block. diff --git a/yarn-project/types/src/constants.ts b/yarn-project/types/src/constants.ts index 941eb52db9c..bd7f22b154c 100644 --- a/yarn-project/types/src/constants.ts +++ b/yarn-project/types/src/constants.ts @@ -1 +1,2 @@ export const INITIAL_L2_BLOCK_NUM = 1; +export const BLOB_SIZE_IN_BYTES = 31 * 4096;