From c63a52701257fe3c3e9a6cf289fbca7c0cabc294 Mon Sep 17 00:00:00 2001 From: erhant Date: Thu, 21 Dec 2023 14:23:36 +0300 Subject: [PATCH 1/2] `set` and `setMany` --- src/contracts/build/hollowdb-set.contract.js | 215 +++++++++++++++++++ src/contracts/hollowdb-set.contract.ts | 166 ++++++++++++++ src/hollowdb-set.ts | 45 ++++ src/hollowdb.ts | 3 +- src/index.ts | 1 + tests/README.md | 1 + tests/set.test.ts | 46 ++++ 7 files changed, 475 insertions(+), 2 deletions(-) create mode 100644 src/contracts/build/hollowdb-set.contract.js create mode 100644 src/contracts/hollowdb-set.contract.ts create mode 100644 src/hollowdb-set.ts create mode 100644 tests/set.test.ts diff --git a/src/contracts/build/hollowdb-set.contract.js b/src/contracts/build/hollowdb-set.contract.js new file mode 100644 index 0000000..9d9f54a --- /dev/null +++ b/src/contracts/build/hollowdb-set.contract.js @@ -0,0 +1,215 @@ + + // src/contracts/errors/index.ts + var KeyExistsError = new ContractError("Key already exists."); + var KeyNotExistsError = new ContractError("Key does not exist."); + var CantEvolveError = new ContractError("Evolving is disabled."); + var NoVerificationKeyError = new ContractError("No verification key."); + var UnknownProtocolError = new ContractError("Unknown protocol."); + var NotWhitelistedError = new ContractError("Not whitelisted."); + var InvalidProofError = new ContractError("Invalid proof."); + var ExpectedProofError = new ContractError("Expected a proof."); + var NullValueError = new ContractError("Value cant be null, use remove instead."); + var NotOwnerError = new ContractError("Not contract owner."); + var InvalidFunctionError = new ContractError("Invalid function."); + var ArrayLengthMismatchError = new ContractError("Key and value counts mismatch."); + + // src/contracts/utils/index.ts + var verifyProof = async (proof, psignals, verificationKey) => { + if (!verificationKey) { + throw NoVerificationKeyError; + } + if (verificationKey.protocol !== "groth16" && verificationKey.protocol !== "plonk") { + throw UnknownProtocolError; + } + return await SmartWeave.extensions[verificationKey.protocol].verify(verificationKey, psignals, proof); + }; + var hashToGroup = (value) => { + if (value) { + return BigInt(SmartWeave.extensions.ethers.utils.ripemd160(Buffer.from(JSON.stringify(value)))); + } else { + return BigInt(0); + } + }; + + // src/contracts/modifiers/index.ts + var onlyOwner = (caller, input, state) => { + if (caller !== state.owner) { + throw NotOwnerError; + } + return input; + }; + var onlyNonNullValue = (_, input) => { + if (input.value === null) { + throw NullValueError; + } + return input; + }; + var onlyNonNullValues = (_, input) => { + if (input.values.some((val) => val === null)) { + throw NullValueError; + } + return input; + }; + var onlyWhitelisted = (list) => { + return (caller, input, state) => { + if (!state.isWhitelistRequired[list]) { + return input; + } + if (!state.whitelists[list][caller]) { + throw NotWhitelistedError; + } + return input; + }; + }; + var onlyProofVerified = (proofName, prepareInputs) => { + return async (caller, input, state) => { + if (!state.isProofRequired[proofName]) { + return input; + } + if (!input.proof) { + throw ExpectedProofError; + } + const ok = await verifyProof( + input.proof, + await prepareInputs(caller, input, state), + state.verificationKeys[proofName] + ); + if (!ok) { + throw InvalidProofError; + } + return input; + }; + }; + async function apply(caller, input, state, ...modifiers) { + for (const modifier of modifiers) { + input = await modifier(caller, input, state); + } + return input; + } + + // src/contracts/hollowdb-set.contract.ts + var handle = async (state, action) => { + const { caller, input } = action; + switch (input.function) { + case "get": { + const { key } = await apply(caller, input.value, state); + return { result: await SmartWeave.kv.get(key) }; + } + case "getMany": { + const { keys } = await apply(caller, input.value, state); + const values = await Promise.all(keys.map((key) => SmartWeave.kv.get(key))); + return { result: values }; + } + case "set": { + const { key, value } = await apply(caller, input.value, state, onlyWhitelisted("set"), onlyNonNullValue); + await SmartWeave.kv.put(key, value); + return { state }; + } + case "setMany": { + const { keys, values } = await apply(caller, input.value, state, onlyWhitelisted("set"), onlyNonNullValues); + if (keys.length !== values.length) { + throw new ContractError("Key and value counts mismatch"); + } + await Promise.all(keys.map((key, i) => SmartWeave.kv.put(key, values[i]))); + return { state }; + } + case "getKeys": { + const { options } = await apply(caller, input.value, state); + return { result: await SmartWeave.kv.keys(options) }; + } + case "getKVMap": { + const { options } = await apply(caller, input.value, state); + return { result: await SmartWeave.kv.kvMap(options) }; + } + case "put": { + const { key, value } = await apply(caller, input.value, state, onlyWhitelisted("put"), onlyNonNullValue); + if (await SmartWeave.kv.get(key) !== null) { + throw KeyExistsError; + } + await SmartWeave.kv.put(key, value); + return { state }; + } + case "putMany": { + const { keys, values } = await apply(caller, input.value, state, onlyWhitelisted("put"), onlyNonNullValues); + if (keys.length !== values.length) { + throw new ContractError("Key and value counts mismatch"); + } + if (await Promise.all(keys.map((key) => SmartWeave.kv.get(key))).then((values2) => values2.some((val) => val !== null))) { + throw KeyExistsError; + } + await Promise.all(keys.map((key, i) => SmartWeave.kv.put(key, values[i]))); + return { state }; + } + case "update": { + const { key, value } = await apply( + caller, + input.value, + state, + onlyNonNullValue, + onlyWhitelisted("update"), + onlyProofVerified("auth", async (_, input2) => { + const oldValue = await SmartWeave.kv.get(input2.key); + return [hashToGroup(oldValue), hashToGroup(input2.value), BigInt(input2.key)]; + }) + ); + await SmartWeave.kv.put(key, value); + return { state }; + } + case "remove": { + const { key } = await apply( + caller, + input.value, + state, + onlyWhitelisted("update"), + onlyProofVerified("auth", async (_, input2) => { + const oldValue = await SmartWeave.kv.get(input2.key); + return [hashToGroup(oldValue), BigInt(0), BigInt(input2.key)]; + }) + ); + await SmartWeave.kv.del(key); + return { state }; + } + case "updateOwner": { + const { newOwner } = await apply(caller, input.value, state, onlyOwner); + state.owner = newOwner; + return { state }; + } + case "updateProofRequirement": { + const { name, value } = await apply(caller, input.value, state, onlyOwner); + state.isProofRequired[name] = value; + return { state }; + } + case "updateVerificationKey": { + const { name, verificationKey } = await apply(caller, input.value, state, onlyOwner); + state.verificationKeys[name] = verificationKey; + return { state }; + } + case "updateWhitelistRequirement": { + const { name, value } = await apply(caller, input.value, state, onlyOwner); + state.isWhitelistRequired[name] = value; + return { state }; + } + case "updateWhitelist": { + const { add, remove, name } = await apply(caller, input.value, state, onlyOwner); + add.forEach((user) => { + state.whitelists[name][user] = true; + }); + remove.forEach((user) => { + delete state.whitelists[name][user]; + }); + return { state }; + } + case "evolve": { + const srcTxId = await apply(caller, input.value, state, onlyOwner); + if (!state.canEvolve) { + throw CantEvolveError; + } + state.evolve = srcTxId; + return { state }; + } + default: + input; + throw InvalidFunctionError; + } + }; + diff --git a/src/contracts/hollowdb-set.contract.ts b/src/contracts/hollowdb-set.contract.ts new file mode 100644 index 0000000..6d5b575 --- /dev/null +++ b/src/contracts/hollowdb-set.contract.ts @@ -0,0 +1,166 @@ +import {CantEvolveError, InvalidFunctionError, KeyExistsError} from './errors'; +import {apply, onlyNonNullValue, onlyNonNullValues, onlyOwner, onlyProofVerified, onlyWhitelisted} from './modifiers'; +import type {ContractHandle} from './types'; +import {hashToGroup} from './utils'; + +type Mode = {proofs: ['auth']; whitelists: ['put', 'update', 'set']}; +type Value = unknown; + +export type SetInput = { + function: 'set'; + value: { + key: string; + value: V; + }; +}; + +export type SetManyInput = { + function: 'setMany'; + value: { + keys: string[]; + values: V[]; + }; +}; + +export const handle: ContractHandle | SetManyInput> = async (state, action) => { + const {caller, input} = action; + switch (input.function) { + case 'get': { + const {key} = await apply(caller, input.value, state); + return {result: (await SmartWeave.kv.get(key)) as Value | null}; + } + + case 'getMany': { + const {keys} = await apply(caller, input.value, state); + const values = (await Promise.all(keys.map(key => SmartWeave.kv.get(key)))) as (Value | null)[]; + return {result: values}; + } + + case 'set': { + const {key, value} = await apply(caller, input.value, state, onlyWhitelisted('set'), onlyNonNullValue); + await SmartWeave.kv.put(key, value); + return {state}; + } + + case 'setMany': { + const {keys, values} = await apply(caller, input.value, state, onlyWhitelisted('set'), onlyNonNullValues); + if (keys.length !== values.length) { + throw new ContractError('Key and value counts mismatch'); + } + await Promise.all(keys.map((key, i) => SmartWeave.kv.put(key, values[i]))); + return {state}; + } + + case 'getKeys': { + const {options} = await apply(caller, input.value, state); + return {result: await SmartWeave.kv.keys(options)}; + } + + case 'getKVMap': { + const {options} = await apply(caller, input.value, state); + return {result: await SmartWeave.kv.kvMap(options)}; + } + + case 'put': { + const {key, value} = await apply(caller, input.value, state, onlyWhitelisted('put'), onlyNonNullValue); + if ((await SmartWeave.kv.get(key)) !== null) { + throw KeyExistsError; + } + await SmartWeave.kv.put(key, value); + return {state}; + } + + case 'putMany': { + const {keys, values} = await apply(caller, input.value, state, onlyWhitelisted('put'), onlyNonNullValues); + if (keys.length !== values.length) { + throw new ContractError('Key and value counts mismatch'); + } + + if (await Promise.all(keys.map(key => SmartWeave.kv.get(key))).then(values => values.some(val => val !== null))) { + throw KeyExistsError; + } + await Promise.all(keys.map((key, i) => SmartWeave.kv.put(key, values[i]))); + return {state}; + } + + case 'update': { + const {key, value} = await apply( + caller, + input.value, + state, + onlyNonNullValue, + onlyWhitelisted('update'), + onlyProofVerified('auth', async (_, input) => { + const oldValue = await SmartWeave.kv.get(input.key); + return [hashToGroup(oldValue), hashToGroup(input.value), BigInt(input.key)]; + }) + ); + await SmartWeave.kv.put(key, value); + return {state}; + } + + case 'remove': { + const {key} = await apply( + caller, + input.value, + state, + onlyWhitelisted('update'), + onlyProofVerified('auth', async (_, input) => { + const oldValue = await SmartWeave.kv.get(input.key); + return [hashToGroup(oldValue), BigInt(0), BigInt(input.key)]; + }) + ); + await SmartWeave.kv.del(key); + return {state}; + } + + case 'updateOwner': { + const {newOwner} = await apply(caller, input.value, state, onlyOwner); + state.owner = newOwner; + return {state}; + } + + case 'updateProofRequirement': { + const {name, value} = await apply(caller, input.value, state, onlyOwner); + state.isProofRequired[name] = value; + return {state}; + } + + case 'updateVerificationKey': { + const {name, verificationKey} = await apply(caller, input.value, state, onlyOwner); + state.verificationKeys[name] = verificationKey; + return {state}; + } + + case 'updateWhitelistRequirement': { + const {name, value} = await apply(caller, input.value, state, onlyOwner); + state.isWhitelistRequired[name] = value; + return {state}; + } + + case 'updateWhitelist': { + const {add, remove, name} = await apply(caller, input.value, state, onlyOwner); + add.forEach(user => { + state.whitelists[name][user] = true; + }); + remove.forEach(user => { + delete state.whitelists[name][user]; + }); + return {state}; + } + + case 'evolve': { + const srcTxId = await apply(caller, input.value, state, onlyOwner); + if (!state.canEvolve) { + throw CantEvolveError; + } + state.evolve = srcTxId; + return {state}; + } + + default: + // type-safe way to make sure all switch cases are handled + input satisfies never; + throw InvalidFunctionError; + } +}; diff --git a/src/hollowdb-set.ts b/src/hollowdb-set.ts new file mode 100644 index 0000000..037184c --- /dev/null +++ b/src/hollowdb-set.ts @@ -0,0 +1,45 @@ +import {BaseSDK} from '.'; +import {SetInput, SetManyInput} from './contracts/hollowdb-set.contract'; + +/** Just like HollowDB SDK, but supports `Set` and `SetMany` operations. + * The user must be whitelisted for `set` separately to use them. + * + * A `set` operation is like a `put` but the key can exist already and will be overwritten. + */ +export class SetSDK extends BaseSDK { + /** + * Inserts the given value into database. + * + * There must not be a value at the given key. + * + * @param key the key of the value to be inserted + * @param value the value to be inserted + */ + async set(key: string, value: V): Promise { + await this.base.dryWriteInteraction>({ + function: 'set', + value: { + key, + value, + }, + }); + } + + /** + * Inserts an array of value into database. + * + * There must not be a value at the given key. + * + * @param keys the keys of the values to be inserted + * @param values the values to be inserted + */ + async setMany(keys: string[], values: V[]): Promise { + await this.base.dryWriteInteraction>({ + function: 'setMany', + value: { + keys, + values, + }, + }); + } +} diff --git a/src/hollowdb.ts b/src/hollowdb.ts index 917ba11..f95ede0 100644 --- a/src/hollowdb.ts +++ b/src/hollowdb.ts @@ -1,4 +1,3 @@ import {BaseSDK} from './'; -type Mode = {proofs: ['auth']; whitelists: ['put', 'update']}; -export class SDK extends BaseSDK {} +export class SDK extends BaseSDK {} diff --git a/src/index.ts b/src/index.ts index 58b4a49..3db8117 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export {SDK as BaseSDK} from './base'; export {SDK} from './hollowdb'; +export {SetSDK} from './hollowdb-set'; diff --git a/tests/README.md b/tests/README.md index f6eb0bc..7f35a5f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -9,3 +9,4 @@ The tests are as follows: - `htx` tests use a custom contract where assume Bundlr usage and instead of values themselves, we store `valueHash.txid` as values. - `multi` tests using a single Warp instance with multiple contracts, and multiple HollowDB instances. - `null` tests null values, which are not allowed due to ambiguity between a null value and non-existent value. +- `set` tests `set` & `setMany` operations. diff --git a/tests/set.test.ts b/tests/set.test.ts new file mode 100644 index 0000000..3330da4 --- /dev/null +++ b/tests/set.test.ts @@ -0,0 +1,46 @@ +import {createValues, deployContract} from './utils'; +import {setupWarp} from './hooks'; +import {hollowdb as initialState} from '../src/contracts/states'; +import {SetSDK} from '../src'; + +type ValueType = {val: string}; + +describe('set tests', () => { + const warpHook = setupWarp(); + let owner: SetSDK; + + const {KEY, VALUE, NEXT_VALUE} = createValues(); + + beforeAll(async () => { + const hook = warpHook(); + const [ownerWallet] = hook.wallets; + const contractTxId = await deployContract(hook.warp, ownerWallet.jwk, initialState, 'hollowdb-set'); + + owner = new SetSDK(ownerWallet.jwk, contractTxId, hook.warp); + }); + + it('should allow putting a value', async () => { + await owner.put(KEY, VALUE); + }); + + it('should NOT allow putting a value again', async () => { + await expect(owner.put(KEY, NEXT_VALUE)).rejects.toThrow('Contract Error [put]: Key already exists.'); + }); + + it('should allow setting a value at an existing key', async () => { + await owner.set(KEY, VALUE); + }); + + it('should allow setting a value at a new key', async () => { + const newKV = createValues(); + await owner.set(newKV.KEY, newKV.VALUE); + }); + + it('should allow setting many values', async () => { + const kvs = Array.from({length: 5}, () => createValues()); + await owner.setMany( + kvs.map(kv => kv.KEY), + kvs.map(kv => kv.VALUE) + ); + }); +}); From 25e68e3d421d6bf8cbb63acef4cfb47c91262b0f Mon Sep 17 00:00:00 2001 From: erhant Date: Thu, 21 Dec 2023 14:28:30 +0300 Subject: [PATCH 2/2] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 481c515..c02b59e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hollowdb", - "version": "1.3.3", + "version": "1.3.4", "description": "A decentralized privacy-preserving key-value database", "license": "MIT", "homepage": "https://github.com/firstbatchxyz/hollowdb#readme",