diff --git a/build.gradle b/build.gradle index 1a138fd31..8a2bc10e9 100644 --- a/build.gradle +++ b/build.gradle @@ -196,17 +196,45 @@ task copyZetoZKPFiles(type: Copy) { // Likely if the range of circuits grows, a separate docker tag will be required for builds // of Paladin that have a wider range of pre-built circuits included. from fileTree('domains/zeto/zkp') { + // anon include 'anon.zkey' include 'anon-vkey.json' include 'anon_js/anon.wasm' + include 'anon_batch.zkey' + include 'anon_batch-vkey.json' + include 'anon_batch_js/anon_batch.wasm' // anon_enc include 'anon_enc.zkey' include 'anon_enc-vkey.json' include 'anon_enc_js/anon_enc.wasm' + include 'anon_enc_batch.zkey' + include 'anon_enc_batch-vkey.json' + include 'anon_enc_batch_js/anon_enc_batch.wasm' // anon_nullifier include 'anon_nullifier.zkey' include 'anon_nullifier-vkey.json' include 'anon_nullifier_js/anon_nullifier.wasm' + include 'anon_nullifier_batch.zkey' + include 'anon_nullifier_batch-vkey.json' + include 'anon_nullifier_batch_js/anon_nullifier_batch.wasm' + // deposit + include 'check_hashes_value.zkey' + include 'check_hashes_value-vkey.json' + include 'check_hashes_value_js/check_hashes_value.wasm' + // withdraw + include 'check_inputs_outputs_value.zkey' + include 'check_inputs_outputs_value-vkey.json' + include 'check_inputs_outputs_value_js/check_inputs_outputs_value.wasm' + include 'check_inputs_outputs_value_batch.zkey' + include 'check_inputs_outputs_value_batch-vkey.json' + include 'check_inputs_outputs_value_batch_js/check_inputs_outputs_value_batch.wasm' + // withdraw_nullifier + include 'check_nullifier_value.zkey' + include 'check_nullifier_value-vkey.json' + include 'check_nullifier_value_js/check_nullifier_value.wasm' + include 'check_nullifier_value_batch.zkey' + include 'check_nullifier_value_batch-vkey.json' + include 'check_nullifier_value_batch_js/check_nullifier_value_batch.wasm' } into zetoZkpDir diff --git a/doc-site/docs/architecture/zeto.md b/doc-site/docs/architecture/zeto.md index 30673a452..6ba5100d6 100644 --- a/doc-site/docs/architecture/zeto.md +++ b/doc-site/docs/architecture/zeto.md @@ -111,6 +111,58 @@ Inputs: - **to** - lookup string for the identity that will receive transferred value - **amount** - amount of value to transfer +### deposit + +The Zeto token implementations support interaction with an ERC20 token, to control the value supply publicly. With this paradigm, the token issuer, such as a central bank for digital currencies, can control the total supply in the ERC20 contract. This makes the supply of the tokens public. + +The Zeto token contract can be configured to allow balances from a designated ERC20 contract to be "swapped" for Zeto tokens, by calling the `deposit` API. This allows any accounts that have a balance in the ERC20 contract to swap them for Zeto tokens. The exchange rate between the ERC20 and Zeto tokens is 1:1. On successful deposit, the ERC20 balance is transferred to the Zeto contract. + +Typically in this paradigm, the `mint` API on the Zeto domain should be locked down (disabled) so that the only way to mint Zeto tokens is by depositing. + +```json +{ + "type": "function", + "name": "deposit", + "inputs": [ + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": null +} +``` + +Inputs: + +- **amount** - amount of value to deposit + +### withdraw + +Opposite to the "deposit" operation, users can swap Zeto tokens back to ERC20 balances. + +On successful withdrawal, the ERC20 balance is released by the Zeto contract and transferred back to the user account. + +```json +{ + "type": "function", + "name": "withdraw", + "inputs": [ + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": null +} +``` + +Inputs: + +- **amount** - amount of value to withdraw + ### lockProof This is a special purpose function used in coordinating multi-party transactions, such as [Delivery-vs-Payment (DvP) contracts](https://github.com/hyperledger-labs/zeto/blob/main/solidity/contracts/zkDvP.sol). When a party commits to the trade first by uploading the ZK proof to the orchestration contract, they must be protected from a malicious party seeing the proof and using it to unilaterally execute the token transfer. The `lockProof()` function allows an account, which can be a smart contract address, to designate the finaly submitter of the proof, thus protecting anybody else from abusing the proof outside of the atomic settlement of the multi-leg trade. diff --git a/doc-site/docs/tutorials/zkp-cbdc.md b/doc-site/docs/tutorials/zkp-cbdc.md index e20bc3da9..2ed158338 100644 --- a/doc-site/docs/tutorials/zkp-cbdc.md +++ b/doc-site/docs/tutorials/zkp-cbdc.md @@ -1,8 +1,8 @@ -# CBDC Tokens based on Zeto +# Cash Tokens based on Zeto The code for this tutorial can be found in [example/zeto](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/zeto). -This shows how to leverage the [Zeto](../../architecture/zeto/) in order to build a wholesale CBDC with privacy, illustrating multiple aspects of Paladin's privacy capabilities. +This shows how to leverage the [Zeto](../../architecture/zeto/) in order to build a cash payment solution, for instance wholesale CBDC or a payment rail with commercial bank money, with privacy, illustrating multiple aspects of Paladin's privacy capabilities. ## Running the example @@ -10,7 +10,9 @@ Follow the [Getting Started](../../getting-started/installation/) instructions t then follow the example [README](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/zeto/README.md) to run the code. -## Explanation +## Scenario #1: cash solution with private minting + +In this scenario, the Zeto tokens are directly minted by the authority in the Zeto contract, making the mint amounts private. This also means the total supply of the Zeto tokens is unknown to the participants. Only the authority performing the minting operations is aware of the total supply. Below is a walkthrough of each step in the example, with an explanation of what it does. @@ -23,8 +25,7 @@ const zetoCBDC = await zetoFactory.newZeto(cbdcIssuer, { }); ``` -This creates a new instance of the Zeto domain, using the [Zeto_AnonNullifier](https://github.com/hyperledger-labs/zeto/tree/main?tab=readme-ov-file#zeto_anonnullifier) contract. -This results in a new cloned contract on the base ledger, with a new unique address. This Zeto token contract will be used to represent +This creates a new instance of the Zeto domain, using the [Zeto_AnonNullifier](https://github.com/hyperledger-labs/zeto/tree/main?tab=readme-ov-file#zeto_anonnullifier) contract. This results in a new cloned contract on the base ledger, with a new unique address. This Zeto token contract will be used to represent tokenized cash/CBDC. The token will be minted by the central bank/CBDC issuer party. Minting is restricted to be requested only by the central bank, the @@ -64,3 +65,83 @@ receipt = await zetoCBDC.using(paladin1).transfer(bank1, { Bank1 can call the `transfer` function to transfer zeto tokens to multiple parties, up to 10. Note that the identity `bank1` exists on the `paladin1` instance, therefore it must use that instance to send the transfer transction (`.using(paladin1)`). + +## Scenario #2: cash solution with public minting + +This scenario supports the requirement to make the total supply of the cash tokens public. This is achieved by making the authority perform the minting operations in an ERC20 contract. The participants can then exchange their ERC20 balances for Zeto tokens, by calling `deposit`, and exchange back to their ERC20 balances by calling `withdraw`. + +Below is a walkthrough of each step in the example, with an explanation of what it does. + +### Create CBDC token + +```typescript +const zetoFactory = new ZetoFactory(paladin3, 'zeto'); +const zetoCBDC = await zetoFactory.newZeto(cbdcIssuer, { + tokenName: 'Zeto_AnonNullifier', +}); +``` + +This creates a new instance of the Zeto domain, using the [Zeto_AnonNullifier](https://github.com/hyperledger-labs/zeto/tree/main?tab=readme-ov-file#zeto_anonnullifier) contract. This results in a new cloned contract on the base ledger, with a new unique address. This Zeto token contract will be used to represent +tokenized cash/CBDC. + +### Create public supply token (ERC20) + +```typescript +const erc20Address = await deployERC20(paladin3, cbdcIssuer); +``` + +This deploys the ERC20 token which will be used by the authority to regulate the CBDC supply, with transparency to the paricipants. + +### Configure the Zeto token contract to accept deposits and withdraws from the ERC20 + +```typescript +const result2 = await zetoCBDC.setERC20(cbdcIssuer, { + _erc20: erc20Address as string, +}); +``` + +When the `deposit` function is called on the Zeto contract, this ERC20 contract will be called to draw the requested funds from the depositor's account. Conversely, when the `withdraw` function is called, this ERC20 contract will be called to transfer back the ERC20 balance to the withdrawer's account. + +### Mint ERC20 tokens to publicly regulate CBDC supplies + +```typescript +await mintERC20(paladin3, cbdcIssuer, bank1, erc20Address!, 100000); +``` + +Because the ERC20 implementation provides full transparency of the token operations, minting in the ERC20 allows all blockchain network participants to be aware of the overall supply of the CBDC tokens. + +### Banks exchange ERC20 balances for Zeto tokens - deposit + +```typescript +const result4 = await zetoCBDC.using(paladin1).deposit(bank1, { + amount: 10000, +}); +``` + +After having been minted ERC20 balances, a partcipant like `bank1` can call `deposit` on the Paladin Zeto domain to exchange for Zeto tokens. Behind the scenes, the ERC20 balance is transferred to the Zeto contract which will hold until `withdraw` is called later. + +### Bank1 transfers tokens to bank2 as payment + +```typescript +receipt = await zetoCBDC.using(paladin1).transfer(bank1, { + transfers: [ + { + to: bank2, + amount: 1000, + }, + ], +}); +``` + +Bank1 can call the `transfer` function to transfer zeto tokens to multiple parties, up to 10. Note that the identity `bank1` exists on the `paladin1` instance, +therefore it must use that instance to send the transfer transction (`.using(paladin1)`). + +### Bank1 exchanges Zeto tokens for ERC20 balances - withdraw + +```typescript +const result5 = await zetoCBDC.using(paladin1).withdraw(bank1, { + amount: 1000, +}); +``` + +A participant like `bank1` who has unspent Zeto tokens can call `withdraw` on the Paladin Zeto domain to exchange them for ERC20 balances. Behind the scenes, the requested amount are "burnt" in the Zeto contract, and the corresponding ERC20 amount are released by the Zeto contract, by transferring to the requesting account. diff --git a/domains/integration-test/zeto/config-for-deploy.yaml b/domains/integration-test/zeto/config-for-deploy.yaml index 4ef29632b..2ec0d71d1 100644 --- a/domains/integration-test/zeto/config-for-deploy.yaml +++ b/domains/integration-test/zeto/config-for-deploy.yaml @@ -23,6 +23,9 @@ contracts: - name: Zeto_Anon verifier: Groth16Verifier_Anon batchVerifier: Groth16Verifier_AnonBatch + depositVerifier: Groth16Verifier_CheckHashesValue + withdrawVerifier: Groth16Verifier_CheckInputsOutputsValue + batchWithdrawVerifier: Groth16Verifier_CheckInputsOutputsValueBatch circuitId: anon cloneable: true abiAndBytecode: diff --git a/domains/noto/pkg/types/abi.go b/domains/noto/pkg/types/abi.go index fa0672f76..31235d595 100644 --- a/domains/noto/pkg/types/abi.go +++ b/domains/noto/pkg/types/abi.go @@ -17,30 +17,17 @@ package types import ( _ "embed" - "encoding/json" "github.com/hyperledger/firefly-signer/pkg/abi" "github.com/kaleido-io/paladin/toolkit/pkg/pldapi" + "github.com/kaleido-io/paladin/toolkit/pkg/solutils" "github.com/kaleido-io/paladin/toolkit/pkg/tktypes" ) //go:embed abis/INotoPrivate.json var notoPrivateJSON []byte -func mustParseBuildABI(buildJSON []byte) abi.ABI { - var buildParsed map[string]tktypes.RawJSON - var buildABI abi.ABI - err := json.Unmarshal(buildJSON, &buildParsed) - if err == nil { - err = json.Unmarshal(buildParsed["abi"], &buildABI) - } - if err != nil { - panic(err) - } - return buildABI -} - -var NotoABI = mustParseBuildABI(notoPrivateJSON) +var NotoABI = solutils.MustParseBuildABI(notoPrivateJSON) type ConstructorParams struct { Notary string `json:"notary"` // Lookup string for the notary identity diff --git a/domains/zeto/.gitignore b/domains/zeto/.gitignore index ac4fb0cfb..f837bf059 100644 --- a/domains/zeto/.gitignore +++ b/domains/zeto/.gitignore @@ -1,4 +1,5 @@ internal/zeto/abis/ +pkg/types/abis/ integration-test/abis/ zkp/ tools/ diff --git a/domains/zeto/build.gradle b/domains/zeto/build.gradle index 6f98f3022..948b06cda 100644 --- a/domains/zeto/build.gradle +++ b/domains/zeto/build.gradle @@ -24,7 +24,7 @@ ext { goFilesE2E = fileTree(".") { include "integration-test/**/*.go" } - targetCoverage = 93.5 + targetCoverage = 94.5 maxCoverageBarGap = 1 coveragePackages = [ "github.com/kaleido-io/paladin/domains/zeto/internal/...", @@ -32,7 +32,7 @@ ext { "github.com/kaleido-io/paladin/domains/zeto/pkg/zetosigner", ] - zetoVersion = "v0.0.7" + zetoVersion = "v0.0.10" zetoHost = "hyperledger-labs" zkpOut = "${projectDir}/zkp" toolsOut = "${projectDir}/tools" @@ -74,7 +74,7 @@ dependencies { coreGo project(path: ":core:go", configuration: "goSource") } -task downloadZetoProver { +task downloadZetoProver() { def outname = "zeto-wasm-${zetoVersion}.tar.gz" def url = "https://github.com/${zetoHost}/zeto/releases/download/${zetoVersion}/${outname}" def f = new File(toolsOut, outname) @@ -85,7 +85,7 @@ task downloadZetoProver { outputs.file(f) } -task downloadZetoTestProvingKeys { +task downloadZetoTestProvingKeys() { def outname = "zeto-test-proving-keys-${zetoVersion}.tar.gz" def url = "https://github.com/${zetoHost}/zeto/releases/download/${zetoVersion}/${outname}" def f = new File(toolsOut, outname) @@ -96,7 +96,7 @@ task downloadZetoTestProvingKeys { outputs.file(f) } -task downloadZetoCompiledContracts { +task downloadZetoCompiledContracts() { def outname = "zeto-contracts-${zetoVersion}.tar.gz" def url = "https://github.com/${zetoHost}/zeto/releases/download/${zetoVersion}/${outname}" def f = new File(toolsOut, outname) @@ -173,6 +173,19 @@ task copySolidity(type: Copy, dependsOn: [protoc, ":solidity:compile", extractZe includeEmptyDirs = false } +task copyPkgSolidity(type: Copy) { + inputs.files(configurations.contractCompile) + + into 'pkg/types/abis' + from fileTree(configurations.contractCompile.asPath) { + include 'contracts/domains/interfaces/IZetoPrivate.sol/IZetoPrivate.json' + } + + // Flatten all paths into the destination folder + eachFile { path = name } + includeEmptyDirs = false +} + task copySolidityForTest(type: Copy, dependsOn: [extractZetoArtifacts, generatePoseidonArtifacts, ":solidity:compile"]) { inputs.files(configurations.contractCompile) from fileTree(configurations.contractCompile.asPath) { @@ -197,7 +210,7 @@ task copySolidityForTest(type: Copy, dependsOn: [extractZetoArtifacts, generateP includeEmptyDirs = false } -task testE2E(type: Exec, dependsOn: [protoc, copySolidity, copySolidityForTest, ':testinfra:startTestInfra', ":core:go:makeMocks"]) { +task testE2E(type: Exec, dependsOn: [protoc, copySolidity, copyPkgSolidity, copySolidityForTest, ':testinfra:startTestInfra', ":core:go:makeMocks"]) { inputs.files(configurations.toolkitGo) inputs.files(configurations.coreGo) inputs.files(goFiles) @@ -217,7 +230,7 @@ task testE2E(type: Exec, dependsOn: [protoc, copySolidity, copySolidityForTest, helpers.dumpLogsOnFailure(it, ':testinfra:startTestInfra') } -task unitTests(type: Exec, dependsOn: [protoc, ":core:go:makeMocks"]) { +task unitTests(type: Exec, dependsOn: [protoc, ":core:go:makeMocks", ":testinfra:startTestInfra", downloadZetoCompiledContracts, copySolidity, copyPkgSolidity]) { inputs.files(configurations.toolkitGo) inputs.files(configurations.coreGo) inputs.files(goFiles) @@ -238,9 +251,6 @@ task unitTests(type: Exec, dependsOn: [protoc, ":core:go:makeMocks"]) { if (project.findProperty('verboseTests') == 'true') { args '-v' } - - dependsOn copySolidity - dependsOn ':testinfra:startTestInfra' helpers.dumpLogsOnFailure(it, ':testinfra:startTestInfra') } @@ -254,7 +264,7 @@ task test { finalizedBy checkCoverage } -task buildGo(type: GoLib, dependsOn: [":toolkit:go:protoc", copySolidity]) { +task buildGo(type: GoLib, dependsOn: [":toolkit:go:protoc", copySolidity, copyPkgSolidity]) { inputs.files(configurations.coreGo) inputs.files(configurations.toolkitGo) baseName "zeto" @@ -270,6 +280,7 @@ task build { task clean(type: Delete) { delete 'coverage' delete 'internal/zeto/abis' + delete 'pkg/types/abis' delete 'integration-test/abis' delete zkpOut delete toolsOut @@ -280,7 +291,7 @@ task assemble { } dependencies { - goSource files(goFiles, copySolidity) + goSource files(goFiles, copySolidity, copyPkgSolidity) zetoArtifacts files(extractZetoArtifacts) poseidonArtifacts files(generatePoseidonArtifacts) } diff --git a/domains/zeto/integration-test/config-for-deploy.yaml b/domains/zeto/integration-test/config-for-deploy.yaml index f356dae3d..571b4b77d 100644 --- a/domains/zeto/integration-test/config-for-deploy.yaml +++ b/domains/zeto/integration-test/config-for-deploy.yaml @@ -17,6 +17,14 @@ contracts: - name: Groth16Verifier_CheckInputsOutputsValueBatch abiAndBytecode: path: ./abis/Groth16Verifier_CheckInputsOutputsValueBatch.json + # for the withdraw transaction with nullifiers + - name: Groth16Verifier_CheckNullifierValue + abiAndBytecode: + path: ./abis/Groth16Verifier_CheckNullifierValue.json + # for the withdraw transaction with batch inputs and nullifiers + - name: Groth16Verifier_CheckNullifierValueBatch + abiAndBytecode: + path: ./abis/Groth16Verifier_CheckNullifierValueBatch.json # for the Zeto_Anon token - name: Groth16Verifier_Anon abiAndBytecode: @@ -27,6 +35,9 @@ contracts: - name: Zeto_Anon verifier: Groth16Verifier_Anon batchVerifier: Groth16Verifier_AnonBatch + depositVerifier: Groth16Verifier_CheckHashesValue + withdrawVerifier: Groth16Verifier_CheckInputsOutputsValue + batchWithdrawVerifier: Groth16Verifier_CheckInputsOutputsValueBatch circuitId: anon cloneable: true abiAndBytecode: @@ -41,6 +52,9 @@ contracts: - name: Zeto_AnonEnc verifier: Groth16Verifier_AnonEnc batchVerifier: Groth16Verifier_AnonEncBatch + depositVerifier: Groth16Verifier_CheckHashesValue + withdrawVerifier: Groth16Verifier_CheckInputsOutputsValue + batchWithdrawVerifier: Groth16Verifier_CheckInputsOutputsValueBatch circuitId: anon_enc cloneable: true abiAndBytecode: @@ -64,6 +78,9 @@ contracts: - name: Zeto_AnonNullifier verifier: Groth16Verifier_AnonNullifier batchVerifier: Groth16Verifier_AnonNullifierBatch + depositVerifier: Groth16Verifier_CheckHashesValue + withdrawVerifier: Groth16Verifier_CheckNullifierValue + batchWithdrawVerifier: Groth16Verifier_CheckNullifierValueBatch circuitId: anon_nullifier cloneable: true abiAndBytecode: diff --git a/domains/zeto/integration-test/contracts.go b/domains/zeto/integration-test/contracts.go index e75c75e13..480540e60 100644 --- a/domains/zeto/integration-test/contracts.go +++ b/domains/zeto/integration-test/contracts.go @@ -40,9 +40,12 @@ type ZetoDomainContracts struct { } type cloneableContract struct { - circuitId string - verifier string - batchVerifier string + circuitId string + verifier string + batchVerifier string + depositVerifier string + withdrawVerifier string + batchWithdrawVerifier string } func newZetoDomainContracts() *ZetoDomainContracts { @@ -88,9 +91,12 @@ func findCloneableContracts(config *domainConfig) map[string]cloneableContract { for _, contract := range config.DomainContracts.Implementations { if contract.Cloneable { cloneableContracts[contract.Name] = cloneableContract{ - circuitId: contract.CircuitId, - verifier: contract.Verifier, - batchVerifier: contract.BatchVerifier, + circuitId: contract.CircuitId, + verifier: contract.Verifier, + batchVerifier: contract.BatchVerifier, + depositVerifier: contract.DepositVerifier, + withdrawVerifier: contract.WithdrawVerifier, + batchWithdrawVerifier: contract.BatchWithdrawVerifier, } } } @@ -165,6 +171,9 @@ func registerImpl(ctx context.Context, name string, domainContracts *ZetoDomainC log.L(ctx).Infof("Registering implementation %s", name) verifierName := domainContracts.cloneableContracts[name].verifier batchVerifierName := domainContracts.cloneableContracts[name].batchVerifier + depositVerifierName := domainContracts.cloneableContracts[name].depositVerifier + withdrawVerifierName := domainContracts.cloneableContracts[name].withdrawVerifier + batchWithdrawVerifierName := domainContracts.cloneableContracts[name].batchWithdrawVerifier implAddr, ok := domainContracts.deployedContracts[name] if !ok { return fmt.Errorf("implementation contract %s not found among the deployed contracts", name) @@ -177,15 +186,15 @@ func registerImpl(ctx context.Context, name string, domainContracts *ZetoDomainC if !ok { return fmt.Errorf("batch verifier contract %s not found among the deployed contracts", batchVerifierName) } - depositVerifierAddr, ok := domainContracts.deployedContracts["Groth16Verifier_CheckHashesValue"] + depositVerifierAddr, ok := domainContracts.deployedContracts[depositVerifierName] if !ok { return fmt.Errorf("deposit verifier contract not found among the deployed contracts") } - withdrawVerifierAddr, ok := domainContracts.deployedContracts["Groth16Verifier_CheckInputsOutputsValue"] + withdrawVerifierAddr, ok := domainContracts.deployedContracts[withdrawVerifierName] if !ok { return fmt.Errorf("withdraw verifier contract not found among the deployed contracts") } - batchWithdrawVerifierAddr, ok := domainContracts.deployedContracts["Groth16Verifier_CheckInputsOutputsValueBatch"] + batchWithdrawVerifierAddr, ok := domainContracts.deployedContracts[batchWithdrawVerifierName] if !ok { return fmt.Errorf("batch withdraw verifier contract not found among the deployed contracts") } diff --git a/domains/zeto/integration-test/e2e_test.go b/domains/zeto/integration-test/e2e_test.go index 5fe41b667..e750592b4 100644 --- a/domains/zeto/integration-test/e2e_test.go +++ b/domains/zeto/integration-test/e2e_test.go @@ -19,8 +19,9 @@ import ( "context" _ "embed" "encoding/json" + "fmt" + "os" "testing" - "time" "github.com/go-resty/resty/v2" "github.com/hyperledger/firefly-signer/pkg/rpcbackend" @@ -30,11 +31,14 @@ import ( "github.com/kaleido-io/paladin/domains/zeto/pkg/types" "github.com/kaleido-io/paladin/domains/zeto/pkg/zeto" "github.com/kaleido-io/paladin/domains/zeto/pkg/zetosigner/zetosignerapi" + "github.com/kaleido-io/paladin/toolkit/pkg/algorithms" "github.com/kaleido-io/paladin/toolkit/pkg/log" "github.com/kaleido-io/paladin/toolkit/pkg/pldapi" "github.com/kaleido-io/paladin/toolkit/pkg/plugintk" "github.com/kaleido-io/paladin/toolkit/pkg/query" + "github.com/kaleido-io/paladin/toolkit/pkg/solutils" "github.com/kaleido-io/paladin/toolkit/pkg/tktypes" + "github.com/kaleido-io/paladin/toolkit/pkg/verifiers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -57,6 +61,7 @@ type zetoDomainTestSuite struct { domainName string domain zeto.Zeto rpc rpcbackend.Backend + tb testbed.Testbed done func() } @@ -70,12 +75,13 @@ func (s *zetoDomainTestSuite) SetupSuite() { log.L(ctx).Infof("Domain name = %s", domainName) config := PrepareZetoConfig(s.T(), s.deployedContracts, "../zkp") zeto, zetoTestbed := newZetoDomain(s.T(), domainContracts, config) - done, _, rpc := newTestbed(s.T(), s.hdWalletSeed, map[string]*testbed.TestbedDomain{ + done, tb, rpc := newTestbed(s.T(), s.hdWalletSeed, map[string]*testbed.TestbedDomain{ domainName: zetoTestbed, }) s.domainName = domainName s.domain = zeto s.rpc = rpc + s.tb = tb s.done = done } @@ -88,7 +94,6 @@ func (s *zetoDomainTestSuite) TestZeto_Anon() { } func (s *zetoDomainTestSuite) TestZeto_AnonBatch() { - s.T().Skip() s.testZetoFungible(s.T(), constants.TOKEN_ANON, true, false) } @@ -110,7 +115,9 @@ func (s *zetoDomainTestSuite) TestZeto_AnonNullifierBatch() { func (s *zetoDomainTestSuite) testZetoFungible(t *testing.T, tokenName string, useBatch bool, isNullifiersToken bool) { ctx := context.Background() + log.L(ctx).Info("*************************************") log.L(ctx).Infof("Deploying an instance of the %s token", tokenName) + log.L(ctx).Info("*************************************") s.setupContractsAbi(t, ctx, tokenName) var zetoAddress tktypes.EthAddress rpcerr := s.rpc.CallRPC(ctx, &zetoAddress, "testbed_deploy", @@ -122,8 +129,33 @@ func (s *zetoDomainTestSuite) testZetoFungible(t *testing.T, tokenName string, u } log.L(ctx).Infof("Zeto instance deployed to %s", zetoAddress) + var controllerEthAddr string + rpcerr = s.rpc.CallRPC(ctx, &controllerEthAddr, "ptx_resolveVerifier", controllerName, algorithms.ECDSA_SECP256K1, verifiers.ETH_ADDRESS) + require.Nil(t, rpcerr) + + log.L(ctx).Infof("Deploying the sample ERC20 with initialOwner %s", controllerEthAddr) + erc20Address, err := deployERC20(ctx, s.rpc, controllerEthAddr) + require.NoError(t, err) + + log.L(ctx).Infof("Setting the ERC20 contract (%s) to the Zeto instance", erc20Address) + paramsJson, err := json.Marshal(&map[string]string{"erc20": erc20Address.String()}) + require.NoError(t, err) + _, err = s.tb.ExecTransactionSync(ctx, &pldapi.TransactionInput{ + TransactionBase: pldapi.TransactionBase{ + Type: pldapi.TransactionTypePublic.Enum(), + From: controllerName, + To: &zetoAddress, + Function: "setERC20", + Data: paramsJson, + }, + ABI: types.ZetoABI, + }) + require.NoError(t, err) + + log.L(ctx).Info("*************************************") log.L(ctx).Infof("Mint two UTXOs (10, 20) from controller to controller") - _, err := s.mint(ctx, zetoAddress, controllerName, []int64{10, 20}) + log.L(ctx).Info("*************************************") + _, err = s.mint(ctx, zetoAddress, controllerName, []int64{10, 20}) require.NoError(t, err) var controllerAddr tktypes.Bytes32 @@ -139,7 +171,9 @@ func (s *zetoDomainTestSuite) testZetoFungible(t *testing.T, tokenName string, u // for testing the batch circuits, we mint the 3rd UTXO if useBatch { + log.L(ctx).Info("*************************************") log.L(ctx).Infof("Mint 30 from controller to controller") + log.L(ctx).Info("*************************************") _, err = s.mint(ctx, zetoAddress, controllerName, []int64{30}) require.NoError(t, err) } @@ -153,12 +187,16 @@ func (s *zetoDomainTestSuite) testZetoFungible(t *testing.T, tokenName string, u // for testing the batch circuits, we transfer 50 which would require 3 UTXOs (>2) amount1 := 10 amount2 := 40 + log.L(ctx).Info("*************************************") log.L(ctx).Infof("Transfer %d from controller to recipient1 (%d) and recipient2 (%d)", amount1+amount2, amount1, amount2) + log.L(ctx).Info("*************************************") _, err = s.transfer(ctx, zetoAddress, controllerName, []string{recipient1Name, recipient2Name}, []int64{int64(amount1), int64(amount2)}) require.NoError(t, err) } else { amount := 25 + log.L(ctx).Info("*************************************") log.L(ctx).Infof("Transfer %d from controller to recipient1", amount) + log.L(ctx).Info("*************************************") _, err = s.transfer(ctx, zetoAddress, controllerName, []string{recipient1Name}, []int64{int64(amount)}) require.NoError(t, err) } @@ -171,14 +209,6 @@ func (s *zetoDomainTestSuite) testZetoFungible(t *testing.T, tokenName string, u if useBatch { expectedCoins = 3 } - - // some tokens like Zeto_AnonNullifier with batch can take some time to process - // so we need to retry at least once - if len(coins) != expectedCoins { - time.Sleep(1 * time.Second) - coins = findAvailableCoins(t, ctx, s.rpc, s.domain, zetoAddress, nil, isNullifiersToken) - } - require.Len(t, coins, expectedCoins) if useBatch { @@ -191,6 +221,30 @@ func (s *zetoDomainTestSuite) testZetoFungible(t *testing.T, tokenName string, u assert.Equal(t, int64(5), coins[1].Data.Amount.Int().Int64()) // change for controller assert.Equal(t, controllerAddr.String(), coins[1].Data.Owner.String()) } + + log.L(ctx).Infof("Mint 100 in ERC20 to controller") + err = s.mintERC20(ctx, *erc20Address, 100, controllerName, controllerEthAddr) + require.NoError(t, err) + + log.L(ctx).Infof("Approve Zeto (%s) to spend from the controller account (%s)", zetoAddress.String(), controllerEthAddr) + err = s.approveERC20(ctx, *erc20Address, zetoAddress, 100, controllerName) + require.NoError(t, err) + + log.L(ctx).Info("*************************************") + log.L(ctx).Infof("Deposit from ERC20 balance to Zeto") + log.L(ctx).Info("*************************************") + _, err = s.deposit(ctx, zetoAddress, controllerName, 100) + require.NoError(t, err) + + expectedCoins += 2 // the deposit call produces 2 output UTXOs for the receiver + coins = findAvailableCoins(t, ctx, s.rpc, s.domain, zetoAddress, nil, isNullifiersToken) + require.Len(t, coins, expectedCoins) + + log.L(ctx).Info("*************************************") + log.L(ctx).Infof("Withdraw back to ERC20 balance from Zeto") + log.L(ctx).Info("*************************************") + _, err = s.withdraw(ctx, zetoAddress, controllerName, 100) + require.NoError(t, err) } func (s *zetoDomainTestSuite) setupContractsAbi(t *testing.T, ctx context.Context, tokenName string) { @@ -265,6 +319,98 @@ func (s *zetoDomainTestSuite) transfer(ctx context.Context, zetoAddress tktypes. return &invokeResult, nil } +func (s *zetoDomainTestSuite) deposit(ctx context.Context, zetoAddress tktypes.EthAddress, sender string, amount int64) (*testbed.TransactionResult, error) { + params := &types.DepositParams{ + Amount: tktypes.Int64ToInt256(amount), + } + paramsJson, err := json.Marshal(params) + if err != nil { + return nil, err + } + var invokeResult testbed.TransactionResult + rpcerr := s.rpc.CallRPC(ctx, &invokeResult, "testbed_invoke", &pldapi.TransactionInput{ + TransactionBase: pldapi.TransactionBase{ + From: sender, + To: &zetoAddress, + Function: "deposit", + Data: paramsJson, + }, + ABI: types.ZetoABI, + }, true) + if rpcerr != nil { + return nil, rpcerr.Error() + } + return &invokeResult, nil +} + +func (s *zetoDomainTestSuite) withdraw(ctx context.Context, zetoAddress tktypes.EthAddress, sender string, amount int64) (*testbed.TransactionResult, error) { + params := &types.WithdrawParams{ + Amount: tktypes.Int64ToInt256(amount), + } + paramsJson, err := json.Marshal(params) + if err != nil { + return nil, err + } + var invokeResult testbed.TransactionResult + rpcerr := s.rpc.CallRPC(ctx, &invokeResult, "testbed_invoke", &pldapi.TransactionInput{ + TransactionBase: pldapi.TransactionBase{ + From: sender, + To: &zetoAddress, + Function: "withdraw", + Data: paramsJson, + }, + ABI: types.ZetoABI, + }, true) + if rpcerr != nil { + return nil, rpcerr.Error() + } + return &invokeResult, nil +} + +func (s *zetoDomainTestSuite) mintERC20(ctx context.Context, erc20Address tktypes.EthAddress, amount int64, from, to string) error { + paramsJson, err := json.Marshal(&map[string]any{"amount": amount, "to": to}) + if err != nil { + return err + } + build, err := getERC20Spec() + if err != nil { + return err + } + _, err = s.tb.ExecTransactionSync(ctx, &pldapi.TransactionInput{ + TransactionBase: pldapi.TransactionBase{ + Type: pldapi.TransactionTypePublic.Enum(), + From: from, + To: &erc20Address, + Function: "mint", + Data: paramsJson, + }, + ABI: build.ABI, + }) + return err +} + +func (s *zetoDomainTestSuite) approveERC20(ctx context.Context, erc20Address, zetoAddress tktypes.EthAddress, amount int64, from string) error { + paramsJson, err := json.Marshal(&map[string]any{"spender": zetoAddress.String(), "value": amount}) + if err != nil { + return err + } + build, err := getERC20Spec() + if err != nil { + return err + } + _, err = s.tb.ExecTransactionSync(ctx, &pldapi.TransactionInput{ + TransactionBase: pldapi.TransactionBase{ + Type: pldapi.TransactionTypePublic.Enum(), + From: from, + To: &erc20Address, + Function: "approve", + Data: paramsJson, + }, + ABI: build.ABI, + }) + return err +} + func findAvailableCoins(t *testing.T, ctx context.Context, rpc rpcbackend.Backend, zeto zeto.Zeto, address tktypes.EthAddress, jq *query.QueryJSON, useNullifiers bool) []*types.ZetoCoinState { if jq == nil { jq = query.NewQueryBuilder().Limit(100).Query() @@ -314,3 +460,27 @@ func newTestbed(t *testing.T, hdWalletSeed *testbed.UTInitFunction, domains map[ rpc := rpcbackend.NewRPCClient(resty.New().SetBaseURL(url)) return done, tb, rpc } + +func getERC20Spec() (*solutils.SolidityBuild, error) { + bytes, err := os.ReadFile("./abis/SampleERC20.json") + if err != nil { + return nil, err + } + build := solutils.MustLoadBuild(bytes) + return build, nil +} + +func deployERC20(ctx context.Context, rpc rpcbackend.Backend, controllerAddr string) (*tktypes.EthAddress, error) { + build, err := getERC20Spec() + if err != nil { + return nil, err + } + params := fmt.Sprintf(`{"initialOwner":"%s"}`, controllerAddr) + var addr string + rpcerr := rpc.CallRPC(ctx, &addr, "testbed_deployBytecode", controllerName, build.ABI, build.Bytecode.String(), tktypes.RawJSON(params)) + if rpcerr != nil { + return nil, rpcerr.Error() + } + return tktypes.MustEthAddress(addr), nil + +} diff --git a/domains/zeto/integration-test/util.go b/domains/zeto/integration-test/util.go index 9772eb7a9..10ffd554f 100644 --- a/domains/zeto/integration-test/util.go +++ b/domains/zeto/integration-test/util.go @@ -41,13 +41,16 @@ type domainContracts struct { } type domainContract struct { - Name string `yaml:"name"` - Verifier string `yaml:"verifier"` - BatchVerifier string `yaml:"batchVerifier"` - CircuitId string `yaml:"circuitId"` - AbiAndBytecode abiAndBytecode `yaml:"abiAndBytecode"` - Libraries []string `yaml:"libraries"` - Cloneable bool `yaml:"cloneable"` + Name string `yaml:"name"` + Verifier string `yaml:"verifier"` + BatchVerifier string `yaml:"batchVerifier"` + DepositVerifier string `yaml:"depositVerifier"` + WithdrawVerifier string `yaml:"withdrawVerifier"` + BatchWithdrawVerifier string `yaml:"batchWithdrawVerifier"` + CircuitId string `yaml:"circuitId"` + AbiAndBytecode abiAndBytecode `yaml:"abiAndBytecode"` + Libraries []string `yaml:"libraries"` + Cloneable bool `yaml:"cloneable"` } type abiAndBytecode struct { diff --git a/domains/zeto/internal/msgs/en_errors.go b/domains/zeto/internal/msgs/en_errors.go index e6c788766..3801c9274 100644 --- a/domains/zeto/internal/msgs/en_errors.go +++ b/domains/zeto/internal/msgs/en_errors.go @@ -144,4 +144,6 @@ var ( MsgNoDomainReceipt = ffe("PD210102", "Not implemented. See state receipt for coin transfers") MsgUnknownSignPayload = ffe("PD210103", "Sign payload type '%s' not recognized") MsgNullifierGenerationFailed = ffe("PD210104", "Failed to generate nullifier for coin") + MsgErrorDecodeDepositCall = ffe("PD210105", "Failed to decode the deposit call. %s") + MsgErrorDecodeWithdrawCall = ffe("PD210106", "Failed to decode the withdraw call. %s") ) diff --git a/domains/zeto/internal/msgs/en_errors_test.go b/domains/zeto/internal/msgs/en_errors_test.go new file mode 100644 index 000000000..df9e25c37 --- /dev/null +++ b/domains/zeto/internal/msgs/en_errors_test.go @@ -0,0 +1,13 @@ +package msgs + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFfeError(t *testing.T) { + assert.Panics(t, func() { + ffe("notvalid", "") + }) +} diff --git a/domains/zeto/internal/zeto/common/utils.go b/domains/zeto/internal/zeto/common/utils.go new file mode 100644 index 000000000..b6b9fb206 --- /dev/null +++ b/domains/zeto/internal/zeto/common/utils.go @@ -0,0 +1,73 @@ +/* + * Copyright © 2024 Kaleido, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package common + +import ( + "github.com/kaleido-io/paladin/domains/zeto/pkg/constants" +) + +func IsNullifiersCircuit(circuitId string) bool { + nullifierCircuits := []string{ + constants.CIRCUIT_ANON_NULLIFIER, + constants.CIRCUIT_ANON_NULLIFIER_BATCH, + constants.CIRCUIT_WITHDRAW_NULLIFIER, + constants.CIRCUIT_WITHDRAW_NULLIFIER_BATCH, + } + for _, c := range nullifierCircuits { + if circuitId == c { + return true + } + } + return false +} + +func IsEncryptionCircuit(circuitId string) bool { + encryptionCircuits := []string{ + constants.CIRCUIT_ANON_ENC, + constants.CIRCUIT_ANON_ENC_BATCH, + } + for _, c := range encryptionCircuits { + if circuitId == c { + return true + } + } + return false +} + +func IsBatchCircuit(sizeOfEndorsableStates int) bool { + if sizeOfEndorsableStates <= 2 { + return false + } + return true +} + +func IsNullifiersToken(tokenName string) bool { + return tokenName == constants.TOKEN_ANON_NULLIFIER +} + +func IsEncryptionToken(tokenName string) bool { + return tokenName == constants.TOKEN_ANON_ENC +} + +// the Zeto implementations support two input/output sizes for the circuits: 2 and 10, +// if the input or output size is larger than 2, then the batch circuit is used with +// input/output size 10 +func GetInputSize(sizeOfEndorsableStates int) int { + if sizeOfEndorsableStates <= 2 { + return 2 + } + return 10 +} diff --git a/domains/zeto/internal/zeto/common/utils_test.go b/domains/zeto/internal/zeto/common/utils_test.go new file mode 100644 index 000000000..ee395e360 --- /dev/null +++ b/domains/zeto/internal/zeto/common/utils_test.go @@ -0,0 +1,12 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsBatchCircuit(t *testing.T) { + assert.False(t, IsBatchCircuit(2)) + assert.True(t, IsBatchCircuit(5)) +} diff --git a/domains/zeto/internal/zeto/events_abi_test.go b/domains/zeto/internal/zeto/events_abi_test.go index 28c36f615..a0cae18c4 100644 --- a/domains/zeto/internal/zeto/events_abi_test.go +++ b/domains/zeto/internal/zeto/events_abi_test.go @@ -24,7 +24,7 @@ import ( func TestGetAllZetoEventAbis(t *testing.T) { events := getAllZetoEventAbis() - assert.Equal(t, 3, len(events)) + assert.Equal(t, 4, len(events)) } func TestBuildEvents(t *testing.T) { diff --git a/domains/zeto/internal/zeto/handler_deposit.go b/domains/zeto/internal/zeto/handler_deposit.go new file mode 100644 index 000000000..fd8215bfb --- /dev/null +++ b/domains/zeto/internal/zeto/handler_deposit.go @@ -0,0 +1,219 @@ +/* + * Copyright © 2024 Kaleido, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package zeto + +import ( + "context" + "encoding/json" + + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-signer/pkg/abi" + "github.com/kaleido-io/paladin/domains/zeto/internal/msgs" + "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/common" + "github.com/kaleido-io/paladin/domains/zeto/pkg/constants" + corepb "github.com/kaleido-io/paladin/domains/zeto/pkg/proto" + "github.com/kaleido-io/paladin/domains/zeto/pkg/types" + "github.com/kaleido-io/paladin/domains/zeto/pkg/zetosigner/zetosignerapi" + "github.com/kaleido-io/paladin/toolkit/pkg/algorithms" + "github.com/kaleido-io/paladin/toolkit/pkg/domain" + pb "github.com/kaleido-io/paladin/toolkit/pkg/prototk" + "github.com/kaleido-io/paladin/toolkit/pkg/tktypes" + "github.com/kaleido-io/paladin/toolkit/pkg/verifiers" + "google.golang.org/protobuf/proto" +) + +type depositHandler struct { + zeto *Zeto +} + +var depositABI = &abi.Entry{ + Type: abi.Function, + Name: "deposit", + Inputs: abi.ParameterArray{ + {Name: "amount", Type: "uint256"}, + {Name: "outputs", Type: "uint256[]"}, + {Name: "proof", Type: "tuple", InternalType: "struct Commonlib.Proof", Components: proofComponents}, + {Name: "data", Type: "bytes"}, + }, +} + +func (h *depositHandler) ValidateParams(ctx context.Context, config *types.DomainInstanceConfig, params string) (interface{}, error) { + var depositParams types.DepositParams + if err := json.Unmarshal([]byte(params), &depositParams); err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorDecodeDepositCall, err) + } + + if err := validateAmountParam(ctx, depositParams.Amount, 0); err != nil { + return nil, err + } + + return depositParams.Amount, nil +} + +func (h *depositHandler) Init(ctx context.Context, tx *types.ParsedTransaction, req *pb.InitTransactionRequest) (*pb.InitTransactionResponse, error) { + res := &pb.InitTransactionResponse{ + RequiredVerifiers: []*pb.ResolveVerifierRequest{ + { + Lookup: tx.Transaction.From, + Algorithm: h.zeto.getAlgoZetoSnarkBJJ(), + VerifierType: zetosignerapi.IDEN3_PUBKEY_BABYJUBJUB_COMPRESSED_0X, + }, + }, + } + return res, nil +} + +func (h *depositHandler) Assemble(ctx context.Context, tx *types.ParsedTransaction, req *pb.AssembleTransactionRequest) (*pb.AssembleTransactionResponse, error) { + amount := tx.Params.(*tktypes.HexUint256) + + resolvedSender := domain.FindVerifier(tx.Transaction.From, h.zeto.getAlgoZetoSnarkBJJ(), zetosignerapi.IDEN3_PUBKEY_BABYJUBJUB_COMPRESSED_0X, req.ResolvedVerifiers) + if resolvedSender == nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorResolveVerifier, tx.Transaction.From) + } + + useNullifiers := common.IsNullifiersToken(tx.DomainConfig.TokenName) + outputCoins, outputStates, err := h.zeto.prepareOutputsForDeposit(ctx, useNullifiers, amount, resolvedSender) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorPrepTxOutputs, err) + } + + payloadBytes, err := h.formatProvingRequest(ctx, outputCoins) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorFormatProvingReq, err) + } + + amountStr := amount.Int().Text(10) + return &pb.AssembleTransactionResponse{ + AssemblyResult: pb.AssembleTransactionResponse_OK, + AssembledTransaction: &pb.AssembledTransaction{ + OutputStates: outputStates, + DomainData: &amountStr, + }, + AttestationPlan: []*pb.AttestationRequest{ + { + Name: "sender", + AttestationType: pb.AttestationType_SIGN, + Algorithm: h.zeto.getAlgoZetoSnarkBJJ(), + VerifierType: zetosignerapi.IDEN3_PUBKEY_BABYJUBJUB_COMPRESSED_0X, + PayloadType: zetosignerapi.PAYLOAD_DOMAIN_ZETO_SNARK, + Payload: payloadBytes, + Parties: []string{tx.Transaction.From}, + }, + { + Name: "submitter", + AttestationType: pb.AttestationType_ENDORSE, + Algorithm: algorithms.ECDSA_SECP256K1, + VerifierType: verifiers.ETH_ADDRESS, + Parties: []string{tx.Transaction.From}, + }, + }, + }, nil +} + +func (h *depositHandler) Endorse(ctx context.Context, tx *types.ParsedTransaction, req *pb.EndorseTransactionRequest) (*pb.EndorseTransactionResponse, error) { + return &pb.EndorseTransactionResponse{ + EndorsementResult: pb.EndorseTransactionResponse_ENDORSER_SUBMIT, + }, nil +} + +func (h *depositHandler) Prepare(ctx context.Context, tx *types.ParsedTransaction, req *pb.PrepareTransactionRequest) (*pb.PrepareTransactionResponse, error) { + var proofRes corepb.ProvingResponse + result := domain.FindAttestation("sender", req.AttestationResult) + if result == nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorFindSenderAttestation) + } + if err := proto.Unmarshal(result.Payload, &proofRes); err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorUnmarshalProvingRes, err) + } + + outputSize := common.GetInputSize(len(req.OutputStates)) + outputs := make([]string, outputSize) + for i := 0; i < outputSize; i++ { + if i < len(req.OutputStates) { + state := req.OutputStates[i] + coin, err := h.zeto.makeCoin(state.StateDataJson) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorParseOutputStates, err) + } + hash, err := coin.Hash(ctx) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorHashOutputState, err) + } + outputs[i] = hash.String() + } else { + outputs[i] = "0" + } + } + + data, err := encodeTransactionData(ctx, req.Transaction) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorEncodeTxData, err) + } + amount := tktypes.MustParseHexUint256(*req.DomainData) + params := map[string]any{ + "amount": amount.Int().Text(10), + "outputs": outputs, + "proof": encodeProof(proofRes.Proof), + "data": data, + } + depositFunction := depositABI + paramsJSON, err := json.Marshal(params) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorMarshalPrepedParams, err) + } + functionJSON, err := json.Marshal(depositFunction) + if err != nil { + return nil, err + } + + return &pb.PrepareTransactionResponse{ + Transaction: &pb.PreparedTransaction{ + FunctionAbiJson: string(functionJSON), + ParamsJson: string(paramsJSON), + RequiredSigner: &req.Transaction.From, // must be signed by the original sender + }, + }, nil +} + +func (h *depositHandler) formatProvingRequest(ctx context.Context, outputCoins []*types.ZetoCoin) ([]byte, error) { + outputSize := common.GetInputSize(len(outputCoins)) + outputCommitments := make([]string, outputSize) + outputValueInts := make([]uint64, outputSize) + outputSalts := make([]string, outputSize) + outputOwners := make([]string, outputSize) + for i := 0; i < outputSize; i++ { + coin := outputCoins[i] + hash, err := coin.Hash(ctx) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorHashInputState, err) + } + outputCommitments[i] = hash.Int().Text(16) + outputValueInts[i] = coin.Amount.Int().Uint64() + outputSalts[i] = coin.Salt.Int().Text(16) + outputOwners[i] = coin.Owner.String() + } + + payload := &corepb.ProvingRequest{ + CircuitId: constants.CIRCUIT_DEPOSIT, + Common: &corepb.ProvingRequestCommon{ + OutputCommitments: outputCommitments, + OutputValues: outputValueInts, + OutputSalts: outputSalts, + OutputOwners: outputOwners, + }, + } + return proto.Marshal(payload) +} diff --git a/domains/zeto/internal/zeto/handler_deposit_test.go b/domains/zeto/internal/zeto/handler_deposit_test.go new file mode 100644 index 000000000..11650787d --- /dev/null +++ b/domains/zeto/internal/zeto/handler_deposit_test.go @@ -0,0 +1,230 @@ +package zeto + +import ( + "context" + "errors" + "testing" + + "github.com/kaleido-io/paladin/domains/zeto/pkg/constants" + corepb "github.com/kaleido-io/paladin/domains/zeto/pkg/proto" + "github.com/kaleido-io/paladin/domains/zeto/pkg/types" + "github.com/kaleido-io/paladin/domains/zeto/pkg/zetosigner/zetosignerapi" + "github.com/kaleido-io/paladin/toolkit/pkg/prototk" + "github.com/kaleido-io/paladin/toolkit/pkg/tktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestDepositValidateParams(t *testing.T) { + h := &depositHandler{} + ctx := context.Background() + config := &types.DomainInstanceConfig{} + v, err := h.ValidateParams(ctx, config, "{\"amount\":100}") + require.NoError(t, err) + require.Equal(t, "0x64", v.(*tktypes.HexUint256).String()) + + _, err = h.ValidateParams(ctx, config, "bad json") + require.ErrorContains(t, err, "PD210105: Failed to decode the deposit call.") + + _, err = h.ValidateParams(ctx, config, "{\"amount\":-100}") + require.ErrorContains(t, err, "PD210027: Parameter 'amount' must be greater than 0 (index=0)") +} + +func TestDepositInit(t *testing.T) { + h := depositHandler{ + zeto: &Zeto{ + name: "test1", + }, + } + ctx := context.Background() + tx := &types.ParsedTransaction{ + Transaction: &prototk.TransactionSpecification{ + From: "Alice", + }, + } + req := &prototk.InitTransactionRequest{} + res, err := h.Init(ctx, tx, req) + assert.NoError(t, err) + assert.Len(t, res.RequiredVerifiers, 1) + assert.Equal(t, "Alice", res.RequiredVerifiers[0].Lookup) + assert.Equal(t, zetosignerapi.IDEN3_PUBKEY_BABYJUBJUB_COMPRESSED_0X, res.RequiredVerifiers[0].VerifierType) + assert.Equal(t, zetosignerapi.AlgoDomainZetoSnarkBJJ("test1"), res.RequiredVerifiers[0].Algorithm) +} + +func TestDepositAssemble(t *testing.T) { + h := depositHandler{ + zeto: &Zeto{ + name: "test1", + coinSchema: &prototk.StateSchema{ + Id: "coin", + }, + merkleTreeRootSchema: &prototk.StateSchema{ + Id: "merkle_tree_root", + }, + merkleTreeNodeSchema: &prototk.StateSchema{ + Id: "merkle_tree_node", + }, + }, + } + ctx := context.Background() + txSpec := &prototk.TransactionSpecification{ + From: "Bob", + ContractInfo: &prototk.ContractInfo{ + ContractAddress: "0x1234567890123456789012345678901234567890", + }, + } + tx := &types.ParsedTransaction{ + Params: tktypes.MustParseHexUint256("100"), + Transaction: txSpec, + DomainConfig: &types.DomainInstanceConfig{ + TokenName: "tokenContract1", + CircuitId: "circuit1", + }, + } + req := &prototk.AssembleTransactionRequest{ + ResolvedVerifiers: []*prototk.ResolvedVerifier{ + { + Lookup: "Alice", + Verifier: "0x19d2ee6b9770a4f8d7c3b7906bc7595684509166fa42d718d1d880b62bcb7922", + Algorithm: h.zeto.getAlgoZetoSnarkBJJ(), + VerifierType: zetosignerapi.IDEN3_PUBKEY_BABYJUBJUB_COMPRESSED_0X, + }, + }, + Transaction: txSpec, + } + _, err := h.Assemble(ctx, tx, req) + assert.EqualError(t, err, "PD210036: Failed to resolve verifier: Bob") + + req.ResolvedVerifiers = append(req.ResolvedVerifiers, &prototk.ResolvedVerifier{ + Lookup: "Bob", + Verifier: "0x1234567890123456789012345678901234567890", + Algorithm: h.zeto.getAlgoZetoSnarkBJJ(), + VerifierType: zetosignerapi.IDEN3_PUBKEY_BABYJUBJUB_COMPRESSED_0X, + }) + testCallbacks := &testDomainCallbacks{ + returnFunc: func() (*prototk.FindAvailableStatesResponse, error) { + return nil, errors.New("test error") + }, + } + h.zeto.Callbacks = testCallbacks + _, err = h.Assemble(ctx, tx, req) + assert.ErrorContains(t, err, "PD210040: Failed to prepare transaction outputs. PD210037: Failed load owner public key") + + req.ResolvedVerifiers[1].Verifier = "0x19d2ee6b9770a4f8d7c3b7906bc7595684509166fa42d718d1d880b62bcb7922" + res, err := h.Assemble(ctx, tx, req) + require.NoError(t, err) + assert.Equal(t, "100", *res.AssembledTransaction.DomainData) +} + +func TestDepositEndorse(t *testing.T) { + h := depositHandler{} + ctx := context.Background() + tx := &types.ParsedTransaction{ + Params: tktypes.MustParseHexUint256("100"), + Transaction: &prototk.TransactionSpecification{ + From: "Bob", + }, + } + + req := &prototk.EndorseTransactionRequest{} + res, err := h.Endorse(ctx, tx, req) + assert.NoError(t, err) + assert.Equal(t, prototk.EndorseTransactionResponse_ENDORSER_SUBMIT, res.EndorsementResult) +} + +func TestDepositPrepare(t *testing.T) { + z := &Zeto{ + name: "test1", + } + h := depositHandler{ + zeto: z, + } + txSpec := &prototk.TransactionSpecification{ + TransactionId: "bad hex", + From: "Bob", + } + tx := &types.ParsedTransaction{ + Params: tktypes.MustParseHexUint256("100"), + Transaction: txSpec, + DomainConfig: &types.DomainInstanceConfig{ + TokenName: constants.TOKEN_ANON_ENC, + }, + } + amountStr := "100" + req := &prototk.PrepareTransactionRequest{ + InputStates: []*prototk.EndorsableState{ + { + SchemaId: "coin", + StateDataJson: "bad json", + }, + }, + OutputStates: []*prototk.EndorsableState{ + { + SchemaId: "coin", + StateDataJson: "bad json", + }, + }, + Transaction: txSpec, + DomainData: &amountStr, + } + ctx := context.Background() + _, err := h.Prepare(ctx, tx, req) + assert.EqualError(t, err, "PD210043: Did not find 'sender' attestation") + + at := zetosignerapi.PAYLOAD_DOMAIN_ZETO_SNARK + req.AttestationResult = []*prototk.AttestationResult{ + { + Name: "sender", + AttestationType: prototk.AttestationType_ENDORSE, + PayloadType: &at, + Payload: []byte("bad payload"), + }, + } + _, err = h.Prepare(ctx, tx, req) + assert.ErrorContains(t, err, "PD210044: Failed to unmarshal proving response") + + proofReq := corepb.ProvingResponse{ + Proof: &corepb.SnarkProof{ + A: []string{"0x1234567890", "0x1234567890"}, + B: []*corepb.B_Item{ + { + Items: []string{"0x1234567890", "0x1234567890"}, + }, + { + Items: []string{"0x1234567890", "0x1234567890"}, + }, + }, + C: []string{"0x1234567890", "0x1234567890"}, + }, + PublicInputs: map[string]string{ + "encryptionNonce": "0x1234567890", + "encryptedValues": "0x1234567890,0x1234567890", + }, + } + payload, err := proto.Marshal(&proofReq) + assert.NoError(t, err) + req.AttestationResult[0].Payload = payload + _, err = h.Prepare(ctx, tx, req) + assert.ErrorContains(t, err, "PD210047: Failed to parse output states.") + + req.OutputStates[0].StateDataJson = "{\"salt\":\"0x042fac32983b19d76425cc54dd80e8a198f5d477c6a327cb286eb81a0c2b95ec\",\"owner\":\"0x7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025\",\"amount\":\"0x0f\",\"hash\":\"0x303eb034d22aacc5dff09647928d757017a35e64e696d48609a250a6505e5d5f\"}" + _, err = h.Prepare(ctx, tx, req) + assert.ErrorContains(t, err, "PD210049: Failed to encode transaction data. PD210028: Failed to parse transaction id. PD020007: Invalid hex:") + + txSpec.TransactionId = "0x1234567890123456789012345678901234567890123456789012345678901234" + z.config = &types.DomainFactoryConfig{ + DomainContracts: types.DomainConfigContracts{ + Implementations: []*types.DomainContract{}, + }, + } + z.config.DomainContracts.Implementations = []*types.DomainContract{ + { + Name: constants.TOKEN_ANON_ENC, + }, + } + + res, err := h.Prepare(ctx, tx, req) + assert.NoError(t, err) + assert.Equal(t, "{\"amount\":\"100\",\"data\":\"0x000100001234567890123456789012345678901234567890123456789012345678901234\",\"outputs\":[\"0x303eb034d22aacc5dff09647928d757017a35e64e696d48609a250a6505e5d5f\",\"0\"],\"proof\":{\"pA\":[\"0x1234567890\",\"0x1234567890\"],\"pB\":[[\"0x1234567890\",\"0x1234567890\"],[\"0x1234567890\",\"0x1234567890\"]],\"pC\":[\"0x1234567890\",\"0x1234567890\"]}}", res.Transaction.ParamsJson) +} diff --git a/domains/zeto/internal/zeto/handler_events.go b/domains/zeto/internal/zeto/handler_events.go index c876d081d..04e3d5e40 100644 --- a/domains/zeto/internal/zeto/handler_events.go +++ b/domains/zeto/internal/zeto/handler_events.go @@ -9,6 +9,7 @@ import ( "github.com/hyperledger-labs/zeto/go-sdk/pkg/sparse-merkle-tree/node" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/kaleido-io/paladin/domains/zeto/internal/msgs" + "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/common" "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/smt" "github.com/kaleido-io/paladin/toolkit/pkg/log" "github.com/kaleido-io/paladin/toolkit/pkg/prototk" @@ -28,7 +29,7 @@ func (z *Zeto) handleMintEvent(ctx context.Context, tree core.SparseMerkleTree, Location: ev.Location, }) res.ConfirmedStates = append(res.ConfirmedStates, parseStatesFromEvent(txID, mint.Outputs)...) - if isNullifiersToken(tokenName) { + if common.IsNullifiersToken(tokenName) { err := z.updateMerkleTree(ctx, tree, storage, txID, mint.Outputs) if err != nil { return i18n.NewError(ctx, msgs.MsgErrorUpdateSMT, "UTXOMint", err) @@ -54,7 +55,7 @@ func (z *Zeto) handleTransferEvent(ctx context.Context, tree core.SparseMerkleTr }) res.SpentStates = append(res.SpentStates, parseStatesFromEvent(txID, transfer.Inputs)...) res.ConfirmedStates = append(res.ConfirmedStates, parseStatesFromEvent(txID, transfer.Outputs)...) - if isNullifiersToken(tokenName) { + if common.IsNullifiersToken(tokenName) { err := z.updateMerkleTree(ctx, tree, storage, txID, transfer.Outputs) if err != nil { return i18n.NewError(ctx, msgs.MsgErrorUpdateSMT, "UTXOTransfer", err) @@ -80,7 +81,7 @@ func (z *Zeto) handleTransferWithEncryptionEvent(ctx context.Context, tree core. }) res.SpentStates = append(res.SpentStates, parseStatesFromEvent(txID, transfer.Inputs)...) res.ConfirmedStates = append(res.ConfirmedStates, parseStatesFromEvent(txID, transfer.Outputs)...) - if isNullifiersToken(tokenName) { + if common.IsNullifiersToken(tokenName) { err := z.updateMerkleTree(ctx, tree, storage, txID, transfer.Outputs) if err != nil { return i18n.NewError(ctx, msgs.MsgErrorUpdateSMT, "UTXOTransferWithEncryptedValues", err) @@ -92,6 +93,32 @@ func (z *Zeto) handleTransferWithEncryptionEvent(ctx context.Context, tree core. return nil } +func (z *Zeto) handleWithdrawEvent(ctx context.Context, tree core.SparseMerkleTree, storage smt.StatesStorage, ev *prototk.OnChainEvent, tokenName string, res *prototk.HandleEventBatchResponse) error { + var withdraw WithdrawEvent + if err := json.Unmarshal([]byte(ev.DataJson), &withdraw); err == nil { + txID := decodeTransactionData(withdraw.Data) + if txID == nil { + log.L(ctx).Errorf("Failed to decode transaction data for withdraw event: %s. Skip to the next event", withdraw.Data) + return nil + } + res.TransactionsComplete = append(res.TransactionsComplete, &prototk.CompletedTransaction{ + TransactionId: txID.String(), + Location: ev.Location, + }) + res.SpentStates = append(res.SpentStates, parseStatesFromEvent(txID, withdraw.Inputs)...) + res.ConfirmedStates = append(res.ConfirmedStates, parseStatesFromEvent(txID, []tktypes.HexUint256{withdraw.Output})...) + if common.IsNullifiersToken(tokenName) { + err := z.updateMerkleTree(ctx, tree, storage, txID, []tktypes.HexUint256{withdraw.Output}) + if err != nil { + return i18n.NewError(ctx, msgs.MsgErrorUpdateSMT, "UTXOWithdraw", err) + } + } + } else { + log.L(ctx).Errorf("Failed to unmarshal withdraw event: %s", err) + } + return nil +} + func (z *Zeto) updateMerkleTree(ctx context.Context, tree core.SparseMerkleTree, storage smt.StatesStorage, txID tktypes.HexBytes, outputs []tktypes.HexUint256) error { storage.SetTransactionId(txID.HexString0xPrefix()) for _, out := range outputs { diff --git a/domains/zeto/internal/zeto/handler_events_test.go b/domains/zeto/internal/zeto/handler_events_test.go index db4001577..5c877b37d 100644 --- a/domains/zeto/internal/zeto/handler_events_test.go +++ b/domains/zeto/internal/zeto/handler_events_test.go @@ -168,3 +168,33 @@ func TestUpdateMerkleTree(t *testing.T) { err = z.updateMerkleTree(ctx, merkleTree, storage, tktypes.HexBytes("0x1234"), []tktypes.HexUint256{*tktypes.MustParseHexUint256("0x1234"), *tktypes.MustParseHexUint256("0x0")}) assert.NoError(t, err) } + +func TestHandleWithdrawEvent(t *testing.T) { + z, testCallbacks := newTestZeto() + storage := smt.NewStatesStorage(testCallbacks, "testToken1", "context1", "merkle_tree_root", "merkle_tree_node") + merkleTree, err := smt.NewSmt(storage) + require.NoError(t, err) + ctx := context.Background() + + ev := &prototk.OnChainEvent{ + DataJson: "bad json", + SoliditySignature: "event UTXOWithdraw(uint256 amount, uint256[] inputs, uint256 output, address indexed submitter, bytes data)", + } + res := &prototk.HandleEventBatchResponse{} + + // bad data for the withdraw event - should be logged and move on + err = z.handleWithdrawEvent(ctx, merkleTree, storage, ev, "Zeto_Anon", res) + assert.NoError(t, err) + + ev.DataJson = "{\"data\":\"0x0001\",\"inputs\":[\"7980718117603030807695495350922077879582656644717071592146865497574198464253\"],\"output\":\"7980718117603030807695495350922077879582656644717071592146865497574198464253\",\"submitter\":\"0x74e71b05854ee819cb9397be01c82570a178d019\"}" + err = z.handleWithdrawEvent(ctx, merkleTree, storage, ev, "Zeto_Anon", res) + assert.NoError(t, err) + + ev.DataJson = "{\"data\":\"0x0001ffff\",\"inputs\":[\"7980718117603030807695495350922077879582656644717071592146865497574198464253\"],\"output\":\"7980718117603030807695495350922077879582656644717071592146865497574198464253\",\"submitter\":\"0x74e71b05854ee819cb9397be01c82570a178d019\"}" + err = z.handleWithdrawEvent(ctx, merkleTree, storage, ev, "Zeto_Anon", res) + assert.NoError(t, err) + + ev.DataJson = "{\"data\":\"0x0001000030e43028afbb41d6887444f4c2b4ed6d00000000000000000000000000000000\",\"output\":\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"submitter\":\"0x74e71b05854ee819cb9397be01c82570a178d019\"}" + err = z.handleWithdrawEvent(ctx, merkleTree, storage, ev, "Zeto_AnonNullifier", res) + assert.ErrorContains(t, err, "PD210061: Failed to update merkle tree for the UTXOWithdraw event. PD210056: Failed to create new node index from hash. 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") +} diff --git a/domains/zeto/internal/zeto/handler_mint.go b/domains/zeto/internal/zeto/handler_mint.go index c8f9e2f6c..ea7af068d 100644 --- a/domains/zeto/internal/zeto/handler_mint.go +++ b/domains/zeto/internal/zeto/handler_mint.go @@ -22,6 +22,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-signer/pkg/abi" "github.com/kaleido-io/paladin/domains/zeto/internal/msgs" + "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/common" "github.com/kaleido-io/paladin/domains/zeto/pkg/types" "github.com/kaleido-io/paladin/domains/zeto/pkg/zetosigner/zetosignerapi" "github.com/kaleido-io/paladin/toolkit/pkg/algorithms" @@ -76,8 +77,8 @@ func (h *mintHandler) Init(ctx context.Context, tx *types.ParsedTransaction, req func (h *mintHandler) Assemble(ctx context.Context, tx *types.ParsedTransaction, req *pb.AssembleTransactionRequest) (*pb.AssembleTransactionResponse, error) { params := tx.Params.([]*types.TransferParamEntry) - useNullifiers := isNullifiersToken(tx.DomainConfig.TokenName) - _, outputStates, err := h.zeto.prepareOutputs(ctx, useNullifiers, params, req.ResolvedVerifiers) + useNullifiers := common.IsNullifiersToken(tx.DomainConfig.TokenName) + _, outputStates, err := h.zeto.prepareOutputsForTransfer(ctx, useNullifiers, params, req.ResolvedVerifiers) if err != nil { return nil, err } diff --git a/domains/zeto/internal/zeto/handler_transfer.go b/domains/zeto/internal/zeto/handler_transfer.go index 8d790519d..dd99362c5 100644 --- a/domains/zeto/internal/zeto/handler_transfer.go +++ b/domains/zeto/internal/zeto/handler_transfer.go @@ -18,18 +18,15 @@ package zeto import ( "context" "encoding/json" - "math/big" "strings" - "github.com/hyperledger-labs/zeto/go-sdk/pkg/sparse-merkle-tree/core" - "github.com/hyperledger-labs/zeto/go-sdk/pkg/sparse-merkle-tree/node" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-signer/pkg/abi" "github.com/kaleido-io/paladin/domains/zeto/internal/msgs" + "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/common" "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/smt" corepb "github.com/kaleido-io/paladin/domains/zeto/pkg/proto" "github.com/kaleido-io/paladin/domains/zeto/pkg/types" - "github.com/kaleido-io/paladin/domains/zeto/pkg/zetosigner" "github.com/kaleido-io/paladin/domains/zeto/pkg/zetosigner/zetosignerapi" "github.com/kaleido-io/paladin/toolkit/pkg/algorithms" "github.com/kaleido-io/paladin/toolkit/pkg/domain" @@ -145,12 +142,12 @@ func (h *transferHandler) Assemble(ctx context.Context, tx *types.ParsedTransact return nil, i18n.NewError(ctx, msgs.MsgErrorResolveVerifier, tx.Transaction.From) } - useNullifiers := isNullifiersToken(tx.DomainConfig.TokenName) - inputCoins, inputStates, _, remainder, err := h.zeto.prepareInputs(ctx, useNullifiers, req.StateQueryContext, resolvedSender.Verifier, params) + useNullifiers := common.IsNullifiersToken(tx.DomainConfig.TokenName) + inputCoins, inputStates, _, remainder, err := h.zeto.prepareInputsForTransfer(ctx, useNullifiers, req.StateQueryContext, resolvedSender.Verifier, params) if err != nil { return nil, i18n.NewError(ctx, msgs.MsgErrorPrepTxInputs, err) } - outputCoins, outputStates, err := h.zeto.prepareOutputs(ctx, useNullifiers, params, req.ResolvedVerifiers) + outputCoins, outputStates, err := h.zeto.prepareOutputsForTransfer(ctx, useNullifiers, params, req.ResolvedVerifiers) if err != nil { return nil, i18n.NewError(ctx, msgs.MsgErrorPrepTxOutputs, err) } @@ -163,7 +160,7 @@ func (h *transferHandler) Assemble(ctx context.Context, tx *types.ParsedTransact Amount: &remainderHex, }, } - returnedCoins, returnedStates, err := h.zeto.prepareOutputs(ctx, useNullifiers, remainderParams, req.ResolvedVerifiers) + returnedCoins, returnedStates, err := h.zeto.prepareOutputsForTransfer(ctx, useNullifiers, remainderParams, req.ResolvedVerifiers) if err != nil { return nil, i18n.NewError(ctx, msgs.MsgErrorPrepTxChange, err) } @@ -213,19 +210,6 @@ func (h *transferHandler) Endorse(ctx context.Context, tx *types.ParsedTransacti }, nil } -func getTransferABI(tokenName string) *abi.Entry { - transferFunction := transferABI - if isEncryptionToken(tokenName) { - transferFunction = transferABI_withEncryption - if isNullifiersToken(tokenName) { - transferFunction = transferABI_withEncryption_nullifiers - } - } else if isNullifiersToken(tokenName) { - transferFunction = transferABI_nullifiers - } - return transferFunction -} - func (h *transferHandler) Prepare(ctx context.Context, tx *types.ParsedTransaction, req *pb.PrepareTransactionRequest) (*pb.PrepareTransactionResponse, error) { var proofRes corepb.ProvingResponse result := domain.FindAttestation("sender", req.AttestationResult) @@ -236,7 +220,7 @@ func (h *transferHandler) Prepare(ctx context.Context, tx *types.ParsedTransacti return nil, i18n.NewError(ctx, msgs.MsgErrorUnmarshalProvingRes, err) } - inputSize := getInputSize(len(req.InputStates)) + inputSize := common.GetInputSize(len(req.InputStates)) inputs := make([]string, inputSize) for i := 0; i < inputSize; i++ { if i < len(req.InputStates) { @@ -279,16 +263,16 @@ func (h *transferHandler) Prepare(ctx context.Context, tx *types.ParsedTransacti params := map[string]any{ "inputs": inputs, "outputs": outputs, - "proof": h.encodeProof(proofRes.Proof), + "proof": encodeProof(proofRes.Proof), "data": data, } transferFunction := getTransferABI(tx.DomainConfig.TokenName) - if isEncryptionToken(tx.DomainConfig.TokenName) { + if common.IsEncryptionToken(tx.DomainConfig.TokenName) { params["ecdhPublicKey"] = strings.Split(proofRes.PublicInputs["ecdhPublicKey"], ",") params["encryptionNonce"] = proofRes.PublicInputs["encryptionNonce"] params["encryptedValues"] = strings.Split(proofRes.PublicInputs["encryptedValues"], ",") } - if isNullifiersToken(tx.DomainConfig.TokenName) { + if common.IsNullifiersToken(tx.DomainConfig.TokenName) { delete(params, "inputs") params["nullifiers"] = strings.Split(proofRes.PublicInputs["nullifiers"], ",") params["root"] = proofRes.PublicInputs["root"] @@ -311,7 +295,7 @@ func (h *transferHandler) Prepare(ctx context.Context, tx *types.ParsedTransacti } func (h *transferHandler) formatProvingRequest(ctx context.Context, inputCoins, outputCoins []*types.ZetoCoin, circuitId, tokenName, stateQueryContext string, contractAddress *tktypes.EthAddress) ([]byte, error) { - inputSize := getInputSize(len(inputCoins)) + inputSize := common.GetInputSize(len(inputCoins)) inputCommitments := make([]string, inputSize) inputValueInts := make([]uint64, inputSize) inputSalts := make([]string, inputSize) @@ -347,8 +331,8 @@ func (h *transferHandler) formatProvingRequest(ctx context.Context, inputCoins, } var extras []byte - if isNullifiersCircuit(circuitId) { - proofs, extrasObj, err := h.generateMerkleProofs(ctx, tokenName, stateQueryContext, contractAddress, inputCoins) + if common.IsNullifiersCircuit(circuitId) { + proofs, extrasObj, err := generateMerkleProofs(ctx, h.zeto, tokenName, stateQueryContext, contractAddress, inputCoins) if err != nil { return nil, i18n.NewError(ctx, msgs.MsgErrorGenerateMTP, err) } @@ -381,84 +365,15 @@ func (h *transferHandler) formatProvingRequest(ctx context.Context, inputCoins, return proto.Marshal(payload) } -func (h *transferHandler) encodeProof(proof *corepb.SnarkProof) map[string]interface{} { - // Convert the proof json to the format that the Solidity verifier expects - return map[string]interface{}{ - "pA": []string{proof.A[0], proof.A[1]}, - "pB": [][]string{ - {proof.B[0].Items[1], proof.B[0].Items[0]}, - {proof.B[1].Items[1], proof.B[1].Items[0]}, - }, - "pC": []string{proof.C[0], proof.C[1]}, - } -} - -func (h *transferHandler) generateMerkleProofs(ctx context.Context, tokenName string, stateQueryContext string, contractAddress *tktypes.EthAddress, inputCoins []*types.ZetoCoin) ([]core.Proof, *corepb.ProvingRequestExtras_Nullifiers, error) { - smtName := smt.MerkleTreeName(tokenName, contractAddress) - storage := smt.NewStatesStorage(h.zeto.Callbacks, smtName, stateQueryContext, h.zeto.merkleTreeRootSchema.Id, h.zeto.merkleTreeNodeSchema.Id) - mt, err := smt.NewSmt(storage) - if err != nil { - return nil, nil, i18n.NewError(ctx, msgs.MsgErrorNewSmt, smtName, err) - } - // verify that the input UTXOs have been indexed by the Merkle tree DB - // and generate a merkle proof for each - var indexes []*big.Int - for _, coin := range inputCoins { - pubKey, err := zetosigner.DecodeBabyJubJubPublicKey(coin.Owner.String()) - if err != nil { - return nil, nil, i18n.NewError(ctx, msgs.MsgErrorLoadOwnerPubKey, err) - } - idx := node.NewFungible(coin.Amount.Int(), pubKey, coin.Salt.Int()) - leaf, err := node.NewLeafNode(idx) - if err != nil { - return nil, nil, i18n.NewError(ctx, msgs.MsgErrorNewLeafNode, err) - } - n, err := mt.GetNode(leaf.Ref()) - if err != nil { - // TODO: deal with when the node is not found in the DB tables for the tree - // e.g because the transaction event hasn't been processed yet - return nil, nil, i18n.NewError(ctx, msgs.MsgErrorQueryLeafNode, leaf.Ref().Hex(), err) - } - hash, err := coin.Hash(ctx) - if err != nil { - return nil, nil, i18n.NewError(ctx, msgs.MsgErrorHashInputState, err) - } - if n.Index().BigInt().Cmp(hash.Int()) != 0 { - expectedIndex, err := node.NewNodeIndexFromBigInt(hash.Int()) - if err != nil { - return nil, nil, i18n.NewError(ctx, msgs.MsgErrorNewNodeIndex, err) - } - return nil, nil, i18n.NewError(ctx, msgs.MsgErrorHashMismatch, leaf.Ref().Hex(), n.Index().BigInt().Text(16), n.Index().Hex(), hash.HexString0xPrefix(), expectedIndex.Hex()) - } - indexes = append(indexes, n.Index().BigInt()) - } - mtRoot := mt.Root() - proofs, _, err := mt.GenerateProofs(indexes, mtRoot) - if err != nil { - return nil, nil, i18n.NewError(ctx, msgs.MsgErrorGenerateMTP, err) - } - var mps []*corepb.MerkleProof - var enabled []bool - for i, proof := range proofs { - cp, err := proof.ToCircomVerifierProof(indexes[i], indexes[i], mtRoot, smt.SMT_HEIGHT_UTXO) - if err != nil { - return nil, nil, i18n.NewError(ctx, msgs.MsgErrorConvertToCircomProof, err) - } - proofSiblings := make([]string, len(cp.Siblings)-1) - for i, s := range cp.Siblings[0 : len(cp.Siblings)-1] { - proofSiblings[i] = s.BigInt().Text(16) - } - p := corepb.MerkleProof{ - Nodes: proofSiblings, +func getTransferABI(tokenName string) *abi.Entry { + transferFunction := transferABI + if common.IsEncryptionToken(tokenName) { + transferFunction = transferABI_withEncryption + if common.IsNullifiersToken(tokenName) { + transferFunction = transferABI_withEncryption_nullifiers } - mps = append(mps, &p) - enabled = append(enabled, true) - } - extrasObj := corepb.ProvingRequestExtras_Nullifiers{ - Root: mt.Root().BigInt().Text(16), - MerkleProofs: mps, - Enabled: enabled, + } else if common.IsNullifiersToken(tokenName) { + transferFunction = transferABI_nullifiers } - - return proofs, &extrasObj, nil + return transferFunction } diff --git a/domains/zeto/internal/zeto/handler_transfer_test.go b/domains/zeto/internal/zeto/handler_transfer_test.go index 72c3b0790..295d9aeac 100644 --- a/domains/zeto/internal/zeto/handler_transfer_test.go +++ b/domains/zeto/internal/zeto/handler_transfer_test.go @@ -247,6 +247,7 @@ func TestTransferAssemble(t *testing.T) { }, nil } + h.zeto.Callbacks = testCallbacks res, err = h.Assemble(ctx, tx, req) assert.NoError(t, err) assert.Len(t, res.AssembledTransaction.OutputStates, 2) @@ -420,7 +421,7 @@ func TestGenerateMerkleProofs(t *testing.T) { } ctx := context.Background() queryContext := "queryContext" - _, _, err = h.generateMerkleProofs(ctx, "Zeto_Anon", queryContext, addr, inputCoins) + _, _, err = generateMerkleProofs(ctx, h.zeto, "Zeto_Anon", queryContext, addr, inputCoins) assert.EqualError(t, err, "PD210019: Failed to create Merkle tree for smt_Zeto_Anon_0x1234567890123456789012345678901234567890: PD210065: Failed to find available states for the merkle tree. test error") testCallbacks.returnFunc = func() (*prototk.FindAvailableStatesResponse, error) { @@ -432,11 +433,11 @@ func TestGenerateMerkleProofs(t *testing.T) { }, }, nil } - _, _, err = h.generateMerkleProofs(ctx, "Zeto_Anon", queryContext, addr, inputCoins) + _, _, err = generateMerkleProofs(ctx, h.zeto, "Zeto_Anon", queryContext, addr, inputCoins) assert.EqualError(t, err, "PD210037: Failed load owner public key. PD210072: Invalid compressed public key length: 2") inputCoins[0].Owner = tktypes.MustParseHexBytes("0x7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025") - _, _, err = h.generateMerkleProofs(ctx, "Zeto_Anon", queryContext, addr, inputCoins) + _, _, err = generateMerkleProofs(ctx, h.zeto, "Zeto_Anon", queryContext, addr, inputCoins) assert.EqualError(t, err, "PD210054: Failed to create new leaf node. inputs values not inside Finite Field") inputCoins[0].Salt = tktypes.MustParseHexUint256("0x042fac32983b19d76425cc54dd80e8a198f5d477c6a327cb286eb81a0c2b95ec") @@ -457,7 +458,7 @@ func TestGenerateMerkleProofs(t *testing.T) { }, nil } } - _, _, err = h.generateMerkleProofs(ctx, "Zeto_Anon", queryContext, addr, inputCoins) + _, _, err = generateMerkleProofs(ctx, h.zeto, "Zeto_Anon", queryContext, addr, inputCoins) assert.EqualError(t, err, "PD210055: Failed to query the smt DB for leaf node (ref=789c99b9a2196addb3ac11567135877e8b86bc9b5f7725808a79757fd36b2a2a). key not found") testCallbacks.returnFunc = func() (*prototk.FindAvailableStatesResponse, error) { @@ -480,7 +481,7 @@ func TestGenerateMerkleProofs(t *testing.T) { }, nil } } - _, _, err = h.generateMerkleProofs(ctx, "Zeto_Anon", queryContext, addr, inputCoins) + _, _, err = generateMerkleProofs(ctx, h.zeto, "Zeto_Anon", queryContext, addr, inputCoins) assert.EqualError(t, err, "PD210057: Coin (ref=789c99b9a2196addb3ac11567135877e8b86bc9b5f7725808a79757fd36b2a2a) found in the merkle tree but the persisted hash 26e3879b46b15a4ddbaca5d96af1bd2743f67f13f0bb85c40782950a2a700138 (index=3801702a0a958207c485bbf0137ff64327bdf16ad9a5acdb4d5ab1469b87e326) did not match the expected hash 0x303eb034d22aacc5dff09647928d757017a35e64e696d48609a250a6505e5d5f (index=5f5d5e50a650a20986d496e6645ea31770758d924796f0dfc5ac2ad234b03e30)") testCallbacks.returnFunc = func() (*prototk.FindAvailableStatesResponse, error) { @@ -503,7 +504,7 @@ func TestGenerateMerkleProofs(t *testing.T) { }, nil } } - _, _, err = h.generateMerkleProofs(ctx, "Zeto_Anon", queryContext, addr, inputCoins) + _, _, err = generateMerkleProofs(ctx, h.zeto, "Zeto_Anon", queryContext, addr, inputCoins) assert.NoError(t, err) } diff --git a/domains/zeto/internal/zeto/handler_withdraw.go b/domains/zeto/internal/zeto/handler_withdraw.go new file mode 100644 index 000000000..b67fade98 --- /dev/null +++ b/domains/zeto/internal/zeto/handler_withdraw.go @@ -0,0 +1,318 @@ +/* + * Copyright © 2024 Kaleido, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package zeto + +import ( + "context" + "encoding/json" + "strings" + + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-signer/pkg/abi" + "github.com/kaleido-io/paladin/domains/zeto/internal/msgs" + "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/common" + "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/smt" + "github.com/kaleido-io/paladin/domains/zeto/pkg/constants" + corepb "github.com/kaleido-io/paladin/domains/zeto/pkg/proto" + "github.com/kaleido-io/paladin/domains/zeto/pkg/types" + "github.com/kaleido-io/paladin/domains/zeto/pkg/zetosigner/zetosignerapi" + "github.com/kaleido-io/paladin/toolkit/pkg/algorithms" + "github.com/kaleido-io/paladin/toolkit/pkg/domain" + pb "github.com/kaleido-io/paladin/toolkit/pkg/prototk" + "github.com/kaleido-io/paladin/toolkit/pkg/tktypes" + "github.com/kaleido-io/paladin/toolkit/pkg/verifiers" + "google.golang.org/protobuf/proto" +) + +type withdrawHandler struct { + zeto *Zeto +} + +var withdrawABI = &abi.Entry{ + Type: abi.Function, + Name: "withdraw", + Inputs: abi.ParameterArray{ + {Name: "amount", Type: "uint256"}, + {Name: "inputs", Type: "uint256[]"}, + {Name: "output", Type: "uint256"}, + {Name: "proof", Type: "tuple", InternalType: "struct Commonlib.Proof", Components: proofComponents}, + {Name: "data", Type: "bytes"}, + }, +} + +var withdrawABI_nullifiers = &abi.Entry{ + Type: abi.Function, + Name: "withdraw", + Inputs: abi.ParameterArray{ + {Name: "amount", Type: "uint256"}, + {Name: "nullifiers", Type: "uint256[]"}, + {Name: "output", Type: "uint256"}, + {Name: "root", Type: "uint256"}, + {Name: "proof", Type: "tuple", InternalType: "struct Commonlib.Proof", Components: proofComponents}, + {Name: "data", Type: "bytes"}, + }, +} + +func (h *withdrawHandler) ValidateParams(ctx context.Context, config *types.DomainInstanceConfig, params string) (interface{}, error) { + var withdrawParams types.WithdrawParams + if err := json.Unmarshal([]byte(params), &withdrawParams); err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorDecodeWithdrawCall, err) + } + + if err := validateAmountParam(ctx, withdrawParams.Amount, 0); err != nil { + return nil, err + } + + return withdrawParams.Amount, nil +} + +func (h *withdrawHandler) Init(ctx context.Context, tx *types.ParsedTransaction, req *pb.InitTransactionRequest) (*pb.InitTransactionResponse, error) { + res := &pb.InitTransactionResponse{ + RequiredVerifiers: []*pb.ResolveVerifierRequest{ + { + Lookup: tx.Transaction.From, + Algorithm: h.zeto.getAlgoZetoSnarkBJJ(), + VerifierType: zetosignerapi.IDEN3_PUBKEY_BABYJUBJUB_COMPRESSED_0X, + }, + }, + } + return res, nil +} + +func (h *withdrawHandler) Assemble(ctx context.Context, tx *types.ParsedTransaction, req *pb.AssembleTransactionRequest) (*pb.AssembleTransactionResponse, error) { + amount := tx.Params.(*tktypes.HexUint256) + + resolvedSender := domain.FindVerifier(tx.Transaction.From, h.zeto.getAlgoZetoSnarkBJJ(), zetosignerapi.IDEN3_PUBKEY_BABYJUBJUB_COMPRESSED_0X, req.ResolvedVerifiers) + if resolvedSender == nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorResolveVerifier, tx.Transaction.From) + } + + useNullifiers := common.IsNullifiersToken(tx.DomainConfig.TokenName) + inputCoins, inputStates, _, remainder, err := h.zeto.prepareInputsForWithdraw(ctx, useNullifiers, req.StateQueryContext, resolvedSender.Verifier, amount) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorPrepTxInputs, err) + } + + outputCoin, outputState, err := h.zeto.prepareOutputForWithdraw(ctx, tktypes.MustParseHexUint256(remainder.Text(10)), resolvedSender) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorPrepTxOutputs, err) + } + + contractAddress, err := tktypes.ParseEthAddress(req.Transaction.ContractInfo.ContractAddress) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorDecodeContractAddress, err) + } + payloadBytes, err := h.formatProvingRequest(ctx, inputCoins, outputCoin, tx.DomainConfig.CircuitId, tx.DomainConfig.TokenName, req.StateQueryContext, contractAddress) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorFormatProvingReq, err) + } + + amountStr := amount.Int().Text(10) + return &pb.AssembleTransactionResponse{ + AssemblyResult: pb.AssembleTransactionResponse_OK, + AssembledTransaction: &pb.AssembledTransaction{ + InputStates: inputStates, + OutputStates: []*pb.NewState{outputState}, + DomainData: &amountStr, + }, + AttestationPlan: []*pb.AttestationRequest{ + { + Name: "sender", + AttestationType: pb.AttestationType_SIGN, + Algorithm: h.zeto.getAlgoZetoSnarkBJJ(), + VerifierType: zetosignerapi.IDEN3_PUBKEY_BABYJUBJUB_COMPRESSED_0X, + PayloadType: zetosignerapi.PAYLOAD_DOMAIN_ZETO_SNARK, + Payload: payloadBytes, + Parties: []string{tx.Transaction.From}, + }, + { + Name: "submitter", + AttestationType: pb.AttestationType_ENDORSE, + Algorithm: algorithms.ECDSA_SECP256K1, + VerifierType: verifiers.ETH_ADDRESS, + Parties: []string{tx.Transaction.From}, + }, + }, + }, nil +} + +func (h *withdrawHandler) Endorse(ctx context.Context, tx *types.ParsedTransaction, req *pb.EndorseTransactionRequest) (*pb.EndorseTransactionResponse, error) { + return &pb.EndorseTransactionResponse{ + EndorsementResult: pb.EndorseTransactionResponse_ENDORSER_SUBMIT, + }, nil +} + +func (h *withdrawHandler) Prepare(ctx context.Context, tx *types.ParsedTransaction, req *pb.PrepareTransactionRequest) (*pb.PrepareTransactionResponse, error) { + var proofRes corepb.ProvingResponse + result := domain.FindAttestation("sender", req.AttestationResult) + if result == nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorFindSenderAttestation) + } + if err := proto.Unmarshal(result.Payload, &proofRes); err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorUnmarshalProvingRes, err) + } + + inputSize := common.GetInputSize(len(req.InputStates)) + inputs := make([]string, inputSize) + for i := 0; i < inputSize; i++ { + if i < len(req.InputStates) { + state := req.InputStates[i] + coin, err := h.zeto.makeCoin(state.StateDataJson) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorParseInputStates, err) + } + hash, err := coin.Hash(ctx) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorHashInputState, err) + } + inputs[i] = hash.String() + } else { + inputs[i] = "0" + } + } + + outputCoin, err := h.zeto.makeCoin(req.OutputStates[0].StateDataJson) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorParseOutputStates, err) + } + hash, err := outputCoin.Hash(ctx) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorHashOutputState, err) + } + output := hash.String() + + data, err := encodeTransactionData(ctx, req.Transaction) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorEncodeTxData, err) + } + amount := tktypes.MustParseHexUint256(*req.DomainData) + params := map[string]any{ + "amount": amount.Int().Text(10), + "inputs": inputs, + "output": output, + "proof": encodeProof(proofRes.Proof), + "data": data, + } + if common.IsNullifiersToken(tx.DomainConfig.TokenName) { + delete(params, "inputs") + params["nullifiers"] = strings.Split(proofRes.PublicInputs["nullifiers"], ",") + params["root"] = proofRes.PublicInputs["root"] + } + withdrawFunction := getWithdrawABI(tx.DomainConfig.TokenName) + paramsJSON, err := json.Marshal(params) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorMarshalPrepedParams, err) + } + functionJSON, err := json.Marshal(withdrawFunction) + if err != nil { + return nil, err + } + + return &pb.PrepareTransactionResponse{ + Transaction: &pb.PreparedTransaction{ + FunctionAbiJson: string(functionJSON), + ParamsJson: string(paramsJSON), + RequiredSigner: &req.Transaction.From, // must be signed by the original sender + }, + }, nil +} + +func (h *withdrawHandler) formatProvingRequest(ctx context.Context, inputCoins []*types.ZetoCoin, outputCoin *types.ZetoCoin, circuitId, tokenName, stateQueryContext string, contractAddress *tktypes.EthAddress) ([]byte, error) { + inputSize := common.GetInputSize(len(inputCoins)) + inputCommitments := make([]string, inputSize) + inputValueInts := make([]uint64, inputSize) + inputSalts := make([]string, inputSize) + inputOwner := inputCoins[0].Owner.String() + for i := 0; i < inputSize; i++ { + if i < len(inputCoins) { + coin := inputCoins[i] + hash, err := coin.Hash(ctx) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorHashInputState, err) + } + inputCommitments[i] = hash.Int().Text(16) + inputValueInts[i] = coin.Amount.Int().Uint64() + inputSalts[i] = coin.Salt.Int().Text(16) + } else { + inputCommitments[i] = "0" + inputSalts[i] = "0" + } + } + + hash, err := outputCoin.Hash(ctx) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorHashOutputState, err) + } + outputCommitment := hash.Int().Text(16) + outputValueInt := outputCoin.Amount.Int().Uint64() + outputSalt := outputCoin.Salt.Int().Text(16) + outputOwner := outputCoin.Owner.String() + + var extras []byte + if common.IsNullifiersCircuit(circuitId) { + proofs, extrasObj, err := generateMerkleProofs(ctx, h.zeto, tokenName, stateQueryContext, contractAddress, inputCoins) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorGenerateMTP, err) + } + for i := len(proofs); i < inputSize; i++ { + extrasObj.MerkleProofs = append(extrasObj.MerkleProofs, &smt.Empty_Proof) + extrasObj.Enabled = append(extrasObj.Enabled, false) + } + protoExtras, err := proto.Marshal(extrasObj) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorMarshalExtraObj, err) + } + extras = protoExtras + } + + payload := &corepb.ProvingRequest{ + CircuitId: getCircuitId(tokenName), + Common: &corepb.ProvingRequestCommon{ + InputCommitments: inputCommitments, + InputValues: inputValueInts, + InputSalts: inputSalts, + InputOwner: inputOwner, + OutputCommitments: []string{outputCommitment}, + OutputValues: []uint64{outputValueInt}, + OutputSalts: []string{outputSalt}, + OutputOwners: []string{outputOwner}, + }, + } + + if extras != nil { + payload.Extras = extras + } + + return proto.Marshal(payload) +} + +func getCircuitId(tokenName string) string { + isNullifier := common.IsNullifiersToken(tokenName) + + if isNullifier { + return constants.CIRCUIT_WITHDRAW_NULLIFIER + } else { + return constants.CIRCUIT_WITHDRAW + } +} + +func getWithdrawABI(tokenName string) *abi.Entry { + withdrawFunction := withdrawABI + if common.IsNullifiersToken(tokenName) { + withdrawFunction = withdrawABI_nullifiers + } + return withdrawFunction +} diff --git a/domains/zeto/internal/zeto/handler_withdraw_test.go b/domains/zeto/internal/zeto/handler_withdraw_test.go new file mode 100644 index 000000000..a49661639 --- /dev/null +++ b/domains/zeto/internal/zeto/handler_withdraw_test.go @@ -0,0 +1,338 @@ +package zeto + +import ( + "context" + "errors" + "testing" + + "github.com/kaleido-io/paladin/domains/zeto/pkg/constants" + corepb "github.com/kaleido-io/paladin/domains/zeto/pkg/proto" + "github.com/kaleido-io/paladin/domains/zeto/pkg/types" + "github.com/kaleido-io/paladin/domains/zeto/pkg/zetosigner/zetosignerapi" + "github.com/kaleido-io/paladin/toolkit/pkg/prototk" + "github.com/kaleido-io/paladin/toolkit/pkg/tktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestWithdrawValidateParams(t *testing.T) { + h := &withdrawHandler{} + ctx := context.Background() + config := &types.DomainInstanceConfig{} + v, err := h.ValidateParams(ctx, config, "{\"amount\":100}") + require.NoError(t, err) + require.Equal(t, "0x64", v.(*tktypes.HexUint256).String()) + + _, err = h.ValidateParams(ctx, config, "bad json") + require.ErrorContains(t, err, "PD210106: Failed to decode the withdraw call.") + + _, err = h.ValidateParams(ctx, config, "{\"amount\":-100}") + require.ErrorContains(t, err, "PD210027: Parameter 'amount' must be greater than 0 (index=0)") +} + +func TestWithdrawInit(t *testing.T) { + h := withdrawHandler{ + zeto: &Zeto{ + name: "test1", + }, + } + ctx := context.Background() + tx := &types.ParsedTransaction{ + Transaction: &prototk.TransactionSpecification{ + From: "Alice", + }, + } + req := &prototk.InitTransactionRequest{} + res, err := h.Init(ctx, tx, req) + assert.NoError(t, err) + assert.Len(t, res.RequiredVerifiers, 1) + assert.Equal(t, "Alice", res.RequiredVerifiers[0].Lookup) + assert.Equal(t, zetosignerapi.IDEN3_PUBKEY_BABYJUBJUB_COMPRESSED_0X, res.RequiredVerifiers[0].VerifierType) + assert.Equal(t, zetosignerapi.AlgoDomainZetoSnarkBJJ("test1"), res.RequiredVerifiers[0].Algorithm) +} + +func TestWithdrawAssemble(t *testing.T) { + h := withdrawHandler{ + zeto: &Zeto{ + name: "test1", + coinSchema: &prototk.StateSchema{ + Id: "coin", + }, + merkleTreeRootSchema: &prototk.StateSchema{ + Id: "merkle_tree_root", + }, + merkleTreeNodeSchema: &prototk.StateSchema{ + Id: "merkle_tree_node", + }, + }, + } + ctx := context.Background() + txSpec := &prototk.TransactionSpecification{ + From: "Bob", + ContractInfo: &prototk.ContractInfo{ + ContractAddress: "bad address", + }, + } + tx := &types.ParsedTransaction{ + Params: tktypes.MustParseHexUint256("100"), + Transaction: txSpec, + DomainConfig: &types.DomainInstanceConfig{ + TokenName: "tokenContract1", + CircuitId: "circuit1", + }, + } + req := &prototk.AssembleTransactionRequest{ + ResolvedVerifiers: []*prototk.ResolvedVerifier{ + { + Lookup: "Alice", + Verifier: "0x19d2ee6b9770a4f8d7c3b7906bc7595684509166fa42d718d1d880b62bcb7922", + Algorithm: h.zeto.getAlgoZetoSnarkBJJ(), + VerifierType: zetosignerapi.IDEN3_PUBKEY_BABYJUBJUB_COMPRESSED_0X, + }, + }, + Transaction: txSpec, + } + _, err := h.Assemble(ctx, tx, req) + assert.EqualError(t, err, "PD210036: Failed to resolve verifier: Bob") + + req.ResolvedVerifiers = append(req.ResolvedVerifiers, &prototk.ResolvedVerifier{ + Lookup: "Bob", + Verifier: "0x1234567890123456789012345678901234567890", + Algorithm: h.zeto.getAlgoZetoSnarkBJJ(), + VerifierType: zetosignerapi.IDEN3_PUBKEY_BABYJUBJUB_COMPRESSED_0X, + }) + testCallbacks := &testDomainCallbacks{ + returnFunc: func() (*prototk.FindAvailableStatesResponse, error) { + return nil, errors.New("test error") + }, + } + h.zeto.Callbacks = testCallbacks + _, err = h.Assemble(ctx, tx, req) + assert.ErrorContains(t, err, "PD210039: Failed to prepare transaction inputs. PD210032: Failed to query the state store for available coins. test error") + + h.zeto.Callbacks = &testDomainCallbacks{ + returnFunc: func() (*prototk.FindAvailableStatesResponse, error) { + return &prototk.FindAvailableStatesResponse{ + States: []*prototk.StoredState{ + { + DataJson: "{\"salt\":\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"owner\":\"0x19d2ee6b9770a4f8d7c3b7906bc7595684509166fa42d718d1d880b62bcb7922\",\"amount\":\"0x0f\"}", + }, + }, + }, nil + }, + } + req.ResolvedVerifiers[1].Verifier = "bad key" + _, err = h.Assemble(ctx, tx, req) + require.ErrorContains(t, err, "PD210040: Failed to prepare transaction outputs. PD210037: Failed load owner public key.") + + req.ResolvedVerifiers[1].Verifier = "0x19d2ee6b9770a4f8d7c3b7906bc7595684509166fa42d718d1d880b62bcb7922" + _, err = h.Assemble(ctx, tx, req) + assert.ErrorContains(t, err, "PD210017: Failed to decode contract address.") + + txSpec.ContractInfo.ContractAddress = "0x1234567890123456789012345678901234567890" + _, err = h.Assemble(ctx, tx, req) + assert.ErrorContains(t, err, "PD210042: Failed to format proving request.") + + h.zeto.Callbacks = &testDomainCallbacks{ + returnFunc: func() (*prototk.FindAvailableStatesResponse, error) { + return &prototk.FindAvailableStatesResponse{ + States: []*prototk.StoredState{ + { + DataJson: "{\"salt\":\"0x042fac32983b19d76425cc54dd80e8a198f5d477c6a327cb286eb81a0c2b95ec\",\"owner\":\"0x19d2ee6b9770a4f8d7c3b7906bc7595684509166fa42d718d1d880b62bcb7922\",\"amount\":\"0x0f\"}", + }, + }, + }, nil + }, + } + res, err := h.Assemble(ctx, tx, req) + require.NoError(t, err) + assert.Equal(t, "100", *res.AssembledTransaction.DomainData) + + tx.DomainConfig.TokenName = constants.TOKEN_ANON_NULLIFIER + tx.DomainConfig.CircuitId = constants.CIRCUIT_ANON_NULLIFIER + called := 0 + h.zeto.Callbacks = &testDomainCallbacks{ + returnFunc: func() (*prototk.FindAvailableStatesResponse, error) { + var dataJson string + if called == 0 { + dataJson = "{\"salt\":\"0x13de02d64a5736a56b2d35d2a83dd60397ba70aae6f8347629f0960d4fee5d58\",\"owner\":\"0xc1d218cf8993f940e75eabd3fee23dadc4e89cd1de479f03a61e91727959281b\",\"amount\":\"0x65\"}" + } else if called == 1 { + dataJson = "{\"rootIndex\": \"0x28025a624a1e83687e84451d04190f081d79d470f9d50a7059508476be02d401\"}" + } else { + dataJson = "{\"index\":\"bad index\",\"leftChild\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"refKey\":\"0x89ea7fc1e5e9722566083823f288a45d6dc7ef30b68094f006530dfe9f5cf90f\",\"rightChild\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"type\":\"0x02\"}" + } + called++ + return &prototk.FindAvailableStatesResponse{ + States: []*prototk.StoredState{ + { + DataJson: dataJson, + }, + }, + }, nil + }, + } + res, err = h.Assemble(ctx, tx, req) + require.ErrorContains(t, err, "PD210042: Failed to format proving request. PD210052: Failed to generate merkle proofs.") + + called = 0 + h.zeto.Callbacks = &testDomainCallbacks{ + returnFunc: func() (*prototk.FindAvailableStatesResponse, error) { + var dataJson string + if called == 0 { + dataJson = "{\"salt\":\"0x13de02d64a5736a56b2d35d2a83dd60397ba70aae6f8347629f0960d4fee5d58\",\"owner\":\"0xc1d218cf8993f940e75eabd3fee23dadc4e89cd1de479f03a61e91727959281b\",\"amount\":\"0x65\"}" + } else if called == 1 { + dataJson = "{\"rootIndex\": \"0x28025a624a1e83687e84451d04190f081d79d470f9d50a7059508476be02d401\"}" + } else { + dataJson = "{\"index\":\"0xb6025832e11338c178467dda6472d74c15aac53d0781f51681df082840e2ca25\",\"leftChild\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"refKey\":\"0x89ea7fc1e5e9722566083823f288a45d6dc7ef30b68094f006530dfe9f5cf90f\",\"rightChild\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"type\":\"0x02\"}" + } + called++ + return &prototk.FindAvailableStatesResponse{ + States: []*prototk.StoredState{ + { + DataJson: dataJson, + }, + }, + }, nil + }, + } + res, err = h.Assemble(ctx, tx, req) + require.NoError(t, err) + assert.Equal(t, "100", *res.AssembledTransaction.DomainData) +} + +func TestWithdrawEndorse(t *testing.T) { + h := withdrawHandler{} + ctx := context.Background() + tx := &types.ParsedTransaction{ + Params: tktypes.MustParseHexUint256("100"), + Transaction: &prototk.TransactionSpecification{ + From: "Bob", + }, + } + + req := &prototk.EndorseTransactionRequest{} + res, err := h.Endorse(ctx, tx, req) + assert.NoError(t, err) + assert.Equal(t, prototk.EndorseTransactionResponse_ENDORSER_SUBMIT, res.EndorsementResult) +} + +func TestWithdrawPrepare(t *testing.T) { + z := &Zeto{ + name: "test1", + } + h := withdrawHandler{ + zeto: z, + } + txSpec := &prototk.TransactionSpecification{ + TransactionId: "bad hex", + From: "Bob", + } + tx := &types.ParsedTransaction{ + Params: tktypes.MustParseHexUint256("100"), + Transaction: txSpec, + DomainConfig: &types.DomainInstanceConfig{ + TokenName: constants.TOKEN_ANON_ENC, + }, + } + amountStr := "100" + req := &prototk.PrepareTransactionRequest{ + InputStates: []*prototk.EndorsableState{ + { + SchemaId: "coin", + StateDataJson: "bad json", + }, + }, + OutputStates: []*prototk.EndorsableState{ + { + SchemaId: "coin", + StateDataJson: "bad json", + }, + }, + Transaction: txSpec, + DomainData: &amountStr, + } + ctx := context.Background() + _, err := h.Prepare(ctx, tx, req) + assert.EqualError(t, err, "PD210043: Did not find 'sender' attestation") + + at := zetosignerapi.PAYLOAD_DOMAIN_ZETO_SNARK + req.AttestationResult = []*prototk.AttestationResult{ + { + Name: "sender", + AttestationType: prototk.AttestationType_ENDORSE, + PayloadType: &at, + Payload: []byte("bad payload"), + }, + } + _, err = h.Prepare(ctx, tx, req) + assert.ErrorContains(t, err, "PD210044: Failed to unmarshal proving response") + + proofReq := corepb.ProvingResponse{ + Proof: &corepb.SnarkProof{ + A: []string{"0x1234567890", "0x1234567890"}, + B: []*corepb.B_Item{ + { + Items: []string{"0x1234567890", "0x1234567890"}, + }, + { + Items: []string{"0x1234567890", "0x1234567890"}, + }, + }, + C: []string{"0x1234567890", "0x1234567890"}, + }, + PublicInputs: map[string]string{ + "encryptionNonce": "0x1234567890", + "encryptedValues": "0x1234567890,0x1234567890", + }, + } + payload, err := proto.Marshal(&proofReq) + assert.NoError(t, err) + req.AttestationResult[0].Payload = payload + _, err = h.Prepare(ctx, tx, req) + assert.ErrorContains(t, err, "PD210045: Failed to parse input states.") + + req.InputStates[0].StateDataJson = "{\"salt\":\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"owner\":\"0x7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025\",\"amount\":\"0x0f\",\"hash\":\"0x303eb034d22aacc5dff09647928d757017a35e64e696d48609a250a6505e5d5f\"}" + _, err = h.Prepare(ctx, tx, req) + assert.ErrorContains(t, err, "PD210046: Failed to create Poseidon hash for an input coin.") + + req.InputStates[0].StateDataJson = "{\"salt\":\"0x042fac32983b19d76425cc54dd80e8a198f5d477c6a327cb286eb81a0c2b95ec\",\"owner\":\"0x7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025\",\"amount\":\"0x0f\",\"hash\":\"0x303eb034d22aacc5dff09647928d757017a35e64e696d48609a250a6505e5d5f\"}" + _, err = h.Prepare(ctx, tx, req) + assert.ErrorContains(t, err, "PD210047: Failed to parse output states.") + + req.OutputStates[0].StateDataJson = "{\"salt\":\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"owner\":\"0x7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025\",\"amount\":\"0x0f\",\"hash\":\"0x303eb034d22aacc5dff09647928d757017a35e64e696d48609a250a6505e5d5f\"}" + _, err = h.Prepare(ctx, tx, req) + assert.ErrorContains(t, err, "PD210048: Failed to create Poseidon hash for an output coin.") + + req.OutputStates[0].StateDataJson = "{\"salt\":\"0x042fac32983b19d76425cc54dd80e8a198f5d477c6a327cb286eb81a0c2b95ec\",\"owner\":\"0x7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025\",\"amount\":\"0x0f\",\"hash\":\"0x303eb034d22aacc5dff09647928d757017a35e64e696d48609a250a6505e5d5f\"}" + _, err = h.Prepare(ctx, tx, req) + assert.ErrorContains(t, err, "PD210049: Failed to encode transaction data. PD210028: Failed to parse transaction id.") + + txSpec.TransactionId = "0x1234567890123456789012345678901234567890123456789012345678901234" + z.config = &types.DomainFactoryConfig{ + DomainContracts: types.DomainConfigContracts{ + Implementations: []*types.DomainContract{}, + }, + } + z.config.DomainContracts.Implementations = []*types.DomainContract{ + { + Name: constants.TOKEN_ANON_ENC, + }, + } + res, err := h.Prepare(ctx, tx, req) + assert.NoError(t, err) + assert.Equal(t, "{\"amount\":\"100\",\"data\":\"0x000100001234567890123456789012345678901234567890123456789012345678901234\",\"inputs\":[\"0x303eb034d22aacc5dff09647928d757017a35e64e696d48609a250a6505e5d5f\",\"0\"],\"output\":\"0x303eb034d22aacc5dff09647928d757017a35e64e696d48609a250a6505e5d5f\",\"proof\":{\"pA\":[\"0x1234567890\",\"0x1234567890\"],\"pB\":[[\"0x1234567890\",\"0x1234567890\"],[\"0x1234567890\",\"0x1234567890\"]],\"pC\":[\"0x1234567890\",\"0x1234567890\"]}}", res.Transaction.ParamsJson) + + tx.DomainConfig.TokenName = constants.TOKEN_ANON_NULLIFIER + tx.DomainConfig.CircuitId = constants.CIRCUIT_ANON_NULLIFIER + proofReq.PublicInputs = map[string]string{ + "nullifiers": "0x1234567890,0x1234567890", + "root": "0x1234567890", + } + payload, err = proto.Marshal(&proofReq) + require.NoError(t, err) + req.AttestationResult[0].Payload = payload + res, err = h.Prepare(ctx, tx, req) + assert.NoError(t, err) + assert.Equal(t, "{\"amount\":\"100\",\"data\":\"0x000100001234567890123456789012345678901234567890123456789012345678901234\",\"nullifiers\":[\"0x1234567890\",\"0x1234567890\"],\"output\":\"0x303eb034d22aacc5dff09647928d757017a35e64e696d48609a250a6505e5d5f\",\"proof\":{\"pA\":[\"0x1234567890\",\"0x1234567890\"],\"pB\":[[\"0x1234567890\",\"0x1234567890\"],[\"0x1234567890\",\"0x1234567890\"]],\"pC\":[\"0x1234567890\",\"0x1234567890\"]},\"root\":\"0x1234567890\"}", res.Transaction.ParamsJson) +} diff --git a/domains/zeto/internal/zeto/handlers.go b/domains/zeto/internal/zeto/handlers.go deleted file mode 100644 index 791756b97..000000000 --- a/domains/zeto/internal/zeto/handlers.go +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright © 2024 Kaleido, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package zeto - -import "github.com/kaleido-io/paladin/domains/zeto/pkg/types" - -func (z *Zeto) GetHandler(method string) types.DomainHandler { - switch method { - case "mint": - return &mintHandler{zeto: z} - case "transfer": - return &transferHandler{zeto: z} - case "lockProof": - return &lockHandler{zeto: z} - default: - return nil - } -} diff --git a/domains/zeto/internal/zeto/handlers_test.go b/domains/zeto/internal/zeto/handlers_test.go deleted file mode 100644 index 2fc7f6e93..000000000 --- a/domains/zeto/internal/zeto/handlers_test.go +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright © 2024 Kaleido, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package zeto - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetHandler(t *testing.T) { - z := &Zeto{ - name: "test1", - } - assert.NotNil(t, z.GetHandler("mint")) - assert.NotNil(t, z.GetHandler("transfer")) - assert.NotNil(t, z.GetHandler("lockProof")) - assert.Nil(t, z.GetHandler("bad")) -} diff --git a/domains/zeto/internal/zeto/signer/circuits_test.go b/domains/zeto/internal/zeto/signer/circuits_test.go index 9ddb62984..73d7214c7 100644 --- a/domains/zeto/internal/zeto/signer/circuits_test.go +++ b/domains/zeto/internal/zeto/signer/circuits_test.go @@ -22,6 +22,7 @@ import ( "path" "testing" + "github.com/iden3/go-rapidsnark/witness/v2" "github.com/kaleido-io/paladin/domains/zeto/pkg/zetosigner/zetosignerapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -38,6 +39,24 @@ func mockWASMModule() []byte { )`) } +func loadTestCircuit(t *testing.T) (witness.Calculator, []byte) { + tmpDir := t.TempDir() + err := os.Mkdir(path.Join(tmpDir, "test_js"), 0755) + require.NoError(t, err) + err = os.WriteFile(path.Join(tmpDir, "test_js", "test.wasm"), testWasm, 0644) + require.NoError(t, err) + err = os.WriteFile(path.Join(tmpDir, "test.zkey"), []byte("test"), 0644) + require.NoError(t, err) + + config := &zetosignerapi.SnarkProverConfig{} + config.CircuitsDir = tmpDir + config.ProvingKeysDir = tmpDir + + circuit, provingKey, err := loadCircuit(context.Background(), "test", config) + require.NoError(t, err) + return circuit, provingKey +} + func TestLoadCircuit(t *testing.T) { tmpDir := t.TempDir() err := os.Mkdir(path.Join(tmpDir, "test_js"), 0755) @@ -57,10 +76,7 @@ func TestLoadCircuit(t *testing.T) { assert.Nil(t, circuit) assert.Equal(t, []byte{}, provingKey) - err = os.WriteFile(path.Join(tmpDir, "test_js", "test.wasm"), testWasm, 0644) - require.NoError(t, err) - circuit, provingKey, err = loadCircuit(ctx, "test", config) - require.NoError(t, err) + circuit, provingKey = loadTestCircuit(t) assert.NotNil(t, circuit) assert.Equal(t, []byte("test"), provingKey) } diff --git a/domains/zeto/internal/zeto/signer/decoder.go b/domains/zeto/internal/zeto/signer/decoder.go index a0bebb5aa..9ae3b30bc 100644 --- a/domains/zeto/internal/zeto/signer/decoder.go +++ b/domains/zeto/internal/zeto/signer/decoder.go @@ -20,7 +20,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/kaleido-io/paladin/domains/zeto/internal/msgs" - "github.com/kaleido-io/paladin/domains/zeto/pkg/constants" + "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/common" pb "github.com/kaleido-io/paladin/domains/zeto/pkg/proto" "google.golang.org/protobuf/proto" ) @@ -32,7 +32,7 @@ func decodeProvingRequest(ctx context.Context, payload []byte) (*pb.ProvingReque if err != nil { return nil, nil, err } - if inputs.CircuitId == constants.CIRCUIT_ANON_ENC { + if common.IsEncryptionCircuit(inputs.CircuitId) { encExtras := pb.ProvingRequestExtras_Encryption{ EncryptionNonce: "", } @@ -43,7 +43,7 @@ func decodeProvingRequest(ctx context.Context, payload []byte) (*pb.ProvingReque } } return &inputs, &encExtras, nil - } else if inputs.CircuitId == constants.CIRCUIT_ANON_NULLIFIER { + } else if common.IsNullifiersCircuit(inputs.CircuitId) { var nullifierExtras pb.ProvingRequestExtras_Nullifiers err := proto.Unmarshal(inputs.Extras, &nullifierExtras) if err != nil { diff --git a/domains/zeto/internal/zeto/signer/inputs_assembler.go b/domains/zeto/internal/zeto/signer/inputs_assembler.go index 367f0664f..20619e60c 100644 --- a/domains/zeto/internal/zeto/signer/inputs_assembler.go +++ b/domains/zeto/internal/zeto/signer/inputs_assembler.go @@ -78,6 +78,76 @@ func assembleInputs_anon_enc(ctx context.Context, inputs *commonWitnessInputs, e } func assembleInputs_anon_nullifier(ctx context.Context, inputs *commonWitnessInputs, extras *pb.ProvingRequestExtras_Nullifiers, keyEntry *core.KeyEntry) (map[string]any, error) { + nullifiers, root, proofs, enabled, err := prepareInputsForNullifiers(ctx, inputs, extras, keyEntry) + if err != nil { + return nil, err + } + + witnessInputs := map[string]interface{}{ + "nullifiers": nullifiers, + "root": root, + "merkleProof": proofs, + "enabled": enabled, + "inputCommitments": inputs.inputCommitments, + "inputValues": inputs.inputValues, + "inputSalts": inputs.inputSalts, + "inputOwnerPrivateKey": keyEntry.PrivateKeyForZkp, + "outputCommitments": inputs.outputCommitments, + "outputValues": inputs.outputValues, + "outputSalts": inputs.outputSalts, + "outputOwnerPublicKeys": inputs.outputOwnerPublicKeys, + } + return witnessInputs, nil +} + +func assembleInputs_deposit(inputs *commonWitnessInputs) map[string]interface{} { + witnessInputs := map[string]interface{}{ + "outputCommitments": inputs.outputCommitments, + "outputValues": inputs.outputValues, + "outputSalts": inputs.outputSalts, + "outputOwnerPublicKeys": inputs.outputOwnerPublicKeys, + } + return witnessInputs +} + +func assembleInputs_withdraw(inputs *commonWitnessInputs, keyEntry *core.KeyEntry) map[string]interface{} { + witnessInputs := map[string]interface{}{ + "inputCommitments": inputs.inputCommitments, + "inputValues": inputs.inputValues, + "inputSalts": inputs.inputSalts, + "inputOwnerPrivateKey": keyEntry.PrivateKeyForZkp, + "outputCommitments": inputs.outputCommitments, + "outputValues": inputs.outputValues, + "outputSalts": inputs.outputSalts, + "outputOwnerPublicKeys": inputs.outputOwnerPublicKeys, + } + return witnessInputs +} + +func assembleInputs_withdraw_nullifier(ctx context.Context, inputs *commonWitnessInputs, extras *pb.ProvingRequestExtras_Nullifiers, keyEntry *core.KeyEntry) (map[string]interface{}, error) { + nullifiers, root, proofs, enabled, err := prepareInputsForNullifiers(ctx, inputs, extras, keyEntry) + if err != nil { + return nil, err + } + + witnessInputs := map[string]interface{}{ + "nullifiers": nullifiers, + "root": root, + "merkleProof": proofs, + "enabled": enabled, + "inputCommitments": inputs.inputCommitments, + "inputValues": inputs.inputValues, + "inputSalts": inputs.inputSalts, + "inputOwnerPrivateKey": keyEntry.PrivateKeyForZkp, + "outputCommitments": inputs.outputCommitments, + "outputValues": inputs.outputValues, + "outputSalts": inputs.outputSalts, + "outputOwnerPublicKeys": inputs.outputOwnerPublicKeys, + } + return witnessInputs, nil +} + +func prepareInputsForNullifiers(ctx context.Context, inputs *commonWitnessInputs, extras *pb.ProvingRequestExtras_Nullifiers, keyEntry *core.KeyEntry) ([]*big.Int, *big.Int, [][]*big.Int, []*big.Int, error) { // calculate the nullifiers for the input UTXOs nullifiers := make([]*big.Int, len(inputs.inputCommitments)) for i := 0; i < len(inputs.inputCommitments); i++ { @@ -88,13 +158,13 @@ func assembleInputs_anon_nullifier(ctx context.Context, inputs *commonWitnessInp } nullifier, err := CalculateNullifier(inputs.inputValues[i], inputs.inputSalts[i], keyEntry.PrivateKeyForZkp) if err != nil { - return nil, i18n.NewError(ctx, msgs.MsgErrorCalcNullifier, err) + return nil, nil, nil, nil, i18n.NewError(ctx, msgs.MsgErrorCalcNullifier, err) } nullifiers[i] = nullifier } root, ok := new(big.Int).SetString(extras.Root, 16) if !ok { - return nil, i18n.NewError(ctx, msgs.MsgErrorDecodeRootExtras) + return nil, nil, nil, nil, i18n.NewError(ctx, msgs.MsgErrorDecodeRootExtras) } var proofs [][]*big.Int for _, proof := range extras.MerkleProofs { @@ -102,7 +172,7 @@ func assembleInputs_anon_nullifier(ctx context.Context, inputs *commonWitnessInp for _, node := range proof.Nodes { n, ok := new(big.Int).SetString(node, 16) if !ok { - return nil, i18n.NewError(ctx, msgs.MsgErrorDecodeMTPNodeExtras) + return nil, nil, nil, nil, i18n.NewError(ctx, msgs.MsgErrorDecodeMTPNodeExtras) } mp = append(mp, n) } @@ -117,19 +187,5 @@ func assembleInputs_anon_nullifier(ctx context.Context, inputs *commonWitnessInp } } - witnessInputs := map[string]interface{}{ - "nullifiers": nullifiers, - "root": root, - "merkleProof": proofs, - "enabled": enabled, - "inputCommitments": inputs.inputCommitments, - "inputValues": inputs.inputValues, - "inputSalts": inputs.inputSalts, - "inputOwnerPrivateKey": keyEntry.PrivateKeyForZkp, - "outputCommitments": inputs.outputCommitments, - "outputValues": inputs.outputValues, - "outputSalts": inputs.outputSalts, - "outputOwnerPublicKeys": inputs.outputOwnerPublicKeys, - } - return witnessInputs, nil + return nullifiers, root, proofs, enabled, nil } diff --git a/domains/zeto/internal/zeto/signer/inputs_assembler_test.go b/domains/zeto/internal/zeto/signer/inputs_assembler_test.go index 187ffb8aa..d21b25bd9 100644 --- a/domains/zeto/internal/zeto/signer/inputs_assembler_test.go +++ b/domains/zeto/internal/zeto/signer/inputs_assembler_test.go @@ -122,3 +122,44 @@ func TestAssembleInputsAnonNullifier_fail(t *testing.T) { _, err = assembleInputs_anon_nullifier(ctx, &inputs, &extras, &key) assert.EqualError(t, err, "PD210079: Failed to calculate nullifier. inputs values not inside Finite Field") } + +func TestAssembleInputsDeposit(t *testing.T) { + inputs := commonWitnessInputs{outputCommitments: []*big.Int{big.NewInt(100)}} + result := assembleInputs_deposit(&inputs) + assert.Equal(t, "100", result["outputCommitments"].([]*big.Int)[0].Text(10)) +} + +func TestAssembleInputsWithdrawNullifier(t *testing.T) { + inputs := commonWitnessInputs{ + inputCommitments: []*big.Int{big.NewInt(100)}, + inputValues: []*big.Int{big.NewInt(100)}, + inputSalts: []*big.Int{big.NewInt(200)}, + outputCommitments: []*big.Int{big.NewInt(100)}, + outputValues: []*big.Int{big.NewInt(200)}, + outputSalts: []*big.Int{big.NewInt(300)}, + } + privKey, pubKey, zkpKey := newKeypair() + key := core.KeyEntry{ + PrivateKey: privKey, + PublicKey: pubKey, + PrivateKeyForZkp: zkpKey, + } + extras := proto.ProvingRequestExtras_Nullifiers{ + Root: "123", + MerkleProofs: []*proto.MerkleProof{ + { + Nodes: []string{"1", "2", "3"}, + }, + { + Nodes: []string{"0", "0", "0"}, + }, + }, + Enabled: []bool{true, false}, + } + ctx := context.Background() + privateInputs, err := assembleInputs_withdraw_nullifier(ctx, &inputs, &extras, &key) + assert.NoError(t, err) + assert.Equal(t, "123", privateInputs["root"].(*big.Int).Text(16)) + assert.Len(t, privateInputs["nullifiers"], 1) + assert.NotEqual(t, "0", privateInputs["nullifiers"].([]*big.Int)[0].Text(10)) +} diff --git a/domains/zeto/internal/zeto/signer/snark_prover.go b/domains/zeto/internal/zeto/signer/snark_prover.go index fc3c9ccae..bcb788fd5 100644 --- a/domains/zeto/internal/zeto/signer/snark_prover.go +++ b/domains/zeto/internal/zeto/signer/snark_prover.go @@ -205,15 +205,6 @@ func getCircuitId(inputs *pb.ProvingRequest) string { } func validateInputs(ctx context.Context, inputs *pb.ProvingRequestCommon) error { - if len(inputs.InputCommitments) == 0 { - return i18n.NewError(ctx, msgs.MsgErrorMissingInputCommitments) - } - if len(inputs.InputValues) == 0 { - return i18n.NewError(ctx, msgs.MsgErrorMissingInputValues) - } - if len(inputs.InputSalts) == 0 { - return i18n.NewError(ctx, msgs.MsgErrorMissingInputSalts) - } if len(inputs.InputCommitments) != len(inputs.InputValues) || len(inputs.InputCommitments) != len(inputs.InputSalts) { return i18n.NewError(ctx, msgs.MsgErrorInputsDiffLength) } @@ -256,6 +247,12 @@ func serializeProofResponse(circuitId string, proof *types.ZKProof) ([]byte, err case constants.CIRCUIT_ANON_NULLIFIER_BATCH: publicInputs["nullifiers"] = strings.Join(proof.PubSignals[:10], ",") publicInputs["root"] = proof.PubSignals[10] + case constants.CIRCUIT_WITHDRAW_NULLIFIER: + publicInputs["nullifiers"] = strings.Join(proof.PubSignals[1:3], ",") + publicInputs["root"] = proof.PubSignals[3] + case constants.CIRCUIT_WITHDRAW_NULLIFIER_BATCH: + publicInputs["nullifiers"] = strings.Join(proof.PubSignals[1:11], ",") + publicInputs["root"] = proof.PubSignals[11] } res := pb.ProvingResponse{ @@ -286,6 +283,15 @@ func calculateWitness(ctx context.Context, circuitId string, commonInputs *pb.Pr if err != nil { return nil, i18n.NewError(ctx, msgs.MsgErrorAssembleInputs, err) } + case constants.CIRCUIT_DEPOSIT: + witnessInputs = assembleInputs_deposit(inputs) + case constants.CIRCUIT_WITHDRAW, constants.CIRCUIT_WITHDRAW_BATCH: + witnessInputs = assembleInputs_withdraw(inputs, keyEntry) + case constants.CIRCUIT_WITHDRAW_NULLIFIER, constants.CIRCUIT_WITHDRAW_NULLIFIER_BATCH: + witnessInputs, err = assembleInputs_withdraw_nullifier(ctx, inputs, extras.(*pb.ProvingRequestExtras_Nullifiers), keyEntry) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgErrorAssembleInputs, err) + } } wtns, err := circuit.CalculateWTNSBin(witnessInputs, true) diff --git a/domains/zeto/internal/zeto/signer/snark_prover_test.go b/domains/zeto/internal/zeto/signer/snark_prover_test.go index cdc8af77f..e1f4e4dfc 100644 --- a/domains/zeto/internal/zeto/signer/snark_prover_test.go +++ b/domains/zeto/internal/zeto/signer/snark_prover_test.go @@ -24,6 +24,7 @@ import ( "time" "github.com/hyperledger-labs/zeto/go-sdk/pkg/crypto" + "github.com/hyperledger-labs/zeto/go-sdk/pkg/key-manager/core" "github.com/iden3/go-iden3-crypto/poseidon" "github.com/iden3/go-rapidsnark/types" "github.com/iden3/go-rapidsnark/witness/v2" @@ -265,42 +266,6 @@ func TestSnarkProveErrorInputs(t *testing.T) { payload, err := proto.Marshal(&req) require.NoError(t, err) _, err = prover.Sign(context.Background(), zetosignerapi.AlgoDomainZetoSnarkBJJ("zeto"), zetosignerapi.PAYLOAD_DOMAIN_ZETO_SNARK, alice.PrivateKey[:], payload) - assert.ErrorContains(t, err, "input commitments are required") - - req = pb.ProvingRequest{ - CircuitId: constants.CIRCUIT_ANON, - Common: &pb.ProvingRequestCommon{ - InputCommitments: []string{"input1", "input2"}, - }, - } - payload, err = proto.Marshal(&req) - require.NoError(t, err) - _, err = prover.Sign(context.Background(), zetosignerapi.AlgoDomainZetoSnarkBJJ("zeto"), zetosignerapi.PAYLOAD_DOMAIN_ZETO_SNARK, alice.PrivateKey[:], payload) - assert.ErrorContains(t, err, "input values are required") - - req = pb.ProvingRequest{ - CircuitId: constants.CIRCUIT_ANON, - Common: &pb.ProvingRequestCommon{ - InputCommitments: []string{"input1", "input2"}, - InputValues: []uint64{30, 40}, - }, - } - payload, err = proto.Marshal(&req) - require.NoError(t, err) - _, err = prover.Sign(context.Background(), zetosignerapi.AlgoDomainZetoSnarkBJJ("zeto"), zetosignerapi.PAYLOAD_DOMAIN_ZETO_SNARK, alice.PrivateKey[:], payload) - assert.ErrorContains(t, err, "input salts are required") - - req = pb.ProvingRequest{ - CircuitId: constants.CIRCUIT_ANON, - Common: &pb.ProvingRequestCommon{ - InputCommitments: []string{"input1", "input2"}, - InputValues: []uint64{30, 40}, - InputSalts: []string{"salt1", "salt2"}, - }, - } - payload, err = proto.Marshal(&req) - require.NoError(t, err) - _, err = prover.Sign(context.Background(), zetosignerapi.AlgoDomainZetoSnarkBJJ("zeto"), zetosignerapi.PAYLOAD_DOMAIN_ZETO_SNARK, alice.PrivateKey[:], payload) assert.ErrorContains(t, err, "output values are required") req = pb.ProvingRequest{ @@ -558,6 +523,19 @@ func TestSerializeProofResponse(t *testing.T) { bytes, err = serializeProofResponse(constants.CIRCUIT_ANON_NULLIFIER_BATCH, &snark) assert.NoError(t, err) assert.Equal(t, 84, len(bytes)) + + bytes, err = serializeProofResponse(constants.CIRCUIT_WITHDRAW_NULLIFIER, &snark) + assert.NoError(t, err) + assert.Equal(t, 66, len(bytes)) + + snark.PubSignals = []string{ + "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", + "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", + "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", + "1", "2", "3"} + bytes, err = serializeProofResponse(constants.CIRCUIT_WITHDRAW_NULLIFIER_BATCH, &snark) + assert.NoError(t, err) + assert.Equal(t, 85, len(bytes)) } func TestZKPProverInvalidAlgos(t *testing.T) { @@ -606,3 +584,65 @@ func TestGetCircuitId(t *testing.T) { circuitId = getCircuitId(inputs) assert.Equal(t, constants.CIRCUIT_ANON_ENC_BATCH, circuitId) } + +func TestCalculateWitness(t *testing.T) { + extras1 := &pb.ProvingRequestExtras_Encryption{ + EncryptionNonce: "bad number", + } + inputs := &pb.ProvingRequestCommon{ + InputCommitments: []string{"1234567890123456789012345678901234567890123456789012345678901234", "1234567890123456789012345678901234567890123456789012345678901234"}, + InputValues: []uint64{10, 20}, + InputSalts: []string{"1234567890123456789012345678901234567890123456789012345678901234", "1234567890123456789012345678901234567890123456789012345678901234"}, + InputOwner: "7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025", + OutputValues: []uint64{30, 0}, + OutputSalts: []string{"1234567890123456789012345678901234567890123456789012345678901234", "1234567890123456789012345678901234567890123456789012345678901234"}, + OutputOwners: []string{"7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025", "7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025"}, + } + ctx := context.Background() + _, err := calculateWitness(ctx, constants.CIRCUIT_ANON_ENC, inputs, extras1, nil, nil) + assert.EqualError(t, err, "PD210099: failed to assemble private inputs for witness calculation. PD210077: Failed to parse encryption nonce") + + extras2 := &pb.ProvingRequestExtras_Nullifiers{ + Root: "123456", + MerkleProofs: []*pb.MerkleProof{ + { + Nodes: []string{"1", "2", "3"}, + }, + { + Nodes: []string{"0", "0", "0"}, + }, + }, + Enabled: []bool{true, false}, + } + privKey, ok := new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) + require.True(t, ok) + keyEntry := &core.KeyEntry{ + PrivateKeyForZkp: privKey, + } + _, err = calculateWitness(ctx, constants.CIRCUIT_ANON_NULLIFIER, inputs, extras2, keyEntry, nil) + assert.EqualError(t, err, "PD210099: failed to assemble private inputs for witness calculation. PD210079: Failed to calculate nullifier. inputs values not inside Finite Field") + + inputs = &pb.ProvingRequestCommon{ + OutputValues: []uint64{30, 0}, + OutputSalts: []string{"1234567890123456789012345678901234567890123456789012345678901234", "1234567890123456789012345678901234567890123456789012345678901234"}, + OutputOwners: []string{"7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025", "7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025"}, + } + circuit, _ := loadTestCircuit(t) + _, err = calculateWitness(ctx, constants.CIRCUIT_DEPOSIT, inputs, nil, keyEntry, circuit) + assert.ErrorContains(t, err, "PD210100: failed to calculate the witness") + + inputs = &pb.ProvingRequestCommon{ + InputCommitments: []string{"1234567890123456789012345678901234567890123456789012345678901234", "1234567890123456789012345678901234567890123456789012345678901234"}, + InputValues: []uint64{10, 20}, + InputSalts: []string{"1234567890123456789012345678901234567890123456789012345678901234", "1234567890123456789012345678901234567890123456789012345678901234"}, + InputOwner: "7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025", + OutputValues: []uint64{30, 0}, + OutputSalts: []string{"1234567890123456789012345678901234567890123456789012345678901234", "1234567890123456789012345678901234567890123456789012345678901234"}, + OutputOwners: []string{"7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025", "7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025"}, + } + _, err = calculateWitness(ctx, constants.CIRCUIT_WITHDRAW, inputs, nil, keyEntry, circuit) + assert.ErrorContains(t, err, "PD210100: failed to calculate the witness") + + _, err = calculateWitness(ctx, constants.CIRCUIT_WITHDRAW_NULLIFIER, inputs, extras2, keyEntry, circuit) + assert.EqualError(t, err, "PD210099: failed to assemble private inputs for witness calculation. PD210079: Failed to calculate nullifier. inputs values not inside Finite Field") +} diff --git a/domains/zeto/internal/zeto/states.go b/domains/zeto/internal/zeto/states.go index 5e724653e..b7d49297c 100644 --- a/domains/zeto/internal/zeto/states.go +++ b/domains/zeto/internal/zeto/states.go @@ -19,6 +19,7 @@ import ( "context" "encoding/json" "math/big" + "math/rand/v2" "github.com/hyperledger-labs/zeto/go-sdk/pkg/crypto" "github.com/hyperledger/firefly-common/pkg/i18n" @@ -94,17 +95,25 @@ func (z *Zeto) makeNewState(ctx context.Context, useNullifiers bool, coin *types return newState, nil } -func (z *Zeto) prepareInputs(ctx context.Context, useNullifiers bool, stateQueryContext, senderKey string, params []*types.TransferParamEntry) ([]*types.ZetoCoin, []*pb.StateRef, *big.Int, *big.Int, error) { - var lastStateTimestamp int64 - total := big.NewInt(0) - stateRefs := []*pb.StateRef{} - coins := []*types.ZetoCoin{} - +func (z *Zeto) prepareInputsForTransfer(ctx context.Context, useNullifiers bool, stateQueryContext, senderKey string, params []*types.TransferParamEntry) ([]*types.ZetoCoin, []*pb.StateRef, *big.Int, *big.Int, error) { expectedTotal := big.NewInt(0) for _, param := range params { expectedTotal = expectedTotal.Add(expectedTotal, param.Amount.Int()) } + return z.buildInputsForExpectedTotal(ctx, useNullifiers, stateQueryContext, senderKey, expectedTotal) +} + +func (z *Zeto) prepareInputsForWithdraw(ctx context.Context, useNullifiers bool, stateQueryContext, senderKey string, amount *tktypes.HexUint256) ([]*types.ZetoCoin, []*pb.StateRef, *big.Int, *big.Int, error) { + expectedTotal := amount.Int() + return z.buildInputsForExpectedTotal(ctx, useNullifiers, stateQueryContext, senderKey, expectedTotal) +} + +func (z *Zeto) buildInputsForExpectedTotal(ctx context.Context, useNullifiers bool, stateQueryContext, senderKey string, expectedTotal *big.Int) ([]*types.ZetoCoin, []*pb.StateRef, *big.Int, *big.Int, error) { + var lastStateTimestamp int64 + total := big.NewInt(0) + stateRefs := []*pb.StateRef{} + coins := []*types.ZetoCoin{} for { queryBuilder := query.NewQueryBuilder(). Limit(10). @@ -144,7 +153,7 @@ func (z *Zeto) prepareInputs(ctx context.Context, useNullifiers bool, stateQuery } } -func (z *Zeto) prepareOutputs(ctx context.Context, useNullifiers bool, params []*types.TransferParamEntry, resolvedVerifiers []*pb.ResolvedVerifier) ([]*types.ZetoCoin, []*pb.NewState, error) { +func (z *Zeto) prepareOutputsForTransfer(ctx context.Context, useNullifiers bool, params []*types.TransferParamEntry, resolvedVerifiers []*pb.ResolvedVerifier) ([]*types.ZetoCoin, []*pb.NewState, error) { var coins []*types.ZetoCoin var newStates []*pb.NewState for _, param := range params { @@ -175,6 +184,63 @@ func (z *Zeto) prepareOutputs(ctx context.Context, useNullifiers bool, params [] return coins, newStates, nil } +func (z *Zeto) prepareOutputsForDeposit(ctx context.Context, useNullifiers bool, amount *tktypes.HexUint256, resolvedSender *pb.ResolvedVerifier) ([]*types.ZetoCoin, []*pb.NewState, error) { + var coins []*types.ZetoCoin + // the token implementation allows up to 2 output states, we will use one of them + // to bear the deposit amount, and set the other to value of 0. we randomize + // which one to use and which one to set to 0 + var newStates []*pb.NewState + amounts := make([]*tktypes.HexUint256, 2) + size := 2 + randomIdx := randomSlot(size) + amounts[randomIdx] = amount + amounts[size-randomIdx-1] = tktypes.MustParseHexUint256("0x0") + for _, amt := range amounts { + resolvedRecipient := resolvedSender + recipientKey, err := loadBabyJubKey([]byte(resolvedRecipient.Verifier)) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorLoadOwnerPubKey, err) + } + + salt := crypto.NewSalt() + compressedKeyStr := zetosigner.EncodeBabyJubJubPublicKey(recipientKey) + newCoin := &types.ZetoCoin{ + Salt: (*tktypes.HexUint256)(salt), + Owner: tktypes.MustParseHexBytes(compressedKeyStr), + Amount: amt, + } + + newState, err := z.makeNewState(ctx, useNullifiers, newCoin, resolvedRecipient.Lookup) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorCreateNewState, err) + } + coins = append(coins, newCoin) + newStates = append(newStates, newState) + } + return coins, newStates, nil +} + +func (z *Zeto) prepareOutputForWithdraw(ctx context.Context, amount *tktypes.HexUint256, resolvedRecipient *pb.ResolvedVerifier) (*types.ZetoCoin, *pb.NewState, error) { + recipientKey, err := loadBabyJubKey([]byte(resolvedRecipient.Verifier)) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorLoadOwnerPubKey, err) + } + + salt := crypto.NewSalt() + compressedKeyStr := zetosigner.EncodeBabyJubJubPublicKey(recipientKey) + newCoin := &types.ZetoCoin{ + Salt: (*tktypes.HexUint256)(salt), + Owner: tktypes.MustParseHexBytes(compressedKeyStr), + Amount: amount, + } + + newState, err := z.makeNewState(ctx, false, newCoin, resolvedRecipient.Lookup) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorCreateNewState, err) + } + return newCoin, newState, nil +} + func (z *Zeto) findAvailableStates(ctx context.Context, useNullifiers bool, stateQueryContext, query string) ([]*pb.StoredState, error) { req := &pb.FindAvailableStatesRequest{ StateQueryContext: stateQueryContext, @@ -188,3 +254,7 @@ func (z *Zeto) findAvailableStates(ctx context.Context, useNullifiers bool, stat } return res.States, nil } + +func randomSlot(size int) int { + return rand.IntN(size) +} diff --git a/domains/zeto/internal/zeto/states_test.go b/domains/zeto/internal/zeto/states_test.go index 00bd4f497..7703706fb 100644 --- a/domains/zeto/internal/zeto/states_test.go +++ b/domains/zeto/internal/zeto/states_test.go @@ -54,13 +54,13 @@ func TestPrepareInputs(t *testing.T) { stateQueryContext := "test" ctx := context.Background() - _, _, _, _, err := zeto.prepareInputs(ctx, false, stateQueryContext, "Alice", []*types.TransferParamEntry{{Amount: tktypes.Uint64ToUint256(100)}}) + _, _, _, _, err := zeto.prepareInputsForTransfer(ctx, false, stateQueryContext, "Alice", []*types.TransferParamEntry{{Amount: tktypes.Uint64ToUint256(100)}}) assert.EqualError(t, err, "PD210032: Failed to query the state store for available coins. test error") testCallbacks.returnFunc = func() (*prototk.FindAvailableStatesResponse, error) { return &prototk.FindAvailableStatesResponse{}, nil } - _, _, _, _, err = zeto.prepareInputs(ctx, false, stateQueryContext, "Alice", []*types.TransferParamEntry{{Amount: tktypes.Uint64ToUint256(100)}}) + _, _, _, _, err = zeto.prepareInputsForTransfer(ctx, false, stateQueryContext, "Alice", []*types.TransferParamEntry{{Amount: tktypes.Uint64ToUint256(100)}}) assert.EqualError(t, err, "PD210033: Insufficient funds (available=0)") testCallbacks.returnFunc = func() (*prototk.FindAvailableStatesResponse, error) { @@ -73,7 +73,7 @@ func TestPrepareInputs(t *testing.T) { }, }, nil } - _, _, _, _, err = zeto.prepareInputs(ctx, false, stateQueryContext, "Alice", []*types.TransferParamEntry{{Amount: tktypes.Uint64ToUint256(100)}}) + _, _, _, _, err = zeto.prepareInputsForTransfer(ctx, false, stateQueryContext, "Alice", []*types.TransferParamEntry{{Amount: tktypes.Uint64ToUint256(100)}}) assert.EqualError(t, err, "PD210034: Coin state-1 is invalid: invalid character 'b' looking for beginning of value") testCallbacks.returnFunc = func() (*prototk.FindAvailableStatesResponse, error) { @@ -92,6 +92,47 @@ func TestPrepareInputs(t *testing.T) { }, }, nil } - _, _, _, _, err = zeto.prepareInputs(ctx, false, stateQueryContext, "Alice", []*types.TransferParamEntry{{Amount: tktypes.Uint64ToUint256(200)}}) + _, _, _, _, err = zeto.prepareInputsForTransfer(ctx, false, stateQueryContext, "Alice", []*types.TransferParamEntry{{Amount: tktypes.Uint64ToUint256(200)}}) assert.EqualError(t, err, "PD210035: Need more than maximum number (10) of coins to fulfill the transfer amount total") + + _, _, _, _, err = zeto.prepareInputsForWithdraw(ctx, false, stateQueryContext, "Alice", tktypes.Uint64ToUint256(100)) + assert.NoError(t, err) +} + +func TestPrepareOutputs(t *testing.T) { + testCallbacks := &testDomainCallbacks{ + returnFunc: func() (*prototk.FindAvailableStatesResponse, error) { + return nil, errors.New("test error") + }, + } + zeto := &Zeto{ + name: "test1", + Callbacks: testCallbacks, + coinSchema: &prototk.StateSchema{ + Id: "coin", + }, + merkleTreeRootSchema: &prototk.StateSchema{ + Id: "merkle_tree_root", + }, + merkleTreeNodeSchema: &prototk.StateSchema{ + Id: "merkle_tree_node", + }, + } + + ctx := context.Background() + sender := &prototk.ResolvedVerifier{ + Lookup: "Alice", + Verifier: "7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025", + } + + _, _, err := zeto.prepareOutputsForDeposit(ctx, false, tktypes.Uint64ToUint256(100), sender) + assert.NoError(t, err) + + sender.Verifier = "bad key" + _, _, err = zeto.prepareOutputForWithdraw(ctx, tktypes.Uint64ToUint256(100), sender) + assert.ErrorContains(t, err, "PD210037: Failed load owner public key.") + + sender.Verifier = "7cdd539f3ed6c283494f47d8481f84308a6d7043087fb6711c9f1df04e2b8025" + _, _, err = zeto.prepareOutputForWithdraw(ctx, tktypes.Uint64ToUint256(100), sender) + assert.NoError(t, err) } diff --git a/domains/zeto/internal/zeto/utils.go b/domains/zeto/internal/zeto/utils.go index e143775c3..b642efe93 100644 --- a/domains/zeto/internal/zeto/utils.go +++ b/domains/zeto/internal/zeto/utils.go @@ -17,46 +17,21 @@ package zeto import ( "context" + "math/big" + "github.com/hyperledger-labs/zeto/go-sdk/pkg/sparse-merkle-tree/core" + "github.com/hyperledger-labs/zeto/go-sdk/pkg/sparse-merkle-tree/node" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/iden3/go-iden3-crypto/babyjub" "github.com/kaleido-io/paladin/domains/zeto/internal/msgs" - "github.com/kaleido-io/paladin/domains/zeto/pkg/constants" + "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/smt" + corepb "github.com/kaleido-io/paladin/domains/zeto/pkg/proto" "github.com/kaleido-io/paladin/domains/zeto/pkg/types" + "github.com/kaleido-io/paladin/domains/zeto/pkg/zetosigner" "github.com/kaleido-io/paladin/toolkit/pkg/prototk" "github.com/kaleido-io/paladin/toolkit/pkg/tktypes" ) -func isNullifiersCircuit(circuitId string) bool { - return circuitId == constants.CIRCUIT_ANON_NULLIFIER || circuitId == constants.CIRCUIT_ANON_NULLIFIER_BATCH -} - -func isNullifiersToken(tokenName string) bool { - return tokenName == constants.TOKEN_ANON_NULLIFIER -} - -func isEncryptionToken(tokenName string) bool { - return tokenName == constants.TOKEN_ANON_ENC -} - -// the Zeto implementations support two input/output sizes for the circuits: 2 and 10, -// if the input or output size is larger than 2, then the batch circuit is used with -// input/output size 10 -func getInputSize(sizeOfEndorsableStates int) int { - if sizeOfEndorsableStates <= 2 { - return 2 - } - return 10 -} - -func loadBabyJubKey(payload []byte) (*babyjub.PublicKey, error) { - var keyCompressed babyjub.PublicKeyComp - if err := keyCompressed.UnmarshalText(payload); err != nil { - return nil, err - } - return keyCompressed.Decompress() -} - func validateTransferParams(ctx context.Context, params []*types.TransferParamEntry) error { if len(params) == 0 { return i18n.NewError(ctx, msgs.MsgNoTransferParams) @@ -65,16 +40,23 @@ func validateTransferParams(ctx context.Context, params []*types.TransferParamEn if param.To == "" { return i18n.NewError(ctx, msgs.MsgNoParamTo, i) } - if param.Amount == nil { - return i18n.NewError(ctx, msgs.MsgNoParamAmount, i) - } - if param.Amount.Int().Sign() != 1 { - return i18n.NewError(ctx, msgs.MsgParamAmountGtZero, i) + if err := validateAmountParam(ctx, param.Amount, i); err != nil { + return err } } return nil } +func validateAmountParam(ctx context.Context, amount *tktypes.HexUint256, i int) error { + if amount == nil { + return i18n.NewError(ctx, msgs.MsgNoParamAmount, i) + } + if amount.Int().Sign() != 1 { + return i18n.NewError(ctx, msgs.MsgParamAmountGtZero, i) + } + return nil +} + func encodeTransactionData(ctx context.Context, transaction *prototk.TransactionSpecification) (tktypes.HexBytes, error) { txID, err := tktypes.ParseHexBytes(ctx, transaction.TransactionId) if err != nil { @@ -96,3 +78,93 @@ func decodeTransactionData(data tktypes.HexBytes) (txID tktypes.HexBytes) { } return data[4:] } + +func encodeProof(proof *corepb.SnarkProof) map[string]interface{} { + // Convert the proof json to the format that the Solidity verifier expects + return map[string]interface{}{ + "pA": []string{proof.A[0], proof.A[1]}, + "pB": [][]string{ + {proof.B[0].Items[1], proof.B[0].Items[0]}, + {proof.B[1].Items[1], proof.B[1].Items[0]}, + }, + "pC": []string{proof.C[0], proof.C[1]}, + } +} + +func loadBabyJubKey(payload []byte) (*babyjub.PublicKey, error) { + var keyCompressed babyjub.PublicKeyComp + if err := keyCompressed.UnmarshalText(payload); err != nil { + return nil, err + } + return keyCompressed.Decompress() +} + +func generateMerkleProofs(ctx context.Context, zeto *Zeto, tokenName string, stateQueryContext string, contractAddress *tktypes.EthAddress, inputCoins []*types.ZetoCoin) ([]core.Proof, *corepb.ProvingRequestExtras_Nullifiers, error) { + smtName := smt.MerkleTreeName(tokenName, contractAddress) + storage := smt.NewStatesStorage(zeto.Callbacks, smtName, stateQueryContext, zeto.merkleTreeRootSchema.Id, zeto.merkleTreeNodeSchema.Id) + mt, err := smt.NewSmt(storage) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorNewSmt, smtName, err) + } + // verify that the input UTXOs have been indexed by the Merkle tree DB + // and generate a merkle proof for each + var indexes []*big.Int + for _, coin := range inputCoins { + pubKey, err := zetosigner.DecodeBabyJubJubPublicKey(coin.Owner.String()) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorLoadOwnerPubKey, err) + } + idx := node.NewFungible(coin.Amount.Int(), pubKey, coin.Salt.Int()) + leaf, err := node.NewLeafNode(idx) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorNewLeafNode, err) + } + n, err := mt.GetNode(leaf.Ref()) + if err != nil { + // TODO: deal with when the node is not found in the DB tables for the tree + // e.g because the transaction event hasn't been processed yet + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorQueryLeafNode, leaf.Ref().Hex(), err) + } + hash, err := coin.Hash(ctx) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorHashInputState, err) + } + if n.Index().BigInt().Cmp(hash.Int()) != 0 { + expectedIndex, err := node.NewNodeIndexFromBigInt(hash.Int()) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorNewNodeIndex, err) + } + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorHashMismatch, leaf.Ref().Hex(), n.Index().BigInt().Text(16), n.Index().Hex(), hash.HexString0xPrefix(), expectedIndex.Hex()) + } + indexes = append(indexes, n.Index().BigInt()) + } + mtRoot := mt.Root() + proofs, _, err := mt.GenerateProofs(indexes, mtRoot) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorGenerateMTP, err) + } + var mps []*corepb.MerkleProof + var enabled []bool + for i, proof := range proofs { + cp, err := proof.ToCircomVerifierProof(indexes[i], indexes[i], mtRoot, smt.SMT_HEIGHT_UTXO) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgErrorConvertToCircomProof, err) + } + proofSiblings := make([]string, len(cp.Siblings)-1) + for i, s := range cp.Siblings[0 : len(cp.Siblings)-1] { + proofSiblings[i] = s.BigInt().Text(16) + } + p := corepb.MerkleProof{ + Nodes: proofSiblings, + } + mps = append(mps, &p) + enabled = append(enabled, true) + } + extrasObj := corepb.ProvingRequestExtras_Nullifiers{ + Root: mt.Root().BigInt().Text(16), + MerkleProofs: mps, + Enabled: enabled, + } + + return proofs, &extrasObj, nil +} diff --git a/domains/zeto/internal/zeto/zeto.go b/domains/zeto/internal/zeto/zeto.go index c654d1fd2..a73133abc 100644 --- a/domains/zeto/internal/zeto/zeto.go +++ b/domains/zeto/internal/zeto/zeto.go @@ -27,6 +27,7 @@ import ( "github.com/hyperledger/firefly-signer/pkg/ethtypes" "github.com/iden3/go-iden3-crypto/babyjub" "github.com/kaleido-io/paladin/domains/zeto/internal/msgs" + "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/common" "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/signer" "github.com/kaleido-io/paladin/domains/zeto/internal/zeto/smt" "github.com/kaleido-io/paladin/domains/zeto/pkg/types" @@ -53,6 +54,7 @@ type Zeto struct { mintSignature string transferSignature string transferWithEncSignature string + withdrawSignature string snarkProver signerapi.InMemorySigner } @@ -75,6 +77,13 @@ type TransferWithEncryptedValuesEvent struct { EncryptedValues []tktypes.HexUint256 `json:"encryptedValues"` } +type WithdrawEvent struct { + Amount tktypes.HexUint256 `json:"amount"` + Inputs []tktypes.HexUint256 `json:"inputs"` + Output tktypes.HexUint256 `json:"output"` + Data tktypes.HexBytes `json:"data"` +} + var factoryDeployABI = &abi.Entry{ Type: abi.Function, Name: "deploy", @@ -274,6 +283,23 @@ func (z *Zeto) PrepareTransaction(ctx context.Context, req *prototk.PrepareTrans return handler.Prepare(ctx, tx, req) } +func (z *Zeto) GetHandler(method string) types.DomainHandler { + switch method { + case "mint": + return &mintHandler{zeto: z} + case "transfer": + return &transferHandler{zeto: z} + case "lockProof": + return &lockHandler{zeto: z} + case "deposit": + return &depositHandler{zeto: z} + case "withdraw": + return &withdrawHandler{zeto: z} + default: + return nil + } +} + func (z *Zeto) decodeDomainConfig(ctx context.Context, domainConfig []byte) (*types.DomainInstanceConfig, error) { configValues, err := types.DomainInstanceConfigABI.DecodeABIDataCtx(ctx, domainConfig, 0) if err != nil { @@ -345,6 +371,8 @@ func (z *Zeto) registerEventSignatures(eventAbis abi.ABI) { z.transferSignature = event.SolString() case "UTXOTransferWithEncryptedValues": z.transferWithEncSignature = event.SolString() + case "UTXOWithdraw": + z.withdrawSignature = event.SolString() } } } @@ -366,7 +394,7 @@ func (z *Zeto) HandleEventBatch(ctx context.Context, req *prototk.HandleEventBat var smtName string var storage smt.StatesStorage var tree core.SparseMerkleTree - if isNullifiersCircuit(domainConfig.CircuitId) { + if common.IsNullifiersToken(domainConfig.TokenName) { smtName = smt.MerkleTreeName(domainConfig.TokenName, contractAddress) storage = smt.NewStatesStorage(z.Callbacks, smtName, req.StateQueryContext, z.merkleTreeRootSchema.Id, z.merkleTreeNodeSchema.Id) tree, err = smt.NewSmt(storage) @@ -383,6 +411,8 @@ func (z *Zeto) HandleEventBatch(ctx context.Context, req *prototk.HandleEventBat err = z.handleTransferEvent(ctx, tree, storage, ev, domainConfig.TokenName, &res) case z.transferWithEncSignature: err = z.handleTransferWithEncryptionEvent(ctx, tree, storage, ev, domainConfig.TokenName, &res) + case z.withdrawSignature: + err = z.handleWithdrawEvent(ctx, tree, storage, ev, domainConfig.TokenName, &res) } if err != nil { errors = append(errors, err.Error()) @@ -391,7 +421,7 @@ func (z *Zeto) HandleEventBatch(ctx context.Context, req *prototk.HandleEventBat if len(errors) > 0 { return &res, i18n.NewError(ctx, msgs.MsgErrorHandleEvents, formatErrors(errors)) } - if isNullifiersCircuit(domainConfig.CircuitId) { + if common.IsNullifiersToken(domainConfig.TokenName) { newStatesForSMT, err := storage.GetNewStates() if err != nil { return nil, i18n.NewError(ctx, msgs.MsgErrorGetNewSmtStates, smtName, err) diff --git a/domains/zeto/internal/zeto/zeto_test.go b/domains/zeto/internal/zeto/zeto_test.go index 6b1a11916..6475695fa 100644 --- a/domains/zeto/internal/zeto/zeto_test.go +++ b/domains/zeto/internal/zeto/zeto_test.go @@ -246,9 +246,9 @@ func TestInitTransaction(t *testing.T) { req.Transaction.FunctionParamsJson = "{\"mints\":[{\"to\":\"Alice\",\"amount\":\"10\"}]}" _, err = z.InitTransaction(context.Background(), req) - assert.EqualError(t, err, "PD210008: Failed to validate init transaction spec. PD210016: Unexpected signature for function 'mint': expected='function mint(mints[] memory mints) external { }; struct mints { string to; uint256 amount; }', actual=''") + assert.EqualError(t, err, "PD210008: Failed to validate init transaction spec. PD210016: Unexpected signature for function 'mint': expected='function mint(TransferParam[] memory mints) external { }; struct TransferParam { string to; uint256 amount; }', actual=''") - req.Transaction.FunctionSignature = "function mint(mints[] memory mints) external { }; struct mints { string to; uint256 amount; }" + req.Transaction.FunctionSignature = "function mint(TransferParam[] memory mints) external { }; struct TransferParam { string to; uint256 amount; }" _, err = z.InitTransaction(context.Background(), req) assert.EqualError(t, err, "PD210008: Failed to validate init transaction spec. PD210017: Failed to decode contract address. bad address - must be 20 bytes (len=0)") @@ -290,7 +290,7 @@ func TestAssembleTransaction(t *testing.T) { _, err := z.AssembleTransaction(context.Background(), req) assert.ErrorContains(t, err, "PD210009") - req.Transaction.FunctionSignature = "function mint(mints[] memory mints) external { }; struct mints { string to; uint256 amount; }" + req.Transaction.FunctionSignature = "function mint(TransferParam[] memory mints) external { }; struct TransferParam { string to; uint256 amount; }" conf := types.DomainInstanceConfig{ CircuitId: "circuit1", TokenName: "testToken1", @@ -316,7 +316,7 @@ func TestEndorseTransaction(t *testing.T) { assert.EqualError(t, err, "PD210010: Failed to validate endorse transaction spec. PD210012: Failed to unmarshal function abi json. unexpected end of JSON input") req.Transaction.FunctionAbiJson = "{\"type\":\"function\",\"name\":\"mint\"}" - req.Transaction.FunctionSignature = "function mint(mints[] memory mints) external { }; struct mints { string to; uint256 amount; }" + req.Transaction.FunctionSignature = "function mint(TransferParam[] memory mints) external { }; struct TransferParam { string to; uint256 amount; }" conf := types.DomainInstanceConfig{ CircuitId: "circuit1", TokenName: "testToken1", @@ -357,7 +357,7 @@ func TestPrepareTransaction(t *testing.T) { assert.EqualError(t, err, "PD210011: Failed to validate prepare transaction spec. PD210012: Failed to unmarshal function abi json. unexpected end of JSON input") req.Transaction.FunctionAbiJson = "{\"type\":\"function\",\"name\":\"mint\"}" - req.Transaction.FunctionSignature = "function mint(mints[] memory mints) external { }; struct mints { string to; uint256 amount; }" + req.Transaction.FunctionSignature = "function mint(TransferParam[] memory mints) external { }; struct TransferParam { string to; uint256 amount; }" conf := types.DomainInstanceConfig{ CircuitId: "circuit1", TokenName: "testToken1", @@ -479,6 +479,11 @@ func TestHandleEventBatch(t *testing.T) { assert.NoError(t, err) assert.Len(t, res4.TransactionsComplete, 1) assert.Len(t, res4.NewStates, 2) + + req.Events[0].SoliditySignature = "event UTXOWithdraw(uint256 amount, uint256[] inputs, uint256 output, address indexed submitter, bytes data)" + req.Events[0].DataJson = "{\"data\":\"0x0001000030e43028afbb41d6887444f4c2b4ed6d00000000000000000000000000000000\",\"output\":\"7980718117603030807695495350922077879582656644717071592146865497574198464253\",\"submitter\":\"0x74e71b05854ee819cb9397be01c82570a178d019\"}" + _, err = z.HandleEventBatch(ctx, req) + assert.NoError(t, err) } func TestGetVerifier(t *testing.T) { @@ -642,3 +647,27 @@ func findCoins(ctx context.Context, z *Zeto, useNullifiers bool, contractAddress } return coins, err } + +func TestGetHandler(t *testing.T) { + z := &Zeto{ + name: "test1", + } + assert.NotNil(t, z.GetHandler("mint")) + assert.NotNil(t, z.GetHandler("transfer")) + assert.NotNil(t, z.GetHandler("lockProof")) + assert.NotNil(t, z.GetHandler("deposit")) + assert.NotNil(t, z.GetHandler("withdraw")) + assert.Nil(t, z.GetHandler("bad")) +} + +func TestUnimplementedMethods(t *testing.T) { + z := &Zeto{} + _, err := z.InitCall(context.Background(), nil) + assert.ErrorContains(t, err, "PD210085: Not implemented") + + _, err = z.ExecCall(context.Background(), nil) + assert.ErrorContains(t, err, "PD210085: Not implemented") + + _, err = z.BuildReceipt(context.Background(), nil) + assert.ErrorContains(t, err, "PD210102: Not implemented") +} diff --git a/domains/zeto/pkg/constants/constants.go b/domains/zeto/pkg/constants/constants.go index a5795544a..452c0d4bb 100644 --- a/domains/zeto/pkg/constants/constants.go +++ b/domains/zeto/pkg/constants/constants.go @@ -17,14 +17,19 @@ package constants const ( // the base circuits support inputs and outputs up to size 2 - CIRCUIT_ANON = "anon" - CIRCUIT_ANON_ENC = "anon_enc" - CIRCUIT_ANON_NULLIFIER = "anon_nullifier" + CIRCUIT_ANON = "anon" + CIRCUIT_ANON_ENC = "anon_enc" + CIRCUIT_ANON_NULLIFIER = "anon_nullifier" + CIRCUIT_DEPOSIT = "check_hashes_value" + CIRCUIT_WITHDRAW = "check_inputs_outputs_value" + CIRCUIT_WITHDRAW_NULLIFIER = "check_nullifier_value" // the batch circuits support inputs and outputs from size 3 up to size 10 - CIRCUIT_ANON_BATCH = "anon_batch" - CIRCUIT_ANON_ENC_BATCH = "anon_enc_batch" - CIRCUIT_ANON_NULLIFIER_BATCH = "anon_nullifier_batch" + CIRCUIT_ANON_BATCH = "anon_batch" + CIRCUIT_ANON_ENC_BATCH = "anon_enc_batch" + CIRCUIT_ANON_NULLIFIER_BATCH = "anon_nullifier_batch" + CIRCUIT_WITHDRAW_BATCH = "check_inputs_outputs_value_batch" + CIRCUIT_WITHDRAW_NULLIFIER_BATCH = "check_nullifier_value_batch" TOKEN_ANON = "Zeto_Anon" TOKEN_ANON_ENC = "Zeto_AnonEnc" diff --git a/domains/zeto/pkg/proto/zeto.proto b/domains/zeto/pkg/proto/zeto.proto index d01232b28..464890977 100644 --- a/domains/zeto/pkg/proto/zeto.proto +++ b/domains/zeto/pkg/proto/zeto.proto @@ -29,9 +29,10 @@ message ProvingRequestCommon { repeated uint64 inputValues = 2; repeated string inputSalts = 3; string inputOwner = 4; - repeated uint64 outputValues = 5; - repeated string outputSalts = 6; - repeated string outputOwners = 7; + repeated string outputCommitments = 5; + repeated uint64 outputValues = 6; + repeated string outputSalts = 7; + repeated string outputOwners = 8; } message ProvingRequestExtras_Encryption { diff --git a/domains/zeto/pkg/types/abi.go b/domains/zeto/pkg/types/abi.go index 37e203f35..5894c405d 100644 --- a/domains/zeto/pkg/types/abi.go +++ b/domains/zeto/pkg/types/abi.go @@ -16,57 +16,16 @@ package types import ( - "github.com/hyperledger/firefly-signer/pkg/abi" + _ "embed" + + "github.com/kaleido-io/paladin/toolkit/pkg/solutils" "github.com/kaleido-io/paladin/toolkit/pkg/tktypes" ) -var ZetoABI = abi.ABI{ - { - Type: abi.Constructor, - Inputs: abi.ParameterArray{ - { - Name: "tokenName", - Type: "string", - }, - }, - }, - { - Name: "mint", - Type: abi.Function, - Inputs: abi.ParameterArray{ - { - Name: "mints", - Type: "tuple[]", - Components: abi.ParameterArray{ - {Name: "to", Type: "string"}, - {Name: "amount", Type: "uint256"}, - }, - }, - }, - }, - { - Name: "transfer", - Type: abi.Function, - Inputs: abi.ParameterArray{ - { - Name: "transfers", - Type: "tuple[]", - Components: abi.ParameterArray{ - {Name: "to", Type: "string"}, - {Name: "amount", Type: "uint256"}, - }, - }, - }, - }, - { - Name: "lockProof", - Type: abi.Function, - Inputs: abi.ParameterArray{ - {Name: "delegate", Type: "address"}, - {Name: "call", Type: "bytes"}, // assumed to be an encoded "transfer" - }, - }, -} +//go:embed abis/IZetoPrivate.json +var zetoPrivateJSON []byte + +var ZetoABI = solutils.MustParseBuildABI(zetoPrivateJSON) type InitializerParams struct { TokenName string `json:"tokenName"` @@ -97,3 +56,11 @@ type LockParams struct { Delegate *tktypes.EthAddress `json:"delegate"` Call tktypes.HexBytes `json:"call"` } + +type DepositParams struct { + Amount *tktypes.HexUint256 `json:"amount"` +} + +type WithdrawParams struct { + Amount *tktypes.HexUint256 `json:"amount"` +} diff --git a/example/zeto/.gitignore b/example/zeto/.gitignore index b38db2f29..4c79bef56 100644 --- a/example/zeto/.gitignore +++ b/example/zeto/.gitignore @@ -1,2 +1,3 @@ node_modules/ build/ +src/abis/*.json \ No newline at end of file diff --git a/example/zeto/build.gradle b/example/zeto/build.gradle index 831c83318..792e5e1c2 100644 --- a/example/zeto/build.gradle +++ b/example/zeto/build.gradle @@ -25,6 +25,17 @@ dependencies { buildSDK project(path: ':sdk:typescript', configuration: 'buildSDK') } +task copyContracts(type: Copy, dependsOn: [":domains:zeto:extractZetoArtifacts"]) { + from fileTree("../../domains/zeto/zkp/artifacts/contracts") { + include '**/SampleERC20.json' + } + into './src/abis' + + // Flatten all paths into the destination folder + eachFile { path = name } + includeEmptyDirs = false +} + task install(type: Exec) { executable 'npm' args 'install' @@ -35,7 +46,7 @@ task install(type: Exec) { outputs.dir('node_modules') } -task build(type: Exec, dependsOn: [install]) { +task build(type: Exec, dependsOn: [install, copyContracts]) { executable 'npm' args 'run' args 'build' diff --git a/example/zeto/package-lock.json b/example/zeto/package-lock.json index d60093b3a..6e3b7971d 100644 --- a/example/zeto/package-lock.json +++ b/example/zeto/package-lock.json @@ -33,253 +33,10 @@ "typescript": "^5.6.3" } }, - "../../sdk/typescript/node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "license": "MIT" - }, - "../../sdk/typescript/node_modules/@noble/curves": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "../../sdk/typescript/node_modules/@noble/hashes": { - "version": "1.3.2", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "../../sdk/typescript/node_modules/@types/node": { - "version": "22.9.0", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.8" - } - }, - "../../sdk/typescript/node_modules/aes-js": { - "version": "4.0.0-beta.5", - "license": "MIT" - }, - "../../sdk/typescript/node_modules/asynckit": { - "version": "0.4.0", - "license": "MIT" - }, - "../../sdk/typescript/node_modules/axios": { - "version": "1.7.7", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "../../sdk/typescript/node_modules/combined-stream": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "../../sdk/typescript/node_modules/copy-file": { - "version": "11.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.11", - "p-event": "^6.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../../sdk/typescript/node_modules/delayed-stream": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "../../sdk/typescript/node_modules/ethers": { - "version": "6.13.4", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "1.10.1", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.2", - "@types/node": "22.7.5", - "aes-js": "4.0.0-beta.5", - "tslib": "2.7.0", - "ws": "8.17.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../../sdk/typescript/node_modules/ethers/node_modules/@types/node": { - "version": "22.7.5", - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "../../sdk/typescript/node_modules/follow-redirects": { - "version": "1.15.9", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "../../sdk/typescript/node_modules/form-data": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "../../sdk/typescript/node_modules/graceful-fs": { - "version": "4.2.11", - "dev": true, - "license": "ISC" - }, - "../../sdk/typescript/node_modules/mime-db": { - "version": "1.52.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "../../sdk/typescript/node_modules/mime-types": { - "version": "2.1.35", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "../../sdk/typescript/node_modules/p-event": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "p-timeout": "^6.1.2" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../../sdk/typescript/node_modules/p-timeout": { - "version": "6.1.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../../sdk/typescript/node_modules/proxy-from-env": { - "version": "1.1.0", - "license": "MIT" - }, - "../../sdk/typescript/node_modules/tslib": { - "version": "2.7.0", - "license": "0BSD" - }, - "../../sdk/typescript/node_modules/typescript": { - "version": "5.6.3", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "../../sdk/typescript/node_modules/undici-types": { - "version": "6.19.8", - "license": "MIT" - }, - "../../sdk/typescript/node_modules/uuid": { - "version": "11.0.2", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "../../sdk/typescript/node_modules/ws": { - "version": "8.17.1", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "license": "MIT", "dependencies": { @@ -291,6 +48,8 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -299,11 +58,15 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -317,34 +80,46 @@ }, "node_modules/@tsconfig/node10": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "22.9.0", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/acorn": { "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", "bin": { @@ -356,6 +131,8 @@ }, "node_modules/acorn-walk": { "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, "license": "MIT", "dependencies": { @@ -367,11 +144,15 @@ }, "node_modules/arg": { "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true, "license": "MIT" }, "node_modules/copy-file": { "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-file/-/copy-file-11.0.0.tgz", + "integrity": "sha512-mFsNh/DIANLqFt5VHZoGirdg7bK5+oTWlhnGu6tgRhzBlnEKWaPX2xrFaLltii/6rmhqFMJqffUgknuRdpYlHw==", "dev": true, "license": "MIT", "dependencies": { @@ -387,11 +168,15 @@ }, "node_modules/create-require": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true, "license": "MIT" }, "node_modules/diff": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -400,16 +185,22 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, "node_modules/make-error": { "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true, "license": "ISC" }, "node_modules/p-event": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", "dev": true, "license": "MIT", "dependencies": { @@ -424,6 +215,8 @@ }, "node_modules/p-timeout": { "version": "6.1.3", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.3.tgz", + "integrity": "sha512-UJUyfKbwvr/uZSV6btANfb+0t/mOhKV/KXcCUTp8FcQI+v/0d+wXqH4htrW0E4rR6WiEO/EPvUFiV9D5OI4vlw==", "dev": true, "license": "MIT", "engines": { @@ -435,6 +228,8 @@ }, "node_modules/ts-node": { "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -476,7 +271,9 @@ } }, "node_modules/typescript": { - "version": "5.6.3", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -488,17 +285,23 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true, "license": "MIT" }, "node_modules/yn": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, "license": "MIT", "engines": { diff --git a/example/zeto/src/index.ts b/example/zeto/src/index.ts index 1f65feae4..47d7746de 100644 --- a/example/zeto/src/index.ts +++ b/example/zeto/src/index.ts @@ -1,6 +1,9 @@ import PaladinClient, { ZetoFactory, + TransactionType, + PaladinVerifier } from "@lfdecentralizedtrust-labs/paladin-sdk"; +import erc20Abi from "./abis/SampleERC20.json"; import { checkDeploy, checkReceipt } from "./util"; const logger = console; @@ -21,16 +24,17 @@ async function main(): Promise { const [bank2] = paladin3.getVerifiers("bank2@node2"); // Deploy a Zeto token to represent cash (CBDC) - logger.log("Deploying Zeto CBDC token..."); + logger.log("Use case #1: Privacy-preserving CBDC token, using private minting..."); + logger.log("- Deploying Zeto token..."); const zetoFactory = new ZetoFactory(paladin3, "zeto"); - const zetoCBDC = await zetoFactory.newZeto(cbdcIssuer, { + const zetoCBDC1 = await zetoFactory.newZeto(cbdcIssuer, { tokenName: "Zeto_AnonNullifier", }); - if (!checkDeploy(zetoCBDC)) return false; + if (!checkDeploy(zetoCBDC1)) return false; // Issue some cash - logger.log("Issuing CBDC to bank1 and bank2 ..."); - let receipt = await zetoCBDC.mint(cbdcIssuer, { + logger.log("- Issuing CBDC to bank1 and bank2 with private minting..."); + let receipt = await zetoCBDC1.mint(cbdcIssuer, { mints: [ { to: bank1, @@ -46,9 +50,9 @@ async function main(): Promise { // Transfer some cash from bank1 to bank2 logger.log( - "Bank1 transferring CBDC to bank2 to pay for some asset trades ..." + "- Bank1 transferring CBDC to bank2 to pay for some asset trades ..." ); - receipt = await zetoCBDC.using(paladin1).transfer(bank1, { + receipt = await zetoCBDC1.using(paladin1).transfer(bank1, { transfers: [ { to: bank2, @@ -57,10 +61,113 @@ async function main(): Promise { ], }); if (!checkReceipt(receipt)) return false; + logger.log("\nUse case #1 complete!\n"); + + logger.log("Use case #2: Privacy-preserving CBDC token, using public minting of an ERC20 token..."); + logger.log("- Deploying Zeto token..."); + const zetoCBDC2 = await zetoFactory.newZeto(cbdcIssuer, { + tokenName: "Zeto_AnonNullifier", + }); + if (!checkDeploy(zetoCBDC2)) return false; + + logger.log("- Deploying ERC20 token to manage the CBDC supply publicly..."); + const erc20Address = await deployERC20(paladin3, cbdcIssuer); + logger.log(` ERC20 deployed at: ${erc20Address}`); + + logger.log("- Setting ERC20 to the Zeto token contract ..."); + const result2 = await zetoCBDC2.setERC20(cbdcIssuer, { + erc20: erc20Address as string + }); + if (!checkReceipt(result2)) return false; + + logger.log("- Issuing CBDC to bank1 with public minting in ERC20..."); + await mintERC20(paladin3, cbdcIssuer, bank1, erc20Address!, 100000); + + logger.log("- Bank1 approve ERC20 balance for the Zeto token contract as spender, to prepare for deposit..."); + await approveERC20(paladin1, bank1, zetoCBDC2.address, erc20Address!, 10000); + + logger.log("- Bank1 deposit ERC20 balance to Zeto ..."); + const result4 = await zetoCBDC2.using(paladin1).deposit(bank1, { + amount: 10000 + }); + if (!checkReceipt(result4)) return false; + + // Transfer some cash from bank1 to bank2 + logger.log("- Bank1 transferring CBDC to bank2 to pay for some asset trades ..."); + receipt = await zetoCBDC2.using(paladin1).transfer(bank1, { + transfers: [ + { + to: bank2, + amount: 1000, + }, + ] + }); + if (!checkReceipt(receipt)) return false; + + logger.log("- Bank1 withdraws Zeto back to ERC20 balance ..."); + const result5 = await zetoCBDC2.using(paladin1).withdraw(bank1, { + amount: 1000 + }); + if (!checkReceipt(result5)) return false; + + logger.log("\nUse case #2 complete!"); return true; } +async function deployERC20(paladin: PaladinClient, cbdcIssuer: PaladinVerifier): Promise { + const txId1 = await paladin3.sendTransaction({ + type: TransactionType.PUBLIC, + from: cbdcIssuer.lookup, + data: { + "initialOwner": await cbdcIssuer.address(), + }, + function: "", + abi: erc20Abi.abi, + bytecode: erc20Abi.bytecode, + }); + const result1 = await paladin.pollForReceipt(txId1, 5000); + if (!checkReceipt(result1)) { + throw new Error("Failed to deploy ERC20 token"); + }; + const erc20Address = result1.contractAddress; + return erc20Address; +} + +async function mintERC20(paladin: PaladinClient, cbdcIssuer: PaladinVerifier, bank1: PaladinVerifier, erc20Address: string, amount: number): Promise { + const txId2 = await paladin.sendTransaction({ + type: TransactionType.PUBLIC, + from: cbdcIssuer.lookup, + to: erc20Address, + data: { + "amount": amount, + "to": await bank1.address(), + }, + function: "mint", + abi: erc20Abi.abi, + }); + const result3 = await paladin.pollForReceipt(txId2, 5000); + if (!checkReceipt(result3)) { + throw new Error("Failed to mint ERC20 tokens to bank1"); + } +} + +async function approveERC20(paladin: PaladinClient, from: PaladinVerifier, spender: string, erc20Address: string, amount: number): Promise { + // first approve the Zeto contract to draw the amount from our balance in the ERC20 + const txID1 = await paladin.sendTransaction({ + type: TransactionType.PUBLIC, + abi: erc20Abi.abi, + function: "approve", + to: erc20Address, + from: from.lookup, + data: { value: amount, spender }, + }); + const result1 = await paladin.pollForReceipt(txID1, 5000); + if (!checkReceipt(result1)) { + throw new Error("Failed to approve transfer"); + } +} + if (require.main === module) { main() .then((success: boolean) => { diff --git a/operator/config/samples/core_v1alpha1_transactioninvoke_zeto_register_anon_nullifier.yaml b/operator/config/samples/core_v1alpha1_transactioninvoke_zeto_register_anon_nullifier.yaml index f33af4c69..4b1dfabab 100644 --- a/operator/config/samples/core_v1alpha1_transactioninvoke_zeto_register_anon_nullifier.yaml +++ b/operator/config/samples/core_v1alpha1_transactioninvoke_zeto_register_anon_nullifier.yaml @@ -14,8 +14,8 @@ spec: - "zeto-factory" - "zeto-impl-anon-nullifier" - "zeto-g16-check-hashes-value" - - "zeto-g16-check-inputs-outputs" - - "zeto-g16-check-inputs-outputs-batch" + - "zeto-g16-check-nullifier" + - "zeto-g16-check-nullifier-batch" - "zeto-g16-verifier-anon-nullifier" - "zeto-g16-verifier-anon-nullifier-batch" function: registerImplementation @@ -25,8 +25,8 @@ spec: "implementation": { "implementation": "{{ index .status.resolvedContractAddresses "zeto-impl-anon-nullifier" }}", "depositVerifier": "{{ index .status.resolvedContractAddresses "zeto-g16-check-hashes-value" }}", - "withdrawVerifier": "{{ index .status.resolvedContractAddresses "zeto-g16-check-inputs-outputs" }}", - "batchWithdrawVerifier": "{{ index .status.resolvedContractAddresses "zeto-g16-check-inputs-outputs-batch" }}", + "withdrawVerifier": "{{ index .status.resolvedContractAddresses "zeto-g16-check-nullifier" }}", + "batchWithdrawVerifier": "{{ index .status.resolvedContractAddresses "zeto-g16-check-nullifier-batch" }}", "verifier": "{{ index .status.resolvedContractAddresses "zeto-g16-verifier-anon-nullifier" }}", "batchVerifier": "{{ index .status.resolvedContractAddresses "zeto-g16-verifier-anon-nullifier-batch" }}" } diff --git a/operator/config/samples/kustomization.yaml b/operator/config/samples/kustomization.yaml index c18cedf49..13f488780 100644 --- a/operator/config/samples/kustomization.yaml +++ b/operator/config/samples/kustomization.yaml @@ -25,6 +25,8 @@ resources: - core_v1alpha1_smartcontractdeployment_zeto_factory.yaml - core_v1alpha1_smartcontractdeployment_zeto_g16_check_inputs_outputs.yaml - core_v1alpha1_smartcontractdeployment_zeto_g16_check_inputs_outputs_batch.yaml +- core_v1alpha1_smartcontractdeployment_zeto_g16_check_nullifier.yaml +- core_v1alpha1_smartcontractdeployment_zeto_g16_check_nullifier_batch.yaml - core_v1alpha1_smartcontractdeployment_zeto_g16_check_hashes_value.yaml - core_v1alpha1_smartcontractdeployment_zeto_g16_verifier_anon.yaml - core_v1alpha1_smartcontractdeployment_zeto_g16_verifier_anon_batch.yaml diff --git a/operator/contractpkg/contract_map.json b/operator/contractpkg/contract_map.json index d1465a10e..e11fcefac 100644 --- a/operator/contractpkg/contract_map.json +++ b/operator/contractpkg/contract_map.json @@ -35,6 +35,12 @@ "zeto_g16_check_inputs_outputs_batch": { "filename": "test/e2e/abis/zeto/Groth16Verifier_CheckInputsOutputsValueBatch.json" }, + "zeto_g16_check_nullifier": { + "filename": "test/e2e/abis/zeto/Groth16Verifier_CheckNullifierValue.json" + }, + "zeto_g16_check_nullifier_batch": { + "filename": "test/e2e/abis/zeto/Groth16Verifier_CheckNullifierValueBatch.json" + }, "zeto_poseidon_unit2l": { "filename": "test/e2e/abis/zeto/Poseidon2.json" }, diff --git a/sdk/typescript/scripts/abi.mjs b/sdk/typescript/scripts/abi.mjs index a0a481f85..9dc579515 100644 --- a/sdk/typescript/scripts/abi.mjs +++ b/sdk/typescript/scripts/abi.mjs @@ -9,3 +9,8 @@ await copyFile( "../../solidity/artifacts/contracts/domains/interfaces/INotoPrivate.sol/INotoPrivate.json", "src/domains/abis/INotoPrivate.json" ); + +await copyFile( + "../../solidity/artifacts/contracts/domains/interfaces/IZetoPrivate.sol/IZetoPrivate.json", + "src/domains/abis/IZetoPrivate.json" +); diff --git a/sdk/typescript/src/domains/zeto.ts b/sdk/typescript/src/domains/zeto.ts index c85bc16b0..7b3117534 100644 --- a/sdk/typescript/src/domains/zeto.ts +++ b/sdk/typescript/src/domains/zeto.ts @@ -1,61 +1,15 @@ import { TransactionType } from "../interfaces"; import PaladinClient from "../paladin"; import { PaladinVerifier } from "../verifier"; +import * as zetoPrivateJSON from "./abis/IZetoPrivate.json"; -const DEFAULT_POLL_TIMEOUT = 10000; +const POLL_TIMEOUT_MS = 10000; export interface ZetoOptions { pollTimeout?: number; } -const zetoPrivateAbi = [ - { - name: "mint", - type: "function", - inputs: [ - { - name: "mints", - type: "tuple[]", - components: [ - { - name: "to", - type: "string", - internalType: "string", - }, - { - name: "amount", - type: "uint256", - internalType: "uint256", - }, - ], - }, - ], - outputs: [], - }, - { - type: "function", - name: "transfer", - inputs: [ - { - name: "transfers", - type: "tuple[]", - components: [ - { - name: "to", - type: "string", - internalType: "string", - }, - { - name: "amount", - type: "uint256", - internalType: "uint256", - }, - ], - }, - ], - outputs: [], - }, -]; +const zetoAbi = zetoPrivateJSON.abi; export const zetoConstructorABI = { type: "constructor", @@ -74,11 +28,23 @@ export interface ZetoTransferParams { transfers: ZetoTransfer[]; } +export interface ZetoSetERC20Params { + erc20: string; +} + export interface ZetoTransfer { to: PaladinVerifier; amount: string | number; } +export interface ZetoDepositParams { + amount: string | number; +} + +export interface ZetoWithdrawParams { + amount: string | number; +} + export class ZetoFactory { private options: Required; @@ -88,7 +54,7 @@ export class ZetoFactory { options?: ZetoOptions ) { this.options = { - pollTimeout: DEFAULT_POLL_TIMEOUT, + pollTimeout: POLL_TIMEOUT_MS, ...options, }; } @@ -118,6 +84,7 @@ export class ZetoFactory { export class ZetoInstance { private options: Required; + private erc20?: string; constructor( private paladin: PaladinClient, @@ -125,25 +92,28 @@ export class ZetoInstance { options?: ZetoOptions ) { this.options = { - pollTimeout: DEFAULT_POLL_TIMEOUT, + pollTimeout: POLL_TIMEOUT_MS, ...options, }; } using(paladin: PaladinClient) { - return new ZetoInstance(paladin, this.address, this.options); + const zeto = new ZetoInstance(paladin, this.address, this.options); + zeto.erc20 = this.erc20; + return zeto; } async mint(from: PaladinVerifier, data: ZetoMintParams) { + const params = { + mints: data.mints.map((t) => ({ ...t, to: t.to.lookup })), + }; const txID = await this.paladin.sendTransaction({ type: TransactionType.PRIVATE, - abi: zetoPrivateAbi, + abi: zetoAbi, function: "mint", to: this.address, from: from.lookup, - data: { - mints: data.mints.map((t) => ({ ...t, to: t.to.lookup })), - }, + data: params, }); return this.paladin.pollForReceipt(txID, this.options.pollTimeout); } @@ -151,7 +121,7 @@ export class ZetoInstance { async transfer(from: PaladinVerifier, data: ZetoTransferParams) { const txID = await this.paladin.sendTransaction({ type: TransactionType.PRIVATE, - abi: zetoPrivateAbi, + abi: zetoAbi, function: "transfer", to: this.address, from: from.lookup, @@ -161,4 +131,41 @@ export class ZetoInstance { }); return this.paladin.pollForReceipt(txID, this.options.pollTimeout); } + + async setERC20(from: PaladinVerifier, data: ZetoSetERC20Params) { + const txID = await this.paladin.sendTransaction({ + type: TransactionType.PUBLIC, + abi: zetoAbi, + function: "setERC20", + to: this.address, + from: from.lookup, + data, + }); + this.erc20 = data.erc20; + return this.paladin.pollForReceipt(txID, POLL_TIMEOUT_MS); + } + + async deposit(from: PaladinVerifier, data: ZetoDepositParams) { + const receipt = await this.paladin.sendTransaction({ + type: TransactionType.PRIVATE, + abi: zetoAbi, + function: "deposit", + to: this.address, + from: from.lookup, + data, + }); + return this.paladin.pollForReceipt(receipt, POLL_TIMEOUT_MS); + } + + async withdraw(from: PaladinVerifier, data: ZetoWithdrawParams) { + const receipt = await this.paladin.sendTransaction({ + type: TransactionType.PRIVATE, + abi: zetoAbi, + function: "withdraw", + to: this.address, + from: from.lookup, + data, + }); + return this.paladin.pollForReceipt(receipt, POLL_TIMEOUT_MS); + } } diff --git a/solidity/contracts/domains/interfaces/IZetoPrivate.sol b/solidity/contracts/domains/interfaces/IZetoPrivate.sol new file mode 100644 index 000000000..d4168325e --- /dev/null +++ b/solidity/contracts/domains/interfaces/IZetoPrivate.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +/** + * @title IZetoPrivate + * @dev This is the ABI of the Zeto domain transaction interface, which is implemented in Go. + * This interface is never expected to be implemented in a smart contract. + */ +interface IZetoPrivate { + struct TransferParam { + string to; + uint256 amount; + } + + function mint(TransferParam[] memory mints) external; + function transfer(TransferParam[] memory transfers) external; + function lockProof(address delegate, bytes memory call) external; + function deposit(uint256 amount) external; + function withdraw(uint256 amount) external; + function setERC20(address erc20) external; +} diff --git a/toolkit/go/pkg/rpcserver/rpcmethod.go b/toolkit/go/pkg/rpcserver/rpcmethod.go index 3bb98e00f..faa312311 100644 --- a/toolkit/go/pkg/rpcserver/rpcmethod.go +++ b/toolkit/go/pkg/rpcserver/rpcmethod.go @@ -22,8 +22,8 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" - "github.com/kaleido-io/paladin/toolkit/pkg/tkmsgs" "github.com/kaleido-io/paladin/toolkit/pkg/rpcclient" + "github.com/kaleido-io/paladin/toolkit/pkg/tkmsgs" ) // RPCHandler should not be implemented directly - use RPCMethod0 ... RPCMethod5 to implement your function diff --git a/toolkit/go/pkg/solutils/solbuild.go b/toolkit/go/pkg/solutils/solbuild.go index 4a8ac779e..a20fc7dab 100644 --- a/toolkit/go/pkg/solutils/solbuild.go +++ b/toolkit/go/pkg/solutils/solbuild.go @@ -104,3 +104,16 @@ func (unlinked *SolidityBuildWithLinks) ResolveLinks(ctx context.Context, librar } return hex.DecodeString(strings.TrimPrefix(bytecode, "0x")) } + +func MustParseBuildABI(buildJSON []byte) abi.ABI { + var buildParsed map[string]tktypes.RawJSON + var buildABI abi.ABI + err := json.Unmarshal(buildJSON, &buildParsed) + if err == nil { + err = json.Unmarshal(buildParsed["abi"], &buildABI) + } + if err != nil { + panic(err) + } + return buildABI +} diff --git a/toolkit/go/pkg/solutils/solbuild_test.go b/toolkit/go/pkg/solutils/solbuild_test.go index 7670f4857..4734160b7 100644 --- a/toolkit/go/pkg/solutils/solbuild_test.go +++ b/toolkit/go/pkg/solutils/solbuild_test.go @@ -87,3 +87,21 @@ func TestLibraryLinkingSuccess(t *testing.T) { require.Regexp(t, "PD021000.*0102030405060708aabbccddeeff998877", err) // bad links } + +func TestMustParseBuildABI(t *testing.T) { + abi := MustParseBuildABI([]byte(`{ + "abi": [ + { + "type":"function", + "name":"foo", + "inputs":[ + { + "name":"a", + "type":"uint256" + } + ] + } + ]}`)) + assert.Equal(t, 1, len(abi.Functions())) + assert.Equal(t, "foo", abi.Functions()["foo"].Name) +}