From 5dca54a9d80ce3abf479990bb39dbba8ee0075a0 Mon Sep 17 00:00:00 2001 From: x100111010 <167847953+x100111010@users.noreply.github.com> Date: Thu, 10 Oct 2024 01:07:51 +0200 Subject: [PATCH] add mass calculator --- ...s_calculator.dart => mass_calculator.dart} | 91 ++++++- lib/spectre/transaction/types.dart | 4 + test/mass_calculator_test.dart | 235 ++++++++++++++++++ 3 files changed, 317 insertions(+), 13 deletions(-) rename lib/spectre/transaction/{txmass_calculator.dart => mass_calculator.dart} (51%) create mode 100644 test/mass_calculator_test.dart diff --git a/lib/spectre/transaction/txmass_calculator.dart b/lib/spectre/transaction/mass_calculator.dart similarity index 51% rename from lib/spectre/transaction/txmass_calculator.dart rename to lib/spectre/transaction/mass_calculator.dart index 7978f32..3735259 100644 --- a/lib/spectre/transaction/txmass_calculator.dart +++ b/lib/spectre/transaction/mass_calculator.dart @@ -1,30 +1,99 @@ import '../utils.dart'; import 'types.dart'; -bool isCoinBase({required Transaction tx}) => - tx.subnetworkId.hex == kSubnetworkIdCoinbaseHex; +//1 byte for OP_DATA_65 + 64 (length of signature) + 1 byte for sig hash type +const kSignatureSize = 1 + 64 + 1; -class TxMassCalculator { +BigInt _max(BigInt a, BigInt b) => a > b ? a : b; + +enum Kip9Version { + alpha, + beta, +} + +class MassCalculator { final int massPerTxByte; final int massPerScriptPubKeyByte; final int massPerSigOp; + final BigInt storageMassParameter; - static TxMassCalculator get defaultCalculator { - return TxMassCalculator( + static MassCalculator get defaultCalculator { + return MassCalculator( massPerTxByte: 1, massPerScriptPubKeyByte: 10, massPerSigOp: 1000, + storageMassParameter: kStorageMassParameter, ); } - TxMassCalculator({ + MassCalculator({ required this.massPerTxByte, required this.massPerScriptPubKeyByte, required this.massPerSigOp, + required this.storageMassParameter, }); - int calculateMass({required Transaction tx}) { - if (isCoinBase(tx: tx)) { + BigInt calcTxOverallMass({ + required Transaction tx, + Kip9Version version = Kip9Version.beta, + }) { + final computeMass = BigInt.from(calcTxComputeMass(tx: tx)); + final storageMass = calcTxStorageMass(tx: tx); + return switch (version) { + Kip9Version.alpha => computeMass + storageMass, + Kip9Version.beta => _max(computeMass, storageMass), + }; + } + + BigInt calcTxStorageMass({ + required Transaction tx, + Kip9Version version = Kip9Version.beta, + }) { + if (tx.isCoinbase) { + return BigInt.zero; + } + + final harmonicOuts = tx.outputs + .map( + (output) => storageMassParameter ~/ output.value.toUnsignedBigInt(), + ) + .fold( + BigInt.zero, + (total, element) => total + element, + ); + + final outsLen = tx.outputs.length; + final insLen = tx.inputs.length; + + final isRelaxed = + outsLen == 1 || insLen == 1 || (outsLen == 2 && insLen == 2); + if (version == Kip9Version.beta && isRelaxed) { + final harmonicIns = tx.inputs + .map( + (input) => storageMassParameter ~/ input.utxoEntry.amount, + ) + .fold( + BigInt.zero, + (total, element) => total + element, + ); + + return _max(BigInt.zero, harmonicOuts - harmonicIns); + } + + final sumIns = tx.inputs + .map((input) => input.utxoEntry.amount) + .fold(BigInt.zero, (previousValue, element) => previousValue + element); + + final meanIns = sumIns ~/ BigInt.from(insLen); + + final arithmeticIns = + BigInt.from(insLen) * (storageMassParameter ~/ meanIns); + + return _max(BigInt.zero, harmonicOuts - arithmeticIns); + } + + int calcTxComputeMass({required Transaction tx}) { + if (tx.isCoinbase) { return 0; } @@ -52,10 +121,6 @@ class TxMassCalculator { } int txEstimatedSerializedSize({required Transaction tx}) { - if (isCoinBase(tx: tx)) { - return 0; - } - int size = 0; size += 2; // version size += 8; // number of inputs @@ -80,7 +145,7 @@ class TxMassCalculator { int size = 0; size += outpointEstimatedSerializedSize(); // previous outpoint size += 8; // signature script length - size += input.signatureScript.length; // signature script + size += kSignatureSize; // input.signatureScript.length; // signature script size += 8; // sequence (uint64) return size; } diff --git a/lib/spectre/transaction/types.dart b/lib/spectre/transaction/types.dart index 01bb05f..ca9b694 100644 --- a/lib/spectre/transaction/types.dart +++ b/lib/spectre/transaction/types.dart @@ -10,6 +10,9 @@ import '../utils.dart'; part 'types.freezed.dart'; part 'types.g.dart'; +final kSompiPerSpectre = BigInt.from(100000000); +final kStorageMassParameter = kSompiPerSpectre * BigInt.from(10000); + final kMinChangeTarget = BigInt.from(20000000); final kFeePerInput = BigInt.from(10000); const kMaxInputsPerTransaction = 84; @@ -246,6 +249,7 @@ class Transaction with _$Transaction { gas: gas, payload: payload?.hex, ); + bool get isCoinbase => subnetworkId.hex == kSubnetworkIdCoinbaseHex; } @unfreezed diff --git a/test/mass_calculator_test.dart b/test/mass_calculator_test.dart new file mode 100644 index 0000000..29735d4 --- /dev/null +++ b/test/mass_calculator_test.dart @@ -0,0 +1,235 @@ +import 'dart:typed_data'; + +import 'package:fixnum/fixnum.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spectrum/spectre/spectre.dart'; +import 'package:spectrum/spectre/transaction/mass_calculator.dart'; + +void main() { + Transaction generateTxFromAmounts( + Iterable ins, Iterable outs) { + final scriptPublicKey = ScriptPublicKey( + scriptPublicKey: Uint8List(0), + version: 0, + ); + final prevTxId = + '880eb9819a31821d9d2399e2f35e2433b72637e393d71ecc9b8d0250f49153c3'; + final address = Address.publicKey( + prefix: AddressPrefix.spectre, + publicKey: Uint8List(32), + ); + final tx = Transaction( + version: 0, + inputs: ins.indexed + .map( + (indexed) => TxInput( + previousOutpoint: + Outpoint(transactionId: prevTxId, index: indexed.$1), + sequence: Int64(0), + sigOpCount: 0, + signatureScript: Uint8List(0), + address: address, + utxoEntry: UtxoEntry( + amount: indexed.$2, + isCoinbase: false, + blockDaaScore: BigInt.zero, + scriptPublicKey: scriptPublicKey, + ), + ), + ) + .toList(), + outputs: outs + .map( + (out) => TxOutput( + value: out.toInt64(), + scriptPublicKey: scriptPublicKey, + ), + ) + .toList(), + lockTime: Int64(1615462089000), + subnetworkId: Uint8List.fromList( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ), + gas: Int64(0), + ); + + return tx; + } + + group('Test Kip9 Alpha', () { + final testVersion = Kip9Version.alpha; + + test('Test mass storage', () { + final tx = generateTxFromAmounts( + [100, 200, 300].map(BigInt.from), + [300, 300].map(BigInt.from), + ); + + final storageMassParameter = BigInt.from(10).pow(12); + + // Assert the formula: max( 0 , C·( |O|/H(O) - |I|/A(I) ) ) + + final calculator = MassCalculator( + massPerTxByte: 0, + massPerScriptPubKeyByte: 0, + massPerSigOp: 0, + storageMassParameter: storageMassParameter, + ); + + final storageMass = calculator.calcTxStorageMass( + tx: tx, + version: testVersion, + ); + // Compounds from 3 to 2, with symmetric outputs and no fee, should be zero + expect(storageMass, BigInt.zero); + }); + test('Test mass storage asymmetry', () { + // Create asymmetry + final tx = generateTxFromAmounts( + [100, 200, 300].map(BigInt.from), + [50, 550].map(BigInt.from), + ); + final storageMassParameter = BigInt.from(10).pow(12); + + final calculator = MassCalculator( + massPerTxByte: 0, + massPerScriptPubKeyByte: 0, + massPerSigOp: 0, + storageMassParameter: storageMassParameter, + ); + + final storageMass = + calculator.calcTxStorageMass(tx: tx, version: testVersion); + expect( + storageMass, + storageMassParameter ~/ BigInt.from(50) + + storageMassParameter ~/ BigInt.from(550) - + BigInt.from(3) * (storageMassParameter ~/ BigInt.from(200)), + ); + }); + + test('Test mass storage more outs than ins', () { + // Create a tx with more outs than ins + final baseValue = BigInt.from(10000) * kSompiPerSpectre; + final tx = generateTxFromAmounts( + [baseValue, baseValue, baseValue * BigInt.two], + List.filled(4, baseValue)); + final storageMassParameter = kStorageMassParameter; + final calculator = MassCalculator( + massPerTxByte: 0, + massPerScriptPubKeyByte: 0, + massPerSigOp: 0, + storageMassParameter: storageMassParameter, + ); + final storageMass = + calculator.calcTxStorageMass(tx: tx, version: testVersion); + + // Inputs are above C so they don't contribute negative mass, 4 outputs exactly equal C each charge 1 + expect(storageMass, BigInt.from(4)); + }); + test('Test mass storage less outs than ins 2', () { + final baseValue = BigInt.from(10000) * kSompiPerSpectre; + final tx = generateTxFromAmounts( + [baseValue, baseValue, baseValue * BigInt.two], + [BigInt.from(10) * kSompiPerSpectre, ...List.filled(3, baseValue)]); + final storageMassParameter = kStorageMassParameter; + final calculator = MassCalculator( + massPerTxByte: 0, + massPerScriptPubKeyByte: 0, + massPerSigOp: 0, + storageMassParameter: storageMassParameter, + ); + final storageMass = calculator.calcTxStorageMass( + tx: tx, + version: testVersion, + ); + expect(storageMass, BigInt.from(1003)); + }); + + test('Test mass storage increase values over the limit', () { + // Increase values over the lim + final baseValue = BigInt.from(10000) * kSompiPerSpectre; + final tx = generateTxFromAmounts( + [baseValue, baseValue, baseValue * BigInt.two], + List.filled(4, baseValue + BigInt.one)); + final storageMassParameter = kStorageMassParameter; + final calculator = MassCalculator( + massPerTxByte: 0, + massPerScriptPubKeyByte: 0, + massPerSigOp: 0, + storageMassParameter: storageMassParameter, + ); + final storageMass = calculator.calcTxStorageMass( + tx: tx, + version: testVersion, + ); + expect(storageMass, BigInt.zero); + }); + }); + + group('Test Kip9 Beta', () { + final testVersion = Kip9Version.beta; + + test('Test 2:2 transaction', () { + final tx = generateTxFromAmounts( + [100, 200].map(BigInt.from), + [50, 250].map(BigInt.from), + ); + final storageMassParameter = BigInt.from(10).pow(12); + // Assert the formula: max( 0 , C·( |O|/H(O) - |I|/O(I) ) ) + + final calculator = MassCalculator( + massPerTxByte: 0, + massPerScriptPubKeyByte: 0, + massPerSigOp: 0, + storageMassParameter: storageMassParameter, + ); + final storageMass = calculator.calcTxStorageMass( + tx: tx, + version: testVersion, + ); + expect(storageMass, BigInt.from(9000000000)); + }); + test('Test outputs equal to inputs', () { + final tx = generateTxFromAmounts( + [100, 200].map(BigInt.from), + [100, 200].map(BigInt.from), + ); + final storageMassParameter = BigInt.from(10).pow(12); + // Assert the formula: max( 0 , C·( |O|/H(O) - |I|/O(I) ) ) + + final calculator = MassCalculator( + massPerTxByte: 0, + massPerScriptPubKeyByte: 0, + massPerSigOp: 0, + storageMassParameter: storageMassParameter, + ); + final storageMass = calculator.calcTxStorageMass( + tx: tx, + version: testVersion, + ); + expect(storageMass, BigInt.zero); + }); + + test('Test mass storage one small output', () { + final tx = generateTxFromAmounts( + [100, 200].map(BigInt.from), + [50].map(BigInt.from), + ); + final storageMassParameter = BigInt.from(10).pow(12); + // Assert the formula: max( 0 , C·( |O|/H(O) - |I|/O(I) ) ) + + final calculator = MassCalculator( + massPerTxByte: 0, + massPerScriptPubKeyByte: 0, + massPerSigOp: 0, + storageMassParameter: storageMassParameter, + ); + final storageMass = calculator.calcTxStorageMass( + tx: tx, + version: testVersion, + ); + expect(storageMass, BigInt.from(5000000000)); + }); + }); +}