diff --git a/packages/jellyfish-api-core/__tests__/category/spv/decodeHtlcScript.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/decodeHtlcScript.test.ts new file mode 100644 index 0000000000..b3c4dd42aa --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/spv/decodeHtlcScript.test.ts @@ -0,0 +1,121 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { RpcApiError } from '@defichain/jellyfish-api-core' +import { ContainerAdapterClient } from '../../container_adapter_client' + +describe('Spv', () => { + const container = new MasterNodeRegTestContainer() + const client = new ContainerAdapterClient(container) + + beforeAll(async () => { + await container.start() + await container.waitForReady() + }) + + afterAll(async () => { + await container.stop() + }) + + /** Util function to replace a char at index in string */ + const replaceAt = (s: string, index: number, replacement: string): string => s.substr(0, index) + replacement + s.substr(index + replacement.length) + + it('should decodeHtlc', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const blocks = 10 + const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, `${blocks}`]) + + const decodedScript = await client.spv.decodeHtlcScript(htlc.redeemScript) + expect(decodedScript.sellerkey).toStrictEqual(pubKeyA) + expect(decodedScript.buyerkey).toStrictEqual(pubKeyB) + expect(decodedScript.blocks).toStrictEqual(blocks) + expect(decodedScript.hash).toStrictEqual(htlc.seedhash) + }) + + it('should not decodeHtlcScript with redeemscript not as hex format', async () => { + const promise = client.spv.decodeHtlcScript('XXXX') + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Redeemscript expected in hex format', code: -3, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with incorrect length', async () => { + const promise = client.spv.decodeHtlcScript('00') + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Incorrect redeemscript length', code: -5, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with invalid seller public key', async () => { + const redeemscript = '63a82001e7c5853b6d96346eb917445f49132f56bfb2dec56aafbf6a2a310b0d0b6e9f8821024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92675ab2752103088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad8368ac' + const sellerPublicKey = '024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92' + const tamperedRedeemScript = redeemscript.replace(sellerPublicKey, '000000000000000000000000000000000000000000000000000000000000000000') // '0' * 66 (sellerPublicKey length) + const promise = client.spv.decodeHtlcScript(tamperedRedeemScript) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid seller pubkey', code: -5, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with invalid buyer public key', async () => { + const redeemscript = '63a82001e7c5853b6d96346eb917445f49132f56bfb2dec56aafbf6a2a310b0d0b6e9f8821024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92675ab2752103088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad8368ac' + const buyerPublicKey = '03088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad83' + const tamperedRedeemScript = redeemscript.replace(buyerPublicKey, '000000000000000000000000000000000000000000000000000000000000000000') // '0' * 66 (buyerPublicKey length) + const promise = client.spv.decodeHtlcScript(tamperedRedeemScript) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid buyer pubkey', code: -5, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with invalid seller public key length', async () => { + const redeemscript = '63a82001e7c5853b6d96346eb917445f49132f56bfb2dec56aafbf6a2a310b0d0b6e9f8821024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92675ab2752103088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad8368ac' + /** + * Tamper seller public key length. + * Should be 0x41 or 0x21 as seen in https://github.com/DeFiCh/ain/blob/master/src/pubkey.h + * PUBLIC_KEY_SIZE=65 + * COMPRESSED_PUBLIC_KEY_SIZE=33 + */ + const tamperedRedeemScript = replaceAt(redeemscript, 72, '9') // Temper seller public key length to 0x91 + const promise = client.spv.decodeHtlcScript(tamperedRedeemScript) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Seller pubkey incorrect pubkey length', code: -5, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with invalid buyer public key length', async () => { + const redeemscript = '63a82001e7c5853b6d96346eb917445f49132f56bfb2dec56aafbf6a2a310b0d0b6e9f8821024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92675ab2752103088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad8368ac' + /** + * Tamper buyer public key length. + * Should be 0x41 or 0x21 as seen in https://github.com/DeFiCh/ain/blob/master/src/pubkey.h + * PUBLIC_KEY_SIZE=65 + * COMPRESSED_PUBLIC_KEY_SIZE=33 + */ + const tamperedRedeemScript = replaceAt(redeemscript, 148, '9') // Temper buyer public key length to 0x91 + const promise = client.spv.decodeHtlcScript(tamperedRedeemScript) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Buyer pubkey incorrect pubkey length', code: -5, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with incorrect timeout length', async () => { + const redeemscript = '63a82001e7c5853b6d96346eb917445f49132f56bfb2dec56aafbf6a2a310b0d0b6e9f8821024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92675ab2752103088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad8368ac' + /** + * Tamper timeout length. + * Should >= OP_1 as seen in https://github.com/DeFiCh/ain/blob/master/src/spv/spv_wrapper.cpp:977 + */ + const tamperedRedeemScript = replaceAt(redeemscript, 142, '1') // Temper timeout length to 0x1a + const promise = client.spv.decodeHtlcScript(tamperedRedeemScript) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Incorrect timeout length', code: -5, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with incorrect seed hash length', async () => { + const redeemscript = '63a82001e7c5853b6d96346eb917445f49132f56bfb2dec56aafbf6a2a310b0d0b6e9f8821024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92675ab2752103088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad8368ac' + /** + * Tamper seed hash length. + * Should be 0x20 as seen in https://github.com/DeFiCh/ain/blob/master/src/spv/spv_wrapper.cpp:949 + */ + const tamperedRedeemScript = replaceAt(redeemscript, 4, '9') // Tamper seed hash length to 0x90 + const promise = client.spv.decodeHtlcScript(tamperedRedeemScript) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Incorrect seed hash length', code: -5, method: spv_decodehtlcscript") + }) +}) diff --git a/packages/jellyfish-api-core/src/category/spv.ts b/packages/jellyfish-api-core/src/category/spv.ts index 8d9332cfc8..05b4d31424 100644 --- a/packages/jellyfish-api-core/src/category/spv.ts +++ b/packages/jellyfish-api-core/src/category/spv.ts @@ -71,6 +71,16 @@ export class Spv { async createHtlc (receiverPubKey: string, ownerPubKey: string, options: CreateHtlcOptions): Promise { return await this.client.call('spv_createhtlc', [receiverPubKey, ownerPubKey, options.timeout, options.seedhash], 'number') } + + /** + * Decode and return value in a HTLC redeemscript. + * + * @param {string} redeemScript HTLC redeem script + * @return {Promise} + */ + async decodeHtlcScript (redeemScript: string): Promise { + return await this.client.call('spv_decodehtlcscript', [redeemScript], 'number') + } } export interface ReceivedByAddressInfo { @@ -112,3 +122,14 @@ export interface CreateHtlcResult { /** Hex-encoded seed hash */ seedhash?: string } + +export interface DecodeHtlcResult { + /** seller's public key */ + sellerkey: string + /** buyer's public key */ + buyerkey: string + /** Timeout of the contract (denominated in blocks) relative to its placement in the blockchain at creation time */ + blocks: number + /** Hex-encoded seed hash */ + hash: string +} diff --git a/website/docs/jellyfish/api/spv.md b/website/docs/jellyfish/api/spv.md index 29873fc341..7f49856273 100644 --- a/website/docs/jellyfish/api/spv.md +++ b/website/docs/jellyfish/api/spv.md @@ -91,3 +91,20 @@ interface CreateHtlcResult { seedhash?: string } ``` + +## decodeHtlcScript + +Decode and return value in a HTLC redeemscript. + +```ts title="client.spv.decodeHtlcScript()" +interface spv { + decodeHtlcScript (redeemScript: string): Promise +} + +interface DecodeHtlcResult { + sellerkey: string + buyerkey: string + blocks: number + hash: string +} +```