diff --git a/lib/bitcoin_flutter.dart b/lib/bitcoin_flutter.dart index a77de46..af11f5f 100644 --- a/lib/bitcoin_flutter.dart +++ b/lib/bitcoin_flutter.dart @@ -9,7 +9,24 @@ export 'src/transaction.dart'; export 'src/address.dart'; export 'src/transaction_builder.dart'; export 'src/ecpair.dart'; +export 'src/ec/ec_public.dart'; +export 'src/ec/ec_encryption.dart'; export 'src/payments/p2pkh.dart'; export 'src/payments/p2wpkh.dart'; export 'src/payments/index.dart'; +export 'src/payments/scanning.dart'; +export 'src/payments/address/core.dart' show AddressType; +export 'src/payments/address/address.dart' show P2shAddress, P2pkhAddress; +export 'src/payments/address/segwit_address.dart' show P2wpkhAddress, P2trAddress; +export 'src/payments/script/script.dart'; +export 'src/templates/silentpaymentaddress.dart'; +export 'src/templates/outpoint.dart'; +export 'src/payments/silentpayments.dart'; +export 'src/utils/keys.dart'; +export 'src/utils/uint8list.dart'; +export 'src/utils/string.dart'; +export 'src/formatting/bytes_num_formatting.dart'; +export 'src/classify.dart'; +export 'package:bech32/bech32.dart'; +export 'package:elliptic/elliptic.dart'; // TODO: Export any libraries intended for clients of this package. diff --git a/lib/src/address.dart b/lib/src/address.dart index 3fa7902..131e9b1 100644 --- a/lib/src/address.dart +++ b/lib/src/address.dart @@ -1,10 +1,8 @@ import 'dart:typed_data'; + import 'models/networks.dart'; -import 'package:bs58check/bs58check.dart' as bs58check; -import 'package:bech32/bech32.dart'; -import 'payments/index.dart' show PaymentData; -import 'payments/p2pkh.dart'; -import 'payments/p2wpkh.dart'; +import 'payments/address/address.dart'; +import 'payments/address/segwit_address.dart'; class Address { static bool validateAddress(String address, [NetworkType? nw]) { @@ -18,31 +16,23 @@ class Address { static Uint8List addressToOutputScript(String address, [NetworkType? nw]) { NetworkType network = nw ?? bitcoin; - var decodeBase58; - var decodeBech32; - try { - decodeBase58 = bs58check.decode(address); - } catch (err) {} - if (decodeBase58 != null) { - if (decodeBase58[0] != network.pubKeyHash) - throw new ArgumentError('Invalid version or Network mismatch'); - P2PKH p2pkh = - new P2PKH(data: new PaymentData(address: address), network: network); - return p2pkh.data.output!; - } else { - try { - decodeBech32 = segwit.decode(address); - } catch (err) {} - if (decodeBech32 != null) { - if (network.bech32 != decodeBech32.hrp) - throw new ArgumentError('Invalid prefix or Network mismatch'); - if (decodeBech32.version != 0) - throw new ArgumentError('Invalid address version'); - P2WPKH p2wpkh = new P2WPKH( - data: new PaymentData(address: address), network: network); - return p2wpkh.data.output!; - } + + if (P2pkhAddress.REGEX.hasMatch(address)) { + return P2pkhAddress(address: address, network: network).toScriptPubKey().toBytes(); + } + + if (P2shAddress.REGEX.hasMatch(address)) { + return P2shAddress(address: address, network: network).toScriptPubKey().toBytes(); } + + if (P2wpkhAddress.REGEX.hasMatch(address)) { + return P2wpkhAddress(address: address, network: network).toScriptPubKey().toBytes(); + } + + if (P2trAddress.REGEX.hasMatch(address)) { + return P2trAddress(address: address, network: network).toScriptPubKey().toBytes(); + } + throw new ArgumentError(address + ' has no matching Script'); } } diff --git a/lib/src/classify.dart b/lib/src/classify.dart index 6e986fd..f47e147 100644 --- a/lib/src/classify.dart +++ b/lib/src/classify.dart @@ -12,12 +12,14 @@ const SCRIPT_TYPES = { 'P2PKH': 'pubkeyhash', 'P2SH': 'scripthash', 'P2WPKH': 'witnesspubkeyhash', + 'P2TR': 'taproot', 'P2WSH': 'witnessscripthash', 'WITNESS_COMMITMENT': 'witnesscommitment' }; String classifyOutput(Uint8List script) { if (witnessPubKeyHash.outputCheck(script)) return SCRIPT_TYPES['P2WPKH']!; + if (witnessPubKeyHash.taprootOutputCheck(script)) return SCRIPT_TYPES['P2TR']!; if (pubkeyhash.outputCheck(script)) return SCRIPT_TYPES['P2PKH']!; final chunks = bscript.decompile(script); if (chunks == null) throw new ArgumentError('Invalid script'); diff --git a/lib/src/crypto.dart b/lib/src/crypto.dart index c6bd7eb..af3de3d 100644 --- a/lib/src/crypto.dart +++ b/lib/src/crypto.dart @@ -19,3 +19,32 @@ Uint8List hash256(Uint8List buffer) { Uint8List _tmp = new SHA256Digest().process(buffer); return new SHA256Digest().process(_tmp); } + +/// Function: singleHash +/// Description: Computes a single SHA-256 hash of the input data. +/// Input: Uint8List buffer - The data to be hashed. +/// Output: Uint8List - The resulting single SHA-256 hash. +/// Note: This function calculates a single SHA-256 hash of the input data. +Uint8List singleHash(Uint8List buffer) { + /// Compute a single SHA-256 hash of the input data. + return SHA256Digest().process(buffer); +} + +/// Function: taggedHash +/// Description: Computes a tagged hash of the input data with a provided tag. +/// Input: +/// - Uint8List data - The data to be hashed. +/// - String tag - A unique tag to differentiate the hash. +/// Output: Uint8List - The resulting tagged hash. +/// Note: This function combines the provided tag with the input data to create a unique +/// hash by applying a double SHA-256 hash. +Uint8List taggedHash(Uint8List data, String tag) { + /// Calculate the hash of the tag as Uint8List. + final tagDigest = singleHash(Uint8List.fromList(tag.codeUnits)); + + /// Concatenate the tag hash with itself and the input data. + final concat = Uint8List.fromList([...tagDigest, ...tagDigest, ...data]); + + /// Compute a double SHA-256 hash of the concatenated data. + return singleHash(concat); +} diff --git a/lib/src/ec/ec_encryption.dart b/lib/src/ec/ec_encryption.dart new file mode 100644 index 0000000..5de5599 --- /dev/null +++ b/lib/src/ec/ec_encryption.dart @@ -0,0 +1,275 @@ +import 'dart:typed_data'; + +import 'package:bitcoin_flutter/src/utils/bigint.dart'; +import '../formatting/bytes_num_formatting.dart'; +import "package:pointycastle/ecc/curves/secp256k1.dart" show ECCurve_secp256k1; +import 'package:pointycastle/ecc/api.dart' show ECPoint; +import '../crypto.dart'; + +final prime = + BigInt.parse("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", radix: 16); + +final _zero32 = Uint8List(32); +final _ecP = hexToBytes("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"); +final secp256k1 = ECCurve_secp256k1(); +final n = secp256k1.n; +final G = secp256k1.G; + +ECPoint? _decodeFrom(Uint8List P) { + return secp256k1.curve.decodePoint(P); +} + +int _compare(Uint8List a, Uint8List b) { + BigInt aa = decodeBigInt(a); + BigInt bb = decodeBigInt(b); + if (aa == bb) return 0; + if (aa > bb) return 1; + return -1; +} + +bool isPoint(Uint8List p) { + if (p.length < 33) { + return false; + } + var t = p[0]; + var x = p.sublist(1, 33); + + if (_compare(x, _zero32) == 0) { + return false; + } + if (_compare(x, _ecP) == 1) { + return false; + } + try { + _decodeFrom(p); + } catch (err) { + return false; + } + if ((t == 0x02 || t == 0x03) && p.length == 33) { + return true; + } + var y = p.sublist(33); + if (_compare(y, _zero32) == 0) { + return false; + } + if (_compare(y, _ecP) == 1) { + return false; + } + if (t == 0x04 && p.length == 65) { + return true; + } + return false; +} + +bool _isPointCompressed(Uint8List p) { + return p[0] != 0x04; +} + +Uint8List reEncodedFromForm(Uint8List p, bool compressed) { + final decode = _decodeFrom(p); + if (decode == null) { + throw ArgumentError("Bad point"); + } + final encode = decode.getEncoded(compressed); + if (!_isPointCompressed(encode)) { + return encode.sublist(1, encode.length); + } + + return encode; +} + +Uint8List taprootPoint(Uint8List pub) { + BigInt x = decodeBigInt(pub.sublist(0, 32)); + BigInt y = decodeBigInt(pub.sublist(32, pub.length)); + if (y.isOdd) { + y = prime - y; + } + + var Q = secp256k1.curve.createPoint(x, y); + + if (Q.y!.toBigInteger()!.isOdd) { + y = prime - Q.y!.toBigInteger()!; + Q = secp256k1.curve.createPoint(Q.x!.toBigInteger()!, y); + } + x = Q.x!.toBigInteger()!; + y = Q.y!.toBigInteger()!; + final r = padUint8ListTo32(x.decode); + final s = padUint8ListTo32(y.decode); + return Uint8List.fromList([...r, ...s]); +} + +Uint8List tweakTaprootPoint(Uint8List pub, Uint8List tweak) { + BigInt x = decodeBigInt(pub.sublist(0, 32)); + BigInt y = decodeBigInt(pub.sublist(32, pub.length)); + if (y.isOdd) { + y = prime - y; + } + final tw = decodeBigInt(tweak); + + final c = secp256k1.curve.createPoint(x, y); + ECPoint qq = (G * tw) as ECPoint; + ECPoint Q = (c + qq) as ECPoint; + + if (Q.y!.toBigInteger()!.isOdd) { + y = prime - Q.y!.toBigInteger()!; + Q = secp256k1.curve.createPoint(Q.x!.toBigInteger()!, y); + } + x = Q.x!.toBigInteger()!; + y = Q.y!.toBigInteger()!; + final r = padUint8ListTo32(x.decode); + final s = padUint8ListTo32(y.decode); + return Uint8List.fromList([...r, ...s]); +} + +Uint8List xorBytes(Uint8List a, Uint8List b) { + if (a.length != b.length) { + throw ArgumentError("Input lists must have the same length"); + } + + Uint8List result = Uint8List(a.length); + + for (int i = 0; i < a.length; i++) { + result[i] = a[i] ^ b[i]; + } + + return result; +} + +Uint8List schnorrSign(Uint8List msg, Uint8List secret, Uint8List aux) { + if (msg.length != 32) { + throw ArgumentError("The message must be a 32-byte array."); + } + final d0 = decodeBigInt(secret); + if (!(BigInt.one <= d0 && d0 <= n - BigInt.one)) { + throw ArgumentError("The secret key must be an integer in the range 1..n-1."); + } + if (aux.length != 32) { + throw ArgumentError("aux_rand must be 32 bytes instead of ${aux.length}"); + } + ECPoint P = (G * d0) as ECPoint; + BigInt d = d0; + if (P.y!.toBigInteger()!.isOdd) { + d = n - d; + } + final t = xorBytes(d.decode, taggedHash(aux, "BIP0340/aux")); + final kHash = taggedHash( + Uint8List.fromList([...t, ...P.x!.toBigInteger()!.decode, ...msg]), "BIP0340/nonce"); + final k0 = decodeBigInt(kHash) % n; + if (k0 == BigInt.zero) { + throw const FormatException('Failure. This happens only with negligible probability.'); + } + final R = (G * k0) as ECPoint; + BigInt k = k0; + if (R.y!.toBigInteger()!.isOdd) { + k = n - k; + } + final eHash = taggedHash( + Uint8List.fromList([...R.x!.toBigInteger()!.decode, ...P.x!.toBigInteger()!.decode, ...msg]), + "BIP0340/challenge"); + + final e = decodeBigInt(eHash) % n; + final eKey = (k + e * d) % n; + final sig = Uint8List.fromList([...R.x!.toBigInteger()!.decode, ...eKey.decode]); + final verify = verifySchnorr(msg, P.x!.toBigInteger()!.decode, sig); + if (!verify) { + throw const FormatException('The created signature does not pass verification.'); + } + return sig; +} + +bool verifySchnorr(Uint8List message, Uint8List publicKey, Uint8List signatur) { + if (message.length != 32) { + throw ArgumentError("The message must be a 32-byte array."); + } + if (publicKey.length != 32) { + throw ArgumentError("The public key must be a 32-byte array."); + } + if (signatur.length != 64) { + throw ArgumentError("The signature must be a 64-byte array."); + } + final P = liftX(decodeBigInt(publicKey)); + final r = decodeBigInt(signatur.sublist(0, 32)); + final s = decodeBigInt(signatur.sublist(32, 64)); + if (P == null || r >= prime || s >= n) { + return false; + } + final eHash = taggedHash( + Uint8List.fromList([...signatur.sublist(0, 32), ...publicKey, ...message]), + "BIP0340/challenge"); + final e = decodeBigInt(eHash) % n; + + final sp = (G * s) as ECPoint; + + final eP = (P * (n - e)) as ECPoint; + + final R = (sp + eP) as ECPoint; + if (R.y!.toBigInteger()!.isOdd || R.x!.toBigInteger()! != r) { + return false; + } + return true; +} + +ECPoint? liftX(BigInt x) { + if (x >= prime) { + return null; + } + final ySq = (_modPow(x, BigInt.from(3), prime) + BigInt.from(7)) % prime; + final y = _modPow(ySq, (prime + BigInt.one) ~/ BigInt.from(4), prime); + if (_modPow(y, BigInt.two, prime) != ySq) return null; + BigInt result = (y & BigInt.one) == BigInt.zero ? y : prime - y; + return secp256k1.curve.createPoint(x, result); +} + +BigInt _modPow(BigInt base, BigInt exponent, BigInt modulus) { + if (exponent == BigInt.zero) { + return BigInt.one; + } + + BigInt result = BigInt.one; + base %= modulus; + + while (exponent > BigInt.zero) { + if ((exponent & BigInt.one) == BigInt.one) { + result = (result * base) % modulus; + } + exponent = exponent ~/ BigInt.two; + base = (base * base) % modulus; + } + + return result; +} + +Uint8List pubKeyGeneration(Uint8List secret) { + final d0 = decodeBigInt(secret); + if (!(BigInt.one <= d0 && d0 <= n - BigInt.one)) { + throw ArgumentError("The secret key must be an integer in the range 1..n-1."); + } + ECPoint qq = (G * d0) as ECPoint; + Uint8List toBytes = qq.getEncoded(false); + if (toBytes[0] == 0x04) { + toBytes = toBytes.sublist(1, toBytes.length); + } + return toBytes; +} + +BigInt _negatePrivateKey(Uint8List secret) { + final bytes = pubKeyGeneration(secret); + final toBigInt = decodeBigInt(bytes.sublist(32)); + BigInt negatedKey = decodeBigInt(secret); + if (toBigInt.isOdd) { + final keyExpend = decodeBigInt(secret); + negatedKey = n - keyExpend; + } + return negatedKey; +} + +Uint8List tweekTapprotPrivate(Uint8List secret, BigInt tweek) { + final bytes = pubKeyGeneration(secret); + final toBigInt = decodeBigInt(bytes.sublist(32)); + BigInt negatedKey = decodeBigInt(secret); + if (toBigInt.isOdd) { + negatedKey = _negatePrivateKey(secret); + } + final tw = (negatedKey + tweek) % n; + return tw.decode; +} diff --git a/lib/src/ec/ec_public.dart b/lib/src/ec/ec_public.dart new file mode 100644 index 0000000..9334017 --- /dev/null +++ b/lib/src/ec/ec_public.dart @@ -0,0 +1,241 @@ +import 'dart:typed_data'; +import '../payments/address/address.dart'; +import '../payments/address/core.dart'; +import '../payments/address/segwit_address.dart'; +import '../payments/constants/constants.dart'; +import '../payments/script/script.dart'; +import '../crypto.dart'; +import '../formatting/bytes_num_formatting.dart'; +import 'ec_encryption.dart'; +import 'package:bitcoin_base/bitcoin.dart' as bitcoin_base; + +class ECPublic { + /// Constructs an ECPublic key from a byte representation. + ECPublic.fromBytes(Uint8List public) { + if (!isPoint(public)) { + throw ArgumentError("Bad point"); + } + final d = reEncodedFromForm(public, false); + _key = d; + } + + /// Constructs an ECPublic key from hex representation. + ECPublic.fromHex(String hex) { + final toBytes = hexToBytes(hex); + if (!isPoint(toBytes)) { + throw ArgumentError("Bad point"); + } + final d = reEncodedFromForm(toBytes, false); + _key = d; + } + + late final Uint8List _key; + + /// toHex converts the ECPublic key to a hex-encoded string. + /// If 'compressed' is true, the key is in compressed format. + String toHex({bool compressed = true}) { + final bytes = toBytes(); + if (compressed) { + final point = reEncodedFromForm(bytes, true); + return bytesToHex(point); + } + return bytesToHex(bytes); + } + + /// _toHash160 computes the RIPEMD160 hash of the ECPublic key. + /// If 'compressed' is true, the key is in compressed format. + Uint8List _toHash160({bool compressed = true}) { + final bytes = hexToBytes(toHex(compressed: compressed)); + return hash160(bytes); + } + + /// toHash160 computes the RIPEMD160 hash of the ECPublic key. + /// If 'compressed' is true, the key is in compressed format. + String toHash160({bool compressed = true}) { + final bytes = hexToBytes(toHex(compressed: compressed)); + return bytesToHex(hash160(bytes)); + } + + /// toAddress generates a P2PKH (Pay-to-Public-Key-Hash) address from the ECPublic key. + /// If 'compressed' is true, the key is in compressed format. + P2pkhAddress toAddress({bool compressed = true}) { + final h16 = _toHash160(compressed: compressed); + final toHex = bytesToHex(h16); + + return P2pkhAddress(hash160: toHex); + } + + /// toSegwitAddress generates a P2WPKH (Pay-to-Witness-Public-Key-Hash) SegWit address + /// from the ECPublic key. If 'compressed' is true, the key is in compressed format. + P2wpkhAddress toSegwitAddress({bool compressed = true}) { + final h16 = _toHash160(compressed: compressed); + final toHex = bytesToHex(h16); + + return P2wpkhAddress(program: toHex); + } + + /// toP2pkAddress generates a P2PK (Pay-to-Public-Key) address from the ECPublic key. + /// If 'compressed' is true, the key is in compressed format. + P2pkAddress toP2pkAddress({bool compressed = true}) { + final h = toHex(compressed: compressed); + return P2pkAddress(publicKey: h); + } + + /// toRedeemScript generates a redeem script from the ECPublic key. + /// If 'compressed' is true, the key is in compressed format. + Script toRedeemScript({bool compressed = true}) { + final redeem = toHex(compressed: compressed); + return Script(script: [redeem, "OP_CHECKSIG"]); + } + + /// toP2pkhInP2sh generates a P2SH (Pay-to-Script-Hash) address + /// wrapping a P2PK (Pay-to-Public-Key) script derived from the ECPublic key. + /// If 'compressed' is true, the key is in compressed format. + P2shAddress toP2pkhInP2sh({bool compressed = true}) { + final addr = toAddress(compressed: compressed); + return P2shAddress.fromScript(script: addr.toScriptPubKey(), type: AddressType.p2pkhInP2sh); + } + + /// toP2pkInP2sh generates a P2SH (Pay-to-Script-Hash) address + /// wrapping a P2PK (Pay-to-Public-Key) script derived from the ECPublic key. + /// If 'compressed' is true, the key is in compressed format. + P2shAddress toP2pkInP2sh({bool compressed = true}) { + return P2shAddress(script: toRedeemScript(compressed: compressed)); + } + + /// ToTaprootAddress generates a P2TR(Taproot) address from the ECPublic key + /// and an optional script. The 'script' parameter can be used to specify + /// custom spending conditions. + P2trAddress toTaprootAddress({List? scripts}) { + final pubKey = toTapRotHex(script: scripts); + + return P2trAddress(program: pubKey); + } + + /// toP2wpkhInP2sh generates a P2SH (Pay-to-Script-Hash) address + /// wrapping a P2WPKH (Pay-to-Witness-Public-Key-Hash) script derived from the ECPublic key. + /// If 'compressed' is true, the key is in compressed format. + P2shAddress toP2wpkhInP2sh({bool compressed = true}) { + final addr = toSegwitAddress(compressed: compressed); + return P2shAddress.fromScript(script: addr.toScriptPubKey(), type: AddressType.p2wpkhInP2sh); + } + + /// toP2wshScript generates a P2WSH (Pay-to-Witness-Script-Hash) script + /// derived from the ECPublic key. If 'compressed' is true, the key is in compressed format. + Script toP2wshScript({bool compressed = true}) { + return Script(script: ['OP_1', toHex(compressed: compressed), "OP_1", "OP_CHECKMULTISIG"]); + } + + /// toP2wshAddress generates a P2WSH (Pay-to-Witness-Script-Hash) address + /// from the ECPublic key. If 'compressed' is true, the key is in compressed format. + P2wshAddress toP2wshAddress({bool compressed = true}) { + return P2wshAddress(script: toP2wshScript(compressed: compressed)); + } + + /// toP2wshInP2sh generates a P2SH (Pay-to-Script-Hash) address + /// wrapping a P2WSH (Pay-to-Witness-Script-Hash) script derived from the ECPublic key. + /// If 'compressed' is true, the key is in compressed format. + P2shAddress toP2wshInP2sh({bool compressed = true}) { + final p2sh = toP2wshAddress(compressed: compressed); + return P2shAddress.fromScript(script: p2sh.toScriptPubKey(), type: AddressType.p2wshInP2sh); + } + + /// calculateTweek computes and returns the TapTweak value based on the ECPublic key + /// and an optional script. It uses the key's x-coordinate and the Merkle root of the script + /// (if provided) to calculate the tweak. + BigInt calculateTweek({dynamic script}) { + final tweak = _calculateTweek(_key, script: script); + return decodeBigInt(tweak); + } + + /// toBytes returns the uncompressed byte representation of the ECPublic key. + Uint8List toBytes({int? prefix = 0x04}) { + if (prefix != null) { + return Uint8List.fromList([prefix, ..._key]); + } + return Uint8List.fromList([..._key]); + } + + /// toCompressedBytes returns the compressed byte representation of the ECPublic key. + Uint8List toCompressedBytes() { + final point = reEncodedFromForm(toBytes(), true); + return point; + } + + /// returns the x coordinate only as hex string after tweaking (needed for taproot) + String toTapPoint() { + final point = taprootPoint(_key); + return bytesToHex(point.sublist(0, 32)); + } + + /// returns the x coordinate only as hex string after tweaking (needed for taproot) + String toTapRotHex({List? script}) { + final tweak = _calculateTweek(_key, script: script); + final point = tweakTaprootPoint(_key, tweak); + return bytesToHex(point.sublist(0, 32)); + } + + /// toXOnlyHex extracts and returns the x-coordinate (first 32 bytes) of the ECPublic key + /// as a hexadecimal string. + String toXOnlyHex() { + return bytesToHex(_key.sublist(0, 32)); + } + + /// _calculateTweek computes and returns the TapTweak value based on the ECPublic key + /// and an optional script. It uses the key's x-coordinate and the Merkle root of the script + /// (if provided) to calculate the tweak. + Uint8List _calculateTweek(Uint8List public, {dynamic script}) { + final keyX = Uint8List.fromList(public.getRange(0, 32).toList()); + if (script == null) { + final tweek = taggedHash(keyX, "TapTweak"); + return tweek; + } + final merkleRoot = _getTagHashedMerkleRoot(script); + final tweek = taggedHash(Uint8List.fromList([...keyX, ...merkleRoot]), "TapTweak"); + return tweek; + } + + /// _getTagHashedMerkleRoot computes and returns the tagged hashed Merkle root for Taproot + /// based on the provided argument. It handles different argument types, including scripts + /// and lists of scripts. + Uint8List _getTagHashedMerkleRoot(dynamic args) { + if (args is bitcoin_base.Script) { + args = Script(script: args.script); + } + if (args is Script) { + final tagged = _tapleafTaggedHash(args); + return tagged; + } + + args as List; + if (args.isEmpty) return Uint8List(0); + if (args.length == 1) { + return _getTagHashedMerkleRoot(args.first); + } else if (args.length == 2) { + final left = _getTagHashedMerkleRoot(args.first); + final right = _getTagHashedMerkleRoot(args.last); + final tap = _tapBranchTaggedHash(left, right); + return tap; + } + throw Exception("List cannot have more than 2 branches."); + } + + /// _tapleafTaggedHash computes and returns the tagged hash of a script for Taproot, + /// using the specified script. It prepends a version byte and then tags the hash with "TapLeaf". + Uint8List _tapleafTaggedHash(Script script) { + final scriptBytes = prependVarint(script.toBytes()); + + final part = Uint8List.fromList([LEAF_VERSION_TAPSCRIPT, ...scriptBytes]); + return taggedHash(part, 'TapLeaf'); + } + + /// _tapBranchTaggedHash computes and returns the tagged hash of two byte slices + /// for Taproot, where 'a' and 'b' are the input byte slices. It ensures that 'a' and 'b' + /// are sorted and concatenated before tagging the hash with "TapBranch". + Uint8List _tapBranchTaggedHash(Uint8List a, Uint8List b) { + if (isLessThanBytes(a, b)) { + return taggedHash(Uint8List.fromList([...a, ...b]), "TapBranch"); + } + return taggedHash(Uint8List.fromList([...b, ...a]), "TapBranch"); + } +} diff --git a/lib/src/ec/schnorr.dart b/lib/src/ec/schnorr.dart new file mode 100644 index 0000000..4da662e --- /dev/null +++ b/lib/src/ec/schnorr.dart @@ -0,0 +1,80 @@ +import 'dart:typed_data'; +import 'package:bitcoin_flutter/src/crypto.dart'; +import 'package:bitcoin_flutter/src/formatting/bytes_num_formatting.dart'; +import 'package:bitcoin_flutter/src/ec/ec_encryption.dart'; +import 'package:bitcoin_flutter/src/utils/bigint.dart'; +import 'package:pointycastle/ecc/api.dart' show ECPoint; + +Uint8List schnorrSign(Uint8List msg, Uint8List secret, Uint8List aux) { + if (msg.length != 32) { + throw ArgumentError("The message must be a 32-byte array."); + } + final d0 = decodeBigInt(secret); + if (!(BigInt.one <= d0 && d0 <= n - BigInt.one)) { + throw ArgumentError("The secret key must be an integer in the range 1..n-1."); + } + if (aux.length != 32) { + throw ArgumentError("aux_rand must be 32 bytes instead of ${aux.length}"); + } + ECPoint P = (G * d0) as ECPoint; + BigInt d = d0; + if (P.y!.toBigInteger()!.isOdd) { + d = n - d; + } + final t = xorBytes(d.decode, taggedHash(aux, "BIP0340/aux")); + final kHash = taggedHash( + Uint8List.fromList([...t, ...P.x!.toBigInteger()!.decode, ...msg]), "BIP0340/nonce"); + final k0 = decodeBigInt(kHash) % n; + if (k0 == BigInt.zero) { + throw const FormatException('Failure. This happens only with negligible probability.'); + } + final R = (G * k0) as ECPoint; + BigInt k = k0; + if (R.y!.toBigInteger()!.isOdd) { + k = n - k; + } + final eHash = taggedHash( + Uint8List.fromList([...R.x!.toBigInteger()!.decode, ...P.x!.toBigInteger()!.decode, ...msg]), + "BIP0340/challenge"); + + final e = decodeBigInt(eHash) % n; + final eKey = (k + e * d) % n; + final sig = Uint8List.fromList([...R.x!.toBigInteger()!.decode, ...eKey.decode]); + final verify = verifySchnorr(msg, P.x!.toBigInteger()!.decode, sig); + if (!verify) { + throw const FormatException('The created signature does not pass verification.'); + } + return sig; +} + +bool verifySchnorr(Uint8List message, Uint8List publicKey, Uint8List signatur) { + if (message.length != 32) { + throw ArgumentError("The message must be a 32-byte array."); + } + if (publicKey.length != 32) { + throw ArgumentError("The public key must be a 32-byte array."); + } + if (signatur.length != 64) { + throw ArgumentError("The signature must be a 64-byte array."); + } + final P = liftX(decodeBigInt(publicKey)); + final r = decodeBigInt(signatur.sublist(0, 32)); + final s = decodeBigInt(signatur.sublist(32, 64)); + if (P == null || r >= prime || s >= n) { + return false; + } + final eHash = taggedHash( + Uint8List.fromList([...signatur.sublist(0, 32), ...publicKey, ...message]), + "BIP0340/challenge"); + final e = decodeBigInt(eHash) % n; + + final sp = (G * s) as ECPoint; + + final eP = (P * (n - e)) as ECPoint; + + final R = (sp + eP) as ECPoint; + if (R.y!.toBigInteger()!.isOdd || R.x!.toBigInteger()! != r) { + return false; + } + return true; +} diff --git a/lib/src/ecpair.dart b/lib/src/ecpair.dart index ee27339..18bc4e4 100644 --- a/lib/src/ecpair.dart +++ b/lib/src/ecpair.dart @@ -3,6 +3,10 @@ import 'dart:math'; import 'package:bip32/src/utils/ecurve.dart' as ecc; import 'package:bip32/src/utils/wif.dart' as wif; import 'models/networks.dart'; +import 'ec/ec_public.dart'; +import 'ec/ec_encryption.dart' as ec; +import 'crypto.dart' as bcrypto; +import 'transaction.dart'; class ECPair { Uint8List? _d; @@ -10,8 +14,8 @@ class ECPair { NetworkType network; bool compressed; ECPair(Uint8List? _d, Uint8List? _Q, {NetworkType? network, bool? compressed}) - : this.network = network ?? bitcoin, - this.compressed = compressed ?? true { + : this.network = network ?? bitcoin, + this.compressed = compressed ?? true { this._d = _d; this._Q = _Q; } @@ -25,8 +29,8 @@ class ECPair { if (privateKey == null) { throw new ArgumentError('Missing private key'); } - return wif.encode(new wif.WIF( - version: network.wif, privateKey: privateKey!, compressed: compressed)); + return wif + .encode(new wif.WIF(version: network.wif, privateKey: privateKey!, compressed: compressed)); } Uint8List sign(Uint8List hash) { @@ -54,29 +58,21 @@ class ECPair { throw new ArgumentError('Unknown network version'); } } - return ECPair.fromPrivateKey(decoded.privateKey, - compressed: decoded.compressed, network: nw); + return ECPair.fromPrivateKey(decoded.privateKey, compressed: decoded.compressed, network: nw); } - factory ECPair.fromPublicKey(Uint8List publicKey, - {NetworkType? network, bool? compressed}) { + factory ECPair.fromPublicKey(Uint8List publicKey, {NetworkType? network, bool? compressed}) { if (!ecc.isPoint(publicKey)) { throw new ArgumentError('Point is not on the curve'); } - return new ECPair(null, publicKey, - network: network, compressed: compressed); + return new ECPair(null, publicKey, network: network, compressed: compressed); } - factory ECPair.fromPrivateKey(Uint8List privateKey, - {NetworkType? network, bool? compressed}) { + factory ECPair.fromPrivateKey(Uint8List privateKey, {NetworkType? network, bool? compressed}) { if (privateKey.length != 32) - throw new ArgumentError( - 'Expected property privateKey of type Buffer(Length: 32)'); - if (!ecc.isPrivate(privateKey)) - throw new ArgumentError('Private key not in range [1, n)'); - return new ECPair(privateKey, null, - network: network, compressed: compressed); + throw new ArgumentError('Expected property privateKey of type Buffer(Length: 32)'); + if (!ecc.isPrivate(privateKey)) throw new ArgumentError('Private key not in range [1, n)'); + return new ECPair(privateKey, null, network: network, compressed: compressed); } - factory ECPair.makeRandom( - {NetworkType? network, bool? compressed, Function? rng}) { + factory ECPair.makeRandom({NetworkType? network, bool? compressed, Function? rng}) { final rfunc = rng ?? _randomBytes; Uint8List d; // int beginTime = DateTime.now().millisecondsSinceEpoch; @@ -87,6 +83,25 @@ class ECPair { } while (!ecc.isPrivate(d)); return ECPair.fromPrivateKey(d, network: network, compressed: compressed); } + + /// sign taproot transaction digest and returns the signature. + Uint8List signTapRoot(Uint8List txDigest, + {int sighash = TAPROOT_SIGHASH_ALL, List scripts = const [], bool tweak = true}) { + Uint8List byteKey = Uint8List(0); + if (tweak) { + final ECPublic publicKey = ECPublic.fromBytes(_Q!); + final t = publicKey.calculateTweek(script: scripts); + byteKey = ec.tweekTapprotPrivate(_d!, t); + } else { + byteKey = _d!; + } + final randAux = bcrypto.singleHash(Uint8List.fromList([...txDigest, ...byteKey])); + Uint8List signatur = ec.schnorrSign(txDigest, byteKey, randAux); + if (sighash != TAPROOT_SIGHASH_ALL) { + signatur = Uint8List.fromList([...signatur, sighash]); + } + return signatur; + } } const int _SIZE_BYTE = 255; diff --git a/lib/src/formatting/bytes_num_formatting.dart b/lib/src/formatting/bytes_num_formatting.dart new file mode 100644 index 0000000..a8f3130 --- /dev/null +++ b/lib/src/formatting/bytes_num_formatting.dart @@ -0,0 +1,210 @@ +import 'dart:core'; +import 'dart:typed_data'; +import 'package:convert/convert.dart'; + +/// ignore: implementation_imports +import 'package:pointycastle/src/utils.dart' as p_utils; + +String bytesToHex( + List bytes, +) => + hex.encode(bytes); + +BigInt bytesToInt(List bytes) => p_utils.decodeBigInt(bytes); + +Uint8List intToBytes(BigInt number) => p_utils.encodeBigInt(number); + +Uint8List padUint8ListTo32(Uint8List data) { + assert(data.length <= 32); + if (data.length == 32) return data; + + /// todo there must be a faster way to do this? + return Uint8List(32)..setRange(32 - data.length, 32, data); +} + +bool isLessThanBytes(List thashedA, List thashedB) { + for (int i = 0; i < thashedA.length && i < thashedB.length; i++) { + if (thashedA[i] < thashedB[i]) { + return true; + } else if (thashedA[i] > thashedB[i]) { + return false; + } + } + return thashedA.length < thashedB.length; +} + +BigInt decodeBigInt(List bytes) { + BigInt result = BigInt.from(0); + for (int i = 0; i < bytes.length; i++) { + result += BigInt.from(bytes[bytes.length - i - 1]) << (8 * i); + } + return result; +} + +List? convertBits(List data, int fromBits, int toBits, {bool pad = true}) { + int acc = 0; + int bits = 0; + List ret = []; + int maxv = (1 << toBits) - 1; + int maxAcc = (1 << (fromBits + toBits - 1)) - 1; + + for (int value in data) { + if (value < 0 || (value >> fromBits) > 0) { + return null; + } + acc = ((acc << fromBits) | value) & maxAcc; + bits += fromBits; + while (bits >= toBits) { + bits -= toBits; + ret.add((acc >> bits) & maxv); + } + } + + if (pad) { + if (bits > 0) { + ret.add((acc << (toBits - bits)) & maxv); + } + } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) > 0) { + return null; + } + + return ret; +} + +Uint8List encodeVarint(int i) { + if (i < 253) { + return Uint8List.fromList([i]); + } else if (i < 0x10000) { + final bytes = Uint8List(3); + bytes[0] = 0xfd; + ByteData.view(bytes.buffer).setUint16(1, i, Endian.little); + return bytes; + } else if (i < 0x100000000) { + final bytes = Uint8List(5); + bytes[0] = 0xfe; + ByteData.view(bytes.buffer).setUint32(1, i, Endian.little); + return bytes; + } else if (BigInt.from(i) < BigInt.parse("0x10000000000000000", radix: 16)) { + final bytes = Uint8List(9); + bytes[0] = 0xff; + ByteData.view(bytes.buffer).setUint64(1, i, Endian.little); + return bytes; + } else { + throw ArgumentError("Integer is too large: $i"); + } +} + +Uint8List prependVarint(Uint8List data) { + final varintBytes = encodeVarint(data.length); + return Uint8List.fromList([...varintBytes, ...data]); +} + +(int, int) viToInt(Uint8List byteint) { + int ni = byteint[0]; + int size = 0; + + if (ni < 253) { + return (ni, 1); + } + + if (ni == 253) { + size = 2; + } else if (ni == 254) { + size = 4; + } else { + size = 8; + } + + int value = ByteData.sublistView(byteint, 1, 1 + size).getInt64(0, Endian.little); + return (value, size + 1); +} + +Uint8List packUint32LE(int value) { + final byteData = ByteData(4); + byteData.setUint32(0, value, Endian.little); + return byteData.buffer.asUint8List(); +} + +Uint8List packBigIntToLittleEndian(BigInt value) { + final buffer = Uint8List(8); + + for (var i = 0; i < 8; i++) { + buffer[i] = (value & BigInt.from(0xff)).toInt(); + value >>= 8; + } + + return buffer; +} + +Uint8List packInt32LE(int value) { + final byteData = ByteData(4); + byteData.setInt32(0, value, Endian.little); + return byteData.buffer.asUint8List(); +} + +String strip0x(String hex) { + if (hex.startsWith('0x')) return hex.substring(2); + return hex; +} + +Uint8List hexToBytes(String hexStr) { + final bytes = hex.decode(strip0x(hexStr)); + if (bytes is Uint8List) return bytes; + + return Uint8List.fromList(bytes); +} + +int intFromBytes(List bytes, Endian endian) { + if (bytes.isEmpty) { + throw ArgumentError("Input bytes should not be empty"); + } + + final buffer = Uint8List.fromList(bytes); + final byteData = ByteData.sublistView(buffer); + + switch (bytes.length) { + case 1: + return byteData.getInt8(0); + case 2: + return byteData.getInt16(0, endian); + case 4: + return byteData.getInt32(0, endian); + default: + throw ArgumentError("Unsupported byte length: ${bytes.length}"); + } +} + +bool bytesListEqual(List? a, List? b) { + if (a == null) { + return b == null; + } + if (b == null || a.length != b.length) { + return false; + } + if (identical(a, b)) { + return true; + } + for (int index = 0; index < a.length; index += 1) { + if (a[index] != b[index]) { + return false; + } + } + return true; +} + +Uint8List packUint32BE(int value) { + var bytes = Uint8List(4); + bytes[0] = (value >> 24) & 0xFF; + bytes[1] = (value >> 16) & 0xFF; + bytes[2] = (value >> 8) & 0xFF; + bytes[3] = value & 0xFF; + return bytes; +} + +int binaryToByte(String binary) { + return int.parse(binary, radix: 2); +} + +String bytesToBinary(Uint8List bytes) { + return bytes.map((byte) => byte.toRadixString(2).padLeft(8, '0')).join(''); +} diff --git a/lib/src/formatting/bytes_tracker.dart b/lib/src/formatting/bytes_tracker.dart new file mode 100644 index 0000000..f49049c --- /dev/null +++ b/lib/src/formatting/bytes_tracker.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:typed_data/typed_buffers.dart'; + +class DynamicByteTracker extends ByteConversionSinkBase { + final Uint8Buffer _buffer = Uint8Buffer(); + int _length = 0; + + int get length => _length; + + Uint8List toBytes() { + return _buffer.buffer.asUint8List(0, _length); + } + + @override + void add(List chunk) { + _buffer.addAll(chunk); + _length += chunk.length; + } + + @override + void close() { + /// dont need + } +} + diff --git a/lib/src/models/networks.dart b/lib/src/models/networks.dart index be39874..265ee67 100644 --- a/lib/src/models/networks.dart +++ b/lib/src/models/networks.dart @@ -1,52 +1,140 @@ -import 'package:meta/meta.dart'; +import 'dart:typed_data'; +import '../payments/address/core.dart'; +import '../formatting/bytes_num_formatting.dart'; + +enum BtcNetwork { mainnet, testnet } + +class Bip32Type { + final int public; + final int private; + + const Bip32Type({required this.public, required this.private}); + + @override + String toString() { + return 'Bip32Type{public: $public, private: $private}'; + } +} class NetworkType { - String messagePrefix; - String bech32; - Bip32Type bip32; - int pubKeyHash; - int scriptHash; - int wif; - - NetworkType( + final String messagePrefix; + final String bech32; + final Bip32Type bip32; + final int pubKeyHash; + final int scriptHash; + final int wif; + final int p2pkhPrefix; + final int p2shPrefix; + final BtcNetwork network; + final Map extendPrivate; + final Map extendPublic; + bool get isMainnet => network == BtcNetwork.mainnet; + + const NetworkType( {required this.messagePrefix, String? bech32, required this.bip32, required this.pubKeyHash, required this.scriptHash, - required this.wif}) + required this.wif, + required this.p2pkhPrefix, + required this.p2shPrefix, + required this.extendPrivate, + required this.extendPublic, + required this.network}) : this.bech32 = bech32 ?? ''; - @override - String toString() { - return 'NetworkType{messagePrefix: $messagePrefix, bech32: $bech32, bip32: ${bip32.toString()}, pubKeyHash: $pubKeyHash, scriptHash: $scriptHash, wif: $wif}'; + static const BITCOIN = NetworkType( + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'bc', + bip32: const Bip32Type(public: 0x0488b21e, private: 0x0488ade4), + pubKeyHash: 0x00, + scriptHash: 0x05, + wif: 0x80, + network: BtcNetwork.mainnet, + p2pkhPrefix: 0x00, + p2shPrefix: 0x05, + extendPrivate: { + AddressType.p2pkh: "0x0488ade4", + AddressType.p2pkhInP2sh: "0x0488ade4", + AddressType.p2wpkh: "0x04b2430c", + AddressType.p2wpkhInP2sh: "0x049d7878", + AddressType.p2wsh: "0x02aa7a99", + AddressType.p2wshInP2sh: "0x0295b005" + }, + extendPublic: { + AddressType.p2pkh: "0x0488b21e", + AddressType.p2pkhInP2sh: "0x0488b21e", + AddressType.p2wpkh: "0x04b24746", + AddressType.p2wpkhInP2sh: "0x049d7cb2", + AddressType.p2wsh: "0x02aa7ed3", + AddressType.p2wshInP2sh: "0x0295b43f" + }); + + static const TESTNET = NetworkType( + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'tb', + bip32: const Bip32Type(public: 0x043587cf, private: 0x04358394), + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef, + network: BtcNetwork.testnet, + p2pkhPrefix: 0x6f, + p2shPrefix: 0xc4, + extendPrivate: { + AddressType.p2pkh: "0x04358394", + AddressType.p2pkhInP2sh: "0x04358394", + AddressType.p2wpkh: "0x045f18bc", + AddressType.p2wpkhInP2sh: "0x044a4e28", + AddressType.p2wsh: "0x02575048", + AddressType.p2wshInP2sh: "0x024285b5" + }, + extendPublic: { + AddressType.p2pkh: "0x043587cf", + AddressType.p2pkhInP2sh: "0x043587cf", + AddressType.p2wpkh: "0x045f1cf6", + AddressType.p2wpkhInP2sh: "0x044a5262", + AddressType.p2wsh: "0x02575483", + AddressType.p2wshInP2sh: "0x024289ef" + }); + + static NetworkType networkFromWif(String wif) { + final w = int.parse(wif, radix: 16); + if (TESTNET.wif == w) { + return TESTNET; + } else if (BITCOIN.wif == w) { + return BITCOIN; + } + throw ArgumentError("wif perefix $wif not supported, only bitcoin or testnet accepted"); } -} -class Bip32Type { - int public; - int private; + static AddressType? networkFromXPrivePrefix(Uint8List prefix) { + final w = "0x${bytesToHex(prefix)}"; + if (TESTNET.extendPrivate.values.contains(w)) { + return TESTNET.extendPrivate.keys + .firstWhere((element) => TESTNET.extendPrivate[element] == w); + } else if (BITCOIN.extendPrivate.values.contains(w)) { + return BITCOIN.extendPrivate.keys + .firstWhere((element) => BITCOIN.extendPrivate[element] == w); + } + return null; + } - Bip32Type({required this.public, required this.private}); + static AddressType? networkFromXPublicPrefix(Uint8List prefix) { + final w = "0x${bytesToHex(prefix)}"; + if (TESTNET.extendPublic.values.contains(w)) { + return TESTNET.extendPublic.keys.firstWhere((element) => TESTNET.extendPublic[element] == w); + } else if (BITCOIN.extendPublic.values.contains(w)) { + return BITCOIN.extendPublic.keys.firstWhere((element) => BITCOIN.extendPublic[element] == w); + } + return null; + } @override String toString() { - return 'Bip32Type{public: $public, private: $private}'; + return 'NetworkType{messagePrefix: $messagePrefix, bech32: $bech32, bip32: ${bip32.toString()}, pubKeyHash: $pubKeyHash, scriptHash: $scriptHash, wif: $wif}'; } } -final bitcoin = new NetworkType( - messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'bc', - bip32: new Bip32Type(public: 0x0488b21e, private: 0x0488ade4), - pubKeyHash: 0x00, - scriptHash: 0x05, - wif: 0x80); - -final testnet = new NetworkType( - messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'tb', - bip32: new Bip32Type(public: 0x043587cf, private: 0x04358394), - pubKeyHash: 0x6f, - scriptHash: 0xc4, - wif: 0xef); +final bitcoin = NetworkType.BITCOIN; +final testnet = NetworkType.TESTNET; diff --git a/lib/src/payments/address/address.dart b/lib/src/payments/address/address.dart new file mode 100644 index 0000000..2418c96 --- /dev/null +++ b/lib/src/payments/address/address.dart @@ -0,0 +1,138 @@ +import 'dart:typed_data'; + +import 'core.dart'; +import '../script/script.dart'; +import '../tools/tools.dart'; +import '../../crypto.dart'; +import '../../formatting/bytes_num_formatting.dart'; +import '../../models/networks.dart'; +import '../../ec/ec_encryption.dart'; +import 'package:bs58check/bs58check.dart' as bs58check; + +abstract class BipAddress implements BitcoinAddress { + /// Represents a Bitcoin address + /// + /// [hash160] the hash160 string representation of the address; hash160 represents + /// two consequtive hashes of the public key or the redeam script, first + /// a SHA-256 and then an RIPEMD-160 + BipAddress({String? address, String? hash160, Script? script, NetworkType? network}) { + if (hash160 != null) { + if (!isValidHash160(hash160)) { + throw Exception("Invalid value for parameter hash160."); + } + _h160 = hash160; + } else if (address != null) { + if (!isValidAddress(address, type, network: network)) { + throw ArgumentError("Invalid version or Network mismatch"); + } + _h160 = _addressToHash160(address); + } else if (script != null) { + _h160 = _scriptToHash160(script); + } else { + if (type == AddressType.p2pk) return; + throw ArgumentError("Invalid parameters"); + } + } + + late final String _h160; + + /// returns the address's hash160 hex string representation + String get getH160 { + if (type == AddressType.p2pk) throw UnimplementedError(); + return _h160; + } + + static String _addressToHash160(String address) { + final decode = bs58check.decode(address); + return bytesToHex(decode.sublist(1)); + } + + static String _scriptToHash160(Script s) { + final b = s.toBytes(); + final h160 = hash160(b); + return bytesToHex(h160); + } + + /// returns the address's string encoding + @override + String toAddress(NetworkType networkType, {Uint8List? h160}) { + Uint8List tobytes = h160 ?? hexToBytes(_h160); + switch (type) { + case AddressType.p2wpkhInP2sh: + case AddressType.p2wshInP2sh: + case AddressType.p2pkhInP2sh: + case AddressType.p2pkInP2sh: + tobytes = Uint8List.fromList([networkType.p2shPrefix, ...tobytes]); + break; + case const (AddressType.p2pkh) || const (AddressType.p2pk): + tobytes = Uint8List.fromList([networkType.p2pkhPrefix, ...tobytes]); + break; + default: + } + return bs58check.encode(tobytes); + } +} + +class P2shAddress extends BipAddress { + static RegExp get REGEX => RegExp(r'^[23][a-km-zA-HJ-NP-Z1-9]{25,34}$'); + + /// Encapsulates a P2SH address. + P2shAddress({super.address, super.hash160, super.script, super.network}) + : type = AddressType.p2pkInP2sh; + P2shAddress.fromScript({super.script, this.type = AddressType.p2pkInP2sh}) + : assert(type == AddressType.p2pkInP2sh || + type == AddressType.p2pkhInP2sh || + type == AddressType.p2wpkhInP2sh || + type == AddressType.p2wshInP2sh); + + @override + final AddressType type; + + /// Returns the scriptPubKey (P2SH) that corresponds to this address + @override + Script toScriptPubKey() { + return Script(script: ['OP_HASH160', _h160, 'OP_EQUAL']); + } +} + +class P2pkhAddress extends BipAddress { + static RegExp get REGEX => RegExp(r'^[1mn][a-km-zA-HJ-NP-Z1-9]{25,34}$'); + + P2pkhAddress({super.address, super.hash160, super.network}); + + /// Returns the scriptPubKey (P2SH) that corresponds to this address + @override + Script toScriptPubKey() { + return Script(script: ['OP_DUP', 'OP_HASH160', _h160, 'OP_EQUALVERIFY', 'OP_CHECKSIG']); + } + + @override + AddressType get type => AddressType.p2pkh; +} + +class P2pkAddress extends BipAddress { + P2pkAddress({required String publicKey}) { + final toBytes = hexToBytes(publicKey); + if (!isPoint(toBytes)) { + throw ArgumentError("The public key is wrong"); + } + publicHex = publicKey; + } + late final String publicHex; + + /// Returns the scriptPubKey (P2SH) that corresponds to this address + @override + Script toScriptPubKey() { + return Script(script: [publicHex, 'OP_CHECKSIG']); + } + + @override + String toAddress(NetworkType networkType, {Uint8List? h160}) { + final bytes = hexToBytes(publicHex); + Uint8List ripemd160Hash = hash160(bytes); + return super.toAddress(networkType, h160: ripemd160Hash); + } + + @override + AddressType get type => AddressType.p2pk; +} diff --git a/lib/src/payments/address/core.dart b/lib/src/payments/address/core.dart new file mode 100644 index 0000000..a7ec27c --- /dev/null +++ b/lib/src/payments/address/core.dart @@ -0,0 +1,49 @@ +import '../script/script.dart'; +import '../../models/networks.dart'; + +enum AddressType { + // deprecated address type + p2pk, + + p2pkh, + p2wpkh, + p2tr, + + // made up for silent payments, doesn't actually exist + p2sp, + + p2wsh, + p2wshInP2sh, + p2wpkhInP2sh, + p2pkhInP2sh, + p2pkInP2sh; + + @override + String toString() { + String label = ''; + switch (this) { + case AddressType.p2pkh: + label = 'Bitcoin Legacy'; + break; + case AddressType.p2wpkh: + label = 'Bitcoin SegWit'; + break; + case AddressType.p2tr: + label = 'Bitcoin Taproot'; + break; + case AddressType.p2sp: + label = 'Bitcoin Silent Payments'; + break; + default: + label = 'Mainnet'; + break; + } + return label; + } +} + +abstract class BitcoinAddress { + AddressType get type; + Script toScriptPubKey(); + String toAddress(NetworkType networkType); +} diff --git a/lib/src/payments/address/segwit_address.dart b/lib/src/payments/address/segwit_address.dart new file mode 100644 index 0000000..f692635 --- /dev/null +++ b/lib/src/payments/address/segwit_address.dart @@ -0,0 +1,159 @@ +import '../../crypto.dart'; +import '../../formatting/bytes_num_formatting.dart'; + +import '../../models/networks.dart'; +import 'core.dart'; +import '../constants/constants.dart'; +import '../script/script.dart'; +import '../../utils/string.dart'; +import '../../utils/uint8list.dart'; +import '../../ec/ec_public.dart'; +import 'package:bech32/bech32.dart'; + +abstract class SegwitAddress implements BitcoinAddress { + /// Represents a Bitcoin segwit address + /// + /// [program] for segwit v0 this is the hash string representation of either the address; + /// it can be either a public key hash (P2WPKH) or the hash of the script (P2WSH) + /// for segwit v1 (aka taproot) this is the public key + SegwitAddress( + {String? address, + String? program, + Script? script, + String? pubkey, + NetworkType? network, + this.version = P2WPKH_ADDRESS_V0}) { + if (version == P2WPKH_ADDRESS_V0 || version == P2WSH_ADDRESS_V0) { + segwitNumVersion = 0; + } else if (version == P2TR_ADDRESS_V1) { + segwitNumVersion = 1; + } else { + throw ArgumentError('A valid segwit version is required.'); + } + if (program != null) { + _program = program; + } else if (address != null) { + _program = _addressToHash(address, network: network); + } else if (script != null) { + _program = _scriptToHash(script); + } else if (pubkey != null) { + _program = hash160(pubkey.fromHex).hex; + } + } + + late final String _program; + + String get getProgram => _program; + + final String version; + late final int segwitNumVersion; + + String _addressToHash(String address, {NetworkType? network}) { + network ??= bitcoin; + Segwit? convert; + try { + convert = segwit.decode(address, isBech32m: this.version == P2TR_ADDRESS_V1); + } catch (_) {} + if (convert == null) { + throw ArgumentError("Invalid value for parameter address."); + } + + if (network.bech32 != convert.hrp) + throw new ArgumentError('Invalid prefix or Network mismatch'); + + if (convert.version != segwitNumVersion) { + throw ArgumentError("Invalid segwit version."); + } + return bytesToHex(convert.program); + } + + /// returns the address's string encoding (Bech32) + @override + String toAddress(NetworkType networkType) { + final bytes = hexToBytes(_program); + String? sw; + try { + sw = segwit.encode(Segwit(networkType.bech32, segwitNumVersion, bytes)); + } catch (_) {} + if (sw == null) { + throw ArgumentError("invalid address"); + } + + return sw; + } + + String _scriptToHash(Script script) { + final toBytes = script.toBytes(); + final toHash = singleHash(toBytes); + return bytesToHex(toHash); + } +} + +class P2wpkhAddress extends SegwitAddress { + static RegExp get REGEX => RegExp(r'^(bc|tb)1q[ac-hj-np-z02-9]{25,39}$'); + + /// Encapsulates a P2WPKH address. + P2wpkhAddress({super.address, super.program, super.pubkey, super.network}) + : super(version: P2WPKH_ADDRESS_V0); + + /// returns the scriptPubKey of a P2WPKH witness script + @override + Script toScriptPubKey() { + return Script(script: ['OP_0', _program]); + } + + /// returns the type of address + @override + AddressType get type => AddressType.p2wpkh; +} + +class P2trAddress extends SegwitAddress { + static RegExp get REGEX => + RegExp(r'^(bc)|(tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59})|1p[ac-hj-np-z02-9]{8,89}$'); + + /// Encapsulates a P2TR (Taproot) address. + P2trAddress({String? program, super.address, String? pubkey, super.network}) + : super( + version: P2TR_ADDRESS_V1, + program: program ?? (pubkey != null ? ECPublic.fromHex(pubkey).toTapPoint() : null)); + + /// returns the address's string encoding (Bech32m different from Bech32) + @override + String toAddress(NetworkType networkType) { + final bytes = hexToBytes(_program); + String? sw; + try { + sw = segwit.encode(Segwit(networkType.bech32, segwitNumVersion, bytes), isBech32m: true); + } catch (_) {} + if (sw == null) { + throw ArgumentError("invalid address"); + } + + return sw; + } + + /// returns the scriptPubKey of a P2TR witness script + @override + Script toScriptPubKey() { + return Script(script: ['OP_1', _program]); + } + + /// returns the type of address + @override + AddressType get type => AddressType.p2tr; +} + +class P2wshAddress extends SegwitAddress { + /// Encapsulates a P2WSH address. + P2wshAddress({super.script, super.address}) : super(version: P2WSH_ADDRESS_V0); + + /// Returns the scriptPubKey of a P2WPKH witness script + @override + Script toScriptPubKey() { + return Script(script: ['OP_0', _program]); + } + + /// Returns the type of address + @override + AddressType get type => AddressType.p2wsh; +} diff --git a/lib/src/payments/constants/constants.dart b/lib/src/payments/constants/constants.dart new file mode 100644 index 0000000..323cd54 --- /dev/null +++ b/lib/src/payments/constants/constants.dart @@ -0,0 +1,258 @@ +/// ignore_for_file: constant_identifier_names, equal_keys_in_map, non_constant_identifier_names +/// Constants and identifiers used in the Bitcoin-related code. +// ignore_for_file: constant_identifier_names, non_constant_identifier_names, equal_keys_in_map + +const Map> OP_CODES = { + 'OP_0': [0x00], + 'OP_FALSE': [0x00], + 'OP_PUSHDATA1': [0x4c], + 'OP_PUSHDATA2': [0x4d], + 'OP_PUSHDATA4': [0x4e], + 'OP_1NEGATE': [0x4f], + 'OP_1': [0x51], + 'OP_TRUE': [0x51], + 'OP_2': [0x52], + 'OP_3': [0x53], + 'OP_4': [0x54], + 'OP_5': [0x55], + 'OP_6': [0x56], + 'OP_7': [0x57], + 'OP_8': [0x58], + 'OP_9': [0x59], + 'OP_10': [0x5a], + 'OP_11': [0x5b], + 'OP_12': [0x5c], + 'OP_13': [0x5d], + 'OP_14': [0x5e], + 'OP_15': [0x5f], + 'OP_16': [0x60], + + /// flow control + 'OP_NOP': [0x61], + 'OP_IF': [0x63], + 'OP_NOTIF': [0x64], + 'OP_ELSE': [0x67], + 'OP_ENDIF': [0x68], + 'OP_VERIFY': [0x69], + 'OP_RETURN': [0x6a], + + /// stack + 'OP_TOALTSTACK': [0x6b], + 'OP_FROMALTSTACK': [0x6c], + 'OP_IFDUP': [0x73], + 'OP_DEPTH': [0x74], + 'OP_DROP': [0x75], + 'OP_DUP': [0x76], + 'OP_NIP': [0x77], + 'OP_OVER': [0x78], + 'OP_PICK': [0x79], + 'OP_ROLL': [0x7a], + 'OP_ROT': [0x7b], + 'OP_SWAP': [0x7c], + 'OP_TUCK': [0x7d], + 'OP_2DROP': [0x6d], + 'OP_2DUP': [0x6e], + 'OP_3DUP': [0x6f], + 'OP_2OVER': [0x70], + 'OP_2ROT': [0x71], + 'OP_2SWAP': [0x72], + + /// splice + /// 'OP_CAT': [0x7e], + /// 'OP_SUBSTR': [0x7f], + /// 'OP_LEFT': [0x80], + /// 'OP_RIGHT': [0x81], + 'OP_SIZE': [0x82], + + /// bitwise logic + /// 'OP_INVERT': [0x83], + /// 'OP_AND': [0x84], + /// 'OP_OR': [0x85], + /// 'OP_XOR': [0x86], + 'OP_EQUAL': [0x87], + 'OP_EQUALVERIFY': [0x88], + + /// arithmetic + 'OP_1ADD': [0x8b], + 'OP_1SUB': [0x8c], + + /// 'OP_2MUL': [0x8d], + /// 'OP_2DIV': [0x8e], + 'OP_NEGATE': [0x8f], + 'OP_ABS': [0x90], + 'OP_NOT': [0x91], + 'OP_0NOTEQUAL': [0x92], + 'OP_ADD': [0x93], + 'OP_SUB': [0x94], + + /// 'OP_MUL': [0x95], + /// 'OP_DIV': [0x96], + /// 'OP_MOD': [0x97], + /// 'OP_LSHIFT': [0x98], + /// 'OP_RSHIFT': [0x99], + 'OP_BOOLAND': [0x9a], + 'OP_BOOLOR': [0x9b], + 'OP_NUMEQUAL': [0x9c], + 'OP_NUMEQUALVERIFY': [0x9d], + 'OP_NUMNOTEQUAL': [0x9e], + 'OP_LESSTHAN': [0x9f], + 'OP_GREATERTHAN': [0xa0], + 'OP_LESSTHANOREQUAL': [0xa1], + 'OP_GREATERTHANOREQUAL': [0xa2], + 'OP_MIN': [0xa3], + 'OP_MAX': [0xa4], + 'OP_WITHIN': [0xa5], + + /// crypto + 'OP_RIPEMD160': [0xa6], + 'OP_SHA1': [0xa7], + 'OP_SHA256': [0xa8], + 'OP_HASH160': [0xa9], + 'OP_HASH256': [0xaa], + 'OP_CODESEPARATOR': [0xab], + 'OP_CHECKSIG': [0xac], + 'OP_CHECKSIGVERIFY': [0xad], + 'OP_CHECKMULTISIG': [0xae], + 'OP_CHECKMULTISIGVERIFY': [0xaf], + + /// locktime + 'OP_NOP2': [0xb1], + 'OP_CHECKLOCKTIMEVERIFY': [0xb1], + 'OP_NOP3': [0xb2], + 'OP_CHECKSEQUENCEVERIFY': [0xb2], +}; + +Map CODE_OPS = { + /// constants + 0: 'OP_0', + 76: 'OP_PUSHDATA1', + 77: 'OP_PUSHDATA2', + 78: 'OP_PUSHDATA4', + 79: 'OP_1NEGATE', + 81: 'OP_1', + 82: 'OP_2', + 83: 'OP_3', + 84: 'OP_4', + 85: 'OP_5', + 86: 'OP_6', + 87: 'OP_7', + 88: 'OP_8', + 89: 'OP_9', + 90: 'OP_10', + 91: 'OP_11', + 92: 'OP_12', + 93: 'OP_13', + 94: 'OP_14', + 95: 'OP_15', + 96: 'OP_16', + + /// flow control + 97: 'OP_NOP', + 99: 'OP_IF', + 100: 'OP_NOTIF', + 103: 'OP_ELSE', + 104: 'OP_ENDIF', + 105: 'OP_VERIFY', + 106: 'OP_RETURN', + + /// stack + 107: 'OP_TOALTSTACK', + 108: 'OP_FROMALTSTACK', + 115: 'OP_IFDUP', + 116: 'OP_DEPTH', + 117: 'OP_DROP', + 118: 'OP_DUP', + 119: 'OP_NIP', + 120: 'OP_OVER', + 121: 'OP_PICK', + 122: 'OP_ROLL', + 123: 'OP_ROT', + 124: 'OP_SWAP', + 125: 'OP_TUCK', + 109: 'OP_2DROP', + 110: 'OP_2DUP', + 111: 'OP_3DUP', + 112: 'OP_2OVER', + 113: 'OP_2ROT', + 114: 'OP_2SWAP', + + /// splice + 130: 'OP_SIZE', + + /// bitwise logic + 135: 'OP_EQUAL', + 136: 'OP_EQUALVERIFY', + + /// arithmetic + 139: 'OP_1ADD', + 140: 'OP_1SUB', + 143: 'OP_NEGATE', + 144: 'OP_ABS', + 145: 'OP_NOT', + 146: 'OP_0NOTEQUAL', + 147: 'OP_ADD', + 148: 'OP_SUB', + 154: 'OP_BOOLAND', + 155: 'OP_BOOLOR', + 156: 'OP_NUMEQUAL', + 157: 'OP_NUMEQUALVERIFY', + 158: 'OP_NUMNOTEQUAL', + 159: 'OP_LESSTHAN', + 160: 'OP_GREATERTHAN', + 161: 'OP_LESSTHANOREQUAL', + 162: 'OP_GREATERTHANOREQUAL', + 163: 'OP_MIN', + 164: 'OP_MAX', + 165: 'OP_WITHIN', + + /// crypto + 166: 'OP_RIPEMD160', + 167: 'OP_SHA1', + 168: 'OP_SHA256', + 169: 'OP_HASH160', + 170: 'OP_HASH256', + 171: 'OP_CODESEPARATOR', + 172: 'OP_CHECKSIG', + 173: 'OP_CHECKSIGVERIFY', + 174: 'OP_CHECKMULTISIG', + 175: 'OP_CHECKMULTISIGVERIFY', + + /// locktime + 177: 'OP_NOP2', + 178: 'OP_NOP3', + 177: 'OP_CHECKLOCKTIMEVERIFY', + 178: 'OP_CHECKSEQUENCEVERIFY', +}; + +/// SIGHASH types +const int SIGHASH_SINGLE = 0x03; +const int SIGHASH_ANYONECANPAY = 0x80; +const int SIGHASH_ALL = 0x01; +const int SIGHASH_NONE = 0x02; +const int TAPROOT_SIGHASH_ALL = 0x00; + +/// Transaction lock types +const int TYPE_ABSOLUTE_TIMELOCK = 0x101; +const int TYPE_RELATIVE_TIMELOCK = 0x201; +const int TYPE_REPLACE_BY_FEE = 0x301; + +/// Default values and sequences +const List DEFAULT_TX_LOCKTIME = [0x00, 0x00, 0x00, 0x00]; +const List EMPTY_TX_SEQUENCE = [0x00, 0x00, 0x00, 0x00]; +const List DEFAULT_TX_SEQUENCE = [0xff, 0xff, 0xff, 0xff]; +const List ABSOLUTE_TIMELOCK_SEQUENCE = [0xfe, 0xff, 0xff, 0xff]; +const List REPLACE_BY_FEE_SEQUENCE = [0x01, 0x00, 0x00, 0x00]; + +/// Script version and Bitcoin-related identifiers +const int LEAF_VERSION_TAPSCRIPT = 0xc0; +const List DEFAULT_TX_VERSION = [0x02, 0x00, 0x00, 0x00]; +const int SATOSHIS_PER_BITCOIN = 100000000; +const int NEGATIVE_SATOSHI = -1; + +/// Bitcoin address types +const String P2PKH_ADDRESS = "p2pkh"; +const String P2SH_ADDRESS = "p2sh"; +const String P2WPKH_ADDRESS_V0 = "p2wpkhv0"; +const String P2WSH_ADDRESS_V0 = "p2wshv0"; +const String P2TR_ADDRESS_V1 = "p2trv1"; + diff --git a/lib/src/payments/constants/constants_lib.dart b/lib/src/payments/constants/constants_lib.dart new file mode 100644 index 0000000..0719a23 --- /dev/null +++ b/lib/src/payments/constants/constants_lib.dart @@ -0,0 +1,26 @@ +library bitcoin_constants; + +export 'constants.dart' + show + SIGHASH_SINGLE, + SIGHASH_ANYONECANPAY, + TYPE_ABSOLUTE_TIMELOCK, + TYPE_RELATIVE_TIMELOCK, + TYPE_REPLACE_BY_FEE, + SIGHASH_ALL, + SIGHASH_NONE, + TAPROOT_SIGHASH_ALL, + DEFAULT_TX_LOCKTIME, + EMPTY_TX_SEQUENCE, + DEFAULT_TX_SEQUENCE, + ABSOLUTE_TIMELOCK_SEQUENCE, + REPLACE_BY_FEE_SEQUENCE, + LEAF_VERSION_TAPSCRIPT, + DEFAULT_TX_VERSION, + SATOSHIS_PER_BITCOIN, + NEGATIVE_SATOSHI, + P2PKH_ADDRESS, + P2SH_ADDRESS, + P2WPKH_ADDRESS_V0, + P2WSH_ADDRESS_V0, + P2TR_ADDRESS_V1; diff --git a/lib/src/payments/p2wpkh.dart b/lib/src/payments/p2wpkh.dart index 93cc20c..23a6246 100644 --- a/lib/src/payments/p2wpkh.dart +++ b/lib/src/payments/p2wpkh.dart @@ -11,12 +11,15 @@ import '../utils/constants/op.dart'; class P2WPKH { final EMPTY_SCRIPT = Uint8List.fromList([]); + final OP = OPS['OP_0']; + final version = 0; + final length = 22; PaymentData data; NetworkType network; P2WPKH({@required data, network}) - : this.network = network ?? bitcoin, - this.data = data { + : this.network = network ?? bitcoin, + this.data = data { _init(); } @@ -36,9 +39,9 @@ class P2WPKH { } if (data.output != null) { - if (data.output!.length != 22 || - data.output![0] != OPS['OP_0'] || - data.output![1] != 20) // 0x14 + if (data.output!.length != length || + data.output![0] != OP || + (version == 0 ? data.output![1] != 20 : false)) // 0x14 throw new ArgumentError('Output is invalid'); if (data.hash == null) { data.hash = data.output!.sublist(2); @@ -52,12 +55,10 @@ class P2WPKH { } if (data.witness != null) { - if (data.witness!.length != 2) - throw new ArgumentError('Witness is invalid'); + if (data.witness!.length != 2) throw new ArgumentError('Witness is invalid'); if (!bscript.isCanonicalScriptSignature(data.witness![0])) throw new ArgumentError('Witness has invalid signature'); - if (!isPoint(data.witness![1])) - throw new ArgumentError('Witness has invalid pubkey'); + if (!isPoint(data.witness![1])) throw new ArgumentError('Witness has invalid pubkey'); _getDataFromWitness(data.witness!); } else if (data.pubkey != null && data.signature != null) { data.witness = [data.signature!, data.pubkey!]; @@ -81,10 +82,11 @@ class P2WPKH { void _getDataFromHash() { if (data.address == null) { - data.address = segwit.encode(Segwit(network.bech32, 0, data.hash!)); + data.address = + segwit.encode(Segwit(network.bech32, version, data.hash!), isBech32m: version == 1); } if (data.output == null) { - data.output = bscript.compile([OPS['OP_0'], data.hash]); + data.output = bscript.compile([OP, data.hash]); } } @@ -93,8 +95,7 @@ class P2WPKH { Segwit _address = segwit.decode(address); if (network.bech32 != _address.hrp) throw new ArgumentError('Invalid prefix or Network mismatch'); - if (_address.version != 0) // Only support version 0 now; - throw new ArgumentError('Invalid address version'); + if (_address.version != version) throw new ArgumentError('Invalid address version'); data.hash = Uint8List.fromList(_address.program); } on InvalidHrp { throw new ArgumentError('Invalid prefix or Network mismatch'); @@ -105,3 +106,14 @@ class P2WPKH { } } } + +class P2TR extends P2WPKH { + @override + final OP = OPS['OP_1']; + @override + final version = 1; + @override + final length = 34; + + P2TR({@required data, network}) : super(data: data, network: network); +} diff --git a/lib/src/payments/scanning.dart b/lib/src/payments/scanning.dart new file mode 100644 index 0000000..e1e8408 --- /dev/null +++ b/lib/src/payments/scanning.dart @@ -0,0 +1,136 @@ +import 'dart:typed_data'; +import 'package:bitcoin_flutter/src/utils/int.dart'; +import 'package:bitcoin_flutter/src/utils/string.dart'; +import 'package:elliptic/elliptic.dart'; +import 'package:crypto/crypto.dart'; +import '../utils/uint8list.dart'; + +// https://github.com/bitcoin/bips/blob/c55f80c53c98642357712c1839cfdc0551d531c4/bip-0352.mediawiki#scanning +// https://github.com/bitcoin-core-review-club/bips/blob/cfe0771a0408a2d2de278d4e95bb9a33bd1615b2/bip-0352/reference.py#L105 + +// RETURNS: [{output: [tweak, label]}] +// Maps the output pubkey to the tweak used for the shared secret, and the label used to derive the tweak if applicable +Map> scanOutputs(PrivateKey b_scan, PublicKey B_spend, PublicKey A_sum, + Uint8List outpointsHash, List outputPubKeys, + {Map? labels}) { + final curve = getSecp256k1(); + + // - Let ecdh_shared_secret = outpoints_hash·b_scan·A + final tweakDataForRecipient = PublicKey.fromPoint(curve, A_sum).tweakMul(outpointsHash.bigint); + final ecdhSharedSecret = tweakDataForRecipient!.tweakMul(b_scan.D); + + // P_k to priv key tweak matches + final matches = >{}; + + // - Starting with k = 0: + var k = 0; + + do { + // - Let t_k = sha256(serP(ecdh_shared_secret) || ser32(k)) + final t_k = sha256 + .convert(ecdhSharedSecret!.toCompressedHex().fromHex.concat([k.toBigEndianBytes])) + .toString() + .fromHex; + + // - Compute P_k = B_spend + t_k·G + final P_k = PublicKey.fromPoint(curve, B_spend).tweakAdd(t_k.bigint).toCompressedHex().fromHex; + final length = outputPubKeys.length; + + // - For each output in outputPubKeys + for (var i = 0; i < length; i++) { + final output = outputPubKeys[i]; + + // - If P_k equals output + if (output.sublist(1) != P_k.sublist(1) + ? output.hex == P_k.sublist(1).hex + : output.sublist(1).hex == P_k.sublist(1).hex) { + // - Add P_k to the wallet + matches[output.hex] = [t_k.hex]; + outputPubKeys.removeAt(i); + k++; // Increment counter + break; + } + + // - Else, if the wallet has precomputed labels (including the change label, if used) + if (labels != null && labels.isNotEmpty) { + final outputPubkey = PublicKey.fromBytes(curve, output); + + // - Compute m·G = output - Pk + // m·G = output + (-P_k) + final negatedP_k = PublicKey.fromBytes(curve, P_k).negate(); + var m_G_sub = PublicKey.fromPoint(curve, outputPubkey) + .pubkeyAdd(negatedP_k) + .toCompressedHex() + .fromHex; + + // - Check if m·G exists in the list of labels used by the wallet + var m_G = labels[m_G_sub.hex]; + + // - If the label is not found, negate output and check again + if (m_G == null) { + outputPubkey.negate(); + m_G_sub = PublicKey.fromPoint(curve, outputPubkey) + .pubkeyAdd(negatedP_k) + .toCompressedHex() + .fromHex; + + m_G = labels[m_G_sub.hex]; + } + + // - If a match is found: + if (m_G != null) { + // - Add the P_k + m·G to the wallet + final P_km = PublicKey.fromBytes(curve, P_k) + .tweakAdd(m_G.fromHex.bigint) + .toCompressedHex() + .fromHex; + + if (P_km[0] == 0x03) { + P_km[0] = 0x02; + } + + matches[output.hex] = [ + PrivateKey.fromBytes(curve, t_k).tweakAdd(m_G.fromHex.bigint)!.toCompressedHex(), + m_G + ]; + + outputPubKeys.removeAt(i); + k++; // Increment counter + break; + } else { + final found = labels.values.any((tweak) { + final B_m = PublicKey.fromPoint(curve, B_spend).tweakAdd(tweak.fromHex.bigint); + final P_km = + PublicKey.fromPoint(curve, B_m).tweakAdd(t_k.bigint).toCompressedHex().fromHex; + + if (output.sublist(1) != P_km.sublist(1) + ? output.hex == P_km.sublist(1).hex + : output.sublist(1).hex == P_km.sublist(1).hex) { + // - Add P_km to the wallet + matches[output.hex] = [ + PrivateKey.fromBytes(curve, t_k).tweakAdd(tweak.fromHex.bigint)!.toCompressedHex(), + tweak + ]; + outputPubKeys.removeAt(i); + k++; // Increment counter + return true; + } + return false; + }); + + if (found) { + break; + } + } + } + + outputPubKeys.removeAt(i); + + if (i + 1 >= outputPubKeys.length) { + break; + } + } + } while (outputPubKeys.isNotEmpty); + + return matches; +} diff --git a/lib/src/payments/script/script.dart b/lib/src/payments/script/script.dart new file mode 100644 index 0000000..f9770cd --- /dev/null +++ b/lib/src/payments/script/script.dart @@ -0,0 +1,146 @@ +import 'dart:typed_data'; + +import '../address/address.dart'; +import '../constants/constants.dart'; +import '../tools/tools.dart'; +import '../../crypto.dart'; +import '../../formatting/bytes_num_formatting.dart'; +import '../../formatting/bytes_tracker.dart'; + +enum ScriptType { P2PKH, P2SH, P2WPKH, P2WSH, P2PK } + +/// A Script contains just a list of OP_CODES and also knows how to serialize into bytes +/// +/// [script] the list with all the script OP_CODES and data +class Script { + const Script({required this.script}); + final List script; + + Uint8List toTapleafTaggedHash() { + final leafVarBytes = Uint8List.fromList([ + ...Uint8List.fromList([LEAF_VERSION_TAPSCRIPT]), + ...prependVarint(toBytes()) + ]); + return taggedHash(leafVarBytes, "TapLeaf"); + } + + /// create p2psh script wit current script + Script toP2shScriptPubKey() { + final address = P2shAddress(script: this); + return Script(script: ['OP_HASH160', address.getH160, 'OP_EQUAL']); + } + + static Script fromRaw({required String hexData, bool hasSegwit = false}) { + List commands = []; + int index = 0; + final scriptraw = hexToBytes(hexData); + while (index < scriptraw.length) { + int byte = scriptraw[index]; + if (CODE_OPS.containsKey(byte)) { + commands.add(CODE_OPS[byte]!); + index = index + 1; + } else if (!hasSegwit && byte == 0x4c) { + int bytesToRead = scriptraw[index + 1]; + index = index + 1; + commands.add(scriptraw + .sublist(index, index + bytesToRead) + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join()); + index = index + bytesToRead; + } else if (!hasSegwit && byte == 0x4d) { + int bytesToRead = + ByteData.sublistView(scriptraw, index + 1, index + 3).getUint16(0, Endian.little); + index = index + 3; + commands.add(scriptraw + .sublist(index, index + bytesToRead) + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join()); + index = index + bytesToRead; + } else if (!hasSegwit && byte == 0x4e) { + int bytesToRead = + ByteData.sublistView(scriptraw, index + 1, index + 5).getUint32(0, Endian.little); + index = index + 5; + commands.add(scriptraw + .sublist(index, index + bytesToRead) + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join()); + index = index + bytesToRead; + } else { + final viAndSize = viToInt(scriptraw.sublist(index, index + 9)); + int dataSize = viAndSize.$1; + int size = viAndSize.$2; + final lastIndex = (index + size + dataSize) > scriptraw.length + ? scriptraw.length + : (index + size + dataSize); + commands.add(bytesToHex(scriptraw.sublist(index + size, lastIndex))); + index = index + dataSize + size; + } + } + return Script(script: commands); + } + + static ScriptType? getType({required String hexData, bool hasSegwit = false}) { + final Script s = fromRaw(hexData: hexData, hasSegwit: hasSegwit); + if (s.script.isEmpty) return null; + final first = s.script.elementAtOrNull(0); + final sec = s.script.elementAtOrNull(1); + final th = s.script.elementAtOrNull(2); + final four = s.script.elementAtOrNull(3); + final five = s.script.elementAtOrNull(4); + if (first == "OP_0") { + if (sec is String?) { + if (sec?.length == 40) { + return ScriptType.P2WPKH; + } else if (sec?.length == 64) { + return ScriptType.P2WSH; + } + } + } else if (first == "OP_DUP") { + if (sec == "OP_HASH160" && four == "OP_EQUALVERIFY" && five == "OP_CHECKSIG") { + return ScriptType.P2PKH; + } + } else if (first == "OP_HASH160" && th == "OP_EQUAL") { + return ScriptType.P2SH; + } else if (sec == "OP_CHECKSIG" && first is String) { + if (first.length == 66) { + return ScriptType.P2PK; + } + } + return null; + } + + /// returns a serialized byte version of the script + Uint8List toBytes() { + DynamicByteTracker scriptBytes = DynamicByteTracker(); + try { + for (var token in script) { + if (OP_CODES.containsKey(token)) { + scriptBytes.add(OP_CODES[token]!); + } else if (token is int && token >= 0 && token <= 16) { + scriptBytes.add(OP_CODES['OP_$token']!); + } else { + if (token is int) { + scriptBytes.add(pushInteger(token)); + } else { + scriptBytes.add(opPushData(token)); + } + } + } + + return scriptBytes.toBytes(); + } finally { + scriptBytes.close(); + } + } + + /// returns a serialized version of the script in hex + String toHex() { + final bytes = toBytes(); + return bytesToHex(bytes); + } + + @override + String toString() { + return script.join(","); + } +} diff --git a/lib/src/payments/silentpayments.dart b/lib/src/payments/silentpayments.dart new file mode 100644 index 0000000..e5cd530 --- /dev/null +++ b/lib/src/payments/silentpayments.dart @@ -0,0 +1,122 @@ +import 'dart:typed_data'; + +import 'package:bitcoin_flutter/src/templates/outpoint.dart'; +import 'package:bitcoin_flutter/src/utils/int.dart'; +import 'package:bitcoin_flutter/src/utils/keys.dart'; +import 'package:bitcoin_flutter/src/utils/string.dart'; +import 'package:crypto/crypto.dart'; +import 'package:bitcoin_flutter/src/templates/silentpaymentaddress.dart'; +import 'package:bitcoin_flutter/src/utils/uint8list.dart'; +import 'package:elliptic/elliptic.dart'; + +class SilentPayment { + SilentPayment(); + + static List decodeOutpoints(List<(String, int)> outpoints) => + outpoints.map((e) => Outpoint(txid: e.$1, index: e.$2)).toList(); + + // https://github.com/bitcoin/bips/blob/c55f80c53c98642357712c1839cfdc0551d531c4/bip-0352.mediawiki#outpoints-hash + // - The sender and receiver MUST calculate an outpoints hash for the transaction in the following manner: + static Uint8List hashOutpoints(List sendingData) { + final outpoints = []; + + // - Collect each outpoint used as an input to the transaction + for (final outpoint in sendingData) { + final bytes = outpoint.txid.fromHex; + final vout = outpoint.index; + + outpoints.add(concatenateUint8Lists( + [Uint8List.fromList(bytes.reversed.toList()), vout.toLittleEndianBytes])); + } + + // - Let outpoints = outpoint_0 || ... || outpoint_n, sorted lexicographically by txid and vout, ascending order + outpoints.sort((a, b) => a.compare(b)); + + // - Let outpoints_hash = sha256(outpoints) + return Uint8List.fromList(sha256.convert(concatenateUint8Lists(outpoints)).bytes); + } + + // https://github.com/bitcoin/bips/blob/c55f80c53c98642357712c1839cfdc0551d531c4/bip-0352.mediawiki#creating-outputs + static Map> generateMultipleRecipientPubkeys( + List inputPrivKeyInfos, + Uint8List outpointsHash, + List silentPaymentDestinations) { + final curve = getSecp256k1(); + + // - Let a_sum = a_0 + a_1 + ... + a_n, where each a_i has been negated if necessary + PrivateKey? a_sum; + PublicKey? A_sum; + + // - Collect the private keys for each input from the Inputs For Shared Secret Derivation list + for (final info in inputPrivKeyInfos) { + final key = info.key; + final isTaproot = info.isTaproot; + + PrivateKey? negated; + + // - For each private key a_i corresponding to a BIP341 taproot output, check that the private key produces a point with an even y-value and negate the private key if not + if (isTaproot && key.toCompressedHex().fromHex[0] == 0x03) { + negated = PrivateKey(curve, key.D).negate()!; + } else { + negated = PrivateKey(curve, key.D); + } + + if (a_sum == null) { + a_sum = negated; + A_sum = negated.publicKey; + } else { + a_sum = a_sum.tweakAdd(negated.D); + A_sum!.pubkeyAdd(negated.publicKey); + } + } + + // Group each destination by a different ecdhSharedSecret + // { : (, [, ...]) } + Map)> silentPaymentGroups = {}; + + silentPaymentDestinations.forEach((silentPaymentDestination) { + final B_scan = silentPaymentDestination.scanPubkey; + final scanPubKeyStr = B_scan.toCompressedHex(); + + if (silentPaymentGroups.containsKey(scanPubKeyStr)) { + // Current key already in silentPaymentGroups, simply add up the new destination + // with the already calculated ecdhSharedSecret + final (ecdhSharedSecret, recipients) = silentPaymentGroups[scanPubKeyStr]!; + silentPaymentGroups[scanPubKeyStr] = + (ecdhSharedSecret, [...recipients, silentPaymentDestination]); + } else { + // New silent payment destination, calculate a new ecdhSharedSecret + final senderPartialSecret = PrivateKey(curve, a_sum!.D).tweakMul(outpointsHash.bigint)!.D; + final ecdhSharedSecret = PublicKey.fromPoint(curve, B_scan).tweakMul(senderPartialSecret)!; + + silentPaymentGroups[scanPubKeyStr] = (ecdhSharedSecret, [silentPaymentDestination]); + } + }); + + // Result destinations with amounts + // { : [(, ), (, )...] } + Map> result = {}; + silentPaymentGroups.entries.forEach((group) { + final (ecdhSharedSecret, destinations) = group.value; + + int k = 0; + destinations.forEach((destination) { + final t_k = + sha256.convert(ecdhSharedSecret.toCompressedHex().fromHex.concat([k.toBigEndianBytes])); + + final P_mn = PublicKey.fromPoint(curve, destination.spendPubkey) + .tweakAdd(Uint8List.fromList(t_k.bytes).bigint); + + if (result.containsKey(destination.toString())) { + result[destination.toString()]!.add((P_mn, destination.amount)); + } else { + result[destination.toString()] = [(P_mn, destination.amount)]; + } + + k++; + }); + }); + + return result; + } +} diff --git a/lib/src/payments/tools/tools.dart b/lib/src/payments/tools/tools.dart new file mode 100644 index 0000000..c21d401 --- /dev/null +++ b/lib/src/payments/tools/tools.dart @@ -0,0 +1,94 @@ +import 'dart:typed_data'; +import '../../payments/address/core.dart'; +import '../../models/networks.dart'; +import '../../formatting/bytes_num_formatting.dart'; +import 'package:convert/convert.dart'; +import 'package:bs58check/bs58check.dart' as bs58check; + +bool isValidAddress(String address, AddressType type, {NetworkType? network}) { + if (address.length < 26 || address.length > 35) { + return false; + } + final decode = bs58check.decode(address); + final int networkPrefix = decode[0]; + switch (type) { + case AddressType.p2pkh: + if (network != null) { + return networkPrefix == network.p2pkhPrefix; + } + return networkPrefix == NetworkType.BITCOIN.p2pkhPrefix || + networkPrefix == NetworkType.TESTNET.p2pkhPrefix; + case AddressType.p2pkhInP2sh: + case AddressType.p2pkInP2sh: + case AddressType.p2wshInP2sh: + case AddressType.p2wpkhInP2sh: + if (network != null) { + return networkPrefix == network.p2shPrefix; + } + return networkPrefix == NetworkType.BITCOIN.p2shPrefix || + networkPrefix == NetworkType.TESTNET.p2shPrefix; + default: + } + return true; +} + +bool isValidHash160(String hash160) { + if (hash160.length != 40) { + return false; + } + try { + BigInt.parse(hash160, radix: 16); + } catch (e) { + return false; + } + return true; +} + +List opPushData(String hexData) { + final Uint8List dataBytes = hexToBytes(hexData); + if (dataBytes.length < 0x4c) { + return Uint8List.fromList([dataBytes.length]) + dataBytes; + } else if (dataBytes.length < 0xff) { + return Uint8List.fromList([0x4c]) + Uint8List.fromList([dataBytes.length]) + dataBytes; + } else if (dataBytes.length < 0xffff) { + var lengthBytes = ByteData(2); + lengthBytes.setUint16(0, dataBytes.length, Endian.little); + return Uint8List.fromList([0x4d]) + Uint8List.view(lengthBytes.buffer) + dataBytes; + } else if (dataBytes.length < 0xffffffff) { + var lengthBytes = ByteData(4); + lengthBytes.setUint32(0, dataBytes.length, Endian.little); + return Uint8List.fromList([0x4e]) + Uint8List.view(lengthBytes.buffer) + dataBytes; + } else { + throw ArgumentError("Data too large. Cannot push into script"); + } +} + +Uint8List pushInteger(int integer) { + if (integer < 0) { + throw ArgumentError('Integer is currently required to be positive.'); + } + + /// Calculate the number of bytes required to represent the integer + int numberOfBytes = (integer.bitLength + 7) ~/ 8; + + /// Convert to little-endian bytes + Uint8List integerBytes = Uint8List(numberOfBytes); + for (int i = 0; i < numberOfBytes; i++) { + integerBytes[i] = (integer >> (i * 8)) & 0xFF; + } + + /// If the last bit is set, add a sign byte to signify a positive integer + if ((integer & (1 << (numberOfBytes * 8 - 1))) != 0) { + integerBytes = Uint8List.fromList([...integerBytes, 0x00]); + } + + return Uint8List.fromList(opPushData(hex.encode(integerBytes))); +} + +Uint8List bytes32FromInt(int x) { + var result = Uint8List(32); + for (var i = 0; i < 32; i++) { + result[32 - i - 1] = (x >> (8 * i)) & 0xFF; + } + return result; +} diff --git a/lib/src/templates/outpoint.dart b/lib/src/templates/outpoint.dart new file mode 100644 index 0000000..693cf21 --- /dev/null +++ b/lib/src/templates/outpoint.dart @@ -0,0 +1,19 @@ +import 'dart:typed_data'; + +import 'package:bitcoin_flutter/bitcoin_flutter.dart'; + +class Outpoint { + Outpoint({required this.txid, required this.index, this.value}); + + String txid; + int index; + int? value; + + factory Outpoint.fromBytes(Uint8List txid, int index, {int? value}) { + return Outpoint(txid: txid.hex, index: index, value: value); + } + + String toString() { + return 'Outpoint{txid: $txid, index: $index, value: $value}'; + } +} diff --git a/lib/src/templates/silentpaymentaddress.dart b/lib/src/templates/silentpaymentaddress.dart new file mode 100644 index 0000000..76ef7a6 --- /dev/null +++ b/lib/src/templates/silentpaymentaddress.dart @@ -0,0 +1,188 @@ +import 'dart:typed_data'; + +import '../bitcoin_flutter_base.dart'; +import '../utils/constants/derivation_paths.dart'; +import '../utils/string.dart'; +import '../utils/uint8list.dart'; +import 'package:elliptic/elliptic.dart'; +import 'package:bech32/bech32.dart'; +import 'package:bip32/bip32.dart' as bip32; +import 'package:bip39/bip39.dart' as bip39; + +class SilentPaymentReceiver extends SilentPaymentAddress { + late int version; + late PublicKey scanPubkey; + late PublicKey spendPubkey; + late String hrp; + + late PrivateKey scanPrivkey; + late PrivateKey spendPrivkey; + + SilentPaymentReceiver({ + required this.version, + required this.scanPubkey, + required this.spendPubkey, + required this.hrp, + required this.scanPrivkey, + required this.spendPrivkey, + }) : super( + version: version, + scanPubkey: scanPubkey, + spendPubkey: spendPubkey, + hrp: hrp, + ); + + factory SilentPaymentReceiver.fromPrivKeys( + {required PrivateKey scanPrivkey, required spendPrivkey, String? hrp, int? version}) { + return SilentPaymentReceiver( + scanPrivkey: scanPrivkey, + spendPrivkey: spendPrivkey, + scanPubkey: scanPrivkey.publicKey, + spendPubkey: spendPrivkey.publicKey, + hrp: hrp ?? 'sp', + version: version ?? 0, + ); + } + + factory SilentPaymentReceiver.fromHd(HDWallet hd, {String? hrp, int? version}) { + final scanPubkey = hd.derivePath(SCAN_PATH); + final spendPubkey = hd.derivePath(SPEND_PATH); + + final curve = getSecp256k1(); + + return SilentPaymentReceiver( + scanPrivkey: PrivateKey.fromBytes(curve, scanPubkey.privKey!.fromHex), + spendPrivkey: PrivateKey.fromBytes(curve, spendPubkey.privKey!.fromHex), + scanPubkey: PublicKey.fromHex(curve, scanPubkey.pubKey!), + spendPubkey: PublicKey.fromHex(curve, spendPubkey.pubKey!), + hrp: hrp ?? 'sp', + version: version ?? 0, + ); + } + + factory SilentPaymentReceiver.fromMnemonic(String mnemonic, {String? hrp, int? version}) { + final seed = bip39.mnemonicToSeed(mnemonic); + final root = bip32.BIP32.fromSeed( + seed, + hrp == "tsp" + ? bip32.NetworkType( + wif: 0xef, bip32: new bip32.Bip32Type(public: 0x043587cf, private: 0x04358394)) + : null); + if (root.depth != 0 || root.parentFingerprint != 0) throw new ArgumentError('Bad master key!'); + + final scanPubkey = root.derivePath(SCAN_PATH); + final spendPubkey = root.derivePath(SPEND_PATH); + + final curve = getSecp256k1(); + + return SilentPaymentReceiver( + scanPrivkey: PrivateKey.fromBytes(curve, scanPubkey.privateKey!), + spendPrivkey: PrivateKey.fromBytes(curve, spendPubkey.privateKey!), + scanPubkey: PublicKey.fromHex(curve, scanPubkey.publicKey.hex), + spendPubkey: PublicKey.fromHex(curve, spendPubkey.publicKey.hex), + hrp: hrp ?? 'sp', + version: version ?? 0, + ); + } +} + +class SilentPaymentDestination extends SilentPaymentAddress { + SilentPaymentDestination({ + required int version, + required PublicKey scanPubkey, + required PublicKey spendPubkey, + required String hrp, + required this.amount, + }) : super(version: version, scanPubkey: scanPubkey, spendPubkey: spendPubkey, hrp: hrp); + + int amount; + + factory SilentPaymentDestination.fromAddress(String address, int amount) { + final receiver = SilentPaymentAddress.fromString(address); + + return SilentPaymentDestination( + scanPubkey: receiver.scanPubkey, + spendPubkey: receiver.spendPubkey, + hrp: receiver.hrp, + version: receiver.version, + amount: amount, + ); + } +} + +class SilentPaymentAddress { + static RegExp get REGEX => RegExp(r'^t?sp1[0-9a-zA-Z]{113}$'); + + int version; + PublicKey scanPubkey; + PublicKey spendPubkey; + // human readable part (sprt, sp, tsp) + String hrp; + + SilentPaymentAddress({ + required this.version, + required this.scanPubkey, + required this.spendPubkey, + required this.hrp, + }) { + if (version != 0) { + throw Exception("Can't have other version than 0 for now"); + } + } + + factory SilentPaymentAddress.fromString(String address) { + final decoded = bech32m.decode(address, 1023); + + final prefix = decoded.hrp; + if (prefix != 'sp' && prefix != 'sprt' && prefix != 'tsp') { + throw Exception('Invalid prefix: $prefix'); + } + + final words = decoded.data; + final version = words[0]; + if (version != 0) throw new ArgumentError('Invalid version'); + + final key = fromWords(Uint8List.fromList(decoded.data.sublist(1))); + final curve = getSecp256k1(); + + return SilentPaymentAddress( + scanPubkey: PublicKey.fromHex(curve, key.sublist(0, 33).hex), + spendPubkey: PublicKey.fromHex(curve, key.sublist(33).hex), + hrp: prefix, + version: version, + ); + } + + factory SilentPaymentAddress.createLabeledSilentPaymentAddress( + PublicKey B_scan, PublicKey B_spend, Uint8List m, + {String hrp = 'sp', int version = 0}) { + final B_m = PublicKey.fromPoint(getSecp256k1(), B_spend).tweakAdd(m.bigint); + return SilentPaymentAddress(scanPubkey: B_scan, spendPubkey: B_m, hrp: hrp, version: version); + } + + @override + String toString() { + final data = toWords(Uint8List.fromList( + [...scanPubkey.toCompressedHex().fromHex, ...spendPubkey.toCompressedHex().fromHex])); + final versionData = Uint8List.fromList([Bech32U5(version).value, ...data]); + + return bech32m.encode(Bech32(hrp, versionData), 1023); + } +} + +class Bech32U5 { + final int value; + + Bech32U5(this.value) { + if (value < 0 || value > 31) { + throw Exception('Value is outside the valid range.'); + } + } + + static Bech32U5 tryFromInt(int value) { + if (value < 0 || value > 31) { + throw Exception('Value is outside the valid range.'); + } + return Bech32U5(value); + } +} diff --git a/lib/src/templates/witnesspubkeyhash.dart b/lib/src/templates/witnesspubkeyhash.dart index ec660d7..12765e0 100644 --- a/lib/src/templates/witnesspubkeyhash.dart +++ b/lib/src/templates/witnesspubkeyhash.dart @@ -13,3 +13,8 @@ bool outputCheck(Uint8List script) { final buffer = bscript.compile(script); return buffer.length == 22 && buffer[0] == OPS['OP_0'] && buffer[1] == 0x14; } + +bool taprootOutputCheck(Uint8List script) { + final buffer = bscript.compile(script); + return buffer[0] == OPS['OP_1']; +} diff --git a/lib/src/transaction.dart b/lib/src/transaction.dart index 35794f6..3f51355 100644 --- a/lib/src/transaction.dart +++ b/lib/src/transaction.dart @@ -1,19 +1,27 @@ import 'dart:typed_data'; +// import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_flutter/src/formatting/bytes_tracker.dart'; import 'package:hex/hex.dart'; +// import 'package:pointycastle/impl.dart'; import 'payments/index.dart' show PaymentData; import 'payments/p2pkh.dart' show P2PKH; import 'payments/p2pk.dart' show P2PK; -import 'payments/p2wpkh.dart' show P2WPKH; +import 'payments/p2wpkh.dart' show P2WPKH, P2TR; import 'crypto.dart' as bcrypto; import 'classify.dart'; import 'utils/check_types.dart'; import 'utils/script.dart' as bscript; +import 'ecpair.dart' as ecpair; import 'utils/constants/op.dart'; import 'utils/varuint.dart' as varuint; +import 'utils/uint8list.dart'; import 'package:collection/collection.dart'; +import 'formatting/bytes_num_formatting.dart'; +const LEAF_VERSION_TAPSCRIPT = 0xc0; const DEFAULT_SEQUENCE = 0xffffffff; const SIGHASH_ALL = 0x01; +const TAPROOT_SIGHASH_ALL = 0x00; const SIGHASH_NONE = 0x02; const SIGHASH_SINGLE = 0x03; const SIGHASH_ANYONECANPAY = 0x80; @@ -21,20 +29,19 @@ const ADVANCED_TRANSACTION_MARKER = 0x00; const ADVANCED_TRANSACTION_FLAG = 0x01; final EMPTY_SCRIPT = Uint8List.fromList([]); final EMPTY_WITNESS = []; -final ZERO = HEX - .decode('0000000000000000000000000000000000000000000000000000000000000000'); -final ONE = HEX - .decode('0000000000000000000000000000000000000000000000000000000000000001'); +final ZERO = HEX.decode('0000000000000000000000000000000000000000000000000000000000000000'); +final ONE = HEX.decode('0000000000000000000000000000000000000000000000000000000000000001'); final VALUE_UINT64_MAX = HEX.decode('ffffffffffffffff'); final BLANK_OUTPUT = new Output(script: EMPTY_SCRIPT, valueBuffer: Uint8List.fromList(VALUE_UINT64_MAX)); class Transaction { int version = 1; + String? txHex; int locktime = 0; List ins = []; List outs = []; - Transaction(); + Transaction({int? version}) : this.version = version ?? 1; int addInput(Uint8List hash, int index, [int? sequence, Uint8List? scriptSig]) { ins.add(new Input( @@ -52,8 +59,8 @@ class Transaction { } bool hasWitnesses() { - var witness = ins.firstWhereOrNull( - (input) => input.witness != null && input.witness!.length != 0); + var witness = + ins.firstWhereOrNull((input) => input.witness != null && input.witness!.length != 0); return witness != null; } @@ -65,8 +72,7 @@ class Transaction { ins[index].witness = witness; } - hashForWitnessV0( - int inIndex, Uint8List prevOutScript, int value, int hashType) { + hashForWitnessV0(int inIndex, Uint8List prevOutScript, int value, int hashType) { var tbuffer = Uint8List.fromList([]); var toffset = 0; // Any changes made to the ByteData will also change the buffer, and vice versa. @@ -142,10 +148,8 @@ class Transaction { hashSequence = bcrypto.hash256(tbuffer); } - if ((hashType & 0x1f) != SIGHASH_SINGLE && - (hashType & 0x1f) != SIGHASH_NONE) { - var txOutsSize = - outs.fold(0, (sum, output) => sum + 8 + varSliceSize(output.script!)); + if ((hashType & 0x1f) != SIGHASH_SINGLE && (hashType & 0x1f) != SIGHASH_NONE) { + var txOutsSize = outs.fold(0, (sum, output) => sum + 8 + varSliceSize(output.script!)); tbuffer = new Uint8List(txOutsSize); bytes = tbuffer.buffer.asByteData(); toffset = 0; @@ -184,11 +188,121 @@ class Transaction { return bcrypto.hash256(tbuffer); } + hashForWitnessV1(int txIndex, Uint8List prevOutScript, int value, int hashType, List amounts, + List scriptPubKeys, + {int sighash = TAPROOT_SIGHASH_ALL}) { + bool sighashNone = (sighash & 0x03) == SIGHASH_NONE; + bool sighashSingle = (sighash & 0x03) == SIGHASH_SINGLE; + bool anyoneCanPay = (sighash & 0x80) == SIGHASH_ANYONECANPAY; + DynamicByteTracker txForSign = DynamicByteTracker(); + txForSign.add([0]); + txForSign.add(Uint16List.fromList([sighash])); + txForSign.add([0x02, 0x00, 0x00, 0x00]); + txForSign.add([0x00, 0x00, 0x00, 0x00]); + Uint8List hashPrevouts = Uint8List(0); + Uint8List hashAmounts = Uint8List(0); + Uint8List hashScriptPubkeys = Uint8List(0); + Uint8List hashSequences = Uint8List(0); + Uint8List hashOutputs = Uint8List(0); + if (!anyoneCanPay) { + for (final txin in ins) { + Uint8List txidBytes = Uint8List.fromList(txin.hash!); + + Uint8List txoutIndexBytes = packUint32LE(txin.index!); + hashPrevouts = Uint8List.fromList([...hashPrevouts, ...txidBytes, ...txoutIndexBytes]); + + final h = txin.script!.hex; + + /// must checked + int scriptLen = h.length ~/ 2; + Uint8List scriptBytes = hexToBytes(h); + Uint8List lenBytes = Uint8List.fromList([scriptLen]); + hashScriptPubkeys = Uint8List.fromList([...hashScriptPubkeys, ...lenBytes, ...scriptBytes]); + + hashSequences = Uint8List.fromList([ + ...hashSequences, + ...[0x00, 0x00, 0x00, 0x00] + ]); + } + hashPrevouts = bcrypto.singleHash(hashPrevouts); + txForSign.add(hashPrevouts); + + for (final i in amounts) { + Uint8List bytes = packBigIntToLittleEndian(BigInt.from(i)); + hashAmounts = Uint8List.fromList([...hashAmounts, ...bytes]); + } + txForSign.add(hashAmounts); + + for (final s in scriptPubKeys) { + final h = s.hex; + + /// must checked + int scriptLen = h.length ~/ 2; + Uint8List scriptBytes = hexToBytes(h); + Uint8List lenBytes = Uint8List.fromList([scriptLen]); + hashScriptPubkeys = Uint8List.fromList([...hashScriptPubkeys, ...lenBytes, ...scriptBytes]); + } + hashScriptPubkeys = bcrypto.singleHash(hashScriptPubkeys); + txForSign.add(hashScriptPubkeys); + + hashSequences = bcrypto.singleHash(hashSequences); + txForSign.add(hashSequences); + } + + if (!(sighashNone || sighashSingle)) { + for (final txOut in outs) { + Uint8List packedAmount = packBigIntToLittleEndian(BigInt.from(txOut.value!)); + Uint8List scriptBytes = txOut.script!; + final lenScriptBytes = Uint8List.fromList([scriptBytes.length]); + hashOutputs = Uint8List.fromList( + [...hashOutputs, ...packedAmount, ...lenScriptBytes, ...scriptBytes]); + } + hashOutputs = bcrypto.singleHash(hashOutputs); + txForSign.add(hashOutputs); + } + + final extFlags = 0; + final int spendType = extFlags * 2 + 0; + txForSign.add(Uint8List.fromList([spendType])); + + int index = txIndex; + ByteData byteData = ByteData(4); + for (int i = 0; i < 4; i++) { + byteData.setUint8(i, index & 0xFF); + index >>= 8; + } + Uint8List bytes = byteData.buffer.asUint8List(); + txForSign.add(bytes); + + if (sighashSingle) { + final txOut = outs[txIndex]; + + Uint8List packedAmount = packBigIntToLittleEndian(BigInt.from(txOut.value!)); + final sBytes = txOut.script!; + Uint8List lenScriptBytes = Uint8List.fromList([sBytes.length]); + + final hashOut = Uint8List.fromList([...packedAmount, ...lenScriptBytes, ...sBytes]); + txForSign.add(bcrypto.singleHash(hashOut)); + } + if (extFlags == 1) { + final leafVar = LEAF_VERSION_TAPSCRIPT; + final leafVarBytes = Uint8List.fromList([ + ...Uint8List.fromList([leafVar]), + ...prependVarint(prevOutScript) + ]); + txForSign.add(bcrypto.taggedHash(leafVarBytes, "TapLeaf")); + txForSign.add(Uint16List.fromList([0])); + txForSign.add(Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF])); + } + final signBytes = txForSign.toBytes(); + txForSign.close(); + return bcrypto.taggedHash(signBytes, "TapSighash"); + } + hashForSignature(int inIndex, Uint8List prevOutScript, int hashType) { if (inIndex >= ins.length) return ONE; // ignore OP_CODESEPARATOR - final ourScript = - bscript.compile(bscript.decompile(prevOutScript)!.where((x) { + final ourScript = bscript.compile(bscript.decompile(prevOutScript)!.where((x) { return x != OPS['OP_CODESEPARATOR']; }).toList()); final txTmp = Transaction.clone(this); @@ -236,9 +350,7 @@ class Transaction { } // serialize and hash final buffer = Uint8List(txTmp.virtualSize() + 4); - buffer.buffer - .asByteData() - .setUint32(buffer.length - 4, hashType, Endian.little); + buffer.buffer.asByteData().setUint32(buffer.length - 4, hashType, Endian.little); txTmp._toBuffer(buffer, 0); return bcrypto.hash256(buffer); } @@ -250,9 +362,7 @@ class Transaction { varuint.encodingLength(outs.length) + ins.fold(0, (sum, input) => sum + 40 + varSliceSize(input.script!)) + outs.fold(0, (sum, output) => sum + 8 + varSliceSize(output.script!)) + - (hasWitness - ? ins.fold(0, (sum, input) => sum + vectorSize(input.witness!)) - : 0); + (hasWitness ? ins.fold(0, (sum, input) => sum + vectorSize(input.witness!)) : 0); } int vectorSize(List someVector) { @@ -475,8 +585,7 @@ class Transaction { final flag = readUInt8(); var hasWitnesses = false; - if (marker == ADVANCED_TRANSACTION_MARKER && - flag == ADVANCED_TRANSACTION_FLAG) { + if (marker == ADVANCED_TRANSACTION_MARKER && flag == ADVANCED_TRANSACTION_FLAG) { hasWitnesses = true; } else { offset -= 2; // Reset offset if not segwit tx @@ -506,8 +615,7 @@ class Transaction { if (noStrict) return tx; - if (offset != buffer.length) - throw new ArgumentError('Transaction has unexpected data'); + if (offset != buffer.length) throw new ArgumentError('Transaction has unexpected data'); return tx; } @@ -547,6 +655,7 @@ class Input { List? pubkeys; List? signatures; List? witness; + ecpair.ECPair? keyPair; Input( {this.hash, @@ -558,7 +667,8 @@ class Input { this.pubkeys, this.signatures, this.witness, - this.prevOutType}) + this.prevOutType, + this.keyPair}) : this.hasWitness = false { if (this.hash != null && !isHash256bit(this.hash!)) throw new ArgumentError('Invalid input hash'); @@ -566,12 +676,11 @@ class Input { throw new ArgumentError('Invalid input index'); if (this.sequence != null && !isUint(this.sequence!, 32)) throw new ArgumentError('Invalid input sequence'); - if (this.value != null && !isShatoshi(this.value!)) - throw ArgumentError('Invalid ouput value'); + if (this.value != null && !isShatoshi(this.value!)) throw ArgumentError('Invalid ouput value'); } factory Input.expandInput(Uint8List scriptSig, List witness, - [String? type, Uint8List? scriptPubKey]) { + [String? type, Uint8List? scriptPubKey, ecpair.ECPair? keyPair, int? value]) { if (type == null || type == '') { String? ssType = classifyInput(scriptSig); String? wsType = classifyWitness(witness); @@ -585,20 +694,35 @@ class Input { prevOutScript: p2wpkh.data.output, prevOutType: SCRIPT_TYPES['P2WPKH']!, pubkeys: [p2wpkh.data.pubkey!], - signatures: [p2wpkh.data.signature!]); + signatures: [p2wpkh.data.signature!], + keyPair: keyPair, + value: value); + } else if (type == SCRIPT_TYPES['P2TR']) { + P2TR p2tr = new P2TR(data: new PaymentData(witness: witness)); + return new Input( + prevOutScript: p2tr.data.output, + prevOutType: SCRIPT_TYPES['P2TR']!, + pubkeys: [p2tr.data.pubkey!], + signatures: [p2tr.data.signature!], + keyPair: keyPair, + value: value); } else if (type == SCRIPT_TYPES['P2PKH']) { P2PKH p2pkh = new P2PKH(data: new PaymentData(input: scriptSig)); return new Input( prevOutScript: p2pkh.data.output, prevOutType: SCRIPT_TYPES['P2PKH']!, pubkeys: [p2pkh.data.pubkey!], - signatures: [p2pkh.data.signature!]); + signatures: [p2pkh.data.signature!], + keyPair: keyPair, + value: value); } else if (type == SCRIPT_TYPES['P2PK']) { P2PK p2pk = new P2PK(data: new PaymentData(input: scriptSig)); return new Input( prevOutType: SCRIPT_TYPES['P2PK']!, pubkeys: [], - signatures: [p2pk.data.signature!]); + signatures: [p2pk.data.signature!], + keyPair: keyPair, + value: value); } throw Exception('Cannot to build Input with expandInput factory'); } @@ -610,17 +734,15 @@ class Input { script: input.script != null ? Uint8List.fromList(input.script!) : null, sequence: input.sequence, value: input.value, - prevOutScript: input.prevOutScript != null - ? Uint8List.fromList(input.prevOutScript!) - : null, + prevOutScript: input.prevOutScript != null ? Uint8List.fromList(input.prevOutScript!) : null, pubkeys: input.pubkeys != null - ? input.pubkeys!.map( - (pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) + ? input.pubkeys! + .map((pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) .toList() : null, signatures: input.signatures != null - ? input.signatures!.map((signature) => - signature != null ? Uint8List.fromList(signature) : null) + ? input.signatures! + .map((signature) => signature != null ? Uint8List.fromList(signature) : null) .toList() : null, ); @@ -639,28 +761,25 @@ class Output { List? pubkeys; List? signatures; - Output( - {this.script, - this.value, - this.pubkeys, - this.signatures, - this.valueBuffer}) { - if (value != null && !isShatoshi(value!)) - throw ArgumentError('Invalid ouput value'); + Output({this.script, this.value, this.pubkeys, this.signatures, this.valueBuffer}) { + if (value != null && !isShatoshi(value!)) throw ArgumentError('Invalid ouput value'); } factory Output.expandOutput(Uint8List script, [Uint8List? ourPubKey]) { if (ourPubKey == null) return new Output(); var type = classifyOutput(script); if (type == SCRIPT_TYPES['P2WPKH']) { - Uint8List wpkh1 = - new P2WPKH(data: new PaymentData(output: script)).data.hash!; + Uint8List wpkh1 = new P2WPKH(data: new PaymentData(output: script)).data.hash!; + Uint8List wpkh2 = bcrypto.hash160(ourPubKey); + if (wpkh1 != wpkh2) throw ArgumentError('Hash mismatch!'); + return new Output(pubkeys: [ourPubKey], signatures: [null]); + } else if (type == SCRIPT_TYPES['P2TR']) { + Uint8List wpkh1 = new P2TR(data: new PaymentData(output: script)).data.hash!; Uint8List wpkh2 = bcrypto.hash160(ourPubKey); if (wpkh1 != wpkh2) throw ArgumentError('Hash mismatch!'); return new Output(pubkeys: [ourPubKey], signatures: [null]); } else if (type == SCRIPT_TYPES['P2PKH']) { - Uint8List pkh1 = - new P2PKH(data: new PaymentData(output: script)).data.hash!; + Uint8List pkh1 = new P2PKH(data: new PaymentData(output: script)).data.hash!; Uint8List pkh2 = bcrypto.hash160(ourPubKey); if (pkh1 != pkh2) throw ArgumentError('Hash mismatch!'); return new Output(pubkeys: [ourPubKey], signatures: [null]); @@ -672,17 +791,15 @@ class Output { return new Output( script: output.script != null ? Uint8List.fromList(output.script!) : null, value: output.value, - valueBuffer: output.valueBuffer != null - ? Uint8List.fromList(output.valueBuffer!) - : null, + valueBuffer: output.valueBuffer != null ? Uint8List.fromList(output.valueBuffer!) : null, pubkeys: output.pubkeys != null - ? output.pubkeys!.map( - (pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) + ? output.pubkeys! + .map((pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) .toList() : null, signatures: output.signatures != null - ? output.signatures!.map((signature) => - signature != null ? Uint8List.fromList(signature) : null) + ? output.signatures! + .map((signature) => signature != null ? Uint8List.fromList(signature) : null) .toList() : null, ); diff --git a/lib/src/transaction_builder.dart b/lib/src/transaction_builder.dart index d3f1064..df5366b 100644 --- a/lib/src/transaction_builder.dart +++ b/lib/src/transaction_builder.dart @@ -1,12 +1,10 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:bitcoin_flutter/src/utils/constants/op.dart'; -import 'package:meta/meta.dart'; import 'package:hex/hex.dart'; -import 'package:bs58check/bs58check.dart' as bs58check; -import 'package:bech32/bech32.dart'; import 'utils/script.dart' as bscript; -import 'ecpair.dart'; +import 'utils/uint8list.dart'; +import 'ecpair.dart' as ecpair; import 'models/networks.dart'; import 'transaction.dart'; import 'address.dart'; @@ -14,6 +12,8 @@ import 'payments/index.dart' show PaymentData; import 'payments/p2pkh.dart'; import 'payments/p2wpkh.dart'; import 'classify.dart'; +import 'package:bitcoin_base/bitcoin.dart' as bitcoin_base; +import 'ec/ec_public.dart'; const int MAX_OP_RETURN_SIZE = 100; @@ -24,18 +24,17 @@ class TransactionBuilder { Transaction _tx; Map _prevTxSet = {}; - TransactionBuilder({NetworkType? network, int? maximumFeeRate}) - : this.network = network ?? bitcoin, - this.maximumFeeRate = maximumFeeRate ?? 2500, - this._inputs = [], - this._tx = new Transaction() { - this._tx.version = 2; + TransactionBuilder({NetworkType? network, int? maximumFeeRate, int? version}) + : this.network = network ?? bitcoin, + this.maximumFeeRate = maximumFeeRate ?? 2500, + this._inputs = [], + this._tx = new Transaction(version: version ?? 1) { + if (version == null) this._tx.version = 2; } List get inputs => _inputs; - factory TransactionBuilder.fromTransaction(Transaction transaction, - [NetworkType? network]) { + factory TransactionBuilder.fromTransaction(Transaction transaction, [NetworkType? network]) { final txb = new TransactionBuilder(network: network); // Copy transaction fields txb.setVersion(transaction.version); @@ -47,13 +46,8 @@ class TransactionBuilder { }); transaction.ins.forEach((txIn) { - txb._addInputUnsafe( - txIn.hash!, - txIn.index!, - new Input( - sequence: txIn.sequence, - script: txIn.script, - witness: txIn.witness)); + txb._addInputUnsafe(txIn.hash!, txIn.index!, + new Input(sequence: txIn.sequence, script: txIn.script, witness: txIn.witness)); }); // fix some things not possible through the public API @@ -66,14 +60,12 @@ class TransactionBuilder { } setVersion(int version) { - if (version < 0 || version > 0xFFFFFFFF) - throw ArgumentError('Expected Uint32'); + if (version < 0 || version > 0xFFFFFFFF) throw ArgumentError('Expected Uint32'); _tx.version = version; } setLockTime(int locktime) { - if (locktime < 0 || locktime > 0xFFFFFFFF) - throw ArgumentError('Expected Uint32'); + if (locktime < 0 || locktime > 0xFFFFFFFF) throw ArgumentError('Expected Uint32'); // if any signatures exist, throw if (this._inputs.map((input) { if (input.signatures == null) return false; @@ -107,7 +99,8 @@ class TransactionBuilder { if (data.length <= MAX_OP_RETURN_SIZE) { scriptPubKey = bscript.compile([OPS['OP_RETURN'], utf8.encode(data)]); } else { - throw new ArgumentError('Too much data embedded, max OP_RETURN size is '+MAX_OP_RETURN_SIZE.toString()); + throw new ArgumentError( + 'Too much data embedded, max OP_RETURN size is ' + MAX_OP_RETURN_SIZE.toString()); } } else if (data is Uint8List) { scriptPubKey = data; @@ -121,12 +114,11 @@ class TransactionBuilder { } int addInput(dynamic txHash, int vout, - [int? sequence, Uint8List? prevOutScript]) { + [int? sequence, Uint8List? prevOutScript, ecpair.ECPair? keyPair, int? value]) { if (!_canModifyInputs()) { throw new ArgumentError('No, this would invalidate signatures'); } Uint8List hash; - var value; if (txHash is String) { hash = Uint8List.fromList(HEX.decode(txHash).reversed.toList()); } else if (txHash is Uint8List) { @@ -143,24 +135,25 @@ class TransactionBuilder { hash, vout, new Input( - sequence: sequence, prevOutScript: prevOutScript, value: value)); + sequence: sequence, prevOutScript: prevOutScript, keyPair: keyPair, value: value)); } sign( {required int vin, - required ECPair keyPair, + ecpair.ECPair? keyPair, Uint8List? redeemScript, int? witnessValue, Uint8List? witnessScript, - int? hashType}) { - if (keyPair.network != null && - keyPair.network.toString().compareTo(network.toString()) != 0) + int? hashType, + List? amounts, + List? scriptPubKeys, + List? inputs}) { + keyPair = keyPair ?? _inputs[vin].keyPair!; + if (keyPair.network != null && keyPair.network.toString().compareTo(network.toString()) != 0) throw new ArgumentError('Inconsistent network'); - if (vin >= _inputs.length) - throw new ArgumentError('No input at index: $vin'); + if (vin >= _inputs.length) throw new ArgumentError('No input at index: $vin'); hashType = hashType ?? SIGHASH_ALL; - if (this._needsOutputs(hashType)) - throw new ArgumentError('Transaction needs outputs'); + if (this._needsOutputs(hashType)) throw new ArgumentError('Transaction needs outputs'); final input = _inputs[vin]; final ourPubKey = keyPair.publicKey; if (!_canSign(input)) { @@ -183,11 +176,19 @@ class TransactionBuilder { input.hasWitness = true; input.signatures = [null]; input.pubkeys = [ourPubKey]; - input.signScript = new P2PKH( - data: new PaymentData(pubkey: ourPubKey), - network: this.network) - .data - .output; + input.signScript = + new P2PKH(data: new PaymentData(pubkey: ourPubKey), network: this.network) + .data + .output; + } else if (type == SCRIPT_TYPES['P2TR']) { + input.prevOutType = SCRIPT_TYPES['P2TR']; + input.hasWitness = true; + input.signatures = [null]; + input.pubkeys = [ourPubKey]; + input.signScript = + new P2PKH(data: new PaymentData(pubkey: ourPubKey), network: this.network) + .data + .output; } else { // DRY CODE Uint8List prevOutScript = pubkeyToOutputScript(ourPubKey); @@ -205,25 +206,71 @@ class TransactionBuilder { } } var signatureHash; - if (input.hasWitness) { - signatureHash = this - ._tx - .hashForWitnessV0(vin, input.signScript!, input.value!, hashType); + if (input.prevOutType == SCRIPT_TYPES['P2TR']) { + List p2trInputs = []; + for (var i = 0; i < inputs!.length; i++) { + final p2trInput = inputs[i]; + p2trInputs.add(bitcoin_base.TxInput(txId: p2trInput.hash, txIndex: p2trInput.vout)); + } + + List p2trOutputs = []; + for (var i = 0; i < _tx.outs.length; i++) { + final p2trOutput = _tx.outs[i]; + p2trOutputs.add(bitcoin_base.TxOutput( + amount: BigInt.from(p2trOutput.value!), + scriptPubKey: + bitcoin_base.Script.fromRaw(hexData: p2trOutput.script!.hex, hasSegwit: true))); + } + + var tx = + bitcoin_base.BtcTransaction(inputs: p2trInputs, outputs: p2trOutputs, hasSegwit: true); + + const int signHash = TAPROOT_SIGHASH_ALL; + + for (var i = 0; i < _inputs.length; i++) { + final txDigit = tx.getTransactionTaprootDigset( + txIndex: i, + scriptPubKeys: scriptPubKeys! + .map((e) => bitcoin_base.Script.fromRaw(hexData: e.hex, hasSegwit: true)) + .toList(), + amounts: amounts!.map((e) => BigInt.from(e)).toList(), + sighash: signHash); + + final signatur = _inputs[i].keyPair!.signTapRoot(txDigit, + scripts: [ + bitcoin_base.Script(script: [ + ECPublic.fromHex(_inputs[i].keyPair!.publicKey.hex).toTapPoint(), + 'OP_CHECKSIG' + ]) + ], + sighash: signHash, + tweak: false); + + tx.witnesses.add(bitcoin_base.TxWitnessInput(stack: [signatur.hex])); + } + + signatureHash = tx.serialize(); + print("signatureHash: $signatureHash"); + } else if (input.hasWitness) { + signatureHash = this._tx.hashForWitnessV0(vin, input.signScript!, input.value!, hashType); } else { - signatureHash = - this._tx.hashForSignature(vin, input.signScript!, hashType); + signatureHash = this._tx.hashForSignature(vin, input.signScript!, hashType); } // enforce in order signing of public keys var signed = false; for (var i = 0; i < input.pubkeys!.length; i++) { - if (HEX.encode(ourPubKey).compareTo(HEX.encode(input.pubkeys![i]!)) != 0) - continue; - if (input.signatures![i] != null) - throw new ArgumentError('Signature already exists'); - final signature = keyPair.sign(signatureHash); - input.signatures![i] = bscript.encodeSignature(signature, hashType); - signed = true; + if (HEX.encode(ourPubKey).compareTo(HEX.encode(input.pubkeys![i]!)) != 0) continue; + if (input.signatures![i] != null) throw new ArgumentError('Signature already exists'); + + if (input.prevOutType == SCRIPT_TYPES['P2TR']) { + input.signatures![i] = Uint8List.fromList(HEX.decode(signatureHash)); + signed = true; + } else { + final signature = keyPair.sign(signatureHash); + input.signatures![i] = bscript.encodeSignature(signature, hashType); + signed = true; + } } if (!signed) throw new ArgumentError('Key pair cannot sign for this input'); } @@ -238,10 +285,8 @@ class TransactionBuilder { Transaction _build(bool allowIncomplete) { if (!allowIncomplete) { - if (_tx.ins.length == 0) - throw new ArgumentError('Transaction has no inputs'); - if (_tx.outs.length == 0) - throw new ArgumentError('Transaction has no outputs'); + if (_tx.ins.length == 0) throw new ArgumentError('Transaction has no inputs'); + if (_tx.outs.length == 0) throw new ArgumentError('Transaction has no outputs'); } final tx = Transaction.clone(_tx); @@ -254,19 +299,19 @@ class TransactionBuilder { if (_inputs[i].prevOutType == SCRIPT_TYPES['P2PKH']) { P2PKH payment = new P2PKH( data: new PaymentData( - pubkey: _inputs[i].pubkeys![0], - signature: _inputs[i].signatures![0]), + pubkey: _inputs[i].pubkeys![0], signature: _inputs[i].signatures![0]), network: network); tx.setInputScript(i, payment.data.input); tx.setWitness(i, payment.data.witness); } else if (_inputs[i].prevOutType == SCRIPT_TYPES['P2WPKH']) { P2WPKH payment = new P2WPKH( data: new PaymentData( - pubkey: _inputs[i].pubkeys![0], - signature: _inputs[i].signatures![0]), + pubkey: _inputs[i].pubkeys![0], signature: _inputs[i].signatures![0]), network: network); tx.setInputScript(i, payment.data.input!); tx.setWitness(i, payment.data.witness!); + } else if (_inputs[i].prevOutType == SCRIPT_TYPES['P2TR']) { + tx.txHex = HEX.encode(_inputs[0].signatures![0]!); } } else if (!allowIncomplete) { throw new ArgumentError('Transaction is not complete'); @@ -330,8 +375,7 @@ class TransactionBuilder { // .build() will fail, but .buildIncomplete() is OK return (this._tx.outs.length == 0) && _inputs.map((input) { - if (input.signatures == null || input.signatures!.length == 0) - return false; + if (input.signatures == null || input.signatures!.length == 0) return false; return input.signatures!.map((signature) { if (signature == null) return false; // no signature, no issue final hashType = _signatureHashType(signature); @@ -357,13 +401,12 @@ class TransactionBuilder { throw new ArgumentError('coinbase inputs not supported'); } final prevTxOut = '$txHash:$vout'; - if (_prevTxSet[prevTxOut] != null) - throw new ArgumentError('Duplicate TxOut: ' + prevTxOut); + if (_prevTxSet[prevTxOut] != null) throw new ArgumentError('Duplicate TxOut: ' + prevTxOut); if (options.script != null) { - input = - Input.expandInput(options.script!, options.witness ?? EMPTY_WITNESS); + input = Input.expandInput(options.script!, options.witness ?? EMPTY_WITNESS, null, null, + options.keyPair, options.value); } else { - input = new Input(); + input = new Input(keyPair: options.keyPair, value: options.value); } if (options.value != null) input.value = options.value; if (input.prevOutScript == null && options.prevOutScript != null) { @@ -377,6 +420,8 @@ class TransactionBuilder { input.prevOutScript = options.prevOutScript; input.prevOutType = classifyOutput(options.prevOutScript!); } + input.hash = hash; + input.index = vout; int vin = _tx.addInput(hash, vout, options.sequence, options.script); _inputs.add(input); _prevTxSet[prevTxOut] = true; @@ -394,7 +439,6 @@ class TransactionBuilder { Uint8List pubkeyToOutputScript(Uint8List pubkey, [NetworkType? nw]) { NetworkType network = nw ?? bitcoin; - P2PKH p2pkh = - new P2PKH(data: new PaymentData(pubkey: pubkey), network: network); + P2PKH p2pkh = new P2PKH(data: new PaymentData(pubkey: pubkey), network: network); return p2pkh.data.output!; } diff --git a/lib/src/utils/bigint.dart b/lib/src/utils/bigint.dart new file mode 100644 index 0000000..2e96fd6 --- /dev/null +++ b/lib/src/utils/bigint.dart @@ -0,0 +1,30 @@ +import 'dart:typed_data'; + +extension BigIntExt on BigInt { + Uint8List get decode { + var number = this; + int needsPaddingByte; + int rawSize; + + if (number > BigInt.zero) { + rawSize = (number.bitLength + 7) >> 3; + needsPaddingByte = + ((number >> (rawSize - 1) * 8) & BigInt.from(0x80)) == BigInt.from(0x80) ? 1 : 0; + + if (rawSize < 32) { + needsPaddingByte = 1; + } + } else { + needsPaddingByte = 0; + rawSize = (number.bitLength + 8) >> 3; + } + + final size = rawSize < 32 ? rawSize + needsPaddingByte : rawSize; + var result = Uint8List(size); + for (int i = 0; i < size; i++) { + result[size - i - 1] = (number & BigInt.from(0xff)).toInt(); + number = number >> 8; + } + return result; + } +} diff --git a/lib/src/utils/constants/derivation_paths.dart b/lib/src/utils/constants/derivation_paths.dart new file mode 100644 index 0000000..bc34334 --- /dev/null +++ b/lib/src/utils/constants/derivation_paths.dart @@ -0,0 +1,2 @@ +const SCAN_PATH = "m/352'/1'/0'/1'/0"; +const SPEND_PATH = "m/352'/1'/0'/0'/0"; diff --git a/lib/src/utils/int.dart b/lib/src/utils/int.dart new file mode 100644 index 0000000..f8609e8 --- /dev/null +++ b/lib/src/utils/int.dart @@ -0,0 +1,15 @@ +import 'dart:typed_data'; + +extension IntExt on int { + Uint8List get toLittleEndianBytes { + var buffer = ByteData(4); + buffer.setInt32(0, this, Endian.little); + return buffer.buffer.asUint8List(); + } + + Uint8List get toBigEndianBytes { + var buffer = ByteData(4); + buffer.setUint32(0, this, Endian.big); + return buffer.buffer.asUint8List(); + } +} diff --git a/lib/src/utils/keys.dart b/lib/src/utils/keys.dart new file mode 100644 index 0000000..159837c --- /dev/null +++ b/lib/src/utils/keys.dart @@ -0,0 +1,34 @@ +import 'package:elliptic/elliptic.dart'; + +class PrivateKeyInfo { + final PrivateKey key; + final bool isTaproot; + + PrivateKeyInfo(this.key, this.isTaproot); +} + +PublicKey getSumInputPubKeys(List pubkeys) { + List negatedKeys = []; + + for (final info in pubkeys) { + negatedKeys.add(PublicKey.fromHex(getSecp256k1(), info)); + // final key = info.key; + // final isTaproot = info.isTaproot; + + // if (isTaproot && key.toCompressedHex().fromHex[0] == 0x03) { + // negatedKeys.add(PublicKey(getSecp256k1(), key.X, key.Y).negate()!); + // } else { + // negatedKeys.add(key); + // } + } + + final head = negatedKeys.first; + final tail = negatedKeys.sublist(1); + + final result = tail.fold( + head, + (acc, item) => PublicKey(getSecp256k1(), acc.X, acc.Y).pubkeyAdd(item), + ); + + return result; +} diff --git a/lib/src/utils/string.dart b/lib/src/utils/string.dart new file mode 100644 index 0000000..2c002fb --- /dev/null +++ b/lib/src/utils/string.dart @@ -0,0 +1,9 @@ +import 'dart:typed_data'; + +import 'package:hex/hex.dart'; + +extension StringExt on String { + Uint8List get fromHex { + return Uint8List.fromList(HEX.decode(this)); + } +} diff --git a/lib/src/utils/uint8list.dart b/lib/src/utils/uint8list.dart new file mode 100644 index 0000000..76fd8e3 --- /dev/null +++ b/lib/src/utils/uint8list.dart @@ -0,0 +1,58 @@ +import 'dart:typed_data'; +import 'package:hex/hex.dart'; + +extension Uint8ListExt on Uint8List { + int compare(Uint8List list2) { + final list1 = this; + + for (var i = 0; i < list1.length && i < list2.length; i += 1) { + if (list1[i] < list2[i]) { + return -1; + } else if (list1[i] > list2[i]) { + return 1; + } + } + + if (list1.length < list2.length) { + return -1; + } else if (list1.length > list2.length) { + return 1; + } + + return 0; + } + + Uint8List concat(List concatLists) { + return concatenateUint8Lists([this, ...concatLists]); + } + + String get hex { + return HEX.encode(this); + } + + BigInt get bigint { + // Create an empty BigInt + BigInt result = BigInt.zero; + + // Iterate over the bytes in the Uint8List + for (int i = 0; i < this.length; i++) { + // Left-shift the existing value by 8 bits and add the current byte + result = (result << 8) + BigInt.from(this[i]); + } + + return result; + } +} + +Uint8List concatenateUint8Lists(List lists) { + var totalLength = lists.fold(0, (sum, list) => sum + list.length); + var result = Uint8List(totalLength); + var offset = 0; + + for (var list in lists) { + result.setRange(offset, offset + list.length, list); + offset += list.length; + } + + return result; +} diff --git a/pubspec.yaml b/pubspec.yaml index cf206c9..57ac156 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/anicdh publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: bip39: ^1.0.6 @@ -15,8 +15,13 @@ dependencies: bs58check: ^1.0.2 bech32: git: - url: https://github.com/cake-tech/bech32.git - ref: cake-0.2.2 + url: https://github.com/saltrafael/bech32.git + ref: bech32m + elliptic: + git: + url: https://github.com/cake-tech/dart-elliptic + ref: silent-payments + bitcoin_base: ^0.5.0 dev_dependencies: test: ^1.21.1 diff --git a/test/address_test.dart b/test/address_test.dart index 95bcb8c..735bd73 100644 --- a/test/address_test.dart +++ b/test/address_test.dart @@ -1,52 +1,138 @@ +import 'package:bip32/bip32.dart'; +import 'package:bitcoin_flutter/src/templates/silentpaymentaddress.dart'; +import 'package:bitcoin_flutter/src/utils/constants/derivation_paths.dart'; +import 'package:elliptic/elliptic.dart'; import 'package:test/test.dart'; import '../lib/src/address.dart' show Address; import '../lib/src/models/networks.dart' as NETWORKS; +import '../lib/src/utils/string.dart'; +import 'package:bip39/bip39.dart' as bip39; main() { group('Address', () { - group('validateAddress', () { - test('base58 addresses and valid network', () { - expect( - Address.validateAddress( - 'mhv6wtF2xzEqMNd3TbXx9TjLLo6mp2MUuT', NETWORKS.testnet), - true); - expect(Address.validateAddress('1K6kARGhcX9nJpJeirgcYdGAgUsXD59nHZ'), - true); - }); - test('base58 addresses and invalid network', () { - expect( - Address.validateAddress( - 'mhv6wtF2xzEqMNd3TbXx9TjLLo6mp2MUuT', NETWORKS.bitcoin), - false); + group('silent payment addresses', () { + final scanKey = '036a1035a192f8f5fd375556f36ea4abc387361d32c709831ec624a5b73d0b7b9d'; + final spendKey = '028eaf19db65cece905cf2b3eab811148d6fe874089a4a68e5d8b0a1a0904f6bd0'; + final silentAddress = + 'sprt1qqd4pqddpjtu0tlfh24t0xm4y40pcwdsaxtrsnqc7ccj2tdeapdae6q5w4uvakewwe6g9eu4na2upz9yddl58gzy6ff5wtk9s5xsfqnmt6q30zssg'; + + final curve = getSecp256k1(); + + test('can encode scan and spend key to silent payment address', () { expect( - Address.validateAddress( - '1K6kARGhcX9nJpJeirgcYdGAgUsXD59nHZ', NETWORKS.testnet), - false); + SilentPaymentAddress( + scanPubkey: PublicKey.fromHex(curve, scanKey), + spendPubkey: PublicKey.fromHex(curve, spendKey), + hrp: 'sprt', + version: 0) + .toString(), + silentAddress); }); - test('bech32 addresses and valid network', () { + test('can decode scan and spend key from silent payment address', () { expect( - Address.validateAddress( - 'tb1qgmp0h7lvexdxx9y05pmdukx09xcteu9sx2h4ya', NETWORKS.testnet), - true); - expect( - Address.validateAddress( - 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), - true); - // expect(Address.validateAddress('tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy'), true); TODO + SilentPaymentAddress.fromString(silentAddress).toString(), + SilentPaymentAddress( + scanPubkey: PublicKey.fromHex(curve, scanKey), + spendPubkey: PublicKey.fromHex(curve, spendKey), + hrp: 'sprt', + version: 0) + .toString()); }); - test('bech32 addresses and invalid network', () { + + test('can derive scan and spend key from master key', () async { + const mnemonic = + 'praise you muffin lion enable neck grocery crumble super myself license ghost'; + final address = await SilentPaymentReceiver.fromMnemonic(mnemonic); + + final seed = bip39.mnemonicToSeed(mnemonic); + final root = BIP32.fromSeed(seed); + expect( - Address.validateAddress( - 'tb1qgmp0h7lvexdxx9y05pmdukx09xcteu9sx2h4ya'), - false); + address.scanPrivkey.toCompressedHex().fromHex, root.derivePath(SCAN_PATH).privateKey!); + expect(address.scanPubkey.toCompressedHex().fromHex, root.derivePath(SCAN_PATH).publicKey); + + expect(address.spendPrivkey.toCompressedHex().fromHex, + root.derivePath(SPEND_PATH).privateKey!); expect( - Address.validateAddress( - 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', NETWORKS.testnet), - false); + address.spendPubkey.toCompressedHex().fromHex, root.derivePath(SPEND_PATH).publicKey); }); - test('invalid addresses', () { - expect(Address.validateAddress('3333333casca'), false); + + test('can create a labeled silent payment address', () { + final given = [ + ( + '0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4', + '025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36', + '0000000000000000000000000000000000000000000000000000000000000001', + 'sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj', + ), + ( + '0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4', + '025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36', + '0000000000000000000000000000000000000000000000000000000000000539', + 'sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq562yg7htxyg8eq60rl37uul37jy62apnf5ru62uef0eajpdfrnp5cmqndj', + ), + ( + '03b4cc0b090b6f49a684558852db60ee5eb1c5f74352839c3d18a8fc04ef7354e0', + '03bc95144daf15336db3456825c70ced0a4462f89aca42c4921ee7ccb2b3a44796', + '91cb04398a508c9d995ff4a18e5eae24d5e9488309f189120a3fdbb977978c46', + 'sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqll5497pp2gcr4cmq0v5nv07x8u5jswmf8ap2q0kxmx8628mkqanyu63ck8', + ), + ]; + + given.forEach((data) { + final (scanKey, spendKey, label, address) = data; + final result = SilentPaymentAddress.createLabeledSilentPaymentAddress( + PublicKey.fromHex(curve, scanKey), PublicKey.fromHex(curve, spendKey), label.fromHex); + + expect(result.toString(), address); + }); }); }); + test('base58 addresses and valid network', () { + expect(Address.validateAddress('mhv6wtF2xzEqMNd3TbXx9TjLLo6mp2MUuT', NETWORKS.testnet), true); + expect(Address.validateAddress('1K6kARGhcX9nJpJeirgcYdGAgUsXD59nHZ'), true); + }); + test('base58 addresses and invalid network', () { + expect( + Address.validateAddress('mhv6wtF2xzEqMNd3TbXx9TjLLo6mp2MUuT', NETWORKS.bitcoin), false); + expect( + Address.validateAddress('1K6kARGhcX9nJpJeirgcYdGAgUsXD59nHZ', NETWORKS.testnet), false); + }); + test('bech32 addresses and valid network', () { + expect( + Address.validateAddress('tb1qgmp0h7lvexdxx9y05pmdukx09xcteu9sx2h4ya', NETWORKS.testnet), + true); + expect(Address.validateAddress('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), true); + // expect(Address.validateAddress('tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy'), true); TODO + }); + test('bech32 addresses and invalid network', () { + expect(Address.validateAddress('tb1qgmp0h7lvexdxx9y05pmdukx09xcteu9sx2h4ya'), false); + expect( + Address.validateAddress('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', NETWORKS.testnet), + false); + }); + test('bech32m addresses and valid network', () { + expect( + Address.validateAddress( + 'tb1pk426x6qvmncj5vzhtp5f2pzhdu4qxsshszswga8ea6sycj9nulmsu7syz0', NETWORKS.testnet), + true); + expect( + Address.validateAddress( + 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y'), + true); + }); + test('bech32m addresses and invalid network', () { + expect( + Address.validateAddress('tb1pk426x6qvmncj5vzhtp5f2pzhdu4qxsshszswga8ea6sycj9nulmsu7syz0'), + false); + expect( + Address.validateAddress( + 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y', + NETWORKS.testnet), + false); + }); + test('invalid addresses', () { + expect(Address.validateAddress('3333333casca'), false); + }); }); } diff --git a/test/fixtures/silent_payments.json b/test/fixtures/silent_payments.json new file mode 100644 index 0000000..ef56fbb --- /dev/null +++ b/test/fixtures/silent_payments.json @@ -0,0 +1,1743 @@ +[ + { + "comment": "Simple send: two inputs", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "39a1e5ff6206cd316151b9b34cee4f80bb48ce61adee0a12ce7ff05ea436a1d9", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": {}, + "outputs": [ + "39a1e5ff6206cd316151b9b34cee4f80bb48ce61adee0a12ce7ff05ea436a1d9" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "39a1e5ff6206cd316151b9b34cee4f80bb48ce61adee0a12ce7ff05ea436a1d9", + "priv_key_tweak": "8e4bbee712779f746337cadf39e8b1eab8e8869dd40f2e3a7281113e858ffc0b", + "signature": "e18fe06280456ed533808606f73e0d46dea49f90751078d127379a8e176a6e56bb1e86f4ca3522a58e760a4ea68e6f3a26b24dcbcb9c614d4d5d2bce9bf956bf" + } + ] + } + } + ] + }, + { + "comment": "Simple send: two inputs, order reversed", + "sending": [ + { + "given": { + "outpoints": [ + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ], + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "39a1e5ff6206cd316151b9b34cee4f80bb48ce61adee0a12ce7ff05ea436a1d9", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ], + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": {}, + "outputs": [ + "39a1e5ff6206cd316151b9b34cee4f80bb48ce61adee0a12ce7ff05ea436a1d9" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "39a1e5ff6206cd316151b9b34cee4f80bb48ce61adee0a12ce7ff05ea436a1d9", + "priv_key_tweak": "8e4bbee712779f746337cadf39e8b1eab8e8869dd40f2e3a7281113e858ffc0b", + "signature": "e18fe06280456ed533808606f73e0d46dea49f90751078d127379a8e176a6e56bb1e86f4ca3522a58e760a4ea68e6f3a26b24dcbcb9c614d4d5d2bce9bf956bf" + } + ] + } + } + ] + }, + { + "comment": "Simple send: two inputs from the same transaction", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 3 + ], + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 7 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "162f2298705b3ddca01ce1d214eedff439df3927582938d08e29e464908db00b", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 3 + ], + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 7 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": {}, + "outputs": [ + "162f2298705b3ddca01ce1d214eedff439df3927582938d08e29e464908db00b" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "162f2298705b3ddca01ce1d214eedff439df3927582938d08e29e464908db00b", + "priv_key_tweak": "f06d8d90561bdbc3e511c3bec7355ad3c858aaf38a132c772d6cd82ec04102ac", + "signature": "4c900d573964d31953acdaedbcbb7866fedbdc215417adfd4173073f86179cad5903ae64490629fae610bf879263c3b9f5c7e6ec1b32a159e2d2e60a16d36597" + } + ] + } + } + ] + }, + { + "comment": "Simple send: two inputs from the same transaction, order reversed", + "sending": [ + { + "given": { + "outpoints": [ + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 7 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 3 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "d9ede52f7e1e64e36ccf895ca0250daad96b174987079c903519b17852b21a3f", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 7 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 3 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": {}, + "outputs": [ + "d9ede52f7e1e64e36ccf895ca0250daad96b174987079c903519b17852b21a3f" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "d9ede52f7e1e64e36ccf895ca0250daad96b174987079c903519b17852b21a3f", + "priv_key_tweak": "44b827516c2128287b1d571add7cfeb42f122e86bc40b4eb2b21ac144607fdb2", + "signature": "1bdb32461dd502ee9c19c7dff5f3801a26c2bc0ffe6f34671053ef7083ea0d5adca6036564252a76e427555deb17edd6f801d45cd7b830d7e3003eb3c8c85263" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: multiple UTXOs from the same public key", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "0aafdcdb5893ae813299b16eea75f34ec16653ac39171da04d7c4e6d2e09ab8e", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": {}, + "outputs": [ + "0aafdcdb5893ae813299b16eea75f34ec16653ac39171da04d7c4e6d2e09ab8e" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "0aafdcdb5893ae813299b16eea75f34ec16653ac39171da04d7c4e6d2e09ab8e", + "priv_key_tweak": "bf7336bdc02f624715aab385cc62b71f6f494bf8a7dd0fd621cfd365039c39d1", + "signature": "e00ba3406cea12127896fbc198a9da889a4afcf3d66e46b3df0e7bb36de400a109442e5bbd005c3cc5ae30ae7d235ea111475ad621e1e2c27374fda906521c69" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot only inputs with even y-values", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + true + ], + [ + "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7", + true + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "15d1dfe4403791509cf47f073be2eb3277decabe90da395e63b1f49a09fe965e", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "5a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": {}, + "outputs": [ + "15d1dfe4403791509cf47f073be2eb3277decabe90da395e63b1f49a09fe965e" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "15d1dfe4403791509cf47f073be2eb3277decabe90da395e63b1f49a09fe965e", + "priv_key_tweak": "0734de077e436e8f6f125e16287cb60dead8ebddc8532be3589ba27156f1add2", + "signature": "d743170ded6bc695f2997caed9886deb7ddc2e0e11d5f1493d6d7e498e8686f94c393c5d20eceb700a4c2035271196897a83fe1658414c38da07e0e4af00fd0a" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot only with mixed even/odd y-values", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + true + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + true + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "15d1dfe4403791509cf47f073be2eb3277decabe90da395e63b1f49a09fe965e", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "5a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": {}, + "outputs": [ + "15d1dfe4403791509cf47f073be2eb3277decabe90da395e63b1f49a09fe965e" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "15d1dfe4403791509cf47f073be2eb3277decabe90da395e63b1f49a09fe965e", + "priv_key_tweak": "0734de077e436e8f6f125e16287cb60dead8ebddc8532be3589ba27156f1add2", + "signature": "d743170ded6bc695f2997caed9886deb7ddc2e0e11d5f1493d6d7e498e8686f94c393c5d20eceb700a4c2035271196897a83fe1658414c38da07e0e4af00fd0a" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot input with even y-value and non-taproot input", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + true + ], + [ + "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "2b4ff8e5bc608cbdd12117171e7d265b6882ad597559caf67b5ecfaf15301dd0", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "5a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": {}, + "outputs": [ + "2b4ff8e5bc608cbdd12117171e7d265b6882ad597559caf67b5ecfaf15301dd0" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "2b4ff8e5bc608cbdd12117171e7d265b6882ad597559caf67b5ecfaf15301dd0", + "priv_key_tweak": "17d93733d2acd8388279c24dc4413483802378c99f266f5961ac3338c5146861", + "signature": "7f8f909460c0357a2c1c784e92967e888c6b63ff799db3ce22e8acc715a42ab9177b9db2237d76db60e72bc30c827008266062506cd57f93f9b872529bd50376" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot input with odd y-value and non-taproot input", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + true + ], + [ + "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "75f501f319db549aaa613717bd7af44da566d4d859b67fe436946564fafc47a3", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "03e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": {}, + "outputs": [ + "75f501f319db549aaa613717bd7af44da566d4d859b67fe436946564fafc47a3" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "75f501f319db549aaa613717bd7af44da566d4d859b67fe436946564fafc47a3", + "priv_key_tweak": "619a5a59a16d4a8e857ef48e63ef7c8195c858191d4e826205e8438ab70d059e", + "signature": "ba2e40de3b3acbc97d282f2d09b9c79936de109710e8d4139409964346f1221c3d4c823a1ee0a946f98b0ce644d136fbc5ea22cd73736fe05475174b25c01e62" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs: multiple outputs, same recipient", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 2.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 3.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + 2.0 + ], + [ + "0a48c6ccc1d516e8244dc0153dc88db45f8f264357667c2057a29ca3c2445d09", + 3.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": {}, + "outputs": [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + "0a48c6ccc1d516e8244dc0153dc88db45f8f264357667c2057a29ca3c2445d09", + "c58e121044b23cba9b4695052229a9fd9e044b579f92864eb886ae7c99b021c9", + "4b15b75f3f184328c4a2f7c79357481ed06cf3b6f95512d5ed946fdc0b60d62b" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + "priv_key_tweak": "96439446f13ddaab2c5bc5a59a08992fd9d33bf8563c8a1b362730f4dc022e30", + "signature": "3f6226feb9e4cafc0bdab8c9cfe085885308f3708c222bcec6cf26467685d897f51597abe39d1d279708e63513c7be23daed78607a98837060950493de188645" + }, + { + "pub_key": "0a48c6ccc1d516e8244dc0153dc88db45f8f264357667c2057a29ca3c2445d09", + "priv_key_tweak": "d39df91bd0e7825bfa1d30096febc5bf6fa7da79d7f25b7b4bea9538cc9a9f7f", + "signature": "be5f139f6eaad2d5eb75c6e307defb29925e16d55dbbc12872b0ab6aca38959c0c6a8f3f72bf82e3deb226cb539e117f9db4b04a5efb4e2eb01a86374f5baa12" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs: multiple outputs, multiple recipients", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 2.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 3.0 + ], + [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + 4.0 + ], + [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + 5.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + 2.0 + ], + [ + "0a48c6ccc1d516e8244dc0153dc88db45f8f264357667c2057a29ca3c2445d09", + 3.0 + ], + [ + "c58e121044b23cba9b4695052229a9fd9e044b579f92864eb886ae7c99b021c9", + 4.0 + ], + [ + "4b15b75f3f184328c4a2f7c79357481ed06cf3b6f95512d5ed946fdc0b60d62b", + 5.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": {}, + "outputs": [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + "0a48c6ccc1d516e8244dc0153dc88db45f8f264357667c2057a29ca3c2445d09", + "c58e121044b23cba9b4695052229a9fd9e044b579f92864eb886ae7c99b021c9", + "4b15b75f3f184328c4a2f7c79357481ed06cf3b6f95512d5ed946fdc0b60d62b" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + "priv_key_tweak": "96439446f13ddaab2c5bc5a59a08992fd9d33bf8563c8a1b362730f4dc022e30", + "signature": "3f6226feb9e4cafc0bdab8c9cfe085885308f3708c222bcec6cf26467685d897f51597abe39d1d279708e63513c7be23daed78607a98837060950493de188645" + }, + { + "pub_key": "0a48c6ccc1d516e8244dc0153dc88db45f8f264357667c2057a29ca3c2445d09", + "priv_key_tweak": "d39df91bd0e7825bfa1d30096febc5bf6fa7da79d7f25b7b4bea9538cc9a9f7f", + "signature": "be5f139f6eaad2d5eb75c6e307defb29925e16d55dbbc12872b0ab6aca38959c0c6a8f3f72bf82e3deb226cb539e117f9db4b04a5efb4e2eb01a86374f5baa12" + } + ] + } + }, + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "decafbad", + "scan_priv_key": "060b751d7892149006ed7b98606955a29fe284a1e900070c0971f5fb93dbf422", + "spend_priv_key": "9902c3c56e84002a7cd410113a9ab21d142be7f53cf5200720bb01314c5eb920", + "labels": {}, + "outputs": [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + "0a48c6ccc1d516e8244dc0153dc88db45f8f264357667c2057a29ca3c2445d09", + "c58e121044b23cba9b4695052229a9fd9e044b579f92864eb886ae7c99b021c9", + "4b15b75f3f184328c4a2f7c79357481ed06cf3b6f95512d5ed946fdc0b60d62b" + ] + }, + "expected": { + "addresses": [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn" + ], + "outputs": [ + { + "pub_key": "c58e121044b23cba9b4695052229a9fd9e044b579f92864eb886ae7c99b021c9", + "priv_key_tweak": "567710d07bdaacc8de3f1cec467bcb162ed7daa6b901b59af257bcd7e39dffcf", + "signature": "d675fd6f55f42b61c8797c80d46048cfca5125bcef06e3a0ff555ace0e8f6d84da9b6f473b559376afd5ee11dc63c4415dc565f8272d2b673d39759f29c0d56a" + }, + { + "pub_key": "4b15b75f3f184328c4a2f7c79357481ed06cf3b6f95512d5ed946fdc0b60d62b", + "priv_key_tweak": "25dd11163a9a2853709c4c837aafb3347e2eaa875cf4c5170e2a3663879f4c58", + "signature": "ab872ee64623cf1ddb646c65159c09bc69cd64c6b60767a94934e12ec074f0fa7c9e4cc6a9bca2ec6592e4d64636a07fcfd71c622619c3bf46c5a2816aeb3456" + } + ] + } + } + ] + }, + { + "comment": "Receiving with labels: label with even parity", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqhmem6grvs4nacsu0v5v5mjs934j7qfgkdkj8c95gyuru3tjpulvcwky2dz", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "2cbceeab2a4982841eb7dc34b8b4f19c04bf3bc083ebf984f5664366778eb50f", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": true, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": { + "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5": "0000000000000000000000000000000000000000000000000000000000000002", + "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9": "0000000000000000000000000000000000000000000000000000000000000003", + "03348b4f5feb64b557dac8cfa10044bdc2094fca9147163bf514f68687e0d1dba6": "00000000000000000000000000000000000000000000000000000000000f4779" + }, + "outputs": [ + "2cbceeab2a4982841eb7dc34b8b4f19c04bf3bc083ebf984f5664366778eb50f" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqhmem6grvs4nacsu0v5v5mjs934j7qfgkdkj8c95gyuru3tjpulvcwky2dz", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqc389f45lq7jyqt8jxq6fkskfukr2tlruf6w8cpcx2krntwe4fr9ykagp3j", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq4umqa5feskydh9xadc9jlc22c89tu0apcv72u2vkuwtsrgzf0uesq45zq9" + ], + "outputs": [ + { + "pub_key": "2cbceeab2a4982841eb7dc34b8b4f19c04bf3bc083ebf984f5664366778eb50f", + "priv_key_tweak": "96439446f13ddaab2c5bc5a59a08992fd9d33bf8563c8a1b362730f4dc022e32", + "signature": "0fa1b43afde9a03901dda91a0bd66fc82b6452c14a20718dc87dc70d4cedd9aeadf7c4c96116b8053c4aa113e26cea2fb64f8c408a8e8bc6e4fc9f6a06672b95" + } + ] + } + } + ] + }, + { + "comment": "Receiving with labels: label with odd parity", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqc389f45lq7jyqt8jxq6fkskfukr2tlruf6w8cpcx2krntwe4fr9ykagp3j", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "6b4455de119f51bf4d4a12dea555f14a5dc2c1369af5fba4871c5367264c028d", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": true, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": { + "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5": "0000000000000000000000000000000000000000000000000000000000000002", + "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9": "0000000000000000000000000000000000000000000000000000000000000003", + "03348b4f5feb64b557dac8cfa10044bdc2094fca9147163bf514f68687e0d1dba6": "00000000000000000000000000000000000000000000000000000000000f4779" + }, + "outputs": [ + "6b4455de119f51bf4d4a12dea555f14a5dc2c1369af5fba4871c5367264c028d" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqhmem6grvs4nacsu0v5v5mjs934j7qfgkdkj8c95gyuru3tjpulvcwky2dz", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqc389f45lq7jyqt8jxq6fkskfukr2tlruf6w8cpcx2krntwe4fr9ykagp3j", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq4umqa5feskydh9xadc9jlc22c89tu0apcv72u2vkuwtsrgzf0uesq45zq9" + ], + "outputs": [ + { + "pub_key": "6b4455de119f51bf4d4a12dea555f14a5dc2c1369af5fba4871c5367264c028d", + "priv_key_tweak": "96439446f13ddaab2c5bc5a59a08992fd9d33bf8563c8a1b362730f4dc022e33", + "signature": "b4ea01f7f47bcdf131b5a3aa3a1c848faae75e661d63bfff84c230bcc96313d0b443b9b3a76718a7474d51994395739bc6041caabe98133e3697412e07e19c0a" + } + ] + } + } + ] + }, + { + "comment": "Receiving with labels: large label integer", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq4umqa5feskydh9xadc9jlc22c89tu0apcv72u2vkuwtsrgzf0uesq45zq9", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "c3473bfcbe5e4d20d0790ae91f1b339bc15b46de64ca068d140118d0e325b849", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": true, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": { + "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5": "0000000000000000000000000000000000000000000000000000000000000002", + "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9": "0000000000000000000000000000000000000000000000000000000000000003", + "03348b4f5feb64b557dac8cfa10044bdc2094fca9147163bf514f68687e0d1dba6": "00000000000000000000000000000000000000000000000000000000000f4779" + }, + "outputs": [ + "c3473bfcbe5e4d20d0790ae91f1b339bc15b46de64ca068d140118d0e325b849" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqhmem6grvs4nacsu0v5v5mjs934j7qfgkdkj8c95gyuru3tjpulvcwky2dz", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqc389f45lq7jyqt8jxq6fkskfukr2tlruf6w8cpcx2krntwe4fr9ykagp3j", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq4umqa5feskydh9xadc9jlc22c89tu0apcv72u2vkuwtsrgzf0uesq45zq9" + ], + "outputs": [ + { + "pub_key": "c3473bfcbe5e4d20d0790ae91f1b339bc15b46de64ca068d140118d0e325b849", + "priv_key_tweak": "96439446f13ddaab2c5bc5a59a08992fd9d33bf8563c8a1b362730f4dc1175a9", + "signature": "ab9f3684cb497951fd013444d35909ed10669691d9fa3ac0be57f874a4df9f43c67647c9f17528110d2df0ce41dd3c05c04f4624629f8758fff1060049dc7d6b" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs with labels: un-labeled and labeled address; same recipient", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", + 2.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + 1.0 + ], + [ + "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", + 2.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": true, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": { + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798": "0000000000000000000000000000000000000000000000000000000000000001" + }, + "outputs": [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj" + ], + "outputs": [ + { + "pub_key": "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + "priv_key_tweak": "96439446f13ddaab2c5bc5a59a08992fd9d33bf8563c8a1b362730f4dc022e30", + "signature": "3f6226feb9e4cafc0bdab8c9cfe085885308f3708c222bcec6cf26467685d897f51597abe39d1d279708e63513c7be23daed78607a98837060950493de188645" + }, + { + "pub_key": "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", + "priv_key_tweak": "d39df91bd0e7825bfa1d30096febc5bf6fa7da79d7f25b7b4bea9538cc9a9f80", + "signature": "567f0d4d914456141ca83fe89e99f008c1f7ab9e9a65d4a60162840824737407acbaa61d7efa1a6af5d6439d213187e2f76696bb657dc709a0077bbf3b40e2f2" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs with labels: multiple outputs for labeled address; same recipient", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", + 3.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", + 4.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "8890c19f005d6f6add5fef92d37ac6b161b7fdd5c1aef6eed1d32be3f216ac4c", + 3.0 + ], + [ + "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", + 4.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": true, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": { + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798": "0000000000000000000000000000000000000000000000000000000000000001" + }, + "outputs": [ + "8890c19f005d6f6add5fef92d37ac6b161b7fdd5c1aef6eed1d32be3f216ac4c", + "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj" + ], + "outputs": [ + { + "pub_key": "8890c19f005d6f6add5fef92d37ac6b161b7fdd5c1aef6eed1d32be3f216ac4c", + "priv_key_tweak": "96439446f13ddaab2c5bc5a59a08992fd9d33bf8563c8a1b362730f4dc022e31", + "signature": "f0eb3b826553709356c351e1ced49a72900f261be18e64914c3c694af94595a4a80417ecbf5e86fde8b08e451fb42ec36b7a9d733eb42f92206f4f6c78da66bb" + }, + { + "pub_key": "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", + "priv_key_tweak": "d39df91bd0e7825bfa1d30096febc5bf6fa7da79d7f25b7b4bea9538cc9a9f80", + "signature": "567f0d4d914456141ca83fe89e99f008c1f7ab9e9a65d4a60162840824737407acbaa61d7efa1a6af5d6439d213187e2f76696bb657dc709a0077bbf3b40e2f2" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs with labels: un-labeled, labeled, and multiple outputs for labeled address; multiple recipients", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 5.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", + 6.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq562yg7htxyg8eq60rl37uul37jy62apnf5ru62uef0eajpdfrnp5cmqndj", + 7.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq562yg7htxyg8eq60rl37uul37jy62apnf5ru62uef0eajpdfrnp5cmqndj", + 8.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + 5.0 + ], + [ + "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", + 6.0 + ], + [ + "1b90a42136fef9ff2ca192abffc7be4536dc83d4e61cf18ae078f7e92b297cce", + 7.0 + ], + [ + "87a82600c08a255bc97d172e10816e322967eed6a77c9f37dd926492d7fdc106", + 8.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": true, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": { + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798": "0000000000000000000000000000000000000000000000000000000000000001", + "02db0c51cc634a4096374b0b895584a3ca2fb3bea4fd0ee2361f8db63a650fcee6": "0000000000000000000000000000000000000000000000000000000000000539" + }, + "outputs": [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", + "1b90a42136fef9ff2ca192abffc7be4536dc83d4e61cf18ae078f7e92b297cce", + "87a82600c08a255bc97d172e10816e322967eed6a77c9f37dd926492d7fdc106" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq562yg7htxyg8eq60rl37uul37jy62apnf5ru62uef0eajpdfrnp5cmqndj" + ], + "outputs": [ + { + "pub_key": "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + "priv_key_tweak": "96439446f13ddaab2c5bc5a59a08992fd9d33bf8563c8a1b362730f4dc022e30", + "signature": "3f6226feb9e4cafc0bdab8c9cfe085885308f3708c222bcec6cf26467685d897f51597abe39d1d279708e63513c7be23daed78607a98837060950493de188645" + }, + { + "pub_key": "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", + "priv_key_tweak": "d39df91bd0e7825bfa1d30096febc5bf6fa7da79d7f25b7b4bea9538cc9a9f80", + "signature": "567f0d4d914456141ca83fe89e99f008c1f7ab9e9a65d4a60162840824737407acbaa61d7efa1a6af5d6439d213187e2f76696bb657dc709a0077bbf3b40e2f2" + }, + { + "pub_key": "1b90a42136fef9ff2ca192abffc7be4536dc83d4e61cf18ae078f7e92b297cce", + "priv_key_tweak": "255a912ad6cdebc0842d49fd9f7b2d81ee37d66c62839879371b699010f78ef1", + "signature": "aa4cc7be2d90f30984d93535058f4894a6e0c7698deaaef179eda55724cc214e8e6ed055d437f1bf37c8c5c5431dad5080d03200cdd861a5b5e3855515e15d61" + }, + { + "pub_key": "87a82600c08a255bc97d172e10816e322967eed6a77c9f37dd926492d7fdc106", + "priv_key_tweak": "d7535d792cb1388ab0b3bd5ff57337436d62f7719c1796beb5d80ab2fa34f307", + "signature": "d68d0005118fcaae6d970925b452d038a03fda40d50aa9d6d3b4aff8189f226c71428838eadaf55662048f549bc7b19380438f09df9344eff30b96497b6aafa3" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: use silent payments for sender change", + "sending": [ + { + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ], + [ + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqll5497pp2gcr4cmq0v5nv07x8u5jswmf8ap2q0kxmx8628mkqanyu63ck8", + 2.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + 1.0 + ], + [ + "0050c52a32566c0dfb517e473c68fedce4bd4543d219348d3bbdceeeb5755e34", + 2.0 + ] + ] + } + } + ], + "receiving": [ + { + "supports_labels": true, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "deadbeef", + "scan_priv_key": "11b7a82e06ca2648d5fded2366478078ec4fc9dc1d8ff487518226f229d768fd", + "spend_priv_key": "b8f87388cbb41934c50daca018901b00070a5ff6cc25a7e9e716a9d5b9e4d664", + "labels": { + "02295dc38e877b754c0d0ed767434f1572cf34a82ccc06ffea1d9e04f1f7878e1a": "91cb04398a508c9d995ff4a18e5eae24d5e9488309f189120a3fdbb977978c46" + }, + "outputs": [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + "0050c52a32566c0dfb517e473c68fedce4bd4543d219348d3bbdceeeb5755e34" + ] + }, + "expected": { + "addresses": [ + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqauj52ymtc4xdkmx3tgyhrsemg2g3303xk2gtzfy8h8ejet8fz8jcw23zua", + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqll5497pp2gcr4cmq0v5nv07x8u5jswmf8ap2q0kxmx8628mkqanyu63ck8" + ], + "outputs": [ + { + "pub_key": "0050c52a32566c0dfb517e473c68fedce4bd4543d219348d3bbdceeeb5755e34", + "priv_key_tweak": "2e9c2a37cfa7827907d36357f0632d258dbd14b3a7854937ecf732fb6acefdc8", + "signature": "6ba068ee36454c5ff002082578e234917de9e384df739c43a8b7c4cce58724cba4479191cf972b235bc4bb6c2a8d6081650d1d5ba043b59bd51d6ac15d55b396" + } + ] + } + }, + { + "supports_labels": false, + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "bip32_seed": "f00dbabe", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "labels": {}, + "outputs": [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + "0050c52a32566c0dfb517e473c68fedce4bd4543d219348d3bbdceeeb5755e34" + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + "priv_key_tweak": "96439446f13ddaab2c5bc5a59a08992fd9d33bf8563c8a1b362730f4dc022e30", + "signature": "3f6226feb9e4cafc0bdab8c9cfe085885308f3708c222bcec6cf26467685d897f51597abe39d1d279708e63513c7be23daed78607a98837060950493de188645" + } + ] + } + } + ] + } +] diff --git a/test/silentpayments.dart b/test/silentpayments.dart new file mode 100644 index 0000000..bf6183b --- /dev/null +++ b/test/silentpayments.dart @@ -0,0 +1,172 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:bitcoin_flutter/src/utils/bigint.dart'; +import 'package:elliptic/elliptic.dart'; +import 'package:bitcoin_flutter/src/payments/scanning.dart'; +import 'package:bitcoin_flutter/src/payments/silentpayments.dart'; +import 'package:bitcoin_flutter/src/utils/string.dart'; +import 'package:bitcoin_flutter/src/utils/uint8list.dart'; +import 'package:bitcoin_flutter/src/utils/keys.dart'; +import 'package:bitcoin_flutter/src/templates/silentpaymentaddress.dart'; +import 'package:bitcoin_flutter/src/templates/outpoint.dart'; +import 'package:bitcoin_flutter/src/ec/schnorr.dart'; +import 'package:hex/hex.dart'; +import 'package:pointycastle/digests/sha256.dart'; +import 'package:test/test.dart'; + +main() { + final curve = getSecp256k1(); + + final fixtures = + json.decode(new File('test/fixtures/silent_payments.json').readAsStringSync(encoding: utf8)); + + for (var testCase in fixtures) { + test(testCase['comment'], () { + Map> sendingOutputs = {}; + List sendingOutputPubKeys = []; + + // Test sending + for (var sendingTest in testCase['sending']) { + var given = sendingTest["given"]; + + List inputPrivKeys = []; + for (List inputPrivKeyInfo in given['input_priv_keys']) { + inputPrivKeys.add( + PrivateKeyInfo(PrivateKey.fromHex(curve, inputPrivKeyInfo[0]), inputPrivKeyInfo[1])); + } + + List outpoints = []; + for (List outpoint in given['outpoints']) { + outpoints.add(Outpoint(txid: outpoint[0], index: outpoint[1])); + } + + List silentPaymentDestinations = []; + for (List recipientInfo in given['recipients']) { + silentPaymentDestinations.add( + SilentPaymentDestination.fromAddress(recipientInfo[0], recipientInfo[1].floor())); + } + + sendingOutputs = SilentPayment.generateMultipleRecipientPubkeys( + inputPrivKeys, SilentPayment.hashOutpoints(outpoints), silentPaymentDestinations); + + var expectedDestinations = sendingTest['expected']['outputs']; + + var i = 0; + sendingOutputs.forEach((silentAddress, generatedOutputs) { + final expectedSilentAddress = silentPaymentDestinations[i].toString(); + expect(silentAddress, expectedSilentAddress); + + generatedOutputs.forEach((output) { + final expectedPubkey = expectedDestinations[i][0]; + final generatedPubkey = output.$1.toCompressedHex(); // TODO: program + + expect(generatedPubkey.fromHex.sublist(1).hex, expectedPubkey); + + sendingOutputPubKeys.add(generatedPubkey); + + final expectedAmount = expectedDestinations[i][1].floor(); + final returnedAmount = output.$2; + expect(returnedAmount, expectedAmount); + + i++; + }); + }); + } + + final msg = SHA256Digest().process(Uint8List.fromList(utf8.encode('message'))); + final aux = SHA256Digest().process(Uint8List.fromList(utf8.encode('random auxiliary data'))); + + // Test receiving + for (var receivingTest in testCase['receiving']) { + var given = receivingTest["given"]; + + List outputsToCheck = given['outputs']; + + // assert that the generated sending outputs are a subset + // of the expected receiving outputs + // i.e. all the generated outputs are present + expect( + sendingOutputPubKeys + .every((element) => given['outputs'].contains(element.fromHex.sublist(1).hex)), + true); + + var receivingAddresses = []; + + var silentPaymentReceiver = SilentPaymentReceiver.fromPrivKeys( + scanPrivkey: PrivateKey.fromHex(curve, given["scan_priv_key"]), + spendPrivkey: PrivateKey.fromHex(curve, given["spend_priv_key"])); + + // Add change address + receivingAddresses.add(silentPaymentReceiver); + + Map? labels = null; + for (var label in given['labels'].entries) { + final m = label.value; + receivingAddresses.add(SilentPaymentAddress.createLabeledSilentPaymentAddress( + silentPaymentReceiver.scanPubkey, + silentPaymentReceiver.spendPubkey, + Uint8List.fromList(HEX.decode(m)))); + + if (labels == null) { + labels = {}; + } + labels[label.key] = m; + } + + List outpoints = []; + for (var outpoint in given['outpoints']) { + outpoints.add(Outpoint(txid: outpoint[0], index: outpoint[1])); + } + + final outpointsHash = SilentPayment.hashOutpoints(outpoints); + + List inputPubKeys = []; + for (var inputPubKey in given['input_pub_keys']) { + inputPubKeys.add(inputPubKey); + } + + final addToWallet = scanOutputs( + silentPaymentReceiver.scanPrivkey, + silentPaymentReceiver.spendPubkey, + getSumInputPubKeys(inputPubKeys), + outpointsHash, + outputsToCheck.map((e) => Uint8List.fromList(HEX.decode(e))).toList(), + labels: labels); + + var expectedDestinations = receivingTest['expected']['outputs']; + + // Check that the private key is correct for the found output public key + for (int i = 0; i < expectedDestinations.length; i++) { + final output = addToWallet.entries.elementAt(i); + final pubkey = output.key; + final expectedPubkey = expectedDestinations[i]["pub_key"]; + expect(pubkey, expectedPubkey); + + final privKeyTweak = output.value[0]; + final expectedPrivKeyTweak = expectedDestinations[i]["priv_key_tweak"]; + expect(privKeyTweak, expectedPrivKeyTweak); + + final fullPrivateKey = PrivateKey(curve, silentPaymentReceiver.spendPrivkey.D) + .tweakAdd(privKeyTweak.fromHex.bigint)!; + + if (fullPrivateKey.toCompressedHex().fromHex[0] == 0x03) { + fullPrivateKey.negate(); + } + + // Sign the message with schnorr + final sig = schnorrSign(msg, fullPrivateKey.D.decode, aux); + + // Verify the message is correct + expect(verifySchnorr(msg, pubkey.fromHex, sig), true); + + // Verify the signature is correct + expect(sig.hex, expectedDestinations[i]["signature"]); + + i++; + } + } + }); + } +}