From 8e77ce329de061e8b7fc0564328fda16e3f889c8 Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Fri, 20 Sep 2024 09:17:40 +0800 Subject: [PATCH] chore: fix npm publish (#3) * chore: fix npm publish * fix: unspentoutput number * chore: release version 1.0.0-alpha.2 * fix: fee estimate * fix: fee error * fix: output change * chore: release version 1.0.0-alpha.5 * chore: release version 1.0.0 * support: fee * release 1.0.1 --- .github/workflows/package-publish.yml | 2 +- index.d.ts | 15 +-- lib/crypto/bn.js | 12 ++- lib/privatekey.js | 2 +- lib/transaction/output.js | 17 ++-- lib/transaction/transaction.js | 139 +++++++++++++++++++------- lib/transaction/unspentoutput.js | 17 +++- lib/util/js.js | 23 ++++- package.json | 4 +- 9 files changed, 162 insertions(+), 69 deletions(-) diff --git a/.github/workflows/package-publish.yml b/.github/workflows/package-publish.yml index 02e4287..b43979c 100644 --- a/.github/workflows/package-publish.yml +++ b/.github/workflows/package-publish.yml @@ -11,6 +11,6 @@ jobs: with: node-version: '18' registry-url: 'https://registry.npmjs.org' - - run: npm publish -y --no-verify-access + - run: npm publish -y --no-verify-access --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 412a2fb..7a36e26 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,4 @@ -declare module '@onekeyfe/kaspacore-lib' { +declare module '@onekeyfe/kaspa-core-lib' { function initRuntime(): Promise; function setDebugLevel(level:number):void; @@ -93,7 +93,7 @@ declare module '@onekeyfe/kaspacore-lib' { export namespace Transaction { static class sighash { - static sign(transaction, privateKey, sighashType, inputIndex, subscript, satoshisBN, flags, signingMethod); + // static sign(transaction, privateKey, sighashType, inputIndex, subscript, satoshisBN, flags, signingMethod); static sighash(transaction, sighashType, inputNumber, subscript, satoshisBN, flags): Buffer; } class UnspentOutput { @@ -103,7 +103,7 @@ declare module '@onekeyfe/kaspacore-lib' { readonly txId: string; readonly outputIndex: number; readonly script: Script; - readonly satoshis: number; + readonly satoshis: number | string; constructor(data: object); @@ -114,7 +114,8 @@ declare module '@onekeyfe/kaspacore-lib' { class Output { readonly script: Script; - readonly satoshis: number; + readonly satoshis: number | string; + readonly satoshisBN: crypto.BN; constructor(data: object); @@ -149,12 +150,12 @@ declare module '@onekeyfe/kaspacore-lib' { constructor(serialized ? : any); from(utxos: Transaction.UnspentOutput[]): this; - to(address: Address[] | Address | string, amount: number): this; + to(address: Address[] | Address | string, amount: number | string): this; change(address: Address | string): this; fee(amount: number): this; setVersion(version: number): this; feePerKb(amount: number): this; - sign(privateKey: PrivateKey|PrivateKey[] | string|string[], sigtype:number, signingMethod:string|undefined): this; + // sign(privateKey: PrivateKey|PrivateKey[] | string|string[], sigtype:number, signingMethod:string|undefined): this; applySignature(sig: crypto.Signature): this; addInput(input: Transaction.Input): this; addOutput(output: Transaction.Output): this; @@ -163,6 +164,8 @@ declare module '@onekeyfe/kaspacore-lib' { lockUntilBlockHeight(height: number): this; getMassAndSize():{txSize:number, mass:number}; + calcStorageMass():number; + calcComputeMass():number; hasWitnesses(): boolean; getFee(): number; diff --git a/lib/crypto/bn.js b/lib/crypto/bn.js index c1ff5e9..947131b 100644 --- a/lib/crypto/bn.js +++ b/lib/crypto/bn.js @@ -17,11 +17,15 @@ BN.One = new BN(1); BN.Minus1 = new BN(-1); BN.fromNumber = function(n) { - $.checkArgument(_.isNumber(n)); - if(n <= 0x1fffffffffffff){ - return new BN(n); + if (_.isNumber(n)) { + if (n <= 0x1fffffffffffff) { + return new BN(n); + } + return new BN(n.toString(16), 16); + } else if (_.isString(n)) { + return new BN(n, 10); } - return new BN(n.toString(16), 16); + throw new Error('Invalid input type'); }; BN.fromString = function(str, base) { diff --git a/lib/privatekey.js b/lib/privatekey.js index a64bf40..9503fd3 100644 --- a/lib/privatekey.js +++ b/lib/privatekey.js @@ -379,7 +379,7 @@ PrivateKey.prototype.toPublicKey = function () { if (!this._pubkey) { //this._pubkey = PublicKey.fromPrivateKey(this); let publicKey = secp256k1.getPublicKey(this.toString(), true); - console.log("publicKey", Buffer.from(publicKey).toString('hex')); + // console.log("publicKey", Buffer.from(publicKey).toString('hex')); this._pubkey = new PublicKey(Buffer.from(publicKey), { network: this.network.name, }); diff --git a/lib/transaction/output.js b/lib/transaction/output.js index 7a730ab..23d4503 100644 --- a/lib/transaction/output.js +++ b/lib/transaction/output.js @@ -57,17 +57,17 @@ Object.defineProperty(Output.prototype, 'satoshis', { set: function(num) { if (num instanceof BN) { this._satoshisBN = num; - this._satoshis = num.toNumber(); + this._satoshis = num.toString(); } else if (_.isString(num)) { - this._satoshis = parseInt(num); - this._satoshisBN = BN.fromNumber(this._satoshis); + this._satoshisBN = BN.fromString(num); + this._satoshis = num; } else { $.checkArgument( JSUtil.isNaturalNumber(num), 'Output satoshis is not a natural number' ); this._satoshisBN = BN.fromNumber(num); - this._satoshis = num; + this._satoshis = BN.fromNumber(num).toString(); } $.checkState( JSUtil.isNaturalNumber(this._satoshis), @@ -77,13 +77,10 @@ Object.defineProperty(Output.prototype, 'satoshis', { }); Output.prototype.invalidSatoshis = function() { - if (this._satoshis > MAX_SAFE_INTEGER) { - return 'transaction txout satoshis greater than max safe integer'; - } - if (this._satoshis !== this._satoshisBN.toNumber()) { + if(!BN.fromString(this._satoshis).eq(this._satoshisBN)) { return 'transaction txout satoshis has corrupted value'; } - if (this._satoshis < 0) { + if(BN.fromString(this._satoshis).lt(BN.Zero)) { return 'transaction txout negative'; } return false; @@ -163,7 +160,7 @@ Output.prototype.inspect = function() { Output.fromBufferReader = function(br) { var obj = {}; - obj.satoshis = br.readUInt64LEBN(); + obj.satoshis = br.readUInt64LEBN().toString(); var version = br.readUInt16LE(); var size = br.readUInt64LEBN().toNumber(); if (size !== 0) { diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 9e60a9e..9afaf40 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -106,6 +106,7 @@ Transaction.NLOCKTIME_MAX_VALUE = 4294967295; // Value used for fee estimation (satoshis per kilobyte) Transaction.FEE_PER_KB = 100000; +Transaction.STORAGE_MASS = new BN(10).pow(new BN(12)); // Safe upper bound for change address script size in bytes @@ -141,12 +142,12 @@ var ioProperty = { configurable: false, enumerable: true, get: function() { - return this._getInputAmount(); + return this._getInputAmount().toString(); } }; Object.defineProperty(Transaction.prototype, 'inputAmount', ioProperty); ioProperty.get = function() { - return this._getOutputAmount(); + return this._getOutputAmount().toString(); }; Object.defineProperty(Transaction.prototype, 'outputAmount', ioProperty); @@ -226,7 +227,7 @@ Transaction.prototype.getSerializationError = function(opts) { var unspent = this._getUnspentValue(); var unspentError; - if (unspent < 0) { + if (unspent.lt(BN.Zero)) { if (!opts.disableMoreOutputThanInput) { unspentError = new errors.Transaction.InvalidOutputAmountSum(); } @@ -249,7 +250,7 @@ Transaction.prototype._hasFeeError = function(opts, unspent) { if (!opts.disableLargeFees) { var maximumFee = Math.floor(Transaction.FEE_SECURITY_MARGIN * this._estimateFee()); - if (unspent > maximumFee) { + if (unspent.gt(new BN(maximumFee))) { if (this._missingChange()) { return new errors.Transaction.ChangeAddressMissing( 'Fee is too large and no change address was provided' @@ -262,7 +263,7 @@ Transaction.prototype._hasFeeError = function(opts, unspent) { } if (!opts.disableSmallFees) { var minimumFee = Math.ceil(this._estimateFee() / Transaction.FEE_SECURITY_MARGIN); - if (unspent < minimumFee) { + if (unspent.lt(new BN(minimumFee))) { return new errors.Transaction.FeeError.TooSmall( 'expected more than ' + minimumFee + ' but got ' + unspent ); @@ -381,17 +382,18 @@ Transaction.prototype.getStandaloneMass = function() { } } Transaction.prototype.getMassAndSize = function() { - let {txSize, mass:standaloneMass} = this.getStandaloneMass(); - let sigOpsCount = 0; - this.inputs.forEach(input=>{ - //console.log("input.script", input.output.script.toASM()) - sigOpsCount += input.output.script.getSignatureOperationsCount() - }); + let {txSize} = this.getStandaloneMass(); + + let storageMass = this.calcStorageMass(); + let computeMass = this.calcComputeMass(); - //console.log("standaloneMass", standaloneMass) - //console.log("sigOpsCount", sigOpsCount) - //console.log("total MASS ", standaloneMass + sigOpsCount * Transaction.MassPerSigOp) - return {txSize, mass: standaloneMass + sigOpsCount * Transaction.MassPerSigOp}; + // console.log("storageMass", storageMass) + // console.log("computeMass", computeMass) + // alpha version 11 + // mass = storageMass + computeMass + + // current version 10 + return {txSize, mass: Math.max(storageMass, computeMass)}; } Transaction.prototype.fromBuffer = function(buffer) { var reader = new BufferReader(buffer); @@ -931,36 +933,34 @@ Transaction.prototype._addOutput = function(output) { /** * Calculates or gets the total output amount in satoshis * - * @return {Number} the transaction total output amount + * @return {BN} the transaction total output amount */ Transaction.prototype._getOutputAmount = function() { if (_.isUndefined(this._outputAmount)) { - var self = this; - this._outputAmount = 0; - _.each(this.outputs, function(output) { - self._outputAmount += output.satoshis; - }); + this._outputAmount = this.outputs.reduce((sum, output) => { + return sum.add(new BN(output.satoshis)); + }, new BN(0)); } return this._outputAmount; -}; + }; /** * Calculates or gets the total input amount in satoshis * - * @return {Number} the transaction total input amount + * @return {BN} the transaction total input amount */ Transaction.prototype._getInputAmount = function() { if (_.isUndefined(this._inputAmount)) { - this._inputAmount = _.sumBy(this.inputs, function(input) { - if (_.isUndefined(input.output)) { - throw new errors.Transaction.Input.MissingPreviousOutput(); - } - return input.output.satoshis; - }); + this._inputAmount = this.inputs.reduce((sum, input) => { + if (_.isUndefined(input.output)) { + throw new errors.Transaction.Input.MissingPreviousOutput(); + } + return sum.add(new BN(input.output.satoshis)); + }, new BN(0)); } return this._inputAmount; -}; + }; Transaction.prototype._updateChangeOutput = function() { if (!this._changeScript) { @@ -972,8 +972,8 @@ Transaction.prototype._updateChangeOutput = function() { } var available = this._getUnspentValue(); var fee = this.getFee(); - var changeAmount = available - fee; - if (changeAmount > 0) { + var changeAmount = available.sub(new BN(fee)); + if (changeAmount.gt(BN.Zero)) { this._changeIndex = this.outputs.length; this._addOutput(new Output({ script: this._changeScript, @@ -1010,7 +1010,7 @@ Transaction.prototype.getFee = function() { } // if no change output is set, fees should equal all the unspent amount if (!this._changeScript) { - return this._getUnspentValue(); + return this._getUnspentValue().toNumber(); } return this._estimateFee(); }; @@ -1028,14 +1028,14 @@ Transaction.prototype._estimateFee = function() { } var fee = Math.ceil(getFee(estimatedSize)); var feeWithChange = Math.ceil(getFee(estimatedSize) + getFee(Transaction.CHANGE_OUTPUT_MAX_SIZE)); - if (!this._changeScript || available <= feeWithChange) { + if (!this._changeScript || available.lte(BN.fromNumber(feeWithChange))) { return fee; } return feeWithChange; }; Transaction.prototype._getUnspentValue = function() { - return this._getInputAmount() - this._getOutputAmount(); + return this._getInputAmount().sub(this._getOutputAmount()); }; Transaction.prototype._clearSignatures = function() { @@ -1044,14 +1044,33 @@ Transaction.prototype._clearSignatures = function() { }); }; +// https://github.com/kaspanet/rusty-kaspa/blob/613d082b0c2e51247819d932af7ddb5ebd5aa460/consensus/core/src/mass/mod.rs#L23-L42 Transaction.prototype._estimateSize = function() { - var result = Transaction.MAXIMUM_EXTRA_SIZE; + var result = 0; + result += 2; // Tx version (u16) + result += 8; // Number of inputs (u64) _.each(this.inputs, function(input) { + if(input._scriptBuffer.length === 0){ + result += 65; // script + } result += input._estimateSize(); }); + + result += 8; // Number of outputs (u64) _.each(this.outputs, function(output) { - result += output.script.toBuffer().length + 9; + result += 8; // value (u64) + result += 2; // output.ScriptPublicKey.Version (u16) + result += 8; // length of script public key (u64) + result += output.script.toBuffer().length; }); + + result += 8; // lock time (u64) + result += 20; // subnetwork id + result += 8; // gas (u64) + result += 32; // payload hash + + result += 8; // length of the payload (u64) + result += 32; // payload hash return result; }; @@ -1382,5 +1401,51 @@ Transaction.prototype.setVersion = function(version) { return this; }; +function negativeMass(inputs, outputs_num) { + const inputs_num = inputs.length; + + if (outputs_num === 1 || inputs_num === 1 || (outputs_num === 2 && inputs_num === 2)) { + return inputs.reduce((sum, v) => sum.add(Transaction.STORAGE_MASS.div(BN.fromNumber(v.output.satoshis))), new BN(0)); + } + + const sumInputs = inputs.reduce((sum, v) => sum.add(BN.fromNumber(v.output.satoshis)), new BN(0)); + const avgInput = sumInputs.div(new BN(inputs_num)); + return new BN(inputs_num).mul(Transaction.STORAGE_MASS.div(avgInput)); +} + +Transaction.prototype.calcStorageMass = function() { + const N = negativeMass(this.inputs, this.outputs.length); + const P = this.outputs.reduce((sum, o) => + sum.add(Transaction.STORAGE_MASS.div(BN.fromNumber(o.satoshis)) + ), new BN(0)); + return BN.max(P.sub(N), new BN(0)); +} + +Transaction.prototype.calcComputeMass = function() { + // 如果是coinbase交易,返回0 + if (this.isCoinbase()) { + return 0; + } + + // 计算序列化大小 + const size = this._estimateSize(); + + // 计算序列化交易的mass + let massForSize = size * Transaction.MassPerTxByte; + + // 计算所有输出的scriptPublicKey的mass + const totalScriptPublicKeySize = this.outputs.reduce((sum, output) => { + return sum + 2 + output._scriptBuffer.length; + }, 0); + const totalScriptPublicKeyMass = totalScriptPublicKeySize * Transaction.MassPerScriptPubKeyByte; + + // 计算所有输入的sigOpCount的mass + const totalSigops = this.inputs.reduce((sum, input) => { + return sum + input.output.script.getSignatureOperationsCount(); + }, 0); + const totalSigopsMass = totalSigops * Transaction.MassPerSigOp; + + return Math.floor(massForSize + totalScriptPublicKeyMass + totalSigopsMass); +}; module.exports = Transaction; \ No newline at end of file diff --git a/lib/transaction/unspentoutput.js b/lib/transaction/unspentoutput.js index 801a8fc..ba01116 100644 --- a/lib/transaction/unspentoutput.js +++ b/lib/transaction/unspentoutput.js @@ -7,6 +7,7 @@ var JSUtil = require('../util/js'); var Script = require('../script'); var Address = require('../address'); var Unit = require('../unit'); +var BN = require('../crypto/bn'); /** * Represents an unspent output information: its script, associated amount and address, @@ -21,7 +22,7 @@ var Unit = require('../unit'); * @param {string|Script} data.scriptPubKey the script that must be resolved to release the funds * @param {string|Script=} data.script alias for `scriptPubKey` * @param {number} data.amount amount of bitcoins associated - * @param {number=} data.satoshis alias for `amount`, but expressed in satoshis (1 BTC = 1e8 satoshis) + * @param {string|number=} data.satoshis alias for `amount`, but expressed in satoshis (1 BTC = 1e8 satoshis) * @param {string|Address=} data.address the associated address to the script, if provided */ function UnspentOutput(data) { @@ -46,14 +47,22 @@ function UnspentOutput(data) { var script = new Script(data.scriptPubKey || data.script); $.checkArgument(!_.isUndefined(data.amount) || !_.isUndefined(data.satoshis), 'Must provide an amount for the output'); - var amount = !_.isUndefined(data.amount) ? new Unit.fromBTC(data.amount).toSatoshis() : data.satoshis; - $.checkArgument(_.isNumber(amount), 'Amount must be a number'); + + var amount; + if (!_.isUndefined(data.amount)) { + amount = BN.fromNumber(data.amount); + amount = amount.mul(new BN(1e8)).toString(); + } else { + amount = BN.fromNumber(data.satoshis).toString(); + } + $.checkArgument(!amount.includes('.'), 'Amount must be a number'); + JSUtil.defineImmutable(this, { address: address, txId: txId, outputIndex: outputIndex, script: script, - satoshis: amount + satoshis: amount.toString() }); } diff --git a/lib/util/js.js b/lib/util/js.js index af9395c..acf6f40 100644 --- a/lib/util/js.js +++ b/lib/util/js.js @@ -2,6 +2,9 @@ var _ = require('lodash'); var logLevels = require('./log').logLevels; +var BN = require('../crypto/bn'); + +const UINT64_MAX = new BN('18446744073709551615'); /** * Determines whether a string contains only hexadecimal values @@ -85,10 +88,22 @@ const JSUtil = { * @return {Boolean} */ isNaturalNumber: function isNaturalNumber(value) { - return typeof value === 'number' && - isFinite(value) && - Math.floor(value) === value && - value >= 0; + if (typeof value === 'string') { + if (!/^\d*$/.test(value)) { + return false; + } + } + + if(typeof value === 'number' || typeof value === 'string'){ + try { + var bnValue = BN.fromNumber(value); + return bnValue.lte(UINT64_MAX); + } catch (e) { + return false; + } + } + + return false; } }; diff --git a/package.json b/package.json index 0f0a0aa..6ac0849 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@onekeyfe/kaspacore-lib", - "version": "1.0.0-alpha.1", + "name": "@onekeyfe/kaspa-core-lib", + "version": "1.0.1", "main": "index.js", "authors": [ {