diff --git a/lib/bitcoin_flutter.dart b/lib/bitcoin_flutter.dart index a77de46..55987b1 100644 --- a/lib/bitcoin_flutter.dart +++ b/lib/bitcoin_flutter.dart @@ -9,7 +9,32 @@ export 'src/transaction.dart'; export 'src/address.dart'; export 'src/transaction_builder.dart'; export 'src/ecpair.dart'; -export 'src/payments/p2pkh.dart'; +export 'src/ec/ec_public.dart'; +export 'src/ec/ec_encryption.dart'; export 'src/payments/p2wpkh.dart'; export 'src/payments/index.dart'; +export 'src/payments/scanning.dart'; +export 'src/payments/address/core.dart'; +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 'src/payments/bitcoin/input.dart'; +export 'src/payments/bitcoin/output.dart'; +export 'src/payments/bitcoin/witness.dart'; +export 'src/payments/bitcoin/transaction_builder.dart'; +export 'src/payments/bitcoin/transaction.dart'; +export 'src/payments/bitcoin/utxo_details.dart'; +export 'src/payments/bitcoin/multisig_script.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..fa3ea52 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 'package:bitcoin_flutter/src/models/networks.dart'; +import 'package:bitcoin_flutter/src/payments/address/address.dart'; +import 'package:bitcoin_flutter/src/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, networkType: network).scriptPubkey.toBytes(); + } + + if (P2shAddress.REGEX.hasMatch(address)) { + return P2shAddress(address: address, networkType: network).scriptPubkey.toBytes(); } + + if (P2wpkhAddress.REGEX.hasMatch(address)) { + return P2wpkhAddress(address: address, networkType: network).scriptPubkey.toBytes(); + } + + if (P2trAddress.REGEX.hasMatch(address)) { + return P2trAddress(address: address, networkType: network).scriptPubkey.toBytes(); + } + throw new ArgumentError(address + ' has no matching Script'); } } diff --git a/lib/src/bitcoin_flutter_base.dart b/lib/src/bitcoin_flutter_base.dart index b310340..cbdd750 100644 --- a/lib/src/bitcoin_flutter_base.dart +++ b/lib/src/bitcoin_flutter_base.dart @@ -2,19 +2,17 @@ import 'dart:typed_data'; import 'package:bitcoin_flutter/src/utils/magic_hash.dart'; import 'package:bitcoin_flutter/src/utils/recoverable_signatures.dart'; +import 'package:bitcoin_flutter/src/utils/uint8list.dart'; +import 'package:bitcoin_flutter/src/payments/address/address.dart'; import 'package:hex/hex.dart'; import 'package:bip32/bip32.dart' as bip32; -import 'models/networks.dart'; -import 'payments/index.dart' show PaymentData; -import 'payments/p2pkh.dart'; +import 'package:bitcoin_flutter/src/models/networks.dart'; import 'ecpair.dart'; -import 'package:meta/meta.dart'; -import 'dart:convert'; /// Checks if you are awesome. Spoiler: you are. class HDWallet { bip32.BIP32? _bip32; - P2PKH? _p2pkh; + P2pkhAddress? _p2pkh; String? seed; NetworkType network; @@ -52,25 +50,22 @@ class HDWallet { } } - String? get address => _p2pkh != null ? _p2pkh?.data.address : null; + String? get address => _p2pkh != null ? _p2pkh?.address : null; - HDWallet( - {bip32.BIP32? bip32, P2PKH? p2pkh, required this.network, this.seed}) { + HDWallet({bip32.BIP32? bip32, P2pkhAddress? p2pkh, required this.network, this.seed}) { this._bip32 = bip32; this._p2pkh = p2pkh; } HDWallet derivePath(String path) { final bip32 = _bip32!.derivePath(path); - final p2pkh = new P2PKH( - data: new PaymentData(pubkey: bip32.publicKey), network: network); + final p2pkh = new P2pkhAddress(pubkey: bip32.publicKey.hex, networkType: network); return HDWallet(bip32: bip32, p2pkh: p2pkh, network: network); } HDWallet derive(int index) { final bip32 = _bip32!.derive(index); - final p2pkh = new P2PKH( - data: new PaymentData(pubkey: bip32.publicKey), network: network); + final p2pkh = new P2pkhAddress(pubkey: bip32.publicKey.hex, networkType: network); return HDWallet(bip32: bip32, p2pkh: p2pkh, network: network); } @@ -80,13 +75,10 @@ class HDWallet { final wallet = bip32.BIP32.fromSeed( seed, bip32.NetworkType( - bip32: bip32.Bip32Type( - public: network.bip32.public, private: network.bip32.private), + bip32: bip32.Bip32Type(public: network.bip32.public, private: network.bip32.private), wif: network.wif)); - final p2pkh = new P2PKH( - data: new PaymentData(pubkey: wallet.publicKey), network: network); - return HDWallet( - bip32: wallet, p2pkh: p2pkh, network: network, seed: seedHex); + final p2pkh = new P2pkhAddress(pubkey: wallet.publicKey.hex, networkType: network); + return HDWallet(bip32: wallet, p2pkh: p2pkh, network: network, seed: seedHex); } factory HDWallet.fromBase58(String xpub, {NetworkType? network}) { @@ -94,11 +86,9 @@ class HDWallet { final wallet = bip32.BIP32.fromBase58( xpub, bip32.NetworkType( - bip32: bip32.Bip32Type( - public: network.bip32.public, private: network.bip32.private), + bip32: bip32.Bip32Type(public: network.bip32.public, private: network.bip32.private), wif: network.wif)); - final p2pkh = new P2PKH( - data: new PaymentData(pubkey: wallet.publicKey), network: network); + final p2pkh = new P2pkhAddress(pubkey: wallet.publicKey.hex, networkType: network); return HDWallet(bip32: wallet, p2pkh: p2pkh, network: network, seed: null); } @@ -116,10 +106,8 @@ class HDWallet { final messageHash = magicHash(message, network); final rs = _bip32!.sign(messageHash) as Uint8List; final rawSig = rs.toECSignature(); - final recId = findRecoveryId( - getHexString(messageHash, offset: 0, length: messageHash.length), - rawSig, - _bip32!.publicKey); + final recId = findRecoveryId(getHexString(messageHash, offset: 0, length: messageHash.length), + rawSig, _bip32!.publicKey); final v = recId + 27 + (_bip32!.publicKey.isCompressedPoint() ? 4 : 0); @@ -129,16 +117,15 @@ class HDWallet { class Wallet { ECPair? _keyPair; - P2PKH? _p2pkh; + P2pkhAddress? _p2pkh; - String? get privKey => - _keyPair != null ? HEX.encode(_keyPair!.privateKey!) : null; + String? get privKey => _keyPair != null ? HEX.encode(_keyPair!.privateKey!) : null; String? get pubKey => _keyPair != null ? HEX.encode(_keyPair!.publicKey) : null; String? get wif => _keyPair != null ? _keyPair!.toWIF() : null; - String? get address => _p2pkh != null ? _p2pkh!.data.address : null; + String? get address => _p2pkh != null ? _p2pkh!.address : null; NetworkType network; @@ -146,16 +133,14 @@ class Wallet { factory Wallet.random(NetworkType network) { final _keyPair = ECPair.makeRandom(network: network); - final _p2pkh = new P2PKH( - data: new PaymentData(pubkey: _keyPair.publicKey), network: network); + final _p2pkh = new P2pkhAddress(pubkey: _keyPair.publicKey.hex, networkType: network); return Wallet(_keyPair, _p2pkh, network); } factory Wallet.fromWIF(String wif, [NetworkType? network]) { network = network ?? bitcoin; final _keyPair = ECPair.fromWIF(wif, network: network); - final _p2pkh = new P2PKH( - data: new PaymentData(pubkey: _keyPair.publicKey), network: network); + final _p2pkh = new P2pkhAddress(pubkey: _keyPair.publicKey.hex, networkType: network); return Wallet(_keyPair, _p2pkh, network); } @@ -169,15 +154,12 @@ class Wallet { return _keyPair!.verify(messageHash, signature); } - Uint8List signMessage(String message) { final messageHash = magicHash(message, network); final rs = _keyPair!.sign(messageHash); final rawSig = rs.toECSignature(); - final recId = findRecoveryId( - getHexString(messageHash, offset: 0, length: messageHash.length), - rawSig, - _keyPair!.publicKey); + final recId = findRecoveryId(getHexString(messageHash, offset: 0, length: messageHash.length), + rawSig, _keyPair!.publicKey); final v = recId + 27 + (_keyPair!.publicKey.isCompressedPoint() ? 4 : 0); diff --git a/lib/src/classify.dart b/lib/src/classify.dart index 6e986fd..390bea9 100644 --- a/lib/src/classify.dart +++ b/lib/src/classify.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; import '../src/utils/script.dart' as bscript; -import 'templates/pubkeyhash.dart' as pubkeyhash; +import '../src/payments/address/address.dart'; import 'templates/pubkey.dart' as pubkey; import 'templates/witnesspubkeyhash.dart' as witnessPubKeyHash; @@ -12,13 +12,15 @@ 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 (pubkeyhash.outputCheck(script)) return SCRIPT_TYPES['P2PKH']!; + if (witnessPubKeyHash.taprootOutputCheck(script)) return SCRIPT_TYPES['P2TR']!; + if (P2pkhAddress.validPubkeyScript(script)) return SCRIPT_TYPES['P2PKH']!; final chunks = bscript.decompile(script); if (chunks == null) throw new ArgumentError('Invalid script'); return SCRIPT_TYPES['NONSTANDARD']!; @@ -27,7 +29,9 @@ String classifyOutput(Uint8List script) { String classifyInput(Uint8List script) { final chunks = bscript.decompile(script); if (chunks == null) throw new ArgumentError('Invalid script'); - if (pubkeyhash.inputCheck(chunks)) return SCRIPT_TYPES['P2PKH']!; + try { + if (P2pkhAddress.validSigScript(script)) return SCRIPT_TYPES['P2PKH']!; + } catch (e) {} if (pubkey.inputCheck(chunks)) return SCRIPT_TYPES['P2PK']!; return SCRIPT_TYPES['NONSTANDARD']!; } diff --git a/lib/src/crypto.dart b/lib/src/crypto.dart index c6bd7eb..8459ed1 100644 --- a/lib/src/crypto.dart +++ b/lib/src/crypto.dart @@ -19,3 +19,45 @@ Uint8List hash256(Uint8List buffer) { Uint8List _tmp = new SHA256Digest().process(buffer); return new SHA256Digest().process(_tmp); } + +/// Function: doubleHash +/// Description: Computes a double SHA-256 hash of the input data. +/// Input: Uint8List buffer - The data to be hashed. +/// Output: Uint8List - The resulting double SHA-256 hash. +/// Note: Double hashing is a common cryptographic technique used to enhance data security. +Uint8List doubleHash(Uint8List buffer) { + /// Compute the first SHA-256 hash of the input data. + Uint8List tmp = SHA256Digest().process(buffer); + + /// Compute the second SHA-256 hash of the first hash. + return 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..0986f19 --- /dev/null +++ b/lib/src/ec/ec_encryption.dart @@ -0,0 +1,293 @@ +import 'dart:typed_data'; + +import 'package:bitcoin_flutter/src/utils/bigint.dart'; +import 'package:bitcoin_flutter/src/utils/uint8list.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) { + final aa = a.bigint; + final bb = b.bigint; + if (aa == bb) return 0; + if (aa > bb) return 1; + return -1; +} + +bool isPoint(Uint8List p) { + // Check if the input list has a length of at least 33 bytes, which is the minimum length required for a compressed representation of a point on the curve + if (p.length < 33) { + return false; + } + + // Extract the first byte of the input list, which represents the type of the point + var t = p[0]; + + // Extract the next 32 bytes, which represent the X coordinate of the point + var x = p.sublist(1, 33); + + // Check if the X coordinate is equal to zero or infinity, which are invalid values for a point on the curve + if (_compare(x, _zero32) == 0) { + return false; + } + if (_compare(x, _ecP) == 1) { + return false; + } + + try { + _decodeFrom(p); + } catch (err) { + return false; + } + + // Check if the point is a compressed representation of a point on the curve. If it is, return true. + if ((t == 0x02 || t == 0x03) && p.length == 33) { + return true; + } + + // Extract the next 32 bytes, which represent the Y coordinate of the point + var y = p.sublist(33); + + // Check if the Y coordinate is equal to zero or infinity, which are invalid values for a point on the curve + if (_compare(y, _zero32) == 0) { + return false; + } + if (_compare(y, _ecP) == 1) { + return false; + } + + // Check if the point is an uncompressed representation of a point on the curve. If it is, return true + if (t == 0x04 && p.length == 65) { + return true; + } + + // If the point is not a valid point on the curve, return false + 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..e91af2a --- /dev/null +++ b/lib/src/ec/ec_public.dart @@ -0,0 +1,237 @@ +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 'package:bitcoin_flutter/src/payments/script/script.dart'; +import '../crypto.dart'; +import '../formatting/bytes_num_formatting.dart'; +import 'ec_encryption.dart'; + +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(pubkey: 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(scriptPubKey: addr.scriptPubkey, 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(scriptPubKey: 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(scriptPubKey: addr.scriptPubkey, 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(scriptPubKey: p2sh.scriptPubkey, 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 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..6694594 100644 --- a/lib/src/ecpair.dart +++ b/lib/src/ecpair.dart @@ -2,7 +2,11 @@ import 'dart:typed_data'; 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 'package:bitcoin_flutter/src/models/networks.dart'; +import 'package:bitcoin_flutter/src/payments/constants/constants.dart'; +import 'ec/ec_public.dart'; +import 'ec/ec_encryption.dart' as ec; +import 'crypto.dart' as bcrypto; 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..342381b --- /dev/null +++ b/lib/src/formatting/bytes_num_formatting.dart @@ -0,0 +1,208 @@ +import 'dart:core'; +import 'dart:typed_data'; +import 'package:convert/convert.dart'; + +/// ignore: implementation_imports +import 'package:pointycastle/src/utils.dart' as p_utils; +import 'package:bitcoin_flutter/src/utils/string.dart'; + +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) { + return strip0x(hexStr).fromHex; +} + +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..32671ef 100644 --- a/lib/src/models/networks.dart +++ b/lib/src/models/networks.dart @@ -1,52 +1,154 @@ -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; + +final litecoin = NetworkType( + messagePrefix: '\x19Litecoin Signed Message:\n', + bech32: 'ltc', + bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), + pubKeyHash: 0x30, + scriptHash: 0x32, + wif: 0xb0, + p2pkhPrefix: 0x30, + network: bitcoin.network, + p2shPrefix: 0x32, + extendPublic: bitcoin.extendPublic, + extendPrivate: bitcoin.extendPrivate, +); diff --git a/lib/src/payments/address/address.dart b/lib/src/payments/address/address.dart new file mode 100644 index 0000000..0c48552 --- /dev/null +++ b/lib/src/payments/address/address.dart @@ -0,0 +1,250 @@ +import 'dart:typed_data'; + +import 'core.dart'; +import 'package:bitcoin_flutter/src/payments/script/script.dart'; +import '../tools/tools.dart'; +import '../constants/constants.dart'; +import 'package:bitcoin_flutter/src/models/networks.dart'; +import '../../utils/string.dart'; +import '../../utils/uint8list.dart'; +import '../../utils/constants/op.dart'; +import 'package:bs58check/bs58check.dart' as bs58check; +import 'package:bitcoin_flutter/src/utils/script.dart' as bscript; + +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 redeem script, first + /// a SHA-256 and then an RIPEMD-160 + BipAddress({ + String? address, + String? pubkey, + String? signature, + String? hash160, + Script? scriptPubKey, + Script? scriptSig, + NetworkType? networkType, + }) { + this._networkType = networkType ?? NetworkType.BITCOIN; + + if (scriptSig != null) { + _decodeScriptSig(scriptSig); + } else { + if (pubkey != null) { + _pubkey = pubkey; + } + if (signature != null) { + _signature = signature; + } + } + + if (_pubkey != null) { + final bytes = _pubkey!.hexToBytes; + if (!bytes.isPoint) { + throw ArgumentError("Input has invalid pubkey"); + } + _h160 = _pubkey!.hexToBytes.ripemd160Hash.hex; + } else if (hash160 != null) { + if (!isValidHash160(hash160)) { + throw Exception("Invalid value for parameter hash160."); + } + _h160 = hash160; + } else if (address != null) { + if (!isValidAddress(address, type, network: this.networkType)) + throw ArgumentError("Invalid address"); + + _h160 = _addressToHash160(address); + } else if (scriptPubKey != null) { + _h160 = _scriptToHash160(scriptPubKey); + } else { + if (type == AddressType.p2pk) return; + throw ArgumentError("Not enough data"); + } + } + + String? _pubkey; + String? _signature; + late final String _h160; + late final NetworkType _networkType; + + String? get pubkey { + return _pubkey; + } + + String? get signature { + return _signature; + } + + NetworkType get networkType { + return _networkType; + } + + String get h160 { + if (type == AddressType.p2pk) throw UnimplementedError(); + return _h160; + } + + String _addressToHash160(String address) { + final decode = bs58check.decode(address); + return decode.sublist(1).hex; + } + + String _scriptToHash160(Script s) { + throw UnimplementedError(); + } + + void _decodeScriptSig(Script s) { + throw UnimplementedError(); + } + + /// returns the address's string encoding + @override + String get address { + Uint8List tobytes = _h160.hexToBytes; + 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 { + P2shAddress({ + super.address, + super.pubkey, + super.signature, + super.hash160, + super.scriptPubKey, + super.scriptSig, + super.networkType, + }) : type = AddressType.p2pkInP2sh; + + static RegExp get REGEX => RegExp(r'(^|\s)[23][a-km-zA-HJ-NP-Z1-9]{25,34}($|\s)'); + + @override + final AddressType type; + + P2shAddress.fromScript({super.scriptPubKey, this.type = AddressType.p2pkInP2sh}) + : assert(type == AddressType.p2pkInP2sh || + type == AddressType.p2pkhInP2sh || + type == AddressType.p2wpkhInP2sh || + type == AddressType.p2wshInP2sh); + + @override + Script get scriptPubkey { + return Script(script: [OP_WORDS.OP_HASH160, _h160, OP_WORDS.OP_EQUAL]); + } +} + +class P2pkhAddress extends BipAddress { + P2pkhAddress({ + super.address, + super.pubkey, + super.signature, + super.hash160, + super.scriptPubKey, + super.scriptSig, + super.networkType, + }); + + static RegExp get REGEX => RegExp(r'(^|\s)[1mn][a-km-zA-HJ-NP-Z1-9]{25,34}($|\s)'); + static get overheadSizeVB => 10; + static get inputSizeVB => 148; + static get outputSizeVB => 34; + + @override + Script get scriptPubkey { + return Script(script: [ + OP_WORDS.OP_DUP, + OP_WORDS.OP_HASH160, + _h160, + OP_WORDS.OP_EQUALVERIFY, + OP_WORDS.OP_CHECKSIG + ]); + } + + @override + AddressType get type => AddressType.p2pkh; + + // https://bitcoin.stackexchange.com/questions/105262/is-a-valid-bitcoin-address-character + static bool validPubkeyScript(Uint8List data) { + // A P2PKH output script is composed of the following instructions: + return data.length == 25 && + // OP_DUP (0x76) + data[0] == OPS['OP_DUP'] && + // OP_HASH160 (0xa9) + data[1] == OPS['OP_HASH160'] && + // 0x14 (20 in hexadecimal, indicating a 20 byte push) + data[2] == 0x14 && + // (20 bytes) + // ... data[3] to data[22] ... + // OP_EQUALVERIFY (0x88) + data[23] == OPS['OP_EQUALVERIFY'] && + // OP_CHECKSIG (0xac) + data[24] == OPS['OP_CHECKSIG']; + } + + @override + String _scriptToHash160(Script s) { + if (!validPubkeyScript(s.toBytes())) throw new ArgumentError('Output is invalid'); + final h160 = s.script[2]; + return h160; + } + + static bool validSigScript(List? data) { + if (data == null || data.isEmpty) throw new ArgumentError('Input is invalid'); + + List chunks = + (data is Uint8List) ? Script.fromRaw(byteData: data).script : (data as List); + + if (chunks.length != 2) throw new ArgumentError('Input is invalid'); + + if (!bscript.isCanonicalScriptSignature(chunks[0].fromHex)) + throw new ArgumentError('Input has invalid signature'); + + if (!bscript.isCanonicalPubKey(chunks[1].fromHex)) + throw new ArgumentError('Input has invalid pubkey'); + + return true; + } + + @override + void _decodeScriptSig(Script sigScript) { + final chunks = sigScript.script; + if (!validSigScript(sigScript.script)) throw new ArgumentError('Input is invalid'); + + _pubkey = chunks[1]; + _signature = chunks[0]; + } + + Script get sigScript { + return Script(script: [_signature, _pubkey]); + } +} + +// Deprecated but may be useful for library uses, like identifying old P2PK payments, parsing addresses etc +class P2pkAddress extends BipAddress { + P2pkAddress({required super.pubkey}); + + static RegExp get REGEX => RegExp(r'(^|\s)1([A-Za-z0-9]{34})($|\s)'); + + late final String publicHex; + + @override + Script get scriptPubkey { + return Script(script: [publicHex, OP_WORDS.OP_CHECKSIG]); + } + + @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..4df46db --- /dev/null +++ b/lib/src/payments/address/core.dart @@ -0,0 +1,50 @@ +import 'package:bitcoin_flutter/src/payments/script/script.dart'; +import 'package:bitcoin_flutter/src/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 { + NetworkType get networkType; + AddressType get type; + Script get scriptPubkey; + String get address; +} diff --git a/lib/src/payments/address/segwit_address.dart b/lib/src/payments/address/segwit_address.dart new file mode 100644 index 0000000..1247229 --- /dev/null +++ b/lib/src/payments/address/segwit_address.dart @@ -0,0 +1,165 @@ +import '../../crypto.dart'; +import '../../formatting/bytes_num_formatting.dart'; + +import 'package:bitcoin_flutter/src/models/networks.dart'; +import 'core.dart'; +import '../constants/constants.dart'; +import 'package:bitcoin_flutter/src/payments/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? networkType, + this.version = P2WPKH_ADDRESS_V0}) { + this._networkType = networkType ?? NetworkType.BITCOIN; + + 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); + } 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; + late final NetworkType _networkType; + + NetworkType get networkType { + return _networkType; + } + + String _addressToHash(String address) { + 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 (networkType.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 get address { + 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'(^|\s)(bc|tb)1q[ac-hj-np-z02-9]{25,39}($|\s)'); + + /// Encapsulates a P2WPKH address. + P2wpkhAddress({super.address, super.program, super.pubkey, super.networkType}) + : super(version: P2WPKH_ADDRESS_V0); + + /// returns the scriptPubKey of a P2WPKH witness script + @override + Script get scriptPubkey { + 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'(^|\s)(bc)|(tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59})|1p[ac-hj-np-z02-9]{8,89}($|\s)'); + + /// Encapsulates a P2TR (Taproot) address. + P2trAddress({String? program, super.address, String? pubkey, super.networkType}) + : 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 get address { + 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 get scriptPubkey { + 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 get scriptPubkey { + return Script(script: ['OP_0', _program]); + } + + /// Returns the type of address + @override + AddressType get type => AddressType.p2wsh; +} diff --git a/lib/src/payments/bitcoin/input.dart b/lib/src/payments/bitcoin/input.dart new file mode 100644 index 0000000..a248e4f --- /dev/null +++ b/lib/src/payments/bitcoin/input.dart @@ -0,0 +1,137 @@ +import 'dart:typed_data'; + +import 'package:bitcoin_flutter/src/payments/script/script.dart'; +import 'package:bitcoin_flutter/src/payments/constants/constants.dart'; +import 'package:bitcoin_flutter/src/utils/string.dart'; +import 'package:bitcoin_flutter/src/utils/check_types.dart'; +import 'package:bitcoin_flutter/src/formatting/bytes_num_formatting.dart'; + +/// A transaction input requires a transaction id of a UTXO and the index of that UTXO. +/// +/// [txId] the transaction id as a hex string +/// [txIndex] the index of the UTXO that we want to spend +/// [scriptSig] the script that satisfies the locking conditions +/// [sequence] the input sequence (for timelocks, RBF, etc.) +class TxInput { + TxInput({required this.txId, required this.txIndex, Script? scriptSig, Uint8List? sequence}) + : sequence = sequence ?? Uint8List.fromList(DEFAULT_TX_SEQUENCE), + scriptSig = scriptSig ?? Script(script: []) { + if (!isHash256bit(txId.fromHex)) throw new ArgumentError('Invalid input hash'); + if (!isUint(txIndex, 32)) throw new ArgumentError('Invalid input index'); + } + + final String txId; + final int txIndex; + Script scriptSig; + Uint8List sequence; + + TxInput copy() { + return TxInput(txId: txId, txIndex: txIndex, scriptSig: scriptSig, sequence: sequence); + } + + @override + String toString() { + return 'TxInput{txId: $txId, txIndex: $txIndex, scriptSig: $scriptSig, sequence: $sequence}'; + } + + /// serializes TxInput to bytes + Uint8List toBytes() { + final txidBytes = Uint16List.fromList(txId.fromHex.reversed.toList()); + + final txoutBytes = Uint8List(4); + ByteData.view(txoutBytes.buffer).setUint32(0, txIndex, Endian.little); + + final scriptSigBytes = scriptSig.toBytes(); + + final scriptSigLengthVarint = encodeVarint(scriptSigBytes.length); + final data = Uint8List.fromList([ + ...txidBytes, + ...txoutBytes, + ...scriptSigLengthVarint, + ...scriptSigBytes, + ...sequence, + ]); + return data; + } + + static (TxInput, int) fromRaw({required String raw, int cursor = 0, bool hasSegwit = false}) { + final txInputRaw = hexToBytes(raw); + Uint8List inpHash = + Uint8List.fromList(txInputRaw.sublist(cursor, cursor + 32).reversed.toList()); + if (inpHash.isEmpty) { + throw ArgumentError("Input transaction hash not found. Probably malformed raw transaction"); + } + Uint8List outputN = + Uint8List.fromList(txInputRaw.sublist(cursor + 32, cursor + 36).reversed.toList()); + cursor += 36; + final vi = viToInt(txInputRaw.sublist(cursor, cursor + 9)); + cursor += vi.$2; + Uint8List unlockingScript = txInputRaw.sublist(cursor, cursor + vi.$1); + cursor += vi.$1; + Uint8List sequenceNumberData = txInputRaw.sublist(cursor, cursor + 4); + cursor += 4; + return ( + TxInput( + txId: bytesToHex(inpHash), + txIndex: int.parse(bytesToHex(outputN), radix: 16), + scriptSig: Script.fromRaw(hexData: bytesToHex(unlockingScript), hasSegwit: hasSegwit), + sequence: sequenceNumberData, + ), + cursor + ); + } +} + +/// Used to provide the sequence to transaction inputs and to scripts. +/// +/// [value] The value of the block height or the 512 seconds increments +/// [seqType] Specifies the type of sequence (TYPE_RELATIVE_TIMELOCK | TYPE_ABSOLUTE_TIMELOCK | TYPE_REPLACE_BY_FEE +/// [isTypeBlock] If type is TYPE_RELATIVE_TIMELOCK then this specifies its type (block height or 512 secs increments) +class Sequence { + Sequence({required this.seqType, required this.value, this.isTypeBlock = true}) { + if (seqType == TYPE_RELATIVE_TIMELOCK && (value < 1 || value > 0xffff)) { + throw ArgumentError('Sequence should be between 1 and 65535'); + } + } + final int seqType; + final int value; + final bool isTypeBlock; + + /// Serializes the relative sequence as required in a transaction + Uint8List forInputSequence() { + if (seqType == TYPE_ABSOLUTE_TIMELOCK) { + return Uint8List.fromList(ABSOLUTE_TIMELOCK_SEQUENCE); + } + + if (seqType == TYPE_REPLACE_BY_FEE) { + return Uint8List.fromList(REPLACE_BY_FEE_SEQUENCE); + } + if (seqType == TYPE_RELATIVE_TIMELOCK) { + int seq = 0; + if (!isTypeBlock) { + seq |= 1 << 22; + } + seq |= value; + return Uint8List.fromList([ + seq & 0xFF, + (seq >> 8) & 0xFF, + (seq >> 16) & 0xFF, + (seq >> 24) & 0xFF, + ]); + } + + throw ArgumentError("Invalid seqType"); + } + + /// Returns the appropriate integer for a script; e.g. for relative timelocks + int forScript() { + if (seqType == TYPE_REPLACE_BY_FEE) { + throw const FormatException("RBF is not to be included in a script."); + } + int scriptIntiger = value; + if (seqType == TYPE_RELATIVE_TIMELOCK && !isTypeBlock) { + scriptIntiger |= 1 << 22; + } + return scriptIntiger; + } +} diff --git a/lib/src/payments/bitcoin/multisig_script.dart b/lib/src/payments/bitcoin/multisig_script.dart new file mode 100644 index 0000000..7813111 --- /dev/null +++ b/lib/src/payments/bitcoin/multisig_script.dart @@ -0,0 +1,120 @@ +import 'package:bitcoin_flutter/src/payments/address/core.dart'; +import 'package:bitcoin_flutter/src/payments/address/address.dart'; +import 'package:bitcoin_flutter/src/payments/address/segwit_address.dart'; +import 'package:bitcoin_flutter/src/payments/script/script.dart'; +import 'package:bitcoin_flutter/src/ec/ec_public.dart'; + +/// MultiSignatureSigner is an interface that defines methods required for representing +/// signers in a multi-signature scheme. A multi-signature signer typically includes +/// information about their public key and weight within the scheme. +class MultiSignatureSigner { + MultiSignatureSigner._(this.publicKey, this.weight); + + /// PublicKey returns the public key associated with the signer. + final String publicKey; + + /// Weight returns the weight or significance of the signer within the multi-signature scheme. + /// The weight is used to determine the number of signatures required for a valid transaction. + final int weight; + + /// creates a new instance of a multi-signature signer with the + /// specified public key and weight. + factory MultiSignatureSigner({required String publicKey, required int weight}) { + ECPublic.fromHex(publicKey); + return MultiSignatureSigner._(publicKey, weight); + } +} + +/// MultiSignatureAddress represents a multi-signature Bitcoin address configuration, including +/// information about the required signers, threshold, the address itself, +/// and the script details used for multi-signature transactions. +class MultiSignatureAddress { + /// Signers is a collection of signers participating in the multi-signature scheme. + final List signers; + + /// Threshold is the minimum number of signatures required to spend the bitcoins associated + /// with this address. + final int threshold; + + /// Address represents the Bitcoin address associated with this multi-signature configuration. + final BitcoinAddress address; + + /// ScriptDetails provides details about the multi-signature script used in transactions, + /// including "OP_M", compressed public keys, "OP_N", and "OP_CHECKMULTISIG." + final String scriptDetails; + + MultiSignatureAddress._({ + required this.signers, + required this.threshold, + required this.address, + required this.scriptDetails, + }); + + /// CreateMultiSignatureAddress creates a new instance of a MultiSignatureAddress, representing + /// a multi-signature Bitcoin address configuration. It allows you to specify the minimum number + /// of required signatures (threshold), provide the collection of signers participating in the + /// multi-signature scheme, and specify the address type. + factory MultiSignatureAddress({ + required int threshold, + required List signers, + required AddressType addressType, + }) { + final sumWeight = signers.fold(0, (sum, signer) => sum + signer.weight); + if (threshold > 16 || threshold < 1) { + throw Exception('The threshold should be between 1 and 16'); + } + if (sumWeight > 16) { + throw Exception('The total weight of the owners should not exceed 16'); + } + if (sumWeight < threshold) { + throw Exception('The total weight of the signatories should reach the threshold'); + } + final multiSigScript = ['OP_$threshold']; + for (final signer in signers) { + for (var w = 0; w < signer.weight; w++) { + multiSigScript.add(signer.publicKey); + } + } + multiSigScript.addAll(['OP_$sumWeight', 'OP_CHECKMULTISIG']); + final script = Script(script: multiSigScript); + final p2wsh = P2wshAddress(script: script); + switch (addressType) { + case AddressType.p2wsh: + { + return MultiSignatureAddress._( + signers: signers, + threshold: threshold, + address: p2wsh, + scriptDetails: script.toHex(), + ); + } + case AddressType.p2wshInP2sh: + { + final addr = P2shAddress.fromScript( + scriptPubKey: p2wsh.scriptPubkey, type: AddressType.p2wshInP2sh); + return MultiSignatureAddress._( + signers: signers, + threshold: threshold, + address: addr, + scriptDetails: script.toHex(), + ); + } + default: + { + throw Exception('addressType should be P2WSH or P2WSHInP2SH'); + } + } + } + + List showScript() { + final sumWeight = signers.fold(0, (sum, signer) => sum + signer.weight); + final multiSigScript = ['OP_$threshold']; + for (final signer in signers) { + for (var w = 0; w < signer.weight; w++) { + multiSigScript.add(signer.publicKey); + } + } + multiSigScript.addAll(['OP_$sumWeight', 'OP_CHECKMULTISIG']); + return multiSigScript; + } +} diff --git a/lib/src/payments/bitcoin/output.dart b/lib/src/payments/bitcoin/output.dart new file mode 100644 index 0000000..cb41c32 --- /dev/null +++ b/lib/src/payments/bitcoin/output.dart @@ -0,0 +1,51 @@ +import 'dart:typed_data'; +import 'package:bitcoin_flutter/src/payments/script/script.dart'; +import 'package:bitcoin_flutter/src/formatting/bytes_num_formatting.dart'; + +/// Represents a transaction output. +/// +/// [amount] the value we want to send to this output in satoshis +/// [scriptPubKey] the script that will lock this amount +class TxOutput { + TxOutput({required this.amount, required this.scriptPubKey}); + + final BigInt amount; + final Script scriptPubKey; + + /// creates a copy of the object + TxOutput copy() { + return TxOutput(amount: amount, scriptPubKey: scriptPubKey); + } + + /// serializes TxInput to bytes + Uint8List toBytes() { + final amountBytes = packBigIntToLittleEndian(amount); + Uint8List scriptBytes = scriptPubKey.toBytes(); + final data = + Uint8List.fromList([...amountBytes, ...encodeVarint(scriptBytes.length), ...scriptBytes]); + return data; + } + + static (TxOutput, int) fromRaw( + {required String raw, required int cursor, bool hasSegwit = false}) { + final txoutputraw = hexToBytes(raw); + int value = ByteData.sublistView(txoutputraw, cursor, cursor + 8).getInt64(0, Endian.little); + cursor += 8; + + final vi = viToInt(txoutputraw.sublist(cursor, cursor + 9)); + cursor += vi.$2; + Uint8List lockScript = txoutputraw.sublist(cursor, cursor + vi.$1); + cursor += vi.$1; + return ( + TxOutput( + amount: BigInt.from(value), + scriptPubKey: Script.fromRaw(hexData: bytesToHex(lockScript), hasSegwit: hasSegwit)), + cursor + ); + } + + @override + String toString() { + return 'Output{scriptPubKey: $scriptPubKey, amount: $amount}'; + } +} diff --git a/lib/src/payments/bitcoin/transaction.dart b/lib/src/payments/bitcoin/transaction.dart new file mode 100644 index 0000000..ef733db --- /dev/null +++ b/lib/src/payments/bitcoin/transaction.dart @@ -0,0 +1,443 @@ +import 'dart:typed_data'; +import 'package:bitcoin_flutter/src/formatting/bytes_tracker.dart'; +import 'package:bitcoin_flutter/src/payments/script/script.dart'; +import 'package:bitcoin_flutter/src/payments/bitcoin/input.dart'; +import 'package:bitcoin_flutter/src/payments/bitcoin/output.dart'; +import 'package:bitcoin_flutter/src/payments/bitcoin/witness.dart'; +import 'package:bitcoin_flutter/src/payments/constants/constants.dart'; +import 'package:bitcoin_flutter/src/formatting/bytes_num_formatting.dart'; +import 'package:bitcoin_flutter/src/crypto.dart'; + +/// Represents a Bitcoin transaction +/// +/// [inputs] A list of all the transaction inputs +/// [outputs] A list of all the transaction outputs +/// [locktime] The transaction's locktime parameter +/// [version] The transaction version +/// [hasSegwit] Specifies a tx that includes segwit inputs +/// [witnesses] The witness structure that corresponds to the inputs +class BtcTransaction { + BtcTransaction({ + required this.inputs, + required this.outputs, + List w = const [], + this.hasSegwit = false, + Uint8List? lock, + Uint8List? v, + bool? hasTaproot, + }) : locktime = lock ?? Uint8List.fromList(DEFAULT_TX_LOCKTIME), + version = hasTaproot == true + ? Uint8List.fromList(TX_VERSION_2) + : (v ?? Uint8List.fromList(DEFAULT_TX_VERSION)) { + witnesses.addAll(w); + } + final List inputs; + final List outputs; + final Uint8List locktime; + late final Uint8List version; + final bool hasSegwit; + final List witnesses = []; + + /// creates a copy of the object (classmethod) + static BtcTransaction copy(BtcTransaction tx) { + return BtcTransaction( + hasSegwit: tx.hasSegwit, + inputs: tx.inputs.map((e) => e.copy()).toList(), + outputs: tx.outputs.map((e) => e.copy()).toList(), + w: tx.witnesses.map((e) => e.copy()).toList(), + lock: tx.locktime, + v: tx.version); + } + + /// Instantiates a Transaction from serialized raw hexadacimal data (classmethod) + static BtcTransaction fromRaw(String raw) { + final rawtx = hexToBytes(raw); + int cursor = 4; + Uint8List? flag; + bool hasSegwit = false; + if (rawtx[4] == 0) { + flag = Uint8List.fromList(rawtx.sublist(5, 6)); + if (flag[0] == 1) { + hasSegwit = true; + } + cursor += 2; + } + final vi = viToInt(rawtx.sublist(cursor, cursor + 9)); + cursor += vi.$2; + + List inputs = []; + for (int index = 0; index < vi.$1; index++) { + final inp = TxInput.fromRaw(raw: raw, hasSegwit: hasSegwit, cursor: cursor); + + inputs.add(inp.$1); + cursor = inp.$2; + } + + List outputs = []; + final viOut = viToInt(rawtx.sublist(cursor, cursor + 9)); + cursor += viOut.$2; + for (int index = 0; index < viOut.$1; index++) { + final inp = TxOutput.fromRaw(raw: raw, hasSegwit: hasSegwit, cursor: cursor); + outputs.add(inp.$1); + cursor = inp.$2; + } + List witnesses = []; + if (hasSegwit) { + for (int n = 0; n < inputs.length; n++) { + final wVi = viToInt(rawtx.sublist(cursor, cursor + 9)); + cursor += wVi.$2; + List witnessesTmp = []; + for (int n = 0; n < wVi.$1; n++) { + Uint8List witness = Uint8List(0); + final wtVi = viToInt(rawtx.sublist(cursor, cursor + 9)); + if (wtVi.$1 != 0) { + witness = rawtx.sublist(cursor + wtVi.$2, cursor + wtVi.$1 + wtVi.$2); + } + cursor += wtVi.$1 + wtVi.$2; + witnessesTmp.add(bytesToHex(witness)); + } + + witnesses.add(TxWitnessInput(stack: witnessesTmp)); + } + } + return BtcTransaction(inputs: inputs, outputs: outputs, w: witnesses, hasSegwit: hasSegwit); + } + + /// returns the transaction input's digest that is to be signed according. + /// + /// [txInIndex] The index of the input that we wish to sign + /// [script] The scriptPubKey of the UTXO that we want to spend + /// [sighash] The type of the signature hash to be created + Uint8List getTransactionDigest( + {required int txInIndex, required Script script, int sighash = SIGHASH_ALL}) { + final tx = copy(this); + for (final i in tx.inputs) { + i.scriptSig = Script(script: []); + } + tx.inputs[txInIndex].scriptSig = script; + if ((sighash & 0x1f) == SIGHASH_NONE) { + tx.outputs.clear(); + for (int i = 0; i < tx.inputs.length; i++) { + if (i != txInIndex) { + tx.inputs[i].sequence = Uint8List.fromList(EMPTY_TX_SEQUENCE); + } + } + } else if ((sighash & 0x1f) == SIGHASH_SINGLE) { + if (txInIndex >= tx.outputs.length) { + throw ArgumentError("Transaction index is greater than theavailable outputs"); + } + final txout = tx.outputs[txInIndex]; + tx.outputs.clear(); + for (int i = 0; i < txInIndex; i++) { + tx.outputs + .add(TxOutput(amount: BigInt.from(NEGATIVE_SATOSHI), scriptPubKey: Script(script: []))); + } + tx.outputs.add(txout); + for (int i = 0; i < tx.inputs.length; i++) { + if (i != txInIndex) { + tx.inputs[i].sequence = Uint8List.fromList(EMPTY_TX_SEQUENCE); + } + } + } + if ((sighash & SIGHASH_ANYONECANPAY) != 0) { + final inp = tx.inputs[txInIndex]; + tx.inputs.clear(); + tx.inputs.add(inp); + } + Uint8List txForSign = tx.toBytes(segwit: false); + + Uint8List packedData = packInt32LE(sighash); + + txForSign = Uint8List.fromList([...txForSign, ...packedData]); + return doubleHash(txForSign); + } + + /// Serializes Transaction to bytes + Uint8List toBytes({bool segwit = false}) { + DynamicByteTracker data = DynamicByteTracker(); + try { + data.add(version); + if (segwit) { + data.add([0x00, 0x01]); + } + final txInCountBytes = encodeVarint(inputs.length); + final txOutCountBytes = encodeVarint(outputs.length); + + data.add(txInCountBytes); + for (final txIn in inputs) { + data.add(txIn.toBytes()); + } + data.add(txOutCountBytes); + for (final txOut in outputs) { + data.add(txOut.toBytes()); + } + if (segwit) { + for (final wit in witnesses) { + final witnessesCountBytes = Uint8List.fromList([wit.stack.length]); + data.add(witnessesCountBytes); + data.add(wit.toBytes()); + } + } + data.add(locktime); + return data.toBytes(); + } finally { + data.close(); + } + } + + /// returns the transaction input's segwit digest that is to be signed according to sighash. + /// + /// [txInIndex] The index of the input that we wish to sign. + /// [script] The scriptCode (template) that corresponds to the segwit, transaction output type that we want to spend. + /// [amount] The amount of the UTXO to spend is included in the signature for segwit (in satoshis). + /// [sighash] The type of the signature hash to be created. + Uint8List getTransactionSegwitDigit( + {required int txInIndex, + required Script script, + int sighash = SIGHASH_ALL, + required BigInt amount}) { + final tx = copy(this); + Uint8List hashPrevouts = Uint8List(32); + Uint8List hashSequence = Uint8List(32); + Uint8List hashOutputs = Uint8List(32); + int basicSigHashType = sighash & 0x1F; + bool anyoneCanPay = (sighash & 0xF0) == SIGHASH_ANYONECANPAY; + bool signAll = (basicSigHashType != SIGHASH_SINGLE) && (basicSigHashType != SIGHASH_NONE); + if (!anyoneCanPay) { + hashPrevouts = Uint8List(0); + for (final txin in tx.inputs) { + Uint8List txidBytes = Uint8List.fromList(hexToBytes(txin.txId).reversed.toList()); + Uint8List txoutIndexBytes = packUint32LE(txin.txIndex); + + hashPrevouts = Uint8List.fromList([...hashPrevouts, ...txidBytes, ...txoutIndexBytes]); + } + hashPrevouts = doubleHash(hashPrevouts); + } + + if (!anyoneCanPay && signAll) { + hashSequence = Uint8List(0); + for (final i in tx.inputs) { + hashSequence = Uint8List.fromList([...hashSequence, ...i.sequence]); + } + hashSequence = doubleHash(hashSequence); + } + if (signAll) { + hashOutputs = Uint8List(0); + for (final i in tx.outputs) { + Uint8List amountBytes = packBigIntToLittleEndian(i.amount); + Uint8List scriptBytes = i.scriptPubKey.toBytes(); + hashOutputs = Uint8List.fromList( + [...hashOutputs, ...amountBytes, scriptBytes.length, ...scriptBytes]); + } + hashOutputs = doubleHash(hashOutputs); + } else if (basicSigHashType == SIGHASH_SINGLE && txInIndex < tx.outputs.length) { + final out = tx.outputs[txInIndex]; + Uint8List packedAmount = packBigIntToLittleEndian(out.amount); + final scriptBytes = out.scriptPubKey.toBytes(); + Uint8List lenScriptBytes = Uint8List.fromList([scriptBytes.length]); + hashOutputs = Uint8List.fromList([...packedAmount, ...lenScriptBytes, ...scriptBytes]); + hashOutputs = doubleHash(hashOutputs); + } + + DynamicByteTracker txForSigning = DynamicByteTracker(); + txForSigning.add(version); + txForSigning.add(hashPrevouts); + txForSigning.add(hashSequence); + final txIn = inputs[txInIndex]; + + Uint8List txidBytes = Uint8List.fromList(hexToBytes(txIn.txId).reversed.toList()); + Uint8List txoutIndexBytes = packUint32LE(txIn.txIndex); + txForSigning.add(Uint8List.fromList([...txidBytes, ...txoutIndexBytes])); + txForSigning.add(Uint8List.fromList([script.toBytes().length])); + txForSigning.add(script.toBytes()); + Uint8List packedAmount = packBigIntToLittleEndian(amount); + txForSigning.add(packedAmount); + txForSigning.add(txIn.sequence); + txForSigning.add(hashOutputs); + txForSigning.add(locktime); + Uint8List packedSighash = packInt32LE(sighash); + txForSigning.add(packedSighash); + txForSigning.close(); + return doubleHash(txForSigning.toBytes()); + } + + /// Returns the segwit v1 (taproot) transaction's digest for signing. + /// + /// [txIndex] The index of the input that we wish to sign + /// [scriptPubKeys] he scriptPubkeys that correspond to all the inputs/UTXOs + /// [amounts] The amounts that correspond to all the inputs/UTXOs + /// [extFlags] Extension mechanism, default is 0; 1 is for script spending (BIP342) + /// [script] The script that we are spending (ext_flag=1) + /// [leafVar] The script version, LEAF_VERSION_TAPSCRIPT for the default tapscript + /// [sighash] The type of the signature hash to be created + Uint8List getTransactionTaprootDigset( + {required int txIndex, + required List