diff --git a/lib/src/payments/p2ms.dart b/lib/src/payments/p2ms.dart new file mode 100644 index 0000000..19b24e1 --- /dev/null +++ b/lib/src/payments/p2ms.dart @@ -0,0 +1,205 @@ +import '../utils/constants/op.dart'; +import 'package:meta/meta.dart'; +import '../utils/script.dart' as bscript; +import '../models/networks.dart'; +import 'dart:typed_data'; +import 'package:bip32/src/utils/ecurve.dart' show isPoint; + +class P2MS { + P2MSData data; + NetworkType network; + List _chunks; + bool _isDecoded; + + P2MS({@required data}) { + this.data = data; + this.network = network ?? bitcoin; + this._isDecoded = false; + _init(); + } + void _init() { + _enoughInformation(data); + _extraValidation(data.options); + _setNetwork(this.network); + _extendedValidation(); + } + + void _enoughInformation(data) { + if (data.input == null && + data.output == null && + !((data.pubkeys != null) && (data.m != null)) && + data.signatures == null) { + throw new ArgumentError('Not enough data'); + } + } + void _setNetwork(network){ + this.network == network; + } + void _extraValidation(options) { + if(data.options == null){data.options = {};} + data.options['validate'] = true; + } + + void _extendedValidation(){ + if(data.options['validate']==true){ + _check(); + } + } + void _decode(output){ + + if(_isDecoded) {return;} + else{ + _isDecoded = true; + _chunks = bscript.decompile(output); + data.m = _chunks[0] - OPS['OP_RESERVED']; + data.n = _chunks[_chunks.length - 2] - OPS['OP_RESERVED']; + data.pubkeys = _chunks.sublist(1,_chunks.length-2); + } + } + void _setOutput(){ + if (data.m == null){ return;} + if (data.n == null) {return;} + if (data.pubkeys == null) {return;} + List list = [OPS['OP_RESERVED']+data.m]; + data.pubkeys.forEach((pubkey) => list.add(pubkey)); + list.add(OPS['OP_RESERVED'] + data.n); + list.add(OPS['OP_CHECKMULTISIG']); + data.output = bscript.compile(list); + } + void _setSigs(){ + if (data.input == null) {return;} + var list = bscript.decompile(data.input); + list.removeAt(0); + List _chunks = []; + + for (var i = 0; i < list.length; i++) { + dynamic temp = list[i]; + if(list[i] is int ){ + List temp1 = []; + temp1.add(list[i]); + temp = Uint8List.fromList(temp1); + } + _chunks.add(temp); + } + + data.signatures = _chunks; + } + void _setInput(){ + if (data.signatures == null) {return;} + String tempString = 'OP_0 '; + List tempsignatures = []; + for (var i = 0; i < data.signatures.length; i++) { + if(data.signatures[i].toString()=='[0]'){ + tempString = tempString + 'OP_0 '; + }else{ + tempsignatures.add(data.signatures[i]); + } + } + tempString = tempString+(bscript.toASM(tempsignatures)); + Uint8List tempList = bscript.fromASM(tempString); + data.input = bscript.compile(tempList); + } + void _setWitness(){ + if (data.input == null && data.input == null) {return;} + List temp = []; + data.witness = temp; + } + + void _setM(){ + if (data.output == null) {return;} + _decode(data.output); + } + void _setN(){ + _setPubkeys(); + if (data.pubkeys == null) {return;} + data.n= data.pubkeys.length; + } + void _setPubkeys(){ + if (data.output == null) {return;} + _decode(data.output); + } + bool _stacksEqual(a, b) { + if (a.length != b.length) {return false;} + for (int i = 0; i <= a.length-1; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } + bool _isAcceptableSignature(signature, options) { + return (bscript.isCanonicalScriptSignature(signature) || + ((options['allowIncomplete'] == true) && (signature[0] == 0))); + } + void _check(){ + if (data.output != null){ + final tempChunks = bscript.decompile(data.output); + if (tempChunks[0] == null) {throw new ArgumentError('Output is invalid');} + if (tempChunks.length < 2 ) {throw new ArgumentError('Output is invalid');} + if (tempChunks[tempChunks.length - 1] != OPS['OP_CHECKMULTISIG']) {throw new ArgumentError('Output is invalid');} + _decode(data.output); + if(data.m <= 0 || + data.n > 16 || + data.m > data.n || + data.n != _chunks.length - 3) {throw new ArgumentError('Output is invalid');} + if (!data.pubkeys.every((x) => isPoint(x))) {throw new ArgumentError('Output is invalid');} + if (data.m != null && data.m != data.m) {throw new ArgumentError('m mismatch');} + if (data.n != null && data.n != data.n) {throw new ArgumentError('n mismatch');} + if (data.pubkeys != null && !_stacksEqual(data.pubkeys, data.pubkeys)) {throw new ArgumentError('Pubkeys mismatch');} + } + if (data.pubkeys != null){ + if (data.n != null && data.n != data.pubkeys.length) {throw new ArgumentError('Pubkey count mismatch');} + data.n = data.pubkeys.length; + _setOutput(); + _setM(); + _setN(); + if (data.n < data.m) {throw new ArgumentError('Pubkey count cannot be less than m');} + } + if (data.signatures != null) { + _setSigs(); + _setInput(); + if (data.signatures.length < data.m) {throw new ArgumentError('Not enough signatures provided');} + if (data.signatures.length > data.m) {throw new ArgumentError('Too many signatures provided');} + } + + + if (data.input != null) { + if (data.input[0] != OPS['OP_0']) {throw new ArgumentError('Input is invalid');} + _setSigs(); + if (data.signatures.length == 0 || !data.signatures.every((x) => _isAcceptableSignature(x,data.options))) + {throw new ArgumentError('Input has invalid signature(s)');} + if (data.signatures != null&& !_stacksEqual(data.signatures,data.signatures)) {throw new ArgumentError('Signature mismatch');} + if (data.m != null && data.m != data.signatures.length) {throw new ArgumentError('Signature count mismatch');} + } + _setInput(); + _setWitness(); + } + + +} + + +class P2MSData { + int m; + int n; + Uint8List output; + Uint8List input; + List pubkeys; + List signatures; + List witness; + Map options; + + P2MSData( + {this.m, + this.n, + this.output, + this.input, + this.pubkeys, + this.signatures, + this.witness, + this.options}); + @override + String toString() { + return 'P2MSData{m: $m, n: $n, output: $output, input: $input, pubkeys: $pubkeys, sigs: $signatures, options: $signatures, witness: $witness}'; + } +} diff --git a/test/fixtures/p2ms.json b/test/fixtures/p2ms.json new file mode 100644 index 0000000..5407548 --- /dev/null +++ b/test/fixtures/p2ms.json @@ -0,0 +1,352 @@ +{ + "valid": [ + { + "description": "output from output", + "arguments": { + "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG" + }, + "options": {}, + "expected": { + "m": 2, + "n": 2, + "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG", + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000002" + ], + "signatures": null, + "input": null, + "witness": null + } + }, + { + "description": "output from m/pubkeys", + "arguments": { + "m": 1, + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000002" + ] + }, + "expected": { + "m": 1, + "n": 2, + "output": "OP_1 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG", + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000002" + ], + "signatures": null, + "input": null, + "witness": null + } + }, + { + "description": "input/output from m/pubkeys/signatures", + "arguments": { + "m": 2, + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000002", + "030000000000000000000000000000000000000000000000000000000000000003" + ], + "signatures": [ + "300602010002010001", + "300602010102010001" + ] + }, + "expected": { + "m": 2, + "n": 3, + "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 030000000000000000000000000000000000000000000000000000000000000003 OP_3 OP_CHECKMULTISIG", + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000002", + "030000000000000000000000000000000000000000000000000000000000000003" + ], + "signatures": [ + "300602010002010001", + "300602010102010001" + ], + "input": "OP_0 300602010002010001 300602010102010001", + "witness": [] + } + }, + { + "description": "input/output from output/signatures", + "arguments": { + "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 030000000000000000000000000000000000000000000000000000000000000003 OP_3 OP_CHECKMULTISIG", + "signatures": [ + "300602010002010001", + "300602010102010001" + ] + }, + "expected": { + "m": 2, + "n": 3, + "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 030000000000000000000000000000000000000000000000000000000000000003 OP_3 OP_CHECKMULTISIG", + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000002", + "030000000000000000000000000000000000000000000000000000000000000003" + ], + "signatures": [ + "300602010002010001", + "300602010102010001" + ], + "input": "OP_0 300602010002010001 300602010102010001", + "witness": [] + } + }, + { + "description": "input/output from input/output", + "arguments": { + "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 030000000000000000000000000000000000000000000000000000000000000003 OP_3 OP_CHECKMULTISIG", + "input": "OP_0 300602010002010001 300602010102010001" + }, + "expected": { + "m": 2, + "n": 3, + "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 030000000000000000000000000000000000000000000000000000000000000003 OP_3 OP_CHECKMULTISIG", + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000002", + "030000000000000000000000000000000000000000000000000000000000000003" + ], + "signatures": [ + "300602010002010001", + "300602010102010001" + ], + "input": "OP_0 300602010002010001 300602010102010001", + "witness": [] + } + }, + { + "description": "input/output from input/output, even if incomplete", + "arguments": { + "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG", + "input": "OP_0 OP_0 300602010102010001" + }, + "options": { + "allowIncomplete": true + }, + "expected": { + "m": 2, + "n": 2, + "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG", + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000002" + ], + "signatures": [ + 0, + "300602010102010001" + ], + "input": "OP_0 OP_0 300602010102010001", + "witness": [] + } + }, + { + "description": "input/output from output/signatures, even if incomplete", + "arguments": { + "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG", + "signatures": [ + 0, + "300602010102010001" + ] + }, + "options": { + "allowIncomplete": true + }, + "expected": { + "m": 2, + "n": 2, + "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG", + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000002" + ], + "signatures": [ + 0, + "300602010102010001" + ], + "input": "OP_0 OP_0 300602010102010001", + "witness": [] + } + } + ], + "invalid": [ + { + "exception": "Not enough data", + "arguments": {} + }, + { + "exception": "Not enough data", + "arguments": { + "m": 2 + } + }, + { + "exception": "Not enough data", + "arguments": { + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000002" + ] + } + }, + { + "description": "Non OP_INT chunk (m)", + "exception": "Output is invalid", + "arguments": { + "output": "OP_RESERVED" + } + }, + { + "description": "Non OP_INT chunk (n)", + "exception": "Output is invalid", + "arguments": { + "output": "OP_1 OP_RESERVED" + } + }, + { + "description": "Missing OP_CHECKMULTISIG", + "exception": "Output is invalid", + "arguments": { + "output": "OP_1 OP_2 OP_RESERVED" + } + }, + { + "description": "m is 0", + "exception": "Output is invalid", + "arguments": { + "output": "OP_0 OP_2 OP_CHECKMULTISIG" + } + }, + { + "description": "n is 0 (m > n)", + "exception": "Output is invalid", + "arguments": { + "output": "OP_2 OP_0 OP_CHECKMULTISIG" + } + }, + { + "description": "m > n", + "exception": "Output is invalid", + "arguments": { + "output": "OP_3 OP_2 OP_CHECKMULTISIG" + } + }, + { + "description": "n !== output pubkeys", + "exception": "Output is invalid", + "options": {}, + "arguments": { + "output": "OP_1 030000000000000000000000000000000000000000000000000000000000000001 OP_2 OP_CHECKMULTISIG" + } + }, + { + "description": "Non-canonical output public key", + "exception": "Output is invalid", + "arguments": { + "output": "OP_1 ffff OP_1 OP_CHECKMULTISIG" + } + }, + { + "exception": "n mismatch", + "arguments": { + "n": 2, + "output": "OP_1 030000000000000000000000000000000000000000000000000000000000000001 OP_1 OP_CHECKMULTISIG" + } + }, + { + "exception": "m mismatch", + "arguments": { + "m": 2, + "output": "OP_1 030000000000000000000000000000000000000000000000000000000000000001 OP_1 OP_CHECKMULTISIG" + } + }, + { + "exception": "Pubkeys mismatch", + "options": {}, + "arguments": { + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001" + ], + "output": "OP_1 030000000000000000000000000000000000000000000000000000000000000002 OP_1 OP_CHECKMULTISIG" + } + }, + { + "exception": "Pubkey count mismatch", + "arguments": { + "m": 2, + "n": 3, + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000002" + ] + } + }, + { + "exception": "Pubkey count cannot be less than m", + "arguments": { + "m": 4, + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000001" + ] + } + }, + { + "exception": "Not enough signatures provided", + "arguments": { + "m": 2, + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000001" + ], + "signatures": [ + "300602010002010001" + ] + } + }, + { + "exception": "Too many signatures provided", + "arguments": { + "m": 2, + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000001" + ], + "signatures": [ + "300602010002010001", + "300602010002010001", + "300602010002010001" + ] + } + }, + { + "description": "Missing OP_0", + "exception": "Input is invalid", + "options": {}, + "arguments": { + "m": 2, + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001", + "030000000000000000000000000000000000000000000000000000000000000001" + ], + "input": "OP_RESERVED" + } + }, + { + "exception": "Input has invalid signature(s)", + "arguments": { + "m": 1, + "pubkeys": [ + "030000000000000000000000000000000000000000000000000000000000000001" + ], + "input": "OP_0 ffffffffffffffff" + } + } + ] + } \ No newline at end of file diff --git a/test/payments/p2ms_test.dart b/test/payments/p2ms_test.dart new file mode 100644 index 0000000..91aa4d8 --- /dev/null +++ b/test/payments/p2ms_test.dart @@ -0,0 +1,87 @@ +import 'package:bitcoin_flutter/src/payments/p2ms.dart'; +import 'package:test/test.dart'; +import 'package:bitcoin_flutter/src/utils/script.dart' as bscript; +import 'dart:io'; +import 'dart:convert'; +import 'package:hex/hex.dart'; +import 'dart:typed_data'; + +main() { + final fixtures = json.decode(new File("./test/fixtures/p2ms.json").readAsStringSync(encoding: utf8)); + group('(valid case)', () { + (fixtures["valid"] as List).forEach((f) { + test(f['description'] + ' as expected', () { + final arguments = _preformP2MS(f['arguments'], f['options']); + final p2ms = new P2MS(data: arguments); + expect(p2ms.data.m, f['expected']['m']); + expect(p2ms.data.n, f['expected']['n']); + expect(_toDataForm(p2ms.data.output), f['expected']['output']); + expect(p2ms.data.pubkeys, _convertToList(f['expected']['pubkeys'])); + expect(p2ms.data.signatures, _convertSigs(f['expected']['signatures'])); + expect(_toDataForm(p2ms.data.input), f['expected']['input']); + expect(_toDataForm(p2ms.data.witness), f['expected']['witness']); + + }); + }); + }); + group('(invalid case)', () { + (fixtures["invalid"] as List).forEach((f) { + test('throws ' + f['exception'] + (f['description'] != null ? ('for ' + f['description']) : ''), () { + final arguments = _preformP2MS(f['arguments'], f['options']); + try { + expect(new P2MS(data: arguments), isArgumentError); + } catch(err) { + expect((err as ArgumentError).message, f['exception']); + } + + }); + }); + }); +} +P2MSData _preformP2MS(dynamic x, option) { + final m = x['m'] != null ? x['m'] : null; + final n = x['n'] != null ? x['n'] : null; + final input = x['input'] != null ? bscript.fromASM(x['input']) : null; + final output = x['output'] != null ? bscript.fromASM(x['output']) : x['outputHex'] != null ? HEX.decode(x['outputHex']) : null; + final pubkeys = x['pubkeys']!= null ? _convertToList(x['pubkeys']) : null; + final signatures = x['signatures']!= null ? _convertSigs(x['signatures']) : null; + final witness = x['witness']; + final options = option; + + return new P2MSData(m: m, n: n, input: input, output: output, pubkeys: pubkeys, signatures: signatures, witness: witness, options: options); +} +dynamic _convertSigs(dynamic x){ + if (x == null){return null;} + else{ List properList = []; + for( var i = 0; i < x.length; i++ ) { + if(x[i]==0){properList.add(HEX.decode('0'));} + else{ + properList.add(HEX.decode(x[i]));} + } + return properList;} +} +List _convertToList(dynamic x){ + List properList = []; + for( var i = 0; i < x.length; i++ ) { + var temp = x[i]; + properList.add(HEX.decode(temp)); + } + return properList;} + +dynamic _toDataForm(dynamic x) { + if (x == null) { + return null; + } + if (x is Uint8List) { + return bscript.toASM(x); + } + if (x is List) { + List temp = []; + for (var i = 0 ; i < x.length; i++ ) { + temp.add(x[0]); + } + + return temp; + } + return ''; +}