diff --git a/contracts/did/did.c b/contracts/did/did.c new file mode 100644 index 0000000..3de93ac --- /dev/null +++ b/contracts/did/did.c @@ -0,0 +1,168 @@ +//------------------------------------------------------------------------------ +/* + Copyright (c) 2024 Transia, LLC + + This financial tool is designed for use by individuals or organizations + that hold the appropriate licenses and qualifications to engage with such + products in a manner that complies with all relevant laws and regulations. + Unauthorized use or redistribution of this tool may result in legal action. + + The content provided in this financial tool is for informational purposes + only and should not be interpreted as financial advice or an endorsement of + any particular investment or financial strategy. Users should seek advice + from a certified professional or financial advisor before making any + investment choices. + + By using this financial tool, the user agrees to indemnify Transia, LLC from + any claims, damages, or losses that may arise from their use or reliance on + the information contained within. The user acknowledges that they are utilizing + this tool at their own risk and that Transia, LLC will not be held responsible + for any direct, indirect, incidental, punitive, special, or consequential + damages, including but not limited to, damages for lost profits, goodwill, + usage, data, or other intangible losses. + +*/ +//============================================================================== + +#include +#include "hookapi.h" + +#define DONE(x)\ + return accept(SBUF(x), __LINE__) + +#define NOPE(x)\ + return rollback(SBUF(x), __LINE__) + +#define SIG_OFFSET 52U + +const uint8_t dids_namespace[] = { + 0x43U, 0xF6U, 0xCBU, 0x20U, 0x14U, 0x01U, 0x66U, 0xCCU, + 0x9EU, 0x7AU, 0x5EU, 0x07U, 0xFCU, 0xA8U, 0xE8U, 0x4CU, + 0xB7U, 0xE5U, 0x76U, 0x21U, 0x92U, 0x78U, 0x8BU, 0x5DU, + 0x4CU, 0xD0U, 0x8FU, 0x1FU, 0xD0U, 0x5FU, 0xF1U, 0x58U +}; + +int64_t hook(uint32_t r) +{ + _g(1,1); + + uint8_t hook_acc[32]; + hook_account(hook_acc + 12, 20); + + uint8_t otx_acc[32]; + otxn_field(otx_acc + 12, 20, sfAccount); + + if (BUFFER_EQUAL_20(hook_acc, otx_acc)) + DONE("did.c: passing outgoing txn"); + + int64_t tt = otxn_type(); + if (tt != ttINVOKE) + NOPE("did.c: Rejecting non-Invoke, non-Payment txn."); + + // get admin account + uint8_t admin[20]; + if (hook_param(SBUF(admin), "ADM", 3) != 20) + NOPE("did.c: Misconfigured. Missing ADM install parameter."); + + uint8_t op; + if (otxn_param(&op, 1, "OP", 2) != 1) + NOPE("did.c: Missing OP parameter on Invoke."); + + TRACEHEX(op); + + uint8_t so; + if (otxn_param(&so, 1, "SO", 2) != 1) + NOPE("did.c: Missing SO parameter on Invoke."); + + TRACEHEX(so); + + // admin invoke ops are: + // VC - validator create (admin) + // VU - validator update (admin) + // VD - validator delete (admin) + + // validator invoke ops are: + // DC - did create (validator) + // DU - did update (validator) + // DD - did delete (validator) + + // admin permission check + int64_t is_admin = BUFFER_EQUAL_20(otx_acc + 12, admin); + if (!is_admin && op == 'V') + NOPE("did.c: Admin only operation."); + + // validator permission check + uint8_t acc[20]; + uint8_t key[33]; + if ((op == 'D') && state(SBUF(key), otx_acc + 12, 20) == DOESNT_EXIST) + { + NOPE("did.c: Validator does not exist."); + } + + // get signature if any + // Signature format is packed binary data of the form: + // <20 byte dest accid><32 byte var string> + uint8_t sig_buf[256]; + int64_t sig_len = otxn_param(SBUF(sig_buf), "SIG", 3); + if (sig_len > 0) + { + if (sig_len < 80) + NOPE("did.c: Signature too short."); + + if (!util_verify(sig_buf, SIG_OFFSET, sig_buf + SIG_OFFSET, sig_len - SIG_OFFSET, SBUF(key))) + NOPE("did.c: Signature verification failed."); + } + // action + switch (op) + { + case 'V': + { + otxn_param(SBUF(acc), "VA", 2); + otxn_param(SBUF(key), "VK", 2); + switch (so) + { + case 'C': + case 'U': + { + state_set(SBUF(key), acc, 20); + DONE("did.c: Validator Created."); + } + case 'D': + { + state_set(0, 0, acc, 20); + DONE("did.c: Validator Deleted."); + } + default: + NOPE("did.c: Unknown sub operation."); + } + } + case 'D': + { + otxn_param(SBUF(acc), "CA", 2); + switch (so) + { + case 'C': + case 'U': + { + state_foreign_set(SBUF(sig_buf), acc, 20, dids_namespace, 32, hook_acc + 12, 20); + DONE("did.c: DID Create."); + } + case 'D': + { + state_foreign_set(0, 0, acc, 20, dids_namespace, 32, hook_acc + 12, 20); + DONE("did.c: DID Create."); + } + + default: + NOPE("did.c: Unknown sub operation."); + } + } + + default: + { + NOPE("did.c: Unknown operation."); + } + } + + return 0; +} \ No newline at end of file diff --git a/test/integration/did/did.test.ts b/test/integration/did/did.test.ts new file mode 100644 index 0000000..333bb0c --- /dev/null +++ b/test/integration/did/did.test.ts @@ -0,0 +1,214 @@ +// xrpl +import { Invoke, SetHookFlags, TransactionMetadata } from '@transia/xrpl' +// xrpl-helpers +import { + XrplIntegrationTestContext, + setupClient, + teardownClient, + serverUrl, +} from '@transia/hooks-toolkit/dist/npm/src/libs/xrpl-helpers' +// src +import { + Xrpld, + SetHookParams, + setHooksV3, + createHookPayload, + ExecutionUtility, + clearHookStateV3, + clearAllHooksV3, + iHook, + iHookParamEntry, + iHookParamName, + iHookParamValue, + StateUtility, + padHexString, + hexNamespace, +} from '@transia/hooks-toolkit' +import { + decodeModel, + xrpAddressToHex, +} from '@transia/hooks-toolkit/dist/npm/src/libs/binary-models' +import { sign, verify } from '@transia/ripple-keypairs' +import { DIDModel } from './models/DIDModel' + +// Step 1: +// - DMV Manager adds/updates/deletes clerk +// Key: clerkAcct: Data: publicKey + +// Step 2: +// - Document created/encrypted (Document ID is generated) +// - DMV Clerk signs DIDModel(owner, document id) +// - DMV Clerk submits invoke to hook +// Key: citizen: Data: message + sig + +// Step 3: +// - Anyone Anywhere verifies user account + +describe('oracle', () => { + let testContext: XrplIntegrationTestContext + + beforeAll(async () => { + testContext = await setupClient(serverUrl) + const hookWallet = testContext.hook1 + const adminWallet = testContext.alice + const hookParam1 = new iHookParamEntry( + new iHookParamName('ADM'), + new iHookParamValue(xrpAddressToHex(adminWallet.classicAddress), true) + ) + const acct1hook1 = createHookPayload({ + version: 0, + createFile: 'did', + namespace: 'validators', + flags: SetHookFlags.hsfOverride, + hookOnArray: ['Invoke'], + hookParams: [hookParam1.toXrpl()], + }) + await setHooksV3({ + client: testContext.client, + seed: hookWallet.seed, + hooks: [{ Hook: acct1hook1 }], + } as SetHookParams) + }) + afterAll(async () => { + const clearHook1 = { + Flags: SetHookFlags.hsfNSDelete, + HookNamespace: hexNamespace('validators'), + } as iHook + const clearHook2 = { + Flags: SetHookFlags.hsfNSDelete, + HookNamespace: hexNamespace('dids'), + } as iHook + await clearHookStateV3({ + client: testContext.client, + seed: testContext.hook1.seed, + hooks: [{ Hook: clearHook1 }, { Hook: clearHook2 }], + } as SetHookParams) + + await clearAllHooksV3({ + client: testContext.client, + seed: testContext.hook1.seed, + } as SetHookParams) + teardownClient(testContext) + }) + + it('did - create validator', async () => { + const hookWallet = testContext.hook1 + const adminWallet = testContext.alice + const clerk1Wallet = testContext.bob + // const keypair = Wallet.generate() + const otxnParam1 = new iHookParamEntry( + new iHookParamName('OP'), + new iHookParamValue('V') + ) + const otxnParam2 = new iHookParamEntry( + new iHookParamName('SO'), + new iHookParamValue('C') + ) + const otxnParam3 = new iHookParamEntry( + new iHookParamName('VA'), + new iHookParamValue(xrpAddressToHex(clerk1Wallet.classicAddress), true) + ) + const otxnParam4 = new iHookParamEntry( + new iHookParamName('VK'), + new iHookParamValue(clerk1Wallet.publicKey, true) + ) + const builtTx1: Invoke = { + TransactionType: 'Invoke', + Account: adminWallet.classicAddress, + Destination: hookWallet.classicAddress, + HookParameters: [ + otxnParam1.toXrpl(), + otxnParam2.toXrpl(), + otxnParam3.toXrpl(), + otxnParam4.toXrpl(), + ], + } + + const result1 = await Xrpld.submit(testContext.client, { + wallet: adminWallet, + tx: builtTx1, + }) + const hookExecutions1 = await ExecutionUtility.getHookExecutionsFromMeta( + testContext.client, + result1.meta as TransactionMetadata + ) + expect(hookExecutions1.executions[0].HookReturnString).toMatch( + 'did.c: Validator Created.' + ) + }) + it('did - create did', async () => { + const hookWallet = testContext.hook1 + const clerk1Wallet = testContext.bob + const citizenWallet = testContext.carol + // const keypair = Wallet.generate() + const otxnParam1 = new iHookParamEntry( + new iHookParamName('OP'), + new iHookParamValue('D') + ) + const otxnParam2 = new iHookParamEntry( + new iHookParamName('SO'), + new iHookParamValue('C') + ) + const otxnParam3 = new iHookParamEntry( + new iHookParamName('CA'), + new iHookParamValue(xrpAddressToHex(citizenWallet.classicAddress), true) + ) + const hex = new DIDModel(clerk1Wallet.classicAddress, '123456').encode() + const signature = sign(hex, clerk1Wallet.privateKey) + expect(verify(hex, signature, clerk1Wallet.publicKey)).toBe(true) + + const otxnParam4 = new iHookParamEntry( + new iHookParamName('SIG'), + new iHookParamValue(hex + signature, true) + ) + const builtTx1: Invoke = { + TransactionType: 'Invoke', + Account: clerk1Wallet.classicAddress, + Destination: hookWallet.classicAddress, + HookParameters: [ + otxnParam1.toXrpl(), + otxnParam2.toXrpl(), + otxnParam3.toXrpl(), + otxnParam4.toXrpl(), + ], + } + + const result1 = await Xrpld.submit(testContext.client, { + wallet: clerk1Wallet, + tx: builtTx1, + }) + const hookExecutions1 = await ExecutionUtility.getHookExecutionsFromMeta( + testContext.client, + result1.meta as TransactionMetadata + ) + expect(hookExecutions1.executions[0].HookReturnString).toMatch( + 'did.c: DID Create.' + ) + + const sigState = await StateUtility.getHookState( + testContext.client, + hookWallet.classicAddress, + padHexString(xrpAddressToHex(citizenWallet.classicAddress)), + hexNamespace('dids') + ) + // Signature Total - 256 (in hook) * 2 + const sigTotal = 256 * 2 + const model = decodeModel( + sigState.HookStateData.slice(0, hex.length), + DIDModel + ) + const clerkState = await StateUtility.getHookState( + testContext.client, + hookWallet.classicAddress, + padHexString(xrpAddressToHex(model.validator)), + hexNamespace('validators') + ) + expect( + verify( + model.encode(), + sigState.HookStateData.slice(hex.length, sigTotal).replace(/0+$/, ''), + clerkState.HookStateData + ) + ).toBe(true) + }) +}) diff --git a/test/integration/did/models/DIDModel.ts b/test/integration/did/models/DIDModel.ts new file mode 100644 index 0000000..47d5042 --- /dev/null +++ b/test/integration/did/models/DIDModel.ts @@ -0,0 +1,35 @@ +import { + BaseModel, + Metadata, + XRPAddress, + VarString, +} from '@transia/hooks-toolkit/dist/npm/src/libs/binary-models' + +export class DIDModel extends BaseModel { + validator: XRPAddress + did: VarString + + // 52 bytes + constructor( + validator: XRPAddress, // 20 byte / 0 + did: VarString // 31 byte + 1 byte / 20 + ) { + super() + this.validator = validator + this.did = did + } + + getMetadata(): Metadata { + return [ + { field: 'validator', type: 'xrpAddress' }, + { field: 'did', type: 'varString', maxStringLength: 31 }, + ] + } + + toJSON() { + return { + validator: this.validator, + did: this.did, + } + } +}