From e112ec013a43206f95bb81ad69d6dd302201550b Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Tue, 30 Jan 2024 14:16:30 +0000 Subject: [PATCH] feat: asset unlock transaction payload (#293) * feat: add assetunlockpayload.js * test: validation --- index.d.ts | 4 +- lib/constants/index.js | 1 + lib/transaction/payload/assetunlockpayload.js | 174 ++++++++ lib/transaction/payload/index.js | 1 + lib/transaction/payload/payload.js | 3 + test-d/index.test-d.ts | 2 + .../transaction/payload/assetunlockpayload.js | 395 ++++++++++++++++++ .../payload/AssetUnlockPayload.d.ts | 73 ++++ typings/transaction/payload/Payload.d.ts | 4 +- 9 files changed, 655 insertions(+), 2 deletions(-) create mode 100644 lib/transaction/payload/assetunlockpayload.js create mode 100644 test/transaction/payload/assetunlockpayload.js create mode 100644 typings/transaction/payload/AssetUnlockPayload.d.ts diff --git a/index.d.ts b/index.d.ts index 8c5b04777..65b32b8f3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -64,6 +64,8 @@ export { ProRegTxPayload } from './typings/transaction/payload/ProRegTxPayload'; export { ProUpRegTxPayload } from './typings/transaction/payload/ProUpRegTxPayload'; export { ProUpRevTxPayload } from './typings/transaction/payload/ProUpRevTxPayload'; export { ProUpServTxPayload } from './typings/transaction/payload/ProUpServTxPayload'; +export { AssetLockPayload } from './typings/transaction/payload/AssetLockPayload'; +export { AssetUnlockPayload } from './typings/transaction/payload/AssetUnlockPayload'; export { Output } from './typings/transaction/Output'; @@ -362,4 +364,4 @@ export function traverseAndBuildPartialTree( position: number, hashes: Buffer[], matches: boolean[] -): any; \ No newline at end of file +): any; diff --git a/lib/constants/index.js b/lib/constants/index.js index 3e3c02778..1de610f1a 100644 --- a/lib/constants/index.js +++ b/lib/constants/index.js @@ -25,6 +25,7 @@ module.exports = { TRANSACTION_COINBASE: 5, TRANSACTION_QUORUM_COMMITMENT: 6, TRANSACTION_ASSET_LOCK: 8, + TRANSACTION_ASSET_UNLOCK: 9, }, EMPTY_SIGNATURE_SIZE: 0, primitives: { diff --git a/lib/transaction/payload/assetunlockpayload.js b/lib/transaction/payload/assetunlockpayload.js new file mode 100644 index 000000000..5c6249f7b --- /dev/null +++ b/lib/transaction/payload/assetunlockpayload.js @@ -0,0 +1,174 @@ +/* eslint-disable */ +// TODO: Remove previous line and work through linting issues at next edit + +var Preconditions = require('../../util/preconditions'); +var BufferWriter = require('../../encoding/bufferwriter'); +var BufferReader = require('../../encoding/bufferreader'); +var AbstractPayload = require('./abstractpayload'); +var utils = require('../../util/js'); +const _ = require('lodash'); +const BN = require('../../crypto/bn'); +const constants = require('../../constants'); + +var isUnsignedInteger = utils.isUnsignedInteger; + +var CURRENT_PAYLOAD_VERSION = 1; + +/** + * @typedef {Object} AssetUnlockPayloadJSON + * @property {number} version + * @property {object} creditOutputs + */ + +/** + * @class AssetUnlockPayload + * @property {Output[]} creditOutputs + */ +function AssetUnlockPayload() { + AbstractPayload.call(this); + this.version = CURRENT_PAYLOAD_VERSION; +} + +AssetUnlockPayload.prototype = Object.create(AbstractPayload.prototype); +AssetUnlockPayload.prototype.constructor = AbstractPayload; + +/* Static methods */ + +/** + * Parse raw transition payload + * @param {Buffer} rawPayload + * @return {AssetUnlockPayload} + */ +AssetUnlockPayload.fromBuffer = function (rawPayload) { + var payloadBufferReader = new BufferReader(rawPayload); + var payload = new AssetUnlockPayload(); + payload.version = payloadBufferReader.readUInt8(); + payload.index = payloadBufferReader.readUInt64LEBN().toNumber(); + payload.fee = payloadBufferReader.readUInt32LE(); + payload.requestHeight = payloadBufferReader.readUInt32LE(); + payload.quorumHash = payloadBufferReader + .read(constants.SHA256_HASH_SIZE) + .toString('hex'); + payload.quorumSig = payloadBufferReader + .read(constants.BLS_SIGNATURE_SIZE) + .toString('hex'); + + if (!payloadBufferReader.finished()) { + throw new Error( + 'Failed to parse payload: raw payload is bigger than expected.' + ); + } + + payload.validate(); + return payload; +}; + +/** + * Create new instance of payload from JSON + * @param {string|AssetUnlockPayloadJSON} payloadJson + * @return {AssetUnlockPayload} + */ +AssetUnlockPayload.fromJSON = function fromJSON(payloadJson) { + var payload = new AssetUnlockPayload(); + payload.version = payloadJson.version; + payload.index = payloadJson.index; + payload.fee = payloadJson.fee; + payload.requestHeight = payloadJson.requestHeight; + payload.quorumHash = payloadJson.quorumHash; + payload.quorumSig = payloadJson.quorumSig; + + payload.validate(); + return payload; +}; + +/* Instance methods */ + +/** + * Validates payload data + * @return {boolean} + */ +AssetUnlockPayload.prototype.validate = function () { + Preconditions.checkArgument( + isUnsignedInteger(this.version), + 'Expect version to be an unsigned integer' + ); + + Preconditions.checkArgument( + this.version !== 0 && this.version <= CURRENT_PAYLOAD_VERSION, + 'Invalid version' + ); + + Preconditions.checkArgument( + isUnsignedInteger(this.index), + `Expect index to be an unsigned integer` + ); + + Preconditions.checkArgument( + isUnsignedInteger(this.fee), + `Expect fee to be an unsigned integer` + ); + + Preconditions.checkArgument( + isUnsignedInteger(this.requestHeight), + `Expect requestHeight to be an unsigned integer` + ); + + Preconditions.checkArgument( + utils.isHexaString(this.quorumHash), + 'Expect quorumHash to be a hex string' + ); + + Preconditions.checkArgument( + utils.isHexaString(this.quorumSig), + 'Expect quorumSig to be a hex string' + ); + + return true; +}; + +/** + * Serializes payload to JSON + * @return {AssetUnlockPayloadJSON} + */ +AssetUnlockPayload.prototype.toJSON = function toJSON() { + this.validate(); + var json = { + version: this.version, + index: this.index, + fee: this.fee, + requestHeight: this.requestHeight, + quorumHash: this.quorumHash, + quorumSig: this.quorumSig, + }; + + return json; +}; + +/** + * Serialize payload to buffer + * @return {Buffer} + */ +AssetUnlockPayload.prototype.toBuffer = function toBuffer() { + this.validate(); + var payloadBufferWriter = new BufferWriter(); + + payloadBufferWriter + .writeUInt8(this.version) + .writeUInt64LEBN(new BN(this.index)) + .writeUInt32LE(this.fee) + .writeUInt32LE(this.requestHeight) + .write(Buffer.from(this.quorumHash, 'hex')) + .write(Buffer.from(this.quorumSig, 'hex')); + + return payloadBufferWriter.toBuffer(); +}; + +/** + * Copy payload instance + * @return {AssetUnlockPayload} + */ +AssetUnlockPayload.prototype.copy = function copy() { + return AssetUnlockPayload.fromJSON(this.toJSON()); +}; + +module.exports = AssetUnlockPayload; diff --git a/lib/transaction/payload/index.js b/lib/transaction/payload/index.js index 228efe8c9..69f87ff4e 100644 --- a/lib/transaction/payload/index.js +++ b/lib/transaction/payload/index.js @@ -11,5 +11,6 @@ Payload.CoinbasePayload = require('./coinbasepayload'); Payload.constants = require('../../constants'); Payload.CommitmentTxPayload = require('./commitmenttxpayload'); Payload.AssetLockPayload = require('./assetlockpayload'); +Payload.AssetUnlockPayload = require('./assetunlockpayload'); module.exports = Payload; diff --git a/lib/transaction/payload/payload.js b/lib/transaction/payload/payload.js index 87185b957..cad426608 100644 --- a/lib/transaction/payload/payload.js +++ b/lib/transaction/payload/payload.js @@ -11,6 +11,7 @@ var ProTxUpServPayload = require('./proupservtxpayload'); var ProUpRegTxPayload = require('./proupregtxpayload'); var ProUpRevTxPayload = require('./prouprevtxpayload'); var AssetLockPayload = require('./assetlockpayload'); +var AssetUnlockPayload = require('./assetunlockpayload'); var PayloadClasses = {}; PayloadClasses[RegisteredPayloadTypes.TRANSACTION_COINBASE] = CoinbasePayload; @@ -26,6 +27,8 @@ PayloadClasses[RegisteredPayloadTypes.TRANSACTION_PROVIDER_UPDATE_REVOKE] = ProUpRevTxPayload; PayloadClasses[RegisteredPayloadTypes.TRANSACTION_ASSET_LOCK] = AssetLockPayload; +PayloadClasses[RegisteredPayloadTypes.TRANSACTION_ASSET_UNLOCK] = + AssetUnlockPayload; /** * diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts index 115995e4c..20499dece 100644 --- a/test-d/index.test-d.ts +++ b/test-d/index.test-d.ts @@ -37,6 +37,8 @@ import type { MultiSigInput, MultiSigScriptHashInput, AbstractPayload, + AssetLockPayload, + AssetUnlockPayload, CoinbasePayload, CommitmentTxPayload, ProRegTxPayload, diff --git a/test/transaction/payload/assetunlockpayload.js b/test/transaction/payload/assetunlockpayload.js new file mode 100644 index 000000000..61d58c543 --- /dev/null +++ b/test/transaction/payload/assetunlockpayload.js @@ -0,0 +1,395 @@ +/* eslint-disable */ +// TODO: Remove previous line and work through linting issues at next edit + +var expect = require('chai').expect; +var sinon = require('sinon'); + +var DashcoreLib = require('../../../index'); + +var BN = require('../../../lib/crypto/bn'); + +var AssetUnlockPayload = DashcoreLib.Transaction.Payload.AssetUnlockPayload; +var Script = DashcoreLib.Script; +var Address = DashcoreLib.Address; +var Output = DashcoreLib.Transaction.Output; + +var output1 = Output.fromObject({ + satoshis: 1000, + script: Script.buildPublicKeyHashOut( + Address.fromString('XxGJLCB7BBXAgA1AbgtNDMyVpQV9yXd7oB', 'mainnet') + ).toHex() +}); + +var output2 = Output.fromObject({ + satoshis: 2000, + script: Script.buildPublicKeyHashOut( + Address.fromString('7hRXBxSmKqaJ6JfsVaSeZqAeyxvrxcHyV1', 'mainnet') + ).toHex() +}); + + +var validAssetUnlockPayloadJSON = { + version: 1, + index: 301, + fee: 70000, + requestHeight: 1317, + quorumHash: '4acfa5c6d92071d206da5b767039d42f24e7ab1a694a5b8014cddc088311e448', + quorumSig: 'aee468c03feec7caada0599457136ef0dfe9365657a42ef81bb4aa53af383d05d90552b2cd23480cae24036b953ba8480d2f98291271a338e4235265dea94feacb54d1fd96083151001eff4156e7475e998154a8e6082575e2ee461b394d24f7' +}; + +// Contains same data as JSON above +var validAssetUnlockPayload = AssetUnlockPayload.fromJSON(validAssetUnlockPayloadJSON); +var validAssetUnlockPayloadHexString = "012d0100000000000070110100250500004acfa5c6d92071d206da5b767039d42f24e7ab1a694a5b8014cddc088311e448aee468c03feec7caada0599457136ef0dfe9365657a42ef81bb4aa53af383d05d90552b2cd23480cae24036b953ba8480d2f98291271a338e4235265dea94feacb54d1fd96083151001eff4156e7475e998154a8e6082575e2ee461b394d24f7"; +var validAssetUnlockPayloadBuffer = Buffer.from(validAssetUnlockPayloadHexString, "hex") + +describe('AssetUnlockPayload', function () { + describe('.fromBuffer', function () { + beforeEach(function () { + sinon.spy(AssetUnlockPayload.prototype, 'validate'); + }); + + afterEach(function () { + AssetUnlockPayload.prototype.validate.restore(); + }); + + it('Should return instance of AssetUnlockPayload and call #validate on it', function () { + var payload = AssetUnlockPayload.fromBuffer(validAssetUnlockPayloadBuffer); + + const { + version, + index, + fee, + requestHeight, + quorumHash, + quorumSig + } = validAssetUnlockPayloadJSON; + + expect(payload).to.be.an.instanceOf(AssetUnlockPayload); + expect(payload.version).to.be.equal(version); + expect(payload.index).to.be.equal(index); + expect(payload.fee).to.be.equal(fee); + expect(payload.requestHeight).to.be.equal(requestHeight); + expect(payload.quorumHash).to.be.equal(quorumHash); + expect(payload.quorumSig).to.be.equal(quorumSig); + expect(payload.validate.callCount).to.be.equal(1); + }); + + it('Should throw in case if there is some unexpected information in raw payload', function () { + var payloadWithAdditionalZeros = Buffer.from( + validAssetUnlockPayloadHexString + '0000', + 'hex' + ); + + expect(function () { + AssetUnlockPayload.fromBuffer(payloadWithAdditionalZeros); + }).to.throw( + 'Failed to parse payload: raw payload is bigger than expected.' + ); + }); + }); + + describe('.fromJSON', function () { + before(function () { + sinon.spy(AssetUnlockPayload.prototype, 'validate'); + }); + + it('Should return instance of AssetUnlockPayload and call #validate on it', function () { + var payload = AssetUnlockPayload.fromJSON(validAssetUnlockPayloadJSON); + + expect(payload).to.be.an.instanceOf(AssetUnlockPayload); + const { + version, + index, + fee, + requestHeight, + quorumHash, + quorumSig + } = validAssetUnlockPayloadJSON; + + expect(payload).to.be.an.instanceOf(AssetUnlockPayload); + expect(payload.version).to.be.equal(version); + expect(payload.index).to.be.equal(index); + expect(payload.fee).to.be.equal(fee); + expect(payload.requestHeight).to.be.equal(requestHeight); + expect(payload.quorumHash).to.be.equal(quorumHash); + expect(payload.quorumSig).to.be.equal(quorumSig); + }); + + after(function () { + AssetUnlockPayload.prototype.validate.restore(); + }); + }); + + describe('#validate', function () { + it('Should allow only unsigned integer as version', function () { + var payload = validAssetUnlockPayload.copy(); + + payload.version = -1; + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect version to be an unsigned integer'); + + payload.version = 1.5; + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect version to be an unsigned integer'); + + payload.version = '12'; + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect version to be an unsigned integer'); + + payload.version = Buffer.from('0a0f', 'hex'); + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect version to be an unsigned integer'); + + payload.version = 123; + + expect(function () { + payload.validate(); + }).not.to.throw; + }); + + it('Should allow only unsigned integer as index', function () { + var payload = validAssetUnlockPayload.copy(); + + payload.index = -1; + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect index to be an unsigned integer'); + + payload.index = 1.5; + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect index to be an unsigned integer'); + + payload.index = '12'; + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect index to be an unsigned integer'); + + payload.index = Buffer.from('0a0f', 'hex'); + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect index to be an unsigned integer'); + + payload.index = 123; + + expect(function () { + payload.validate(); + }).not.to.throw; + }); + + it('Should allow only unsigned integer as fee', function () { + var payload = validAssetUnlockPayload.copy(); + + payload.fee = -1; + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect fee to be an unsigned integer'); + + payload.fee = 1.5; + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect fee to be an unsigned integer'); + + payload.fee = '12'; + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect fee to be an unsigned integer'); + + payload.fee = Buffer.from('0a0f', 'hex'); + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect fee to be an unsigned integer'); + + payload.fee = 123; + + expect(function () { + payload.validate(); + }).not.to.throw; + }); + + it('Should allow only unsigned integer as requestHeight', function () { + var payload = validAssetUnlockPayload.copy(); + + payload.requestHeight = -1; + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect requestHeight to be an unsigned integer'); + + payload.requestHeight = 1.5; + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect requestHeight to be an unsigned integer'); + + payload.requestHeight = '12'; + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect requestHeight to be an unsigned integer'); + + payload.requestHeight = Buffer.from('0a0f', 'hex'); + + expect(function () { + payload.validate(); + }).to.throw('Invalid Argument: Expect requestHeight to be an unsigned integer'); + + payload.requestHeight = 123; + + expect(function () { + payload.validate(); + }).not.to.throw; + }); + + it('Should allow only hex string as quorumHash', function () { + var payload = validAssetUnlockPayload.copy(); + + payload.quorumHash = -1; + + expect(function () { + payload.validate(); + }).to.throw('Expect quorumHash to be a hex string'); + + payload.quorumHash = 1.5; + + expect(function () { + payload.validate(); + }).to.throw('Expect quorumHash to be a hex string'); + + payload.quorumHash = Buffer.from('0a0f', 'hex'); + + expect(function () { + payload.validate(); + }).to.throw('Expect quorumHash to be a hex string'); + + payload.quorumHash = 123; + + expect(function () { + payload.validate(); + }).not.to.throw; + }); + + it('Should allow only hex string as quorumSig', function () { + var payload = validAssetUnlockPayload.copy(); + + payload.quorumSig = -1; + + expect(function () { + payload.validate(); + }).to.throw('Expect quorumSig to be a hex string'); + + payload.quorumSig = 1.5; + + expect(function () { + payload.validate(); + }).to.throw('Expect quorumSig to be a hex string'); + + payload.quorumSig = Buffer.from('0a0f', 'hex'); + + expect(function () { + payload.validate(); + }).to.throw('Expect quorumSig to be a hex string'); + + payload.quorumSig = 123; + + expect(function () { + payload.validate(); + }).not.to.throw; + }); + }); + + describe('#toJSON', function () { + beforeEach(function () { + sinon.spy(AssetUnlockPayload.prototype, 'validate'); + }); + + afterEach(function () { + AssetUnlockPayload.prototype.validate.restore(); + }); + + it('Should be able to serialize payload JSON', function () { + var payload = validAssetUnlockPayload.copy(); + + var payloadJSON = payload.toJSON(); + + const { + version, + index, + fee, + requestHeight, + quorumHash, + quorumSig + } = validAssetUnlockPayloadJSON; + + expect(payload).to.be.an.instanceOf(AssetUnlockPayload); + expect(payload.version).to.be.equal(version); + expect(payload.index).to.be.equal(index); + expect(payload.fee).to.be.equal(fee); + expect(payload.requestHeight).to.be.equal(requestHeight); + expect(payload.quorumHash).to.be.equal(quorumHash); + expect(payload.quorumSig).to.be.equal(quorumSig); + }); + it('Should call #validate', function () { + var payload = AssetUnlockPayload.fromJSON(validAssetUnlockPayloadJSON); + AssetUnlockPayload.prototype.validate.resetHistory(); + payload.toJSON(); + expect(payload.validate.callCount).to.be.equal(1); + }); + }); + + describe('#toBuffer', function () { + beforeEach(function () { + sinon.spy(AssetUnlockPayload.prototype, 'validate'); + }); + + afterEach(function () { + AssetUnlockPayload.prototype.validate.restore(); + }); + + it('Should be able to serialize payload to Buffer', function () { + var payload = validAssetUnlockPayload.copy(); + + var serializedPayload = payload.toBuffer(); + var restoredPayload = AssetUnlockPayload.fromBuffer(serializedPayload); + + const { + version, + index, + fee, + requestHeight, + quorumHash, + quorumSig + } = validAssetUnlockPayloadJSON; + + expect(restoredPayload).to.be.an.instanceOf(AssetUnlockPayload); + expect(restoredPayload.version).to.be.equal(version); + expect(restoredPayload.index).to.be.equal(index); + expect(restoredPayload.fee).to.be.equal(fee); + expect(restoredPayload.requestHeight).to.be.equal(requestHeight); + expect(restoredPayload.quorumHash).to.be.equal(quorumHash); + expect(restoredPayload.quorumSig).to.be.equal(quorumSig); + }); + it('Should call #validate', function () { + var payload = AssetUnlockPayload.fromJSON(validAssetUnlockPayloadJSON); + AssetUnlockPayload.prototype.validate.resetHistory(); + payload.toBuffer(); + expect(payload.validate.callCount).to.be.equal(1); + }); + }); +}); diff --git a/typings/transaction/payload/AssetUnlockPayload.d.ts b/typings/transaction/payload/AssetUnlockPayload.d.ts new file mode 100644 index 000000000..34cbf5ac1 --- /dev/null +++ b/typings/transaction/payload/AssetUnlockPayload.d.ts @@ -0,0 +1,73 @@ +/** + * @typedef {Object} AssetUnlockPayloadJSON + * @property {number} version + * @property {number} index + * @property {number} fee + * @property {number} requestHeight + * @property {string} quorumHash + * @property {string} quorumSig + */ +export type AssetUnlockPayloadJSON = { + version: number; + index: number, + fee: number, + requestHeight: number, + quorumHash: string, + quorumSig: string, +}; + +/** + * @class AssetUnlockPayload + * @property {number} version + * @property {number} index + * @property {number} fee + * @property {number} requestHeight + * @property {string} quorumHash + * @property {string} quorumSig + */ +export class AssetUnlockPayload { + /** + * Parse raw transition payload + * @param {Buffer} rawPayload + * @return {AssetUnlockPayload} + */ + static fromBuffer(rawPayload: Buffer): AssetUnlockPayload; + + /** + * Create new instance of payload from JSON + * @param {string|AssetUnlockPayloadJSON} payloadJson + * @return {AssetUnlockPayload} + */ + static fromJSON(payloadJson: string | AssetUnlockPayloadJSON): AssetUnlockPayload; + + /** + * Validates payload data + * @return {boolean} + */ + validate(): boolean; + + /** + * Serializes payload to JSON + * @return {AssetUnlockPayloadJSON} + */ + toJSON(): AssetUnlockPayloadJSON; + + /** + * Serialize payload to buffer + * @return {Buffer} + */ + toBuffer(): Buffer; + + /** + * Copy payload instance + * @return {AssetUnlockPayload} + */ + copy(): AssetUnlockPayload; + + version: number; + index: number; + fee: number; + requestHeight: number; + quorumHash: string; + quorumSig: string; +} diff --git a/typings/transaction/payload/Payload.d.ts b/typings/transaction/payload/Payload.d.ts index 5fbdcb24e..7fcef71ff 100644 --- a/typings/transaction/payload/Payload.d.ts +++ b/typings/transaction/payload/Payload.d.ts @@ -5,6 +5,7 @@ import { ProUpServTxPayload } from './ProUpServTxPayload'; import { CoinbasePayload } from './CoinbasePayload'; import { CommitmentTxPayload } from './CommitmentTxPayload'; import { AssetLockPayload } from './AssetLockPayload'; +import { AssetUnlockPayload } from './AssetUnlockPayload'; export namespace Payload { export { ProRegTxPayload }; @@ -14,4 +15,5 @@ export namespace Payload { export { CoinbasePayload }; export { CommitmentTxPayload }; export { AssetLockPayload }; -} \ No newline at end of file + export { AssetUnlockPayload }; +}