diff --git a/doc-site/docs/tutorials/private-storage.md b/doc-site/docs/tutorials/private-storage.md new file mode 100644 index 000000000..0217e715f --- /dev/null +++ b/doc-site/docs/tutorials/private-storage.md @@ -0,0 +1,143 @@ +# Private Storage Contract + +In this tutorial, you'll learn how to deploy and interact with a **private storage contract** using Paladin's privacy groups. Unlike the public storage example, here only authorized members of a privacy group can interact with the contract, ensuring secure and private data handling. + +--- + +## Prerequisites + +Before starting, make sure you have: + +1. Completed the [Public Storage Tutorial](./public-storage.md) and are familiar with: + - Deploying and interacting with contracts. + - Using Paladin SDK for blockchain transactions. +2. A running Paladin network with multiple nodes (at least 3 for this tutorial). + +--- + +## Overview + +The `PrivateStorage` tutorial demonstrates how to: + +1. Create a **privacy group** with selected members. +2. Deploy a **private Storage contract** within the group. +3. Interact with the contract securely within the group. +4. Test privacy by attempting access from a non-member node. + +The example code can be found in the [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/private-storage). + +The Solidity contract remains the same as in the [Public Storage Tutorial](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/solidity/contracts/tutorial/Storage.sol). However, the interaction is scoped to the privacy group. + +--- + +## Step 1: Create a Privacy Group + +To enable private interactions, start by creating a privacy group with selected members. + +```typescript +// Create a privacy group with Node1 and Node2 +logger.log("Creating a privacy group for Node1 and Node2..."); +const penteFactory = new PenteFactory(paladinNode1, "pente"); +const memberPrivacyGroup = await penteFactory.newPrivacyGroup(verifierNode1, { + group: { + salt: newGroupSalt(), // Generate a unique salt for the group + members: [verifierNode1, verifierNode2], // Include Node1 and Node2 as members + }, + evmVersion: "shanghai", + endorsementType: "group_scoped_identities", + externalCallsEnabled: true, +}); + +if (!checkDeploy(memberPrivacyGroup)) { + logger.error("Failed to create the privacy group."); + return false; +} +logger.log("Privacy group created successfully!"); +``` + +--- + +## Step 2: Deploy the Contract in the Privacy Group + +Deploy the `Storage` contract within the created privacy group. + +```typescript +logger.log("Deploying a private Storage contract..."); +const contractAddress = await memberPrivacyGroup.deploy( + storageJson.abi, // ABI of the Storage contract + storageJson.bytecode, // Bytecode of the Storage contract + verifierNode1 // Deploying as Node1 +); + +if (!contractAddress) { + logger.error("Failed to deploy the private Storage contract."); + return false; +} +logger.log(`Private Storage contract deployed! Address: ${contractAddress}`); +``` + +--- + +## Step 3: Store and Retrieve Values as Group Members + +### Store a Value + +Group members can store values securely in the private contract. + +```typescript +const privateStorage = new PrivateStorage(memberPrivacyGroup, contractAddress); + +logger.log("Storing value (125) in the private Storage contract..."); +const storeTx = await privateStorage.invoke(verifierNode1, "store", { num: 125 }); +logger.log("Value stored successfully! Transaction hash:", storeTx?.transactionHash); +``` + +--- + +### Retrieve the Value as a Member + +Authorized group members can retrieve the stored value. + +```typescript +logger.log("Node1 retrieving the stored value..."); +const retrievedValueNode1 = await privateStorage.call(verifierNode1, "retrieve", []); +logger.log("Node1 retrieved the value successfully:", retrievedValueNode1["0"]); + +logger.log("Node2 retrieving the stored value..."); +const retrievedValueNode2 = await privateStorage + .using(paladinNode2) + .call(verifierNode2, "retrieve", []); +logger.log("Node2 retrieved the value successfully:", retrievedValueNode2["0"]); +``` + +--- + +## Step 4: Verify Privacy by Testing Unauthorized Access + +When an outsider (Node3) tries to access the private contract, the attempt should fail. + +```typescript +try { + logger.log("Node3 (outsider) attempting to retrieve the value..."); + await privateStorage.using(paladinNode3).call(verifierNode3, "retrieve", []); + logger.error("Node3 (outsider) should not have access to the private Storage contract!"); + return false; +} catch (error) { + logger.info("Node3 (outsider) cannot retrieve data. Access denied."); +} +``` + +--- + +## Conclusion + +Congratulations! You’ve successfully: + +1. Created a privacy group with selected members. +2. Deployed a `Storage` contract in the privacy group. +3. Ensured secure interactions with the contract for group members. +4. Verified that unauthorized access is blocked. + +--- + +## Next Steps diff --git a/doc-site/docs/tutorials/public-storage.md b/doc-site/docs/tutorials/public-storage.md index f337d6523..f05b4165d 100644 --- a/doc-site/docs/tutorials/public-storage.md +++ b/doc-site/docs/tutorials/public-storage.md @@ -125,4 +125,7 @@ Congratulations! You’ve successfully: --- ## Next Steps - \ No newline at end of file + +Now that you've mastered deploying and interacting with a **public storage contract**, it's time to take things to the next level. In the next tutorial, you'll learn about **Storage with Privacy**, where you will add a privacy layer to the blockchain.the blockchain! + +[Continue to the Privacy Storage Contract Tutorial →](./private-storage.md) \ No newline at end of file diff --git a/doc-site/mkdocs.yml b/doc-site/mkdocs.yml index 0e0469b85..43f3319fc 100644 --- a/doc-site/mkdocs.yml +++ b/doc-site/mkdocs.yml @@ -117,6 +117,8 @@ nav: - Tutorials: - Introduction: tutorials/index.md - Hello World: tutorials/hello-world.md + - Public Storage: tutorials/public-storage.md + - Private Storage: tutorials/private-storage.md - Bond Issuance: tutorials/bond-issuance.md - Wholesale CBDC: tutorials/zkp-cbdc.md - Reference: diff --git a/example/privacy-storage/.vscode/launch.json b/example/privacy-storage/.vscode/launch.json new file mode 100644 index 000000000..1f1326095 --- /dev/null +++ b/example/privacy-storage/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run", + "runtimeExecutable": "npm", + "args": ["run", "start"], + "request": "launch", + "type": "node", + "outputCapture": "std" + } + ] + } \ No newline at end of file diff --git a/example/privacy-storage/README.md b/example/privacy-storage/README.md new file mode 100644 index 000000000..fc4472b2b --- /dev/null +++ b/example/privacy-storage/README.md @@ -0,0 +1,46 @@ +# Example: Hello World + + +See the [tutorial](https://lf-decentralized-trust-labs.github.io/paladin/head/tutorials/private-storage/) for a detailed explanation. + +## Pre-requisites + +Requires a local Paladin instance running on `localhost:31548`. +Requires a local Paladin instance running on `localhost:31648`. +Requires a local Paladin instance running on `localhost:31748`. + +## Run standalone + +Compile [Solidity contracts](../../solidity): + +```shell +cd ../../solidity +npm install +npm run compile +``` + +Build [TypeScript SDK](../../sdk/typescript): + +```shell +cd ../../sdk/typescript +npm install +npm run abi +npm run build +``` + +Run example: + +```shell +npm install +npm run abi +npm run start +``` + +## Run with Gradle + +The following will perform all pre-requisites and then run the example: + +```shell +../../gradlew build +npm run start +``` diff --git a/example/privacy-storage/build.gradle b/example/privacy-storage/build.gradle new file mode 100644 index 000000000..59e2e8f00 --- /dev/null +++ b/example/privacy-storage/build.gradle @@ -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 + */ + +configurations { + // Resolvable configurations + contractCompile { + canBeConsumed = false + canBeResolved = true + } + buildSDK { + canBeConsumed = false + canBeResolved = true + } +} + +dependencies { + contractCompile project(path: ':solidity', configuration: 'compiledContracts') + buildSDK project(path: ':sdk:typescript', configuration: 'buildSDK') +} + +task install(type: Exec) { + executable 'npm' + args 'install' + + inputs.files(configurations.buildSDK) + inputs.files('package.json') + outputs.files('package-lock.json') + outputs.dir('node_modules') +} + +task copyABI(type: Exec, dependsOn: install) { + executable 'npm' + args 'run' + args 'abi' + + inputs.files(configurations.contractCompile) + inputs.dir('scripts') + outputs.dir('src/abis') +} + +task build(type: Exec, dependsOn: [install, copyABI]) { + executable 'npm' + args 'run' + args 'build' + + inputs.dir('src') + outputs.dir('build') +} + +task e2e(type: Exec, dependsOn: [build]) { + dependsOn ':operator:deploy' + + executable 'npm' + args 'run' + args 'start' +} + +task clean(type: Delete) { + delete 'node_modules' + delete 'build' +} diff --git a/example/privacy-storage/package-lock.json b/example/privacy-storage/package-lock.json new file mode 100644 index 000000000..75bb2c1e5 --- /dev/null +++ b/example/privacy-storage/package-lock.json @@ -0,0 +1,312 @@ +{ + "name": "paladin-example-privacy-storage", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "paladin-example-privacy-storage", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@lfdecentralizedtrust-labs/paladin-sdk": "file:../../sdk/typescript" + }, + "devDependencies": { + "@types/node": "^22.8.7", + "copy-file": "^11.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } + }, + "../../sdk/typescript": { + "name": "@lfdecentralizedtrust-labs/paladin-sdk", + "version": "0.0.6-alpha.2", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.7.7", + "ethers": "^6.13.4", + "uuid": "^11.0.2" + }, + "devDependencies": { + "@types/node": "^22.9.0", + "copy-file": "^11.0.0", + "typescript": "^5.6.3" + } + }, + "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": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "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": { + "node": ">=6.0.0" + } + }, + "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": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@lfdecentralizedtrust-labs/paladin-sdk": { + "resolved": "../../sdk/typescript", + "link": true + }, + "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.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "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": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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": { + "graceful-fs": "^4.2.11", + "p-event": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "node": ">=0.3.1" + } + }, + "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": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "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": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "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": { + "node": ">=6" + } + } + } +} diff --git a/example/privacy-storage/package.json b/example/privacy-storage/package.json new file mode 100644 index 000000000..e49390adc --- /dev/null +++ b/example/privacy-storage/package.json @@ -0,0 +1,23 @@ +{ + "name": "paladin-example-privacy-storage", + "version": "0.0.1", + "description": "", + "main": "build/index.js", + "scripts": { + "build": "tsc", + "start": "ts-node ./src/index.ts", + "start:prod": "node ./build/index.js", + "abi": "node scripts/abi.mjs" + }, + "author": "", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^22.8.7", + "copy-file": "^11.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + }, + "dependencies": { + "@lfdecentralizedtrust-labs/paladin-sdk": "file:../../sdk/typescript" + } +} diff --git a/example/privacy-storage/scripts/abi.mjs b/example/privacy-storage/scripts/abi.mjs new file mode 100644 index 000000000..eea65b73d --- /dev/null +++ b/example/privacy-storage/scripts/abi.mjs @@ -0,0 +1,6 @@ +import { copyFile } from "copy-file"; + +await copyFile( + "../../solidity/artifacts/contracts/tutorials/Storage.sol/Storage.json", + "src/abis/Storage.json", +); \ No newline at end of file diff --git a/example/privacy-storage/src/helpers/storage.ts b/example/privacy-storage/src/helpers/storage.ts new file mode 100644 index 000000000..76fedc1b2 --- /dev/null +++ b/example/privacy-storage/src/helpers/storage.ts @@ -0,0 +1,36 @@ +import PaladinClient, { + PaladinVerifier, + PentePrivacyGroup, + PentePrivateContract, +} from "@lfdecentralizedtrust-labs/paladin-sdk"; +import storage from "../abis/Storage.json"; + +// export interface AddStorageParams { +// addr: string; +// } + +export const newPrivateStorage = async ( + pente: PentePrivacyGroup, + from: PaladinVerifier, +) => { + const address = await pente.deploy( + storage.abi, + storage.bytecode, + from, + ); + return address ? new PrivateStorage(pente, address) : undefined; +}; + + +export class PrivateStorage extends PentePrivateContract<{}> { + constructor( + protected evm: PentePrivacyGroup, + public readonly address: string + ) { + super(evm, storage.abi, address); + } + + using(paladin: PaladinClient) { + return new PrivateStorage(this.evm.using(paladin), this.address); + } +} diff --git a/example/privacy-storage/src/index.ts b/example/privacy-storage/src/index.ts new file mode 100644 index 000000000..48c117235 --- /dev/null +++ b/example/privacy-storage/src/index.ts @@ -0,0 +1,95 @@ +import PaladinClient, { + PenteFactory, + newGroupSalt, +} from "@lfdecentralizedtrust-labs/paladin-sdk"; +import storageJson from "./abis/Storage.json"; +import { checkDeploy } from "./util"; +import { PrivateStorage } from "./helpers/storage"; + +const logger = console; + +// Initialize Paladin clients for three nodes +const paladinNode1 = new PaladinClient({ url: "http://127.0.0.1:31548" }); +const paladinNode2 = new PaladinClient({ url: "http://127.0.0.1:31648" }); +const paladinNode3 = new PaladinClient({ url: "http://127.0.0.1:31748" }); + +async function main(): Promise { + // Get verifiers for each node + const [verifierNode1] = paladinNode1.getVerifiers("member@node1"); + const [verifierNode2] = paladinNode2.getVerifiers("member@node2"); + const [verifierNode3] = paladinNode3.getVerifiers("outsider@node3"); + + // Step 1: Create a privacy group for members + logger.log("Creating a privacy group for Node1 and Node2..."); + const penteFactory = new PenteFactory(paladinNode1, "pente"); + const memberPrivacyGroup = await penteFactory.newPrivacyGroup(verifierNode1, { + group: { + salt: newGroupSalt(), // Generate a new salt for the group + members: [verifierNode1, verifierNode2], // Add members to the group + }, + evmVersion: "shanghai", + endorsementType: "group_scoped_identities", + externalCallsEnabled: true, + }); + + if (!checkDeploy(memberPrivacyGroup)) return false; + + // Step 2: Deploy a smart contract within the privacy group + logger.log("Deploying a smart contract to the privacy group..."); + const contractAddress = await memberPrivacyGroup.deploy( + storageJson.abi, // ABI of the contract + storageJson.bytecode, // Bytecode of the contract + verifierNode1 // Deploying as Node1 + ); + + if (!contractAddress) { + logger.error("Failed to deploy the contract. No address returned."); + return false; + } + + logger.log(`Contract deployed successfully! Address: ${contractAddress}`); + + // Step 3: Use the deployed contract for private storage + const privateStorageContract = new PrivateStorage(memberPrivacyGroup, contractAddress); + + // Store a value in the contract + logger.log("Storing a value (125) in the contract..."); + const storeTx = await privateStorageContract.invoke(verifierNode1, "store", { num: 125 }); + logger.log("Value stored successfully! Transaction hash:", storeTx?.transactionHash); + + // Retrieve the value as Node1 + logger.log("Node1 retrieving the value from the contract..."); + const retrievedValueNode1 = await privateStorageContract.call(verifierNode1, "retrieve", []); + logger.log("Node1 retrieved the value successfully:", retrievedValueNode1["0"]); + + // Retrieve the value as Node2 + logger.log("Node2 retrieving the value from the contract..."); + const retrievedValueNode2 = await privateStorageContract + .using(paladinNode2) + .call(verifierNode2, "retrieve", []); + logger.log("Node2 retrieved the value successfully:", retrievedValueNode2["0"]); + + // Attempt to retrieve the value as Node3 (outsider) + try { + logger.log("Node3 (outsider) attempting to retrieve the value..."); + await privateStorageContract.using(paladinNode3).call(verifierNode3, "retrieve", []); + logger.error("Node3 (outsider) should not have access to the privacy group!"); + return false; + } catch (error) { + logger.info("Node3 (outsider) cannot retrieve the data from the privacy group. Access denied."); + } + + return true; +} + +// Execute the main function when this file is run directly +if (require.main === module) { + main() + .then((success: boolean) => { + process.exit(success ? 0 : 1); // Exit with status 0 for success, 1 for failure + }) + .catch((err) => { + logger.error("Exiting due to an uncaught error:", err); + process.exit(1); // Exit with status 1 for any uncaught errors + }); +} \ No newline at end of file diff --git a/example/privacy-storage/src/util.ts b/example/privacy-storage/src/util.ts new file mode 100644 index 000000000..1379fed65 --- /dev/null +++ b/example/privacy-storage/src/util.ts @@ -0,0 +1,32 @@ +import { ITransactionReceipt } from "@lfdecentralizedtrust-labs/paladin-sdk"; + +const logger = console; + +export interface DeployedContract { + address: string; +} + +export function checkDeploy( + contract: DeployedContract | undefined +): contract is DeployedContract { + if (contract === undefined) { + logger.error("Failed!"); + return false; + } + logger.log(`Success! address: ${contract.address}`); + return true; +} + +export function checkReceipt( + receipt: ITransactionReceipt | undefined +): receipt is ITransactionReceipt { + if (receipt === undefined) { + logger.error("Failed!"); + return false; + } else if (receipt.failureMessage !== undefined) { + logger.error(`Failed: ${receipt.failureMessage}`); + return false; + } + logger.log("Success!"); + return true; +} diff --git a/example/privacy-storage/tsconfig.json b/example/privacy-storage/tsconfig.json new file mode 100644 index 000000000..f0fd3378b --- /dev/null +++ b/example/privacy-storage/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "outDir": "build", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} diff --git a/example/public-storage/README.md b/example/public-storage/README.md index 39a74588c..b3be2f3dd 100644 --- a/example/public-storage/README.md +++ b/example/public-storage/README.md @@ -1,7 +1,7 @@ # Example: Hello World -See the [tutorial](https://lf-decentralized-trust-labs.github.io/paladin/head/tutorials/hello-world/) for a detailed explanation. +See the [tutorial](https://lf-decentralized-trust-labs.github.io/paladin/head/tutorials/public-storage/) for a detailed explanation. ## Pre-requisites diff --git a/example/public-storage/scripts/abi.mjs b/example/public-storage/scripts/abi.mjs index 684271dfa..2da0b9f18 100644 --- a/example/public-storage/scripts/abi.mjs +++ b/example/public-storage/scripts/abi.mjs @@ -1,6 +1,6 @@ import { copyFile } from "copy-file"; await copyFile( - "../../solidity/artifacts/contracts/shared/PublicStorage.sol/Storage.json", - "src/abis/PublicStorage.json" + "../../solidity/artifacts/contracts/tutorials/Storage.sol/Storage.json", + "src/abis/Storage.json" ); \ No newline at end of file diff --git a/example/public-storage/src/index.ts b/example/public-storage/src/index.ts index d05836893..e176fc5b7 100644 --- a/example/public-storage/src/index.ts +++ b/example/public-storage/src/index.ts @@ -1,82 +1,90 @@ import PaladinClient, { TransactionType, } from "@lfdecentralizedtrust-labs/paladin-sdk"; -import storageJson from "./abis/PublicStorage.json"; +import storageJson from "./abis/Storage.json"; const logger = console; -// Instantiate Paladin client (e.g., if you're connecting to "node1") +// Instantiate Paladin client const paladin = new PaladinClient({ url: "http://127.0.0.1:31548", }); - + async function main(): Promise { + // Get the owner account verifier const [owner] = paladin.getVerifiers("owner@node1"); - - logger.log("Deploying Storage contract..."); - const txIDdeploy = await paladin.sendTransaction({ - type: TransactionType.PUBLIC, - abi: storageJson.abi, - bytecode: storageJson.bytecode, - function: "", - from: owner.lookup, - data: {}, + // Step 1: Deploy the Storage contract + logger.log("Step 1: Deploying the Storage contract..."); + const deploymentTxID = await paladin.sendTransaction({ + type: TransactionType.PUBLIC, // Public deployment + abi: storageJson.abi, // ABI of the Storage contract + bytecode: storageJson.bytecode, // Compiled bytecode + function: "", // No constructor arguments + from: owner.lookup, // Account signing the transaction + data: {}, // No additional data }); - const receiptDeploy = await paladin.pollForReceipt(txIDdeploy, 10000); - if (!receiptDeploy?.contractAddress) { + // Wait for deployment receipt + const deploymentReceipt = await paladin.pollForReceipt(deploymentTxID, 10000); + if (!deploymentReceipt?.contractAddress) { logger.error("Deployment failed!"); return false; } - logger.log("Contract deployed successfully! Address:", receiptDeploy.contractAddress); - - const storeValue = 125; + logger.log("Step 1: Storage contract deployed successfully!"); - logger.log(`Storing value "${storeValue}"...`); - const txIDSet = await paladin.sendTransaction({ - type: TransactionType.PUBLIC, - abi: storageJson.abi, - function: "store", - from: owner.lookup, - to: receiptDeploy.contractAddress, - data: { "num": 125}, + // Step 3: Store a value in the contract + const valueToStore = 125; // Example value to store + logger.log(`Step 2: Storing value "${valueToStore}" in the contract...`); + const storeTxID = await paladin.sendTransaction({ + type: TransactionType.PUBLIC, // Public transaction + abi: storageJson.abi, // ABI of the Storage contract + function: "store", // Name of the function to call + from: owner.lookup, // Account signing the transaction + to: deploymentReceipt.contractAddress, // Address of the deployed contract + data: { num: valueToStore }, // Function arguments }); - const receiptSet = await paladin.pollForReceipt(txIDSet, 10000); - if (!receiptSet?.transactionHash) { - logger.error("Store failed!"); + // Wait for the store transaction receipt + const storeReceipt = await paladin.pollForReceipt(storeTxID, 10000); + if (!storeReceipt?.transactionHash) { + logger.error("Failed to store value in the contract!"); return false; } - logger.log("Value stored successfully! transactionHash:", receiptSet.transactionHash); + logger.log("Step 2: Value stored successfully!" ); - logger.log(`Retrieving value...`); - const receiptRetrieve = await paladin.call({ - type: TransactionType.PUBLIC, - abi: storageJson.abi, - function: "retrieve", - from: owner.lookup, - to: receiptDeploy.contractAddress, - data: {}, + // Step 4: Retrieve the stored value from the contract + logger.log("Step 3: Retrieving the stored value..."); + const retrieveResult = await paladin.call({ + type: TransactionType.PUBLIC, // Public call + abi: storageJson.abi, // ABI of the Storage contract + function: "retrieve", // Name of the function to call + from: owner.lookup, // Account making the call + to: deploymentReceipt.contractAddress, // Address of the deployed contract + data: {}, // No arguments required for this function }); - if (receiptRetrieve['0'] !== storeValue.toString()) { - logger.error("Retrieved value does not match stored value!"); + + // Validate the retrieved value + const retrievedValue = retrieveResult["0"]; + if (retrievedValue !== valueToStore.toString()) { + logger.error(`Retrieved value "${retrievedValue}" does not match stored value "${valueToStore}"!`); return false; } - logger.log("Value retrieved successfully! value:", receiptRetrieve['0']); + logger.log(`Step 3: Value retrieved successfully! Retrieved value: "${retrievedValue}"`); return true; } - + +// Entry point if (require.main === module) { main() .then((success: boolean) => { process.exit(success ? 0 : 1); }) .catch((err) => { - console.error("Exiting with uncaught error"); - console.error(err); + logger.error("Exiting with uncaught error"); + logger.error(err); process.exit(1); }); } diff --git a/sdk/typescript/src/domains/pente.ts b/sdk/typescript/src/domains/pente.ts index e3011b385..418519631 100644 --- a/sdk/typescript/src/domains/pente.ts +++ b/sdk/typescript/src/domains/pente.ts @@ -174,23 +174,34 @@ export class PentePrivacyGroup { ); } + // deploy a contract async deploy( abi: ReadonlyArray, bytecode: string, from: PaladinVerifier, - inputs: ConstructorParams + inputs?: ConstructorParams ) { + + // Find the constructor in the ABI const constructor = abi.find((entry) => entry.type === "constructor"); - if (constructor === undefined) { - throw new Error("Constructor not found"); - } + + // Handle the absence of a constructor + const constructorInputs = constructor?.inputs ?? []; + + // Prepare the data object + const data: Record = { + group: this.group, + bytecode, + inputs: inputs ?? [], // Ensure `inputs` is always included, defaulting to an empty array + }; + const txID = await this.paladin.sendTransaction({ type: TransactionType.PRIVATE, - abi: [privateDeployABI(constructor.inputs ?? [])], + abi: [privateDeployABI(constructorInputs ?? [])], function: "deploy", to: this.address, from: from.lookup, - data: { group: this.group, bytecode, inputs }, + data: data }); const receipt = await this.paladin.pollForReceipt( txID, @@ -200,6 +211,7 @@ export class PentePrivacyGroup { return receipt?.domainReceipt?.receipt.contractAddress; } + // invoke functions in the contract async invoke( from: PaladinVerifier, to: string, @@ -217,6 +229,7 @@ export class PentePrivacyGroup { return this.paladin.pollForReceipt(txID, this.options.pollTimeout); } + // call functions in the contract (read-only) async call( from: PaladinVerifier, to: string,