diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index f6c130c..cc6bc3f 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -42,5 +42,7 @@ jobs: run: dart test ./test - name: Run tests web - run: dart test -p chrome ./test - + run: dart test -p chrome ./test --timeout=20m + + - name: Run tests web wasm + run: dart test -p chrome ./test -c dart2wasm --timeout=20m diff --git a/CHANGELOG.md b/CHANGELOG.md index 7012651..7b9a7bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## 6.5.0 + +- Fix transaction parsing in Mempool API. + +## 6.4.0 + +- Update dependencies. +- Improved security: Private key operations now use blinded ecmult for safer public key generation. +- All signing methods now use constant-time operations with blinded ecmult to securely generate signatures. + + + +## 6.3.0 + +- Update dependencies. +- Add estimate transaction size for psbt +- Add more script validation in psbt + +## 6.2.0 + +- Update dependencies. +- Support PSBT for BCH +- Support for BIP-173 +- Support for BCH schnorr signing + ## 6.1.0 - Fix der signature validation. diff --git a/README.md b/README.md index 9c16cf0..9fd09d6 100644 --- a/README.md +++ b/README.md @@ -141,10 +141,10 @@ We have integrated three APIs—Mempool, BlockCypher, and Electrum—into the pl final publicKey = privateKey.getPublic(); // Sign an input using the private key. - final signSegwitV0OrLagacy = privateKey.signInput(); + final signSegwitV0OrLagacy = privateKey.signECDSA(); // Sign a Taproot transaction using the private key. - final signSegwitV1TapprotTransaction = privateKey.signTapRoot(); + final signSegwitV1TapprotTransaction = privateKey.signBip340(); // Convert the private key to a WIF (Wallet Import Format) encoded string. // The boolean argument specifies whether to use the compressed format. @@ -161,10 +161,10 @@ We have integrated three APIs—Mempool, BlockCypher, and Electrum—into the pl final publicKey = ECPublic.fromHex('.....'); // Generate a Pay-to-Public-Key-Hash (P2PKH) address from the public key. - final p2pkh = publicKey.toAddress(); + final p2pkh = publicKey.toP2pkhAddress(); // Generate a Pay-to-Witness-Public-Key-Hash (P2WPKH) Segregated Witness (SegWit) address from the public key. - final p2wpkh = publicKey.toSegwitAddress(); + final p2wpkh = publicKey.toP2wpkhAddress(); // Generate a Pay-to-Witness-Script-Hash (P2WSH) Segregated Witness (SegWit) address from the public key. final p2wsh = publicKey.toP2wshAddress(); @@ -303,8 +303,8 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li final privateKey = ECPrivate.fromHex( "76257aafc9b954351c7f6445b2d07277f681a5e83d515a1f32ebf54989c2af4f"); final examplePublicKey = privateKey.getPublic(); - final spender1 = examplePublicKey.toAddress(); - final spender2 = examplePublicKey.toSegwitAddress(); + final spender1 = examplePublicKey.toP2pkhAddress(); + final spender2 = examplePublicKey.toP2wpkhAddress(); final spender3 = examplePublicKey.toTaprootAddress(); final spender4 = examplePublicKey.toP2pkhInP2sh(); final spender5 = examplePublicKey.toP2pkInP2sh(); @@ -353,9 +353,9 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li /// P2pkhAddress.fromAddress(address: ".....", network: network); /// P2trAddress.fromAddress(address: "....", network: network) /// .... - final List outPuts = [ + final List outputs = [ BitcoinOutput( - address: examplePublicKey2.toSegwitAddress(), + address: examplePublicKey2.toP2wpkhAddress(), value: BtcUtils.toSatoshi("0.00001")), BitcoinOutput( address: examplePublicKey2.toTaprootAddress(), @@ -382,11 +382,11 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li int transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: accountsUtxos, outputs: [ - ...outPuts, + ...outputs, /// I add more output for change value to get correct transaction size BitcoinOutput( - address: examplePublicKey2.toAddress(), value: BigInt.zero) + address: examplePublicKey2.toP2pkhAddress(), value: BigInt.zero) ], /// network @@ -413,13 +413,13 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li } //// if we have change value we back amount to account if (changeValue > BigInt.zero) { - outPuts.add(BitcoinOutput( - address: examplePublicKey2.toAddress(), value: changeValue)); + outputs.add(BitcoinOutput( + address: examplePublicKey2.toP2pkhAddress(), value: changeValue)); } /// create transaction builder final builder = BitcoinTransactionBuilder( - outPuts: outPuts, + outputs: outputs, fee: fee, network: network, utxos: accountsUtxos, @@ -432,9 +432,9 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li final transaction = builder.buildTransaction((trDigest, utxo, publicKey, sighash) { if (utxo.utxo.isP2tr()) { - return privateKey.signTapRoot(trDigest, sighash: sighash); + return privateKey.signBip340(trDigest, sighash: sighash); } - return privateKey.signInput(trDigest, sigHash: sighash); + return privateKey.signECDSA(trDigest, sighash: sighash); }); /// get tx id @@ -482,7 +482,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li /// p2pkh with token address () final receiver1 = P2pkhAddress.fromHash160( - addrHash: publicKey.toAddress().addressProgram, + h160: publicKey.toP2pkhAddress().addressProgram, type: P2pkhAddressType.p2pkhwt); /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. @@ -531,7 +531,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li previousValue + element.utxo.token!.amount); final bchTransaction = ForkedTransactionBuilder( - outPuts: [ + outputs: [ /// change address for bch values (sum of bch amout - (outputs amount + fee)) BitcoinOutput( address: p2pkhAddress.baseAddress, @@ -569,7 +569,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li ); final transaaction = bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { - return privateKey.signInput(trDigest, sigHash: sighash); + return privateKey.signECDSA(trDigest, sighash: sighash); }); /// transaction ID @@ -645,7 +645,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li extFlags: 0, ); - // sign transaction using `signTapRoot` method of thransaction + // sign transaction using `signBip340` method of thransaction final signedTx = sign(txDigit, utxo[i].public().toHex(), SIGHASH_ALL); // add witness for current index @@ -695,7 +695,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li // index of utxo txInIndex: i, // spender script pub key - script: utxo[i].public().toAddress().toScriptPubKey(), + script: utxo[i].public().toP2pkhAddress().toScriptPubKey(), ); // sign transaction diff --git a/analysis_options.yaml b/analysis_options.yaml index 3c029d1..1c26b14 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,56 +1,29 @@ -include: package:lints/recommended.yaml -# include: package:flutter_lints/flutter.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options -# Uncomment the following section to specify additional rules. +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. rules: - # - always_declare_return_types - # - annotate_overrides - # - avoid_init_to_null - # - avoid_null_checks_in_equality_operators - # - avoid_relative_lib_imports - # - avoid_return_types_on_setters - # - avoid_shadowing_type_parameters - # - avoid_single_cascade_in_expression_statements - # - avoid_types_as_parameter_names - # - await_only_futures - # - camel_case_extensions - # - curly_braces_in_flow_control_structures - # - empty_catches - # - empty_constructor_bodies - # - library_names - # - library_prefixes - # - no_duplicate_case_values - # - null_closures - # - omit_local_variable_types - # - prefer_adjacent_string_concatenation - # - prefer_collection_literals - # - prefer_conditional_assignment - # - prefer_contains - # - prefer_equal_for_default_values - # - prefer_final_fields - # - prefer_for_elements_to_map_fromIterable - # - prefer_generic_function_type_aliases - # - prefer_if_null_operators - # - prefer_inlined_adds - # - prefer_is_empty - # - prefer_is_not_empty - # - prefer_iterable_whereType - # - prefer_single_quotes - # - prefer_spread_collections - # - recursive_getters - # - slash_for_doc_comments - # - sort_child_properties_last - # - type_init_formals - # - unawaited_futures - # - unnecessary_brace_in_string_interps - # - unnecessary_const - # - unnecessary_getters_setters - # - unnecessary_new - # - unnecessary_null_in_if_null_operators - # - unnecessary_this - # - unrelated_type_equality_checks - # - use_function_type_syntax_for_parameters - # - use_rethrow_when_possible - # - valid_regexps \ No newline at end of file + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options + diff --git a/example/lib/bitcoin_cash/burn_token_example.dart b/example/lib/bitcoin_cash/burn_token_example.dart index 31fc72c..33c530a 100644 --- a/example/lib/bitcoin_cash/burn_token_example.dart +++ b/example/lib/bitcoin_cash/burn_token_example.dart @@ -32,7 +32,7 @@ void main() async { addrHash: publicKey.toAddress().addressProgram, type: P2pkhAddressType.p2pkhwt); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. final elctrumUtxos = await provider.request(ElectrumRequestScriptHashListUnspent( @@ -128,7 +128,7 @@ void main() async { ); final transaaction = bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { - return privateKey.signInput(trDigest, sigHash: sighash); + return privateKey.signECDSA(trDigest, sighash: sighash); }); /// transaction ID diff --git a/example/lib/bitcoin_cash/create_cash_token_example.dart b/example/lib/bitcoin_cash/create_cash_token_example.dart index 0b07035..a93e635 100644 --- a/example/lib/bitcoin_cash/create_cash_token_example.dart +++ b/example/lib/bitcoin_cash/create_cash_token_example.dart @@ -27,7 +27,7 @@ void main() async { /// for enhanced accessibility within the network. final p2pkhAddress = publicKey.toAddress(); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. final elctrumUtxos = await provider.request(ElectrumRequestScriptHashListUnspent( @@ -121,7 +121,7 @@ void main() async { outputOrdering: BitcoinOrdering.none); final transaaction = bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { - return privateKey.signInput(trDigest, sigHash: sighash); + return privateKey.signECDSA(trDigest, sighash: sighash); }); /// transaction ID diff --git a/example/lib/bitcoin_cash/create_nft_example.dart b/example/lib/bitcoin_cash/create_nft_example.dart index e9af101..42e3b5a 100644 --- a/example/lib/bitcoin_cash/create_nft_example.dart +++ b/example/lib/bitcoin_cash/create_nft_example.dart @@ -28,7 +28,7 @@ void main() async { final p2pkhAddress = BitcoinCashAddress.fromBaseAddress(publicKey.toAddress()); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. final elctrumUtxos = await provider.request(ElectrumRequestScriptHashListUnspent( @@ -109,7 +109,7 @@ void main() async { ); final transaaction = bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { - return privateKey.signInput(trDigest, sigHash: sighash); + return privateKey.signECDSA(trDigest, sighash: sighash); }); /// transaction ID diff --git a/example/lib/bitcoin_cash/make_vout0_example.dart b/example/lib/bitcoin_cash/make_vout0_example.dart index 4f1730c..e48c169 100644 --- a/example/lib/bitcoin_cash/make_vout0_example.dart +++ b/example/lib/bitcoin_cash/make_vout0_example.dart @@ -28,7 +28,7 @@ void main() async { /// for enhanced accessibility within the network. final p2pkhAddress = publicKey.toAddress(); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. final elctrumUtxos = await provider.request(ElectrumRequestScriptHashListUnspent( @@ -65,7 +65,7 @@ void main() async { ); final transaaction = bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { - return privateKey.signInput(trDigest, sigHash: sighash); + return privateKey.signECDSA(trDigest, sighash: sighash); }); /// transaction ID diff --git a/example/lib/bitcoin_cash/minting_nft_example.dart b/example/lib/bitcoin_cash/minting_nft_example.dart index 621c241..eb7d6fc 100644 --- a/example/lib/bitcoin_cash/minting_nft_example.dart +++ b/example/lib/bitcoin_cash/minting_nft_example.dart @@ -28,7 +28,7 @@ void main() async { final p2pkhAddress = BitcoinCashAddress.fromBaseAddress( publicKey.toP2pkInP2sh(useBCHP2sh32: true)); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. final elctrumUtxos = await provider.request(ElectrumRequestScriptHashListUnspent( @@ -165,7 +165,7 @@ void main() async { ); final transaaction = bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { - return privateKey.signInput(trDigest, sigHash: sighash); + return privateKey.signECDSA(trDigest, sighash: sighash); }); /// transaction ID diff --git a/example/lib/bitcoin_cash/old_examples/old_example.dart b/example/lib/bitcoin_cash/old_examples/old_example.dart index 15403f5..ce71cea 100644 --- a/example/lib/bitcoin_cash/old_examples/old_example.dart +++ b/example/lib/bitcoin_cash/old_examples/old_example.dart @@ -121,7 +121,7 @@ void _spendFrom2P2SHAnd2P2PKHAddress() async { /// Specify the network for the litcoin transaction network: network, - /// Define a list of Unspent Transaction Outputs (UTXOs) for the Bitcoin transaction + /// Define a list of Unspent Transaction outputs (UTXOs) for the Bitcoin transaction utxos: [ UtxoWithAddress( @@ -152,13 +152,13 @@ void _spendFrom2P2SHAnd2P2PKHAddress() async { /// For each input in the transaction, locate the corresponding private key /// and sign the transaction digest to construct the unlocking script. if (publicKey == childKey1PublicKey.toHex()) { - return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey1PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey.toHex()) { - return childKey2PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey2PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey2.toHex()) { - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); @@ -267,7 +267,7 @@ void _spendFrom2P2SHAnd1P2PKHAddress() async { /// Add a memo to the transaction, linking to the GitHub repository memo: "https://github.com/mrtnetwork", - /// Define a list of Unspent Transaction Outputs (UTXOs) for the Bitcoin transaction + /// Define a list of Unspent Transaction outputs (UTXOs) for the Bitcoin transaction utxos: [ UtxoWithAddress( @@ -353,13 +353,13 @@ void _spendFrom2P2SHAnd1P2PKHAddress() async { ]); final tr = b.buildTransaction((trDigest, utxo, publicKey, int sighash) { if (publicKey == childKey1PublicKey.toHex()) { - return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey1PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey.toHex()) { - return childKey2PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey2PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey2.toHex()) { - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); diff --git a/example/lib/bitcoin_cash/p2sh32_spend_example.dart b/example/lib/bitcoin_cash/p2sh32_spend_example.dart index 0081118..93cfc27 100644 --- a/example/lib/bitcoin_cash/p2sh32_spend_example.dart +++ b/example/lib/bitcoin_cash/p2sh32_spend_example.dart @@ -42,7 +42,7 @@ void main() async { final p2sh32Example2 = BitcoinCashAddress.fromBaseAddress( publicKey.toP2pkInP2sh(useBCHP2sh32: true)); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. final example1ElectrumUtxos = await provider.request(ElectrumRequestScriptHashListUnspent( @@ -98,7 +98,7 @@ void main() async { ); final transaaction = bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { - return privateKey.signInput(trDigest, sigHash: sighash); + return privateKey.signECDSA(trDigest, sighash: sighash); }); /// transaction ID diff --git a/example/lib/bitcoin_cash/send_ft_token_example.dart b/example/lib/bitcoin_cash/send_ft_token_example.dart index cfbfc2b..a3acb9a 100644 --- a/example/lib/bitcoin_cash/send_ft_token_example.dart +++ b/example/lib/bitcoin_cash/send_ft_token_example.dart @@ -33,7 +33,7 @@ void main() async { addrHash: publicKey.toAddress().addressProgram, type: P2pkhAddressType.p2pkhwt); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. final elctrumUtxos = await provider.request(ElectrumRequestScriptHashListUnspent( @@ -124,7 +124,7 @@ void main() async { ); final transaaction = bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { - return privateKey.signInput(trDigest, sigHash: sighash); + return privateKey.signECDSA(trDigest, sighash: sighash); }); /// transaction ID diff --git a/example/lib/bitcoin_cash/transfer_bch_example.dart b/example/lib/bitcoin_cash/transfer_bch_example.dart index a51ac29..5df0ab5 100644 --- a/example/lib/bitcoin_cash/transfer_bch_example.dart +++ b/example/lib/bitcoin_cash/transfer_bch_example.dart @@ -37,7 +37,7 @@ void main() async { "bchtest:pvw39llgap0a4vm8jn9sjsvfsthah4wgemjlh6epdtzr3pl2fqtmsn3s4vcm7", network: network); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. final elctrumUtxos = await provider.request(ElectrumRequestScriptHashListUnspent( @@ -84,7 +84,7 @@ void main() async { ); final transaaction = bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { - return privateKey.signInput(trDigest, sigHash: sighash); + return privateKey.signECDSA(trDigest, sighash: sighash); }); /// transaction ID diff --git a/example/lib/global/bch_example.dart b/example/lib/global/bch_example.dart index 73b5952..7ab8975 100644 --- a/example/lib/global/bch_example.dart +++ b/example/lib/global/bch_example.dart @@ -34,7 +34,7 @@ void main() async { "bchtest:pvw39llgap0a4vm8jn9sjsvfsthah4wgemjlh6epdtzr3pl2fqtmsn3s4vcm7", network: network); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. final elctrumUtxos = await provider.request(ElectrumRequestScriptHashListUnspent( @@ -81,7 +81,7 @@ void main() async { ); final transaaction = bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { - return privateKey.signInput(trDigest, sigHash: sighash); + return privateKey.signECDSA(trDigest, sighash: sighash); }); /// transaction ID diff --git a/example/lib/global/btc_examples_multisig_taproot_legacy_uncomp.dart b/example/lib/global/btc_examples_multisig_taproot_legacy_uncomp.dart index 617fa11..41e331a 100644 --- a/example/lib/global/btc_examples_multisig_taproot_legacy_uncomp.dart +++ b/example/lib/global/btc_examples_multisig_taproot_legacy_uncomp.dart @@ -125,9 +125,9 @@ void main() async { bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { final pk = ECPublic.fromHex(publicKey); if (utxo.utxo.isP2tr) { - return keys[pk.toHex()]!.signTapRoot(trDigest, sighash: sighash); + return keys[pk.toHex()]!.signBip340(trDigest, sighash: sighash); } - return keys[pk.toHex()]!.signInput(trDigest, sigHash: sighash); + return keys[pk.toHex()]!.signECDSA(trDigest, sighash: sighash); }); final transactionRaw = transaaction.toHex(); diff --git a/example/lib/global/old_examples/bitcoin_example.dart b/example/lib/global/old_examples/bitcoin_example.dart index 4084aa1..2fe0924 100644 --- a/example/lib/global/old_examples/bitcoin_example.dart +++ b/example/lib/global/old_examples/bitcoin_example.dart @@ -68,7 +68,7 @@ void _spendFromP2pkhTo10DifferentType() async { network: network); final out3 = P2wpkhAddress.fromAddress( address: "tb1q3zqgu9j368wgk8u5f9vtmkdwq8geetdxry690d", network: network); - final out4 = P2pkAddress(publicKey: examplePublicKey.publicKey.toHex()); + final out4 = P2pkAddress.fromPubkey(pubkey: examplePublicKey.publicKey.toHex()); final out5 = P2shAddress.fromAddress( address: "2N5hVdETdJMwLDxxttfqeWgMuny6K4SYGSc", network: network); final out6 = P2shAddress.fromAddress( @@ -122,7 +122,7 @@ void _spendFromP2pkhTo10DifferentType() async { /// memo memo: "https://github.com/mrtnetwork", - /// Define a list of Unspent Transaction Outputs (UTXOs) for the Bitcoin transaction + /// Define a list of Unspent Transaction outputs (UTXOs) for the Bitcoin transaction utxos: [ /// Create a UTXO using a BitcoinUtxo with specific details UtxoWithAddress( @@ -167,9 +167,9 @@ void _spendFromP2pkhTo10DifferentType() async { if (publicKey == examplePublicKey2.toHex()) { if (utxo.utxo.isP2tr) { - return examplePrivateKey.signTapRoot(trDigest); + return examplePrivateKey.signBip340(trDigest); } - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); @@ -265,7 +265,7 @@ void _spendFrom10DifferentTypeToP2pkh() async { /// Add a memo to the transaction, linking to the GitHub repository memo: "https://github.com/mrtnetwork", - /// Define a list of Unspent Transaction Outputs (UTXOs) for the Bitcoin transaction. + /// Define a list of Unspent Transaction outputs (UTXOs) for the Bitcoin transaction. /// We are selecting 10 UTXOs for spending, and each UTXO has a different address type. /// These UTXOs are related to the previous example at the top of this page. utxos: [ @@ -401,21 +401,21 @@ void _spendFrom10DifferentTypeToP2pkh() async { /// and sign the transaction digest to construct the unlocking script. if (publicKey == childKey1PublicKey.toHex()) { if (utxo.utxo.isP2tr) { - return childKey1PrivateKey.signTapRoot(trDigest, sighash: sighash); + return childKey1PrivateKey.signBip340(trDigest, sighash: sighash); } - return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey1PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey.toHex()) { if (utxo.utxo.isP2tr) { - return childKey2PrivateKey.signTapRoot(trDigest, sighash: sighash); + return childKey2PrivateKey.signBip340(trDigest, sighash: sighash); } - return childKey2PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey2PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey2.toHex()) { if (utxo.utxo.isP2tr) { - return examplePrivateKey.signTapRoot(trDigest, sighash: sighash); + return examplePrivateKey.signBip340(trDigest, sighash: sighash); } - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); diff --git a/example/lib/global/old_examples/dash_example/dash.dart b/example/lib/global/old_examples/dash_example/dash.dart index 0479f3d..d6a8880 100644 --- a/example/lib/global/old_examples/dash_example/dash.dart +++ b/example/lib/global/old_examples/dash_example/dash.dart @@ -126,7 +126,7 @@ void _spendFromTwoP2shAndOneP2PKH() async { /// For each input in the transaction, locate the corresponding private key /// and sign the transaction digest to construct the unlocking script. if (publicKey == examplePublicKey2.toHex()) { - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); @@ -225,7 +225,7 @@ void _spendP2SH() async { /// For each input in the transaction, locate the corresponding private key /// and sign the transaction digest to construct the unlocking script. if (publicKey == childKey1PublicKey.toHex()) { - return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey1PrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); }); diff --git a/example/lib/global/old_examples/doge_example/doge_example.dart b/example/lib/global/old_examples/doge_example/doge_example.dart index 1ff568c..6eab54b 100644 --- a/example/lib/global/old_examples/doge_example/doge_example.dart +++ b/example/lib/global/old_examples/doge_example/doge_example.dart @@ -129,13 +129,13 @@ void _spendFrom3P2shAddress() async { ]); final tr = builder.buildTransaction((trDigest, utxo, publicKey, sighash) { if (publicKey == childKey1PublicKey.toHex()) { - return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey1PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey.toHex()) { - return childKey2PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey2PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey2.toHex()) { - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); @@ -280,13 +280,13 @@ void _spendFromP2pkhAndP2sh() async { /// For each input in the transaction, locate the corresponding private key /// and sign the transaction digest to construct the unlocking script. if (publicKey == childKey1PublicKey.toHex()) { - return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey1PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey.toHex()) { - return childKey2PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey2PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey2.toHex()) { - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); diff --git a/example/lib/global/old_examples/litecoin_example/litecoin_example.dart b/example/lib/global/old_examples/litecoin_example/litecoin_example.dart index f225cb3..eb55185 100644 --- a/example/lib/global/old_examples/litecoin_example/litecoin_example.dart +++ b/example/lib/global/old_examples/litecoin_example/litecoin_example.dart @@ -129,7 +129,7 @@ void _spendLTCP2pkhAddress() async { /// and sign the transaction digest to construct the unlocking script. if (publicKey == pub.toHex()) { /// sign the transaction input using specified sighash or default to SIGHASH_ALL - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); @@ -269,13 +269,13 @@ void _spendFrom2P2shAddressAndOneMultiSigP2shAddress() async { /// and sign the transaction digest to construct the unlocking script. if (publicKey == childKey1PublicKey.toHex()) { - return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey1PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey.toHex()) { - return childKey2PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey2PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey2.toHex()) { - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); @@ -381,13 +381,13 @@ void _spendFromNestedSegwitP2WPKHInP2SH() async { /// For each input in the transaction, locate the corresponding private key /// and sign the transaction digest to construct the unlocking script. if (publicKey == childKey1PublicKey.toHex()) { - return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey1PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey.toHex()) { - return childKey2PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey2PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey2.toHex()) { - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); @@ -501,13 +501,13 @@ void _spendFromSegwitP2WPKHAddress() async { /// For each input in the transaction, locate the corresponding private key /// and sign the transaction digest to construct the unlocking script. if (publicKey == childKey1PublicKey.toHex()) { - return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey1PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey.toHex()) { - return childKey2PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey2PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey2.toHex()) { - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); diff --git a/example/lib/global/old_examples/spending_with_scripts/spending_builders.dart b/example/lib/global/old_examples/spending_with_scripts/spending_builders.dart index 0f49320..f3eff8c 100644 --- a/example/lib/global/old_examples/spending_with_scripts/spending_builders.dart +++ b/example/lib/global/old_examples/spending_with_scripts/spending_builders.dart @@ -2,7 +2,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; BtcTransaction buildP2wpkTransaction({ required List receiver, - required String Function(List, String publicKey, int sigHash) sign, + required String Function(List, String publicKey, int sighash) sign, required List utxo, }) { // We define transaction inputs by specifying the transaction ID and index. @@ -66,7 +66,7 @@ BtcTransaction buildP2wpkTransaction({ BtcTransaction buildP2WSHTransaction({ required List receiver, - required String Function(List, String publicKey, int sigHash) sign, + required String Function(List, String publicKey, int sighash) sign, required List utxo, }) { // We define transaction inputs by specifying the transaction ID and index. @@ -132,7 +132,7 @@ BtcTransaction buildP2WSHTransaction({ BtcTransaction buildP2pkhTransaction({ required List receiver, - required String Function(List, String publicKey, int sigHash) sign, + required String Function(List, String publicKey, int sighash) sign, required List utxo, }) { // We define transaction inputs by specifying the transaction ID and index. @@ -178,7 +178,7 @@ BtcTransaction buildP2pkhTransaction({ BtcTransaction buildP2shNoneSegwitTransaction({ required List receiver, - required String Function(List, String publicKey, int sigHash) sign, + required String Function(List, String publicKey, int sighash) sign, required List utxo, }) { // We define transaction inputs by specifying the transaction ID and index. @@ -238,7 +238,7 @@ BtcTransaction buildP2shNoneSegwitTransaction({ BtcTransaction buildP2SHSegwitTransaction({ required List receiver, - required String Function(List, String publicKey, int sigHash) sign, + required String Function(List, String publicKey, int sighash) sign, required List utxo, }) { // We define transaction inputs by specifying the transaction ID and index. @@ -323,7 +323,7 @@ BtcTransaction buildP2SHSegwitTransaction({ BtcTransaction buildP2trTransaction({ required List receiver, - required String Function(List, String publicKey, int sigHash) sign, + required String Function(List, String publicKey, int sighash) sign, required List utxo, }) { // We define transaction inputs by specifying the transaction ID and index. @@ -373,7 +373,7 @@ BtcTransaction buildP2trTransaction({ sighash: BitcoinOpCodeConst.sighashDefault, ); - // sign transaction using `signTapRoot` method of thransaction + // sign transaction using `signBip340` method of thransaction final signedTx = sign(txDigit, utxo[i].public().toHex(), BitcoinOpCodeConst.sighashAll); diff --git a/example/lib/global/old_examples/spending_with_scripts/spending_single_type.dart b/example/lib/global/old_examples/spending_with_scripts/spending_single_type.dart index 1c7acee..9e51c71 100644 --- a/example/lib/global/old_examples/spending_with_scripts/spending_single_type.dart +++ b/example/lib/global/old_examples/spending_with_scripts/spending_single_type.dart @@ -83,11 +83,11 @@ Future spendingP2WPKH(ECPrivate sWallet, ECPrivate rWallet) async { // You can refer to this method to learn how to create a transaction. final transaction = buildP2wpkTransaction( receiver: outPutWithValue, - sign: (p0, publicKey, sigHash) { + sign: (p0, publicKey, sighash) { // Here, we find the corresponding private key based on the public key and proceed to sign the transaction." - // Note that to sign Taproot transactions, you must use the 'signTapRoot' method for signing. + // Note that to sign Taproot transactions, you must use the 'signBip340' method for signing. // Below is a method for spending Taproot transactions that you can review. - return prive.signInput(p0, sigHash: sigHash); + return prive.signECDSA(p0, sighash: sighash); }, utxo: utxo, ); @@ -143,8 +143,8 @@ Future spendingP2WSH(ECPrivate sWallet, ECPrivate rWallet) async { .toList(); final transaction = buildP2WSHTransaction( receiver: outPutWithValue, - sign: (p0, publicKey, sigHash) { - return prive.signInput(p0, sigHash: sigHash); + sign: (p0, publicKey, sighash) { + return prive.signECDSA(p0, sighash: sighash); }, utxo: utxo, ); @@ -192,8 +192,8 @@ Future spendingP2PKH(ECPrivate sWallet, ECPrivate rWallet) async { final transaction = buildP2pkhTransaction( receiver: outPutWithValue, - sign: (p0, publicKey, sigHash) { - return prive.signInput(p0, sigHash: sigHash); + sign: (p0, publicKey, sighash) { + return prive.signECDSA(p0, sighash: sighash); }, utxo: utxo, ); @@ -243,8 +243,8 @@ Future spendingP2SHNoneSegwit( .toList(); final transaction = buildP2shNoneSegwitTransaction( receiver: outPutWithValue, - sign: (p0, publicKey, sigHash) { - return prive.signInput(p0, sigHash: sigHash); + sign: (p0, publicKey, sighash) { + return prive.signECDSA(p0, sighash: sighash); }, utxo: utxo, ); @@ -296,8 +296,8 @@ Future spendingP2shSegwit(ECPrivate sWallet, ECPrivate rWallet) async { // return; final transaction = buildP2SHSegwitTransaction( receiver: outPutWithValue, - sign: (p0, publicKey, sigHash) { - return prive.signInput(p0, sigHash: sigHash); + sign: (p0, publicKey, sighash) { + return prive.signECDSA(p0, sighash: sighash); }, utxo: utxo, ); @@ -311,7 +311,7 @@ Future spendingP2TR(ECPrivate sWallet, ECPrivate rWallet) async { // All the steps are the same as in the first tutorial; // the only difference is the transaction input type, // and we use method `buildP2trTransaction` to create the transaction. - // we use `signTapRoot` of ECPrivate for signing taproot transaction + // we use `signBip340` of ECPrivate for signing taproot transaction final addr = sWallet.getPublic(); // P2TR address final sender = addr.toTaprootAddress(); @@ -346,9 +346,9 @@ Future spendingP2TR(ECPrivate sWallet, ECPrivate rWallet) async { final transaction = buildP2trTransaction( receiver: outPutWithValue, - sign: (p0, publicKey, sigHash) { - // Use signTapRoot instead of signInput for the taproot transaction input. - return prive.signTapRoot(p0, sighash: sigHash, tweak: true); + sign: (p0, publicKey, sighash) { + // Use signBip340 instead of signECDSA for the taproot transaction input. + return prive.signBip340(p0, sighash: sighash, tweak: true); }, utxo: utxo, ); diff --git a/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart b/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart index e9a5a85..2320f5d 100644 --- a/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart +++ b/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart @@ -269,10 +269,10 @@ void main() async { // yes is p2tr utxo and now we use SignTaprootTransaction(Schnorr sign) // for now this transaction builder support only tweak transaction // If you want to spend a Taproot script-path spending, you must create your own transaction builder. - return key.signTapRoot(trDigest, sighash: sighash); + return key.signBip340(trDigest, sighash: sighash); } else { // is seqwit(v0) or lagacy address we use SingInput (ECDSA) - return key.signInput(trDigest, sigHash: sighash); + return key.signECDSA(trDigest, sighash: sighash); } }); diff --git a/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart b/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart index ecfd778..49fd392 100644 --- a/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart +++ b/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart @@ -216,10 +216,10 @@ void main() async { // yes is p2tr utxo and now we use SignTaprootTransaction(Schnorr sign) // for now this transaction builder support only tweak transaction // If you want to spend a Taproot script-path spending, you must create your own transaction builder. - return key.signTapRoot(trDigest, sighash: sighash); + return key.signBip340(trDigest, sighash: sighash); } else { // is seqwit(v0) or lagacy address we use SingInput (ECDSA) - return key.signInput(trDigest, sigHash: sighash); + return key.signECDSA(trDigest, sighash: sighash); } }); diff --git a/example/lib/global/transfer_from_7_account_to_6_accout_example.dart b/example/lib/global/transfer_from_7_account_to_6_accout_example.dart index bdfcaee..da2876f 100644 --- a/example/lib/global/transfer_from_7_account_to_6_accout_example.dart +++ b/example/lib/global/transfer_from_7_account_to_6_accout_example.dart @@ -159,9 +159,9 @@ void main() async { final transaction = builder.buildTransaction((trDigest, utxo, publicKey, sighash) { if (utxo.utxo.isP2tr) { - return privateKey.signTapRoot(trDigest, sighash: sighash); + return privateKey.signBip340(trDigest, sighash: sighash); } - return privateKey.signInput(trDigest, sigHash: sighash); + return privateKey.signECDSA(trDigest, sighash: sighash); }); /// get tx id diff --git a/example/lib/global/transfer_to_8_account_example.dart b/example/lib/global/transfer_to_8_account_example.dart index 3469d37..b0c869c 100644 --- a/example/lib/global/transfer_to_8_account_example.dart +++ b/example/lib/global/transfer_to_8_account_example.dart @@ -138,9 +138,9 @@ void main() async { final transaction = builder.buildTransaction((trDigest, utxo, publicKey, sighash) { if (utxo.utxo.isP2tr) { - return examplePrivateKey2.signTapRoot(trDigest, sighash: sighash); + return examplePrivateKey2.signBip340(trDigest, sighash: sighash); } - return examplePrivateKey2.signInput(trDigest, sigHash: sighash); + return examplePrivateKey2.signECDSA(trDigest, sighash: sighash); }); /// get tx id diff --git a/example/lib/main.dart b/example/lib/main.dart index 5e85d9f..6a41b96 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -121,7 +121,7 @@ void _spendFrom2P2SHAnd2P2PKHAddress() async { /// Specify the network for the litcoin transaction network: network, - /// Define a list of Unspent Transaction Outputs (UTXOs) for the Bitcoin transaction + /// Define a list of Unspent Transaction outputs (UTXOs) for the Bitcoin transaction utxos: [ UtxoWithAddress( @@ -152,13 +152,13 @@ void _spendFrom2P2SHAnd2P2PKHAddress() async { /// For each input in the transaction, locate the corresponding private key /// and sign the transaction digest to construct the unlocking script. if (publicKey == childKey1PublicKey.toHex()) { - return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey1PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey.toHex()) { - return childKey2PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey2PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey2.toHex()) { - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); @@ -267,7 +267,7 @@ void _spendFrom2P2SHAnd1P2PKHAddress() async { /// Add a memo to the transaction, linking to the GitHub repository memo: "https://github.com/mrtnetwork", - /// Define a list of Unspent Transaction Outputs (UTXOs) for the Bitcoin transaction + /// Define a list of Unspent Transaction outputs (UTXOs) for the Bitcoin transaction utxos: [ UtxoWithAddress( @@ -353,13 +353,13 @@ void _spendFrom2P2SHAnd1P2PKHAddress() async { ]); final tr = b.buildTransaction((trDigest, utxo, publicKey, int sighash) { if (publicKey == childKey1PublicKey.toHex()) { - return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey1PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey.toHex()) { - return childKey2PrivateKey.signInput(trDigest, sigHash: sighash); + return childKey2PrivateKey.signECDSA(trDigest, sighash: sighash); } if (publicKey == examplePublicKey2.toHex()) { - return examplePrivateKey.signInput(trDigest, sigHash: sighash); + return examplePrivateKey.signECDSA(trDigest, sighash: sighash); } throw UnimplementedError(); diff --git a/example/lib/musig/methods.dart b/example/lib/musig/methods.dart index d67b119..e5c22ee 100644 --- a/example/lib/musig/methods.dart +++ b/example/lib/musig/methods.dart @@ -3,8 +3,11 @@ import 'package:example/services_examples/electrum/electrum_ssl_service.dart'; Future getProvider( {String url = "testnet4-electrumx.wakiyamap.dev:51002"}) async { - final service = await ElectrumSSLService.connect( - "testnet4-electrumx.wakiyamap.dev:51002"); + // final service = await ElectrumSSLService.connect( + // "testnet4-electrumx.wakiyamap.dev:51002"); + + final service = + await ElectrumSSLService.connect("testnet.aranguren.org:51002"); return ElectrumProvider(service); } @@ -73,7 +76,6 @@ Future> getPsbtUtxo( merkleProof: request.merkleProof, treeScript: request.treeScript, merkleRoot: request.merkleRoot, - privateKeys: request.privateKeys, xOnlyOrInternalPubKey: request.xOnlyOrInternalPubKey, muSig2ParticipantPublicKeys: request.muSig2ParticipantPublicKeys, hash160: request.hash160, diff --git a/example/lib/musig/musig_example.dart b/example/lib/musig/musig_example.dart index fdbf52f..4dcab65 100644 --- a/example/lib/musig/musig_example.dart +++ b/example/lib/musig/musig_example.dart @@ -143,8 +143,8 @@ BtcTransaction _spendWithLeafC( existsUtxosScriptPubKeys.map((e) => e.toScriptPubKey()).toList(), amounts: existsUtxosAmounts) .asImmutableBytes; - final sig1 = key1.signTapRoot(digest, tweak: false); - final sig2 = key2.signTapRoot(digest, tweak: false); + final sig1 = key1.signBip340(digest, tweak: false); + final sig2 = key2.signBip340(digest, tweak: false); final controlBlock = TaprootControlBlock.generate( xOnlyOrInternalPubKey: internalKey.toXOnly(), leafScript: leafC, diff --git a/example/pubspec.lock b/example/pubspec.lock index b6caa97..077b5e5 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -9,21 +9,29 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bip32: + dependency: transitive + description: + name: bip32 + sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97" + url: "https://pub.dev" + source: hosted + version: "2.0.0" bitcoin_base: dependency: "direct main" description: path: ".." relative: true source: path - version: "6.0.0" + version: "6.5.0" blockchain_utils: dependency: "direct main" description: name: blockchain_utils - sha256: "2f0e9bed57e55a0bdb8dfa7471aab41d83c488f7865c0bb8a55ce67b4f87f486" + sha256: fbddd2a7f1849d2244a35bf996b6fb00bf6bd557f0e13aed65ba0c16ad133cb7 url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "5.0.0" boolean_selector: dependency: transitive description: @@ -32,6 +40,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + bs58check: + dependency: transitive + description: + name: bs58check + sha256: c4a164d42b25c2f6bc88a8beccb9fc7d01440f3c60ba23663a20a70faf484ea9 + url: "https://pub.dev" + source: hosted + version: "1.0.2" characters: dependency: transitive description: @@ -56,6 +72,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -90,6 +122,14 @@ packages: description: flutter source: sdk version: "0.0.0" + hex: + dependency: transitive + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" http: dependency: "direct main" description: @@ -106,6 +146,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" leak_tracker: dependency: transitive description: @@ -170,6 +218,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" sky_engine: dependency: transitive description: flutter diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 30eb12e..5612b28 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -38,8 +38,8 @@ dependencies: bitcoin_base: path: ../ # blockchain_utils: - # path: ../../../blockchain_utils - blockchain_utils: ^4.3.0 + # path: ../../blockchain_utils + blockchain_utils: ^5.0.0 http: ^1.2.0 dev_dependencies: diff --git a/lib/bitcoin_base.dart b/lib/bitcoin_base.dart index a9c0611..6327c9a 100644 --- a/lib/bitcoin_base.dart +++ b/lib/bitcoin_base.dart @@ -19,3 +19,7 @@ export 'src/bitcoin_cash/bitcoin_cash.dart'; export 'src/transaction_builder/builder.dart'; export 'src/psbt/psbt.dart'; + +export 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; + +export 'package:bitcoin_base/src/bitcoin/script/op_code/constant.dart'; diff --git a/lib/src/bitcoin/address/address.dart b/lib/src/bitcoin/address/address.dart index 99cf14f..e9c79d8 100644 --- a/lib/src/bitcoin/address/address.dart +++ b/lib/src/bitcoin/address/address.dart @@ -9,11 +9,13 @@ library; import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +import 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; import 'package:bitcoin_base/src/bitcoin/taproot/taproot.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:bitcoin_base/src/models/network.dart'; import 'package:bitcoin_base/src/utils/enumerate.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:bitcoin_base/src/utils/script.dart'; part 'core.dart'; part 'legacy_address.dart'; part 'utils/address_utils.dart'; diff --git a/lib/src/bitcoin/address/core.dart b/lib/src/bitcoin/address/core.dart index 72b3231..89c4d78 100644 --- a/lib/src/bitcoin/address/core.dart +++ b/lib/src/bitcoin/address/core.dart @@ -13,6 +13,24 @@ abstract class BitcoinAddressType implements Enumerate { throw DartBitcoinPluginException('Unknown address type. $value')); } + // static BitcoinAddressType fromAddress(BitcoinBaseAddress address) { + // if (address is P2pkhAddress) { + // return P2pkhAddressType.p2pkh; + // } else if (address is P2shAddress) { + // return P2shAddressType.p2wpkhInP2sh; + // } else if (address is P2wshAddress) { + // return SegwitAddressType.p2wsh; + // } else if (address is P2trAddress) { + // return SegwitAddressType.p2tr; + // } else if (address is SilentPaymentsAddresType) { + // return SilentPaymentsAddresType.p2sp; + // } else if (address is P2wpkhAddress) { + // return SegwitAddressType.p2wpkh; + // } + // + // throw DartBitcoinPluginException('Invalid BitcoinAddressType: $address'); + // } + /// Check if the address type is Pay-to-Script-Hash (P2SH). bool get isP2sh; bool get isSegwit; @@ -20,12 +38,20 @@ abstract class BitcoinAddressType implements Enumerate { int get hashLength; bool get isP2sh32 => isP2sh && hashLength == 32; + bool get supportBip137 => switch (this) { + P2pkhAddressType.p2pkh || P2pkhAddressType.p2pkhwt => true, + P2shAddressType.p2wpkhInP2sh => true, + SegwitAddressType.p2wpkh => true, + _ => false + }; + // Enum values as a list for iteration static const List values = [ P2pkhAddressType.p2pkh, SegwitAddressType.p2wpkh, SegwitAddressType.p2tr, SegwitAddressType.p2wsh, + SegwitAddressType.mweb, P2shAddressType.p2wshInP2sh, P2shAddressType.p2wpkhInP2sh, P2shAddressType.p2pkhInP2sh, @@ -36,7 +62,8 @@ abstract class BitcoinAddressType implements Enumerate { P2shAddressType.p2pkInP2sh32wt, P2shAddressType.p2pkhInP2shwt, P2shAddressType.p2pkInP2shwt, - P2pkhAddressType.p2pkhwt + P2pkhAddressType.p2pkhwt, + SilentPaymentsAddresType.p2sp ]; T cast() { if (this is! T) { @@ -72,6 +99,8 @@ abstract class BitcoinBaseAddress { P2wshAddress.fromProgram(program: addressProgram), SegwitAddressType.p2tr => P2trAddress.fromProgram(program: addressProgram), + SilentPaymentsAddresType.p2sp => + P2sp.fromProgram(program: addressProgram), _ => throw DartBitcoinPluginException("Unsuported bitcoin address type."), }; } @@ -80,6 +109,36 @@ abstract class BitcoinBaseAddress { Script toScriptPubKey(); String pubKeyHash(); String get addressProgram; + + static BitcoinBaseAddress fromString( + String address, [ + BasedUtxoNetwork network = BitcoinNetwork.mainnet, + ]) { + if (network is BitcoinCashNetwork) { + if (!address.startsWith("bitcoincash:") && + (address.startsWith("q") || address.startsWith("p"))) { + address = "bitcoincash:$address"; + } + + return BitcoinCashAddress(address).baseAddress; + } + + if (P2pkhAddress.regex.hasMatch(address)) { + return P2pkhAddress.fromAddress(address: address, network: network); + } else if (P2shAddress.regex.hasMatch(address)) { + return P2shAddress.fromAddress(address: address, network: network); + } else if (P2wshAddress.regex.hasMatch(address)) { + return P2wshAddress.fromAddress(address: address, network: network); + } else if (P2trAddress.regex.hasMatch(address)) { + return P2trAddress.fromAddress(address: address, network: network); + } else if (SilentPaymentAddress.regex.hasMatch(address)) { + return SilentPaymentAddress.fromAddress(address); + } else if (P2wpkhAddress.regex.hasMatch(address)) { + return P2wpkhAddress.fromAddress(address: address, network: network); + } + + throw DartBitcoinPluginException('Invalid BitcoinBaseAddress: $address'); + } } class PubKeyAddressType extends BitcoinAddressType { @@ -171,6 +230,7 @@ class SegwitAddressType extends BitcoinAddressType { static const SegwitAddressType p2wpkh = SegwitAddressType._('P2WPKH'); static const SegwitAddressType p2tr = SegwitAddressType._('P2TR'); static const SegwitAddressType p2wsh = SegwitAddressType._('P2WSH'); + static const SegwitAddressType mweb = SegwitAddressType._("MWEB"); @override bool get isP2sh => false; @override @@ -184,6 +244,8 @@ class SegwitAddressType extends BitcoinAddressType { switch (this) { case SegwitAddressType.p2wpkh: return 20; + case SegwitAddressType.mweb: + return 66; default: return 32; } @@ -194,3 +256,25 @@ class SegwitAddressType extends BitcoinAddressType { return 'SegwitAddressType.$value'; } } + +class SilentPaymentsAddresType implements BitcoinAddressType { + const SilentPaymentsAddresType._(this.value); + static const SilentPaymentsAddresType p2sp = SilentPaymentsAddresType._("P2SP"); + @override + bool get isP2sh => false; + @override + bool get isSegwit => true; + + @override + final String value; + + @override + int get hashLength { + return 32; + } + + @override + String toString() { + return 'SilentPaymentsAddresType.$value'; + } +} diff --git a/lib/src/bitcoin/address/legacy_address.dart b/lib/src/bitcoin/address/legacy_address.dart index 0b7627d..545583a 100644 --- a/lib/src/bitcoin/address/legacy_address.dart +++ b/lib/src/bitcoin/address/legacy_address.dart @@ -25,6 +25,9 @@ abstract class LegacyAddress implements BitcoinBaseAddress { late final String _addressProgram; + // ECPublic? get pubkey => _pubkey; + // String? get signature => _signature; + @override String get addressProgram { if (type == PubKeyAddressType.p2pk) throw UnimplementedError(); @@ -57,6 +60,8 @@ abstract class LegacyAddress implements BitcoinBaseAddress { } class P2shAddress extends LegacyAddress { + static RegExp get regex => RegExp(r'[23M][a-km-zA-HJ-NP-Z1-9]{25,34}'); + factory P2shAddress.fromScript32( {required Script script, P2shAddressType addressType = P2shAddressType.p2pkInP2sh32}) { @@ -65,7 +70,8 @@ class P2shAddress extends LegacyAddress { } return P2shAddress.fromHash160( addrHash: BytesUtils.toHexString( - QuickCrypto.sha256DoubleHash(script.toBytes()))); + QuickCrypto.sha256DoubleHash(script.toBytes())), + type: addressType); } P2shAddress.fromScript( {required super.script, this.type = P2shAddressType.p2pkInP2sh}) @@ -95,10 +101,18 @@ class P2shAddress extends LegacyAddress { /// Returns the scriptPubKey (P2SH) that corresponds to this address @override Script toScriptPubKey() { - if (addressProgram.length == 64) { - return Script(script: ['OP_HASH256', addressProgram, 'OP_EQUAL']); + if (addressProgram.length == QuickCrypto.sha256DigestSize * 2) { + return Script(script: [ + BitcoinOpcode.opHash256, + addressProgram, + BitcoinOpcode.opEqual + ]); } - return Script(script: ['OP_HASH160', addressProgram, 'OP_EQUAL']); + return Script(script: [ + BitcoinOpcode.opHash160, + addressProgram, + BitcoinOpcode.opEqual + ]); } @override @@ -114,6 +128,8 @@ class P2shAddress extends LegacyAddress { } class P2pkhAddress extends LegacyAddress { + static RegExp get regex => RegExp(r'[1mnL][a-km-zA-HJ-NP-Z1-9]{25,34}'); + P2pkhAddress.fromScript( {required super.script, this.type = P2pkhAddressType.p2pkh}) : super.fromScript(); @@ -129,23 +145,29 @@ class P2pkhAddress extends LegacyAddress { @override Script toScriptPubKey() { return Script(script: [ - 'OP_DUP', - 'OP_HASH160', + BitcoinOpcode.opDup, + BitcoinOpcode.opHash160, addressProgram, - 'OP_EQUALVERIFY', - 'OP_CHECKSIG' + BitcoinOpcode.opEqualVerify, + BitcoinOpcode.opCheckSig ]); } @override final P2pkhAddressType type; + + // Script toScriptSig() { + // return Script(script: [_signature, _pubkey]); + // } } class P2pkAddress extends LegacyAddress { + static RegExp get regex => RegExp(r'1([A-Za-z0-9]{34})'); + P2pkAddress._(this.publicKey) : super._(); factory P2pkAddress({required String publicKey}) { final toBytes = BytesUtils.fromHexString(publicKey); - if (!Secp256k1PublicKeyEcdsa.isValidBytes(toBytes)) { + if (!Secp256k1PublicKey.isValidBytes(toBytes)) { throw const DartBitcoinPluginException('Invalid Public key.'); } return P2pkAddress._(StringUtils.strip0x(publicKey.toLowerCase())); @@ -154,7 +176,7 @@ class P2pkAddress extends LegacyAddress { @override Script toScriptPubKey() { - return Script(script: [publicKey, 'OP_CHECKSIG']); + return Script(script: [publicKey, BitcoinOpcode.opCheckSig]); } @override diff --git a/lib/src/bitcoin/address/network_address.dart b/lib/src/bitcoin/address/network_address.dart index 62770b4..a451bf7 100644 --- a/lib/src/bitcoin/address/network_address.dart +++ b/lib/src/bitcoin/address/network_address.dart @@ -7,6 +7,107 @@ abstract class BitcoinNetworkAddress { required this.network, required this.baseAddress}); + static ADDRESS fromBaseAddress
( + {required BitcoinBaseAddress address, + required BasedUtxoNetwork network}) { + BitcoinNetworkAddress baseAddress; + switch (network) { + case BitcoinSVNetwork.mainnet: + case BitcoinSVNetwork.testnet: + baseAddress = BitcoinSVAddress.fromBaseAddress(address, + network: network as BitcoinSVNetwork); + case BitcoinNetwork.mainnet: + case BitcoinNetwork.testnet: + case BitcoinNetwork.testnet4: + baseAddress = BitcoinAddress.fromBaseAddress(address, + network: network as BitcoinNetwork); + case LitecoinNetwork.mainnet: + case LitecoinNetwork.testnet: + baseAddress = LitecoinAddress.fromBaseAddress(address, + network: network as LitecoinNetwork); + case DashNetwork.mainnet: + case DashNetwork.testnet: + baseAddress = DashAddress.fromBaseAddress(address, + network: network as DashNetwork); + case DogecoinNetwork.mainnet: + case DogecoinNetwork.testnet: + baseAddress = DogeAddress.fromBaseAddress(address, + network: network as DogecoinNetwork); + case BitcoinCashNetwork.mainnet: + case BitcoinCashNetwork.testnet: + baseAddress = BitcoinCashAddress.fromBaseAddress(address, + network: network as BitcoinCashNetwork); + case PepeNetwork.mainnet: + baseAddress = PepeAddress.fromBaseAddress(address, + network: network as PepeNetwork); + case ElectraProtocolNetwork.mainnet: + case ElectraProtocolNetwork.testnet: + baseAddress = ElectraProtocolAddress.fromBaseAddress(address, + network: network as ElectraProtocolNetwork); + default: + throw DartBitcoinPluginException("Unknown network. ${network.value}"); + } + if (baseAddress is! ADDRESS) { + throw DartBitcoinPluginException( + "Invalid cast: expected ${ADDRESS.runtimeType}, but found ${baseAddress.runtimeType}.", + ); + } + return baseAddress; + } + + static ADDRESS parse
( + {required String address, required BasedUtxoNetwork network}) { + BitcoinNetworkAddress baseAddress; + switch (network) { + case BitcoinSVNetwork.mainnet: + case BitcoinSVNetwork.testnet: + baseAddress = + BitcoinSVAddress(address, network: network as BitcoinSVNetwork); + case BitcoinNetwork.mainnet: + case BitcoinNetwork.testnet: + case BitcoinNetwork.testnet4: + baseAddress = + BitcoinAddress(address, network: network as BitcoinNetwork); + case LitecoinNetwork.mainnet: + case LitecoinNetwork.testnet: + baseAddress = + LitecoinAddress(address, network: network as LitecoinNetwork); + case DashNetwork.mainnet: + case DashNetwork.testnet: + baseAddress = DashAddress(address, network: network as DashNetwork); + case DogecoinNetwork.mainnet: + case DogecoinNetwork.testnet: + baseAddress = DogeAddress(address, network: network as DogecoinNetwork); + case BitcoinCashNetwork.mainnet: + case BitcoinCashNetwork.testnet: + baseAddress = + BitcoinCashAddress(address, network: network as BitcoinCashNetwork); + case PepeNetwork.mainnet: + baseAddress = PepeAddress(address, network: network as PepeNetwork); + case ElectraProtocolNetwork.mainnet: + case ElectraProtocolNetwork.testnet: + baseAddress = ElectraProtocolAddress(address, + network: network as ElectraProtocolNetwork); + default: + throw DartBitcoinPluginException("Unknown network. ${network.value}"); + } + if (baseAddress is! ADDRESS) { + throw DartBitcoinPluginException( + "Invalid cast: expected ${ADDRESS.runtimeType}, but found ${baseAddress.runtimeType}.", + ); + } + return baseAddress; + } + + static ADDRESS? tryParse
( + {required String address, required BasedUtxoNetwork network}) { + try { + return parse(address: address, network: network); + } catch (_) { + return null; + } + } + /// The underlying Bitcoin base address. final BitcoinBaseAddress baseAddress; diff --git a/lib/src/bitcoin/address/segwit_address.dart b/lib/src/bitcoin/address/segwit_address.dart index 20a2eda..da1ad2e 100644 --- a/lib/src/bitcoin/address/segwit_address.dart +++ b/lib/src/bitcoin/address/segwit_address.dart @@ -26,6 +26,7 @@ abstract class SegwitAddress implements BitcoinBaseAddress { late final String addressProgram; final int segwitVersion; + // ECPublic? pubkey; @override String toAddress(BasedUtxoNetwork network) { @@ -60,6 +61,8 @@ abstract class SegwitAddress implements BitcoinBaseAddress { } class P2wpkhAddress extends SegwitAddress { + static RegExp get regex => RegExp(r'(bc|tb|ltc)1q[ac-hj-np-z02-9]{25,39}'); + P2wpkhAddress.fromAddress({required super.address, required super.network}) : super.fromAddress(segwitVersion: _BitcoinAddressUtils.segwitV0); @@ -68,10 +71,22 @@ class P2wpkhAddress extends SegwitAddress { segwitVersion: _BitcoinAddressUtils.segwitV0, addresType: SegwitAddressType.p2wpkh); + + // P2wpkhAddress.fromRedeemScript({required super.script, super.network}) + // : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV0); + + factory P2wpkhAddress.fromScriptPubkey({required Script script, BasedUtxoNetwork? network}) { + if (script.getAddressType() != SegwitAddressType.p2wpkh) { + throw ArgumentError("Invalid scriptPubKey"); + } + + return P2wpkhAddress.fromProgram(program: script.findScriptParam(1)); + } + /// returns the scriptPubKey of a P2WPKH witness script @override Script toScriptPubKey() { - return Script(script: ['OP_0', addressProgram]); + return Script(script: [BitcoinOpcode.op0, addressProgram]); } /// returns the type of address @@ -80,6 +95,9 @@ class P2wpkhAddress extends SegwitAddress { } 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}|[ac-hj-np-z02-9]{8,89})'); + P2trAddress.fromAddress({required super.address, required super.network}) : super.fromAddress(segwitVersion: _BitcoinAddressUtils.segwitV1); P2trAddress.fromProgram({required super.program}) @@ -99,10 +117,21 @@ class P2trAddress extends SegwitAddress { segwitVersion: _BitcoinAddressUtils.segwitV1, addresType: SegwitAddressType.p2tr); + // P2trAddress.fromRedeemScript({required super.script, super.network}) + // : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV1); + + factory P2trAddress.fromScriptPubkey({required Script script, BasedUtxoNetwork? network}) { + if (script.getAddressType() != SegwitAddressType.p2tr) { + throw ArgumentError("Invalid scriptPubKey"); + } + + return P2trAddress.fromProgram(program: script.findScriptParam(1)); + } + /// returns the scriptPubKey of a P2TR witness script @override Script toScriptPubKey() { - return Script(script: ['OP_1', addressProgram]); + return Script(script: [BitcoinOpcode.op1, addressProgram]); } /// returns the type of address @@ -111,6 +140,8 @@ class P2trAddress extends SegwitAddress { } class P2wshAddress extends SegwitAddress { + static RegExp get regex => RegExp(r'(bc|tb)1q[ac-hj-np-z02-9]{40,80}'); + P2wshAddress.fromAddress({required super.address, required super.network}) : super.fromAddress(segwitVersion: _BitcoinAddressUtils.segwitV0); P2wshAddress.fromProgram({required super.program}) @@ -120,13 +151,76 @@ class P2wshAddress extends SegwitAddress { P2wshAddress.fromScript({required super.script}) : super.fromScript(segwitVersion: _BitcoinAddressUtils.segwitV0); + // P2wshAddress.fromRedeemScript({required super.script, super.network}) + // : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV0); + + factory P2wshAddress.fromScriptPubkey({required Script script, BasedUtxoNetwork? network}) { + if (script.getAddressType() != SegwitAddressType.p2wsh) { + throw ArgumentError("Invalid scriptPubKey"); + } + + return P2wshAddress.fromProgram(program: script.findScriptParam(1)); + } + /// Returns the scriptPubKey of a P2WPKH witness script @override Script toScriptPubKey() { - return Script(script: ['OP_0', addressProgram]); + return Script(script: [BitcoinOpcode.op0, addressProgram]); } /// Returns the type of address @override SegwitAddressType get type => SegwitAddressType.p2wsh; } + +class MwebAddress extends SegwitAddress { + static RegExp get regex => RegExp(r'(ltc|t)mweb1q[ac-hj-np-z02-9]{90,120}'); + + factory MwebAddress.fromAddress({required String address, required BasedUtxoNetwork network}) { + final decoded = Bech32DecoderBase.decodeBech32( + address, + Bech32Const.separator, + Bech32Const.checksumStrLen, + (hrp, data) => Bech32Utils.verifyChecksum(hrp, data, Bech32Encodings.bech32)); + final hrp = decoded.item1; + final data = decoded.item2; + if (hrp != 'ltcmweb') { + throw ArgumentException('Invalid format (HRP not valid, expected ltcmweb, got $hrp)'); + } + if (data[0] != _BitcoinAddressUtils.segwitV0) { + throw const ArgumentException("Invalid segwit version"); + } + final convData = Bech32BaseUtils.convertFromBase32(data.sublist(1)); + if (convData.length != 66) { + throw ArgumentException( + 'Invalid format (witness program length not valid: ${convData.length})'); + } + + return MwebAddress.fromProgram(program: BytesUtils.toHexString(convData)); + } + + MwebAddress.fromProgram({required super.program}) + : super.fromProgram( + segwitVersion: _BitcoinAddressUtils.segwitV0, + addresType: SegwitAddressType.mweb, + ); + // MwebAddress.fromRedeemScript({required super.script}) + // : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV0); + + factory MwebAddress.fromScriptPubkey({required Script script, type = SegwitAddressType.mweb}) { + if (script.getAddressType() != SegwitAddressType.mweb) { + throw ArgumentError("Invalid scriptPubKey"); + } + return MwebAddress.fromProgram(program: BytesUtils.toHexString(script.script as List)); + } + + /// returns the scriptPubKey of a MWEB witness script + @override + Script toScriptPubKey() { + return Script(script: BytesUtils.fromHexString(addressProgram)); + } + + /// returns the type of address + @override + SegwitAddressType get type => SegwitAddressType.mweb; +} diff --git a/lib/src/bitcoin/address/util.dart b/lib/src/bitcoin/address/util.dart new file mode 100644 index 0000000..ad099e2 --- /dev/null +++ b/lib/src/bitcoin/address/util.dart @@ -0,0 +1,44 @@ +import 'package:bitcoin_base/src/utils/utils.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:bitcoin_base/src/bitcoin/address/address.dart'; +import 'package:bitcoin_base/src/models/network.dart'; + +class BitcoinAddressUtils { + static bool validateAddress({required String address, required BasedUtxoNetwork network}) { + try { + addressToOutputScript(address: address, network: network); + return true; + } catch (_) { + return false; + } + } + + static List addressToOutputScript( + {required String address, required BasedUtxoNetwork network}) { + final addressType = RegexUtils.addressTypeFromStr(address, network); + + if (addressType.type == SegwitAddressType.mweb) { + return BytesUtils.fromHexString( + MwebAddress.fromAddress(address: address, network: network).addressProgram, + ); + } + + return addressType.toScriptPubKey().toBytes(); + } + + static String scriptHash(String address, {required BasedUtxoNetwork network}) { + final outputScript = addressToOutputScript(address: address, network: network); + final parts = BytesUtils.toHexString(QuickCrypto.sha256Hash(outputScript)).split(''); + var res = ''; + + for (var i = parts.length - 1; i >= 0; i--) { + final char = parts[i]; + i--; + final nextChar = parts[i]; + res += nextChar; + res += char; + } + + return res; + } +} diff --git a/lib/src/bitcoin/address/utils/address_utils.dart b/lib/src/bitcoin/address/utils/address_utils.dart index a3df388..784b8c3 100644 --- a/lib/src/bitcoin/address/utils/address_utils.dart +++ b/lib/src/bitcoin/address/utils/address_utils.dart @@ -3,10 +3,10 @@ part of 'package:bitcoin_base/src/bitcoin/address/address.dart'; /// Utility class for working with Bitcoin addresses and related operations. class _BitcoinAddressUtils { /// Length of a script hash in bytes. - static const int scriptHashLenght = 32; + static const int scriptHashLenght = QuickCrypto.sha256DigestSize; /// Length of a hash160 digest in bytes. - static const int hash160DigestLength = 20; + static const int hash160DigestLength = QuickCrypto.hash160DigestSize; /// Segregated Witness version 0. static const int segwitV0 = 0; @@ -158,7 +158,7 @@ class _BitcoinAddressUtils { } baseAddress ??= toLegacy(address, network); if (baseAddress == null) { - throw const DartBitcoinPluginException('Invalid Bitcoin address'); + throw const DartBitcoinPluginException('Invalid Bitcoin address.'); } return validateAddress(baseAddress, network); } @@ -178,8 +178,7 @@ class _BitcoinAddressUtils { if (toBytes.length == addressType.hashLength) { return StringUtils.strip0x(hash160.toLowerCase()); } - // ignore: empty_catches - } catch (e) {} + } catch (_) {} throw const DartBitcoinPluginException( 'Invalid Bitcoin address program length (program length should be 32 or 20 bytes)'); } @@ -205,7 +204,7 @@ class _BitcoinAddressUtils { final version = decode.item1; return _validateBchScriptBytes( network: network, scriptBytes: scriptBytes, version: version); - } catch (e) { + } catch (_) { return null; } } @@ -293,7 +292,6 @@ class _BitcoinAddressUtils { final version = decode.item2; final addrBytes = decode.item1; final scriptHex = BytesUtils.toHexString(addrBytes); - switch (type) { case P2pkhAddressType.p2pkh: if (BytesUtils.bytesEqual(version, network.p2pkhNetVer)) { diff --git a/lib/src/bitcoin/script/op_code/constant.dart b/lib/src/bitcoin/script/op_code/constant.dart index d761839..59ac86e 100644 --- a/lib/src/bitcoin/script/op_code/constant.dart +++ b/lib/src/bitcoin/script/op_code/constant.dart @@ -127,210 +127,6 @@ class BitcoinOpCodeConst { byte == BitcoinOpCodeConst.opPushData4; } - // static 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], - // 'OP_SIZE': [0x82], - // 'OP_EQUAL': [0x87], - // 'OP_EQUALVERIFY': [0x88], - - // /// arithmetic - // 'OP_1ADD': [0x8b], - // 'OP_1SUB': [0x8c], - // 'OP_NEGATE': [0x8f], - // 'OP_ABS': [0x90], - // 'OP_NOT': [0x91], - // 'OP_0NOTEQUAL': [0x92], - // 'OP_ADD': [0x93], - // 'OP_SUB': [0x94], - // '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], - // "OP_CHECKSIGADD": [0xba], - - // /// locktime - // // 'OP_NOP2': [0xb1], - // 'OP_CHECKLOCKTIMEVERIFY': [0xb1], - // // 'OP_NOP3': [0xb2], - // 'OP_CHECKSEQUENCEVERIFY': [0xb2], - // }; - - // static final Map CODE_OPS = { - // /// constants - // 0: 'OP_0', - // // 0: 'OP_FALSE', - // 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', - // 0xba: "OP_CHECKSIGADD", - - // /// locktime - // // 177: 'OP_NOP2', - // // 178: 'OP_NOP3', - // 177: 'OP_CHECKLOCKTIMEVERIFY', - // 178: 'OP_CHECKSEQUENCEVERIFY', - // }; - static const int sighashSingle = 0x03; static const int sighashAnyoneCanPay = 0x80; static const int sighashAll = 0x01; @@ -369,7 +165,6 @@ class BitcoinOpCodeConst { static const int sighashByteLength = 4; static const String opReturn = "OP_RETURN"; static const String opTrue = "OP_TRUE"; - static const String opCheckSig = "OP_CHECKSIG"; static const String opCheckMultiSig = "OP_CHECKMULTISIG"; static const String opCheckMultiSigVerify = "OP_CHECKMULTISIGVERIFY"; static const String opCheckSigAdd = "OP_CHECKSIGADD"; diff --git a/lib/src/bitcoin/script/outpoint.dart b/lib/src/bitcoin/script/outpoint.dart new file mode 100644 index 0000000..8af5748 --- /dev/null +++ b/lib/src/bitcoin/script/outpoint.dart @@ -0,0 +1,18 @@ +import 'package:blockchain_utils/blockchain_utils.dart'; + +class Outpoint { + Outpoint({required this.txid, required this.index, this.value}); + + String txid; + int index; + int? value; + + factory Outpoint.fromBytes(List txid, int index, {int? value}) { + return Outpoint(txid: BytesUtils.toHexString(txid), index: index, value: value); + } + + @override + String toString() { + return 'Outpoint{txid: $txid, index: $index, value: $value}'; + } +} diff --git a/lib/src/bitcoin/script/output.dart b/lib/src/bitcoin/script/output.dart index 4654f56..fc172f2 100644 --- a/lib/src/bitcoin/script/output.dart +++ b/lib/src/bitcoin/script/output.dart @@ -18,31 +18,41 @@ class TxOutput { } // BitcoinOpCodeConst.negativeSatoshi const TxOutput._( - {required this.amount, required this.scriptPubKey, this.cashToken}); + {required this.amount, required this.scriptPubKey, this.cashToken, this.isSilentPayment = false, this.isChange = false}); factory TxOutput( {required BigInt amount, required Script scriptPubKey, - CashToken? cashToken}) { + CashToken? cashToken, + bool isSilentPayment = false, + bool isChange = false, + }) { try { return TxOutput._( - amount: amount.asUint64, + amount: amount.asInt64, scriptPubKey: scriptPubKey, - cashToken: cashToken); + cashToken: cashToken, + isSilentPayment: isSilentPayment, + isChange: isChange, + ); } catch (_) { - throw DartBitcoinPluginException( - "Invalid output amount: must be a non-negative 64-bit integer."); + throw DartBitcoinPluginException("Invalid output amount."); } } final CashToken? cashToken; final BigInt amount; final Script scriptPubKey; + final bool isSilentPayment; + final bool isChange; /// creates a copy of the object TxOutput clone() { return TxOutput( - amount: amount, - scriptPubKey: Script(script: List.from(scriptPubKey.script)), - cashToken: cashToken); + amount: amount, + scriptPubKey: Script(script: List.from(scriptPubKey.script)), + cashToken: cashToken, + isSilentPayment: isSilentPayment, + isChange: isChange, + ); } List toBytes() { diff --git a/lib/src/bitcoin/script/script.dart b/lib/src/bitcoin/script/script.dart index 56fed68..9b088fc 100644 --- a/lib/src/bitcoin/script/script.dart +++ b/lib/src/bitcoin/script/script.dart @@ -1,3 +1,4 @@ +import 'package:bitcoin_base/src/bitcoin/address/address.dart'; import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -127,6 +128,66 @@ class Script { return scriptBytes.toBytes(); } + dynamic findScriptParam(int index) { + if (index < script.length) { + return script[index]; + } + return null; + } + + BitcoinAddressType? getAddressType() { + if (script.isEmpty) return null; + + if (script.every((x) => x is int) && + script.length == 66 && + (script[0] == 2 || script[0] == 3) && + (script[33] == 2 || script[33] == 3)) { + return SegwitAddressType.mweb; + } + + final first = findScriptParam(0); + final sec = findScriptParam(1); + if (sec == null || sec is! String) { + return null; + } + + if (first == "OP_0") { + final lockingScriptBytes = opPushData(sec); + + if (lockingScriptBytes.length == 21) { + return SegwitAddressType.p2wpkh; + } else if (lockingScriptBytes.length == 33) { + return SegwitAddressType.p2wsh; + } + } else if (first == "OP_1") { + final lockingScriptBytes = opPushData(sec); + + if (lockingScriptBytes.length == 33) { + return SegwitAddressType.p2tr; + } + } + + final third = findScriptParam(2); + final fourth = findScriptParam(3); + final fifth = findScriptParam(4); + if (first == "OP_DUP") { + if (sec == "OP_HASH160" && + opPushData(third).length == 21 && + fourth == "OP_EQUALVERIFY" && + fifth == "OP_CHECKSIG") { + return P2pkhAddressType.p2pkh; + } + } else if (first == "OP_HASH160" && opPushData(sec).length == 21 && third == "OP_EQUAL") { + return P2shAddressType.p2pkhInP2sh; + } else if (sec == "OP_CHECKSIG") { + if (first.length == 66) { + return PubKeyAddressType.p2pk; + } + } + + return null; + } + List toBytes() { return _scriptBytes.clone(); } diff --git a/lib/src/bitcoin/script/scripts.dart b/lib/src/bitcoin/script/scripts.dart index ef82d93..710c963 100644 --- a/lib/src/bitcoin/script/scripts.dart +++ b/lib/src/bitcoin/script/scripts.dart @@ -6,3 +6,4 @@ export 'transaction.dart'; export 'witness.dart'; export 'utils.dart'; export 'op_code/constant.dart'; +export 'outpoint.dart'; diff --git a/lib/src/bitcoin/script/transaction.dart b/lib/src/bitcoin/script/transaction.dart index 849e8de..31abbf9 100644 --- a/lib/src/bitcoin/script/transaction.dart +++ b/lib/src/bitcoin/script/transaction.dart @@ -6,6 +6,7 @@ import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:blockchain_utils/helper/helper.dart'; import 'package:blockchain_utils/utils/utils.dart'; import 'package:blockchain_utils/crypto/quick_crypto.dart'; +import 'package:collection/collection.dart'; import 'input.dart'; import 'output.dart'; import 'script.dart'; @@ -23,6 +24,10 @@ class BtcTransaction { {List inputs = const [], List outputs = const [], List witnesses = const [], + this.hasSegwit = false, + this.canReplaceByFee = false, + this.mwebBytes, + this.hasSilentPayment = false, required List locktime, required List version}) : locktime = locktime.asImmutableBytes, @@ -35,7 +40,11 @@ class BtcTransaction { List outputs = const [], List witnesses = const [], List locktime = BitcoinOpCodeConst.defaultTxLocktime, - List version = BitcoinOpCodeConst.defaultTxVersion}) { + List version = BitcoinOpCodeConst.defaultTxVersion, + bool hasSegwit = false, + bool canReplaceByFee = false, + List? mwebBytes, + bool hasSilentPayment = false,}) { if (locktime.length != BitcoinOpCodeConst.locktimeLengthInBytes) { throw DartBitcoinPluginException( "Invalid locktime length: expected ${BitcoinOpCodeConst.locktimeLengthInBytes}, but got ${locktime.length}."); @@ -49,27 +58,40 @@ class BtcTransaction { outputs: outputs, witnesses: witnesses, version: version, + hasSegwit: hasSegwit, + canReplaceByFee: canReplaceByFee, + hasSilentPayment: hasSilentPayment, + mwebBytes: mwebBytes, locktime: locktime); } final List inputs; final List outputs; final List locktime; final List version; + final bool hasSegwit; + final bool canReplaceByFee; final List witnesses; + final List? mwebBytes; + final bool hasSilentPayment; BtcTransaction copyWith({ List? inputs, List? outputs, List? witnesses, + bool? hasSegwit, List? locktime, List? version, }) { return BtcTransaction( - inputs: inputs ?? this.inputs, - outputs: outputs ?? this.outputs, - witnesses: witnesses ?? this.witnesses, + inputs: inputs ?? this.inputs.map((e) => e.clone()).toList(), + outputs: outputs ?? this.outputs.map((e) => e.clone()).toList(), + witnesses: witnesses ?? this.witnesses.map((e) => e.clone()).toList(), + hasSegwit: hasSegwit ?? this.hasSegwit, + mwebBytes: mwebBytes, locktime: locktime ?? this.locktime, - version: version ?? this.version); + version: version ?? this.version, + hasSilentPayment: hasSilentPayment, + ); } /// creates a copy of the object (classmethod) @@ -79,27 +101,49 @@ class BtcTransaction { outputs: tx.outputs.map((e) => e.clone()).toList(), witnesses: tx.witnesses.map((e) => e.clone()).toList(), locktime: tx.locktime, + hasSegwit: tx.hasSegwit, + mwebBytes: tx.mwebBytes, version: tx.version); } + static BtcTransaction fromRaw(String raw) { + final txBytes = BytesUtils.fromHexString(raw); + return deserialize(txBytes); + } + /// Instantiates a Transaction from serialized raw hexadacimal data (classmethod) static BtcTransaction deserialize(List txBytes, {bool allowWitness = true}) { try { final version = txBytes.sublist(0, 4); int cursor = 4; + List? flag; bool hasWitness = false; - if (allowWitness && txBytes[4] == 0 && txBytes[5] == 1) { - hasWitness = true; + bool hasMweb = false; + if (txBytes[4] == 0) { + flag = List.from(txBytes.sublist(5, 6)); + if (allowWitness && (flag[0] & 1 > 0)) { + hasWitness = true; + } + if (flag[0] & 8 > 0) { + hasMweb = true; + } cursor += 2; } final vi = IntUtils.decodeVarint(txBytes.sublist(cursor)); cursor += vi.item2; + + bool canReplaceByFee = false; final List inputs = []; for (int index = 0; index < vi.item1; index++) { final inp = TxInput.deserialize(bytes: txBytes, cursor: cursor); inputs.add(inp.item1); cursor = inp.item2; + + if (canReplaceByFee == false) { + canReplaceByFee = + const ListEquality().equals(inp.item1.sequence, BitcoinOpCodeConst.replaceByFeeSequence); + } } final outputs = []; final viOut = IntUtils.decodeVarint(txBytes.sublist(cursor)); @@ -131,6 +175,12 @@ class BtcTransaction { } } } + List? mwebBytes; + if (hasMweb) { + mwebBytes = txBytes.sublist(cursor, txBytes.length - 4); + } + // TODO: should this be added + // cursor = rawtx.length - 4; List locktime = BitcoinOpCodeConst.defaultTxLocktime; if ((txBytes.length - cursor) >= 4) { locktime = txBytes.sublist(cursor, cursor + 4); @@ -143,6 +193,9 @@ class BtcTransaction { inputs: inputs, outputs: outputs, witnesses: witnesses, + hasSegwit: hasWitness, + canReplaceByFee: canReplaceByFee, + mwebBytes: mwebBytes, version: version, locktime: locktime); } catch (e) { @@ -205,8 +258,11 @@ class BtcTransaction { List toBytes({bool allowWitness = true}) { final data = DynamicByteTracker(); data.add(version); - if (allowWitness && witnesses.isNotEmpty) { - data.add([0x00, 0x01]); + var flag = 0; + if (allowWitness && witnesses.isNotEmpty) flag |= 1; + if (mwebBytes != null) flag |= 8; + if (flag > 0) { + data.add([0x00, flag]); } final txInCountBytes = IntUtils.encodeVarint(inputs.length); final txOutCountBytes = IntUtils.encodeVarint(outputs.length); @@ -225,6 +281,9 @@ class BtcTransaction { data.add(wit.toBytes()); } } + if (mwebBytes != null) { + data.add(mwebBytes!); + } data.add(locktime); return data.toBytes(); } diff --git a/lib/src/bitcoin/script/utils.dart b/lib/src/bitcoin/script/utils.dart index 19c673b..028c0b6 100644 --- a/lib/src/bitcoin/script/utils.dart +++ b/lib/src/bitcoin/script/utils.dart @@ -22,18 +22,29 @@ enum ScriptPubKeyType { this == ScriptPubKeyType.p2wsh || this == ScriptPubKeyType.p2tr; bool get isP2tr => this == ScriptPubKeyType.p2tr; - bool get isP2sh => this == ScriptPubKeyType.p2sh; + bool get isP2sh => isP2sh32 || this == ScriptPubKeyType.p2sh; bool get isP2sh32 => this == ScriptPubKeyType.p2sh32; } class BitcoinScriptUtils { + static Script buildOpReturn(List> data) { + return Script(script: [ + BitcoinOpcode.opReturn, + ...data.map((e) => BytesUtils.toHexString(e)) + ]); + } + static bool scriptContains( {required Script script, required List elements}) { if (elements.length != script.script.length) return false; for (int i = 0; i < script.script.length; i++) { final element = elements[i]; if (element != null) { - if (script.script[i] != element) { + if (element is BitcoinOpcode) { + if (script.script[i] != element.name) { + return false; + } + } else if (script.script[i] != element) { return false; } } @@ -43,11 +54,11 @@ class BitcoinScriptUtils { static bool isP2pkh(Script script) { if (scriptContains(script: script, elements: [ - 'OP_DUP', - 'OP_HASH160', + BitcoinOpcode.opDup, + BitcoinOpcode.opHash160, null, - 'OP_EQUALVERIFY', - 'OP_CHECKSIG' + BitcoinOpcode.opEqualVerify, + BitcoinOpcode.opCheckSig ])) { final addressProgram = script.script[2]; return (addressProgram is String && addressProgram.length == 40); @@ -57,7 +68,8 @@ class BitcoinScriptUtils { static bool isP2sh(Script script) { if (scriptContains( - script: script, elements: ['OP_HASH160', null, 'OP_EQUAL'])) { + script: script, + elements: [BitcoinOpcode.opHash160, null, BitcoinOpcode.opEqual])) { final pubKeyBytes = BytesUtils.tryFromHexString(script.script[1]); return pubKeyBytes?.length == P2shAddressType.p2pkInP2sh.hashLength; } @@ -66,7 +78,8 @@ class BitcoinScriptUtils { static bool isP2sh32(Script script) { if (scriptContains( - script: script, elements: ['OP_HASH256', null, 'OP_EQUAL'])) { + script: script, + elements: [BitcoinOpcode.opHash256, null, BitcoinOpcode.opEqual])) { final pubKeyBytes = BytesUtils.tryFromHexString(script.script[1]); return pubKeyBytes?.length == P2shAddressType.p2pkhInP2sh32.hashLength; } @@ -74,7 +87,8 @@ class BitcoinScriptUtils { } static bool isP2pk(Script script) { - if (scriptContains(script: script, elements: [null, "OP_CHECKSIG"])) { + if (scriptContains( + script: script, elements: [null, BitcoinOpcode.opCheckSig])) { final pubKeyBytes = BytesUtils.tryFromHexString(script.script[0]); return pubKeyBytes?.length == EcdsaKeysConst.pubKeyCompressedByteLen || pubKeyBytes?.length == EcdsaKeysConst.pubKeyUncompressedByteLen; @@ -83,7 +97,7 @@ class BitcoinScriptUtils { } static bool isP2tr(Script script) { - if (scriptContains(script: script, elements: ['OP_1', null])) { + if (scriptContains(script: script, elements: [BitcoinOpcode.op1, null])) { final pubKeyBytes = BytesUtils.tryFromHexString(script.script[1]); return pubKeyBytes?.length == SegwitAddressType.p2tr.hashLength; } @@ -91,7 +105,7 @@ class BitcoinScriptUtils { } static bool isP2wpkh(Script script) { - if (scriptContains(script: script, elements: ['OP_0', null])) { + if (scriptContains(script: script, elements: [BitcoinOpcode.op0, null])) { final pubKeyBytes = BytesUtils.tryFromHexString(script.script[1]); return pubKeyBytes?.length == SegwitAddressType.p2wpkh.hashLength; } @@ -100,7 +114,8 @@ class BitcoinScriptUtils { static bool isRipemd160(Script script) { if (scriptContains( - script: script, elements: ['OP_RIPEMD160', null, "OP_EQUAL"])) { + script: script, + elements: [BitcoinOpcode.opRipemd160, null, BitcoinOpcode.opEqual])) { final toBytes = BytesUtils.tryFromHexString(script.script[1]); return toBytes?.length == QuickCrypto.hash160DigestSize; } @@ -109,7 +124,8 @@ class BitcoinScriptUtils { static bool isSha256(Script script) { if (scriptContains( - script: script, elements: ['OP_SHA256', null, "OP_EQUAL"])) { + script: script, + elements: [BitcoinOpcode.opSha256, null, BitcoinOpcode.opEqual])) { final toBytes = BytesUtils.tryFromHexString(script.script[1]); return toBytes?.length == QuickCrypto.sha256DigestSize; } @@ -118,7 +134,8 @@ class BitcoinScriptUtils { static bool isHash256(Script script) { if (scriptContains( - script: script, elements: ['OP_HASH256', null, "OP_EQUAL"])) { + script: script, + elements: [BitcoinOpcode.opHash256, null, BitcoinOpcode.opEqual])) { final toBytes = BytesUtils.tryFromHexString(script.script[1]); return toBytes?.length == QuickCrypto.sha256DigestSize; } @@ -127,7 +144,8 @@ class BitcoinScriptUtils { static bool isHash160(Script script) { if (scriptContains( - script: script, elements: ['OP_HASH160', null, "OP_EQUAL"])) { + script: script, + elements: [BitcoinOpcode.opHash160, null, BitcoinOpcode.opEqual])) { final toBytes = BytesUtils.tryFromHexString(script.script[1]); return toBytes?.length == QuickCrypto.hash160DigestSize; } @@ -135,7 +153,7 @@ class BitcoinScriptUtils { } static bool isP2wsh(Script script) { - if (scriptContains(script: script, elements: ['OP_0', null])) { + if (scriptContains(script: script, elements: [BitcoinOpcode.op0, null])) { final pubKeyBytes = BytesUtils.tryFromHexString(script.script[1]); return pubKeyBytes?.length == SegwitAddressType.p2wsh.hashLength; } @@ -143,7 +161,8 @@ class BitcoinScriptUtils { } static bool isXOnlyOpChecksig(Script script) { - if (scriptContains(script: script, elements: [null, "OP_CHECKSIG"])) { + if (scriptContains( + script: script, elements: [null, BitcoinOpcode.opCheckSig])) { final xOnlyKey = BytesUtils.tryFromHexString(script.script[0]); return xOnlyKey?.length == EcdsaKeysConst.pointCoordByteLen; } @@ -181,7 +200,8 @@ class BitcoinScriptUtils { } static bool isPubKeyOpCheckSig(Script script) { - if (scriptContains(script: script, elements: [null, "OP_CHECKSIG"])) { + if (scriptContains( + script: script, elements: [null, BitcoinOpcode.opCheckSig])) { final pubKeyBytes = BytesUtils.tryFromHexString(script.script[0]); return pubKeyBytes?.length == EcdsaKeysConst.pubKeyCompressedByteLen || pubKeyBytes?.length == EcdsaKeysConst.pubKeyUncompressedByteLen; @@ -225,8 +245,8 @@ class BitcoinScriptUtils { static bool isMultisigScript(Script script) { final opCodes = script.script; if (opCodes.length < 4) return false; - if (opCodes.last != 'OP_CHECKMULTISIG' && - opCodes.last != "OP_CHECKMULTISIGVERIFY") { + if (opCodes.last != BitcoinOpcode.opCheckMultiSig.name && + opCodes.last != BitcoinOpcode.opCheckMultiSigVerify.name) { return false; } final int? threshold = decodeOpN(opCodes.first.toString()); @@ -244,8 +264,8 @@ class BitcoinScriptUtils { static MultiSignatureAddress? parseMultisigScript(Script script) { final opCodes = script.script; if (opCodes.length < 4) return null; - if (opCodes.last != 'OP_CHECKMULTISIG' && - opCodes.last != "OP_CHECKMULTISIGVERIFY") { + if (opCodes.last != BitcoinOpcode.opCheckMultiSig.name && + opCodes.last != BitcoinOpcode.opCheckMultiSigVerify.name) { return null; } final int? threshold = decodeOpN(opCodes.first.toString()); diff --git a/lib/src/bitcoin/script/witness.dart b/lib/src/bitcoin/script/witness.dart index 1de84ba..9424b6d 100644 --- a/lib/src/bitcoin/script/witness.dart +++ b/lib/src/bitcoin/script/witness.dart @@ -1,12 +1,26 @@ +import 'dart:typed_data'; + import 'package:blockchain_utils/helper/extensions/extensions.dart'; import 'package:blockchain_utils/utils/utils.dart'; +class ScriptWitness { + List stack; + + ScriptWitness({List? stack}) : stack = stack ?? []; + + bool isNull() { + return stack.isEmpty; + } +} + /// A list of the witness items required to satisfy the locking conditions of a segwit input (aka witness stack). /// /// [stack] the witness items (hex str) list class TxWitnessInput { - TxWitnessInput({required List stack}) : stack = stack.immutable; + TxWitnessInput({required List stack, ScriptWitness? scriptWitness}) : stack = stack.immutable, + scriptWitness = scriptWitness ?? ScriptWitness(); final List stack; + ScriptWitness scriptWitness; /// creates a copy of the object (classmethod) TxWitnessInput clone() { diff --git a/lib/src/bitcoin/silent_payments/address.dart b/lib/src/bitcoin/silent_payments/address.dart new file mode 100644 index 0000000..9dd7d00 --- /dev/null +++ b/lib/src/bitcoin/silent_payments/address.dart @@ -0,0 +1,201 @@ +// ignore_for_file: constant_identifier_names +// ignore_for_file: non_constant_identifier_names +part of 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; + +const SCAN_PATH = "m/352'/1'/0'/1'/0"; + +const SPEND_PATH = "m/352'/1'/0'/0'/0"; + +class SilentPaymentOwner extends SilentPaymentAddress { + final ECPrivate b_scan; + final ECPrivate b_spend; + + SilentPaymentOwner({ + required super.version, + required super.B_scan, + required super.B_spend, + required this.b_scan, + required this.b_spend, + super.network, + }) : super(); + + factory SilentPaymentOwner.fromPrivateKeys({ + required ECPrivate b_scan, + required ECPrivate b_spend, + required BasedUtxoNetwork network, + int? version, + }) { + return SilentPaymentOwner( + b_scan: b_scan, + b_spend: b_spend, + B_scan: b_scan.getPublic(), + B_spend: b_spend.getPublic(), + network: network, + version: version ?? 0, + ); + } + + factory SilentPaymentOwner.fromHd(Bip32Slip10Secp256k1 bip32, {String? hrp, int? version}) { + final scanDerivation = bip32.derivePath(SCAN_PATH); + final spendDerivation = bip32.derivePath(SPEND_PATH); + + return SilentPaymentOwner( + b_scan: ECPrivate(scanDerivation.privateKey), + b_spend: ECPrivate(spendDerivation.privateKey), + B_scan: ECPublic.fromBip32(scanDerivation.publicKey), + B_spend: ECPublic.fromBip32(spendDerivation.publicKey), + network: hrp == "tsp" ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet, + version: version ?? 0, + ); + } + + factory SilentPaymentOwner.fromMnemonic(String mnemonic, {String? hrp, int? version}) { + return SilentPaymentOwner.fromHd( + Bip32Slip10Secp256k1.fromSeed( + Bip39MnemonicDecoder().decode(mnemonic), + hrp == "tsp" ? Bip32Const.testNetKeyNetVersions : Bip32Const.mainNetKeyNetVersions, + ), + hrp: hrp, + version: version); + } + + List generateLabel(int m) { + return taggedHash(BytesUtils.concatBytes([b_scan.toBytes(), serUint32(m)]), "BIP0352/Label"); + } + + SilentPaymentOwner toLabeledSilentPaymentAddress(int m) { + final B_m = B_spend.tweakAdd(BigintUtils.fromBytes(generateLabel(m))); + return SilentPaymentOwner( + b_scan: b_scan, + b_spend: b_spend, + B_scan: B_scan, + B_spend: B_m, + network: network, + version: version, + ); + } +} + +class SilentPaymentDestination extends SilentPaymentAddress { + SilentPaymentDestination({ + required super.version, + required ECPublic scanPubkey, + required ECPublic spendPubkey, + super.network, + required this.amount, + }) : super(B_scan: scanPubkey, B_spend: spendPubkey); + + int amount; + + factory SilentPaymentDestination.fromAddress(String address, int amount) { + final receiver = SilentPaymentAddress.fromAddress(address); + + return SilentPaymentDestination( + scanPubkey: receiver.B_scan, + spendPubkey: receiver.B_spend, + network: receiver.network, + version: receiver.version, + amount: amount, + ); + } +} + +class SilentPaymentAddress implements BitcoinBaseAddress { + static RegExp get regex => RegExp(r'(tsp|sp|sprt)1[0-9a-zA-Z]{113}'); + + final int version; + final ECPublic B_scan; + final ECPublic B_spend; + BasedUtxoNetwork? network; + final String hrp; + + SilentPaymentAddress({ + required this.B_scan, + required this.B_spend, + this.network = BitcoinNetwork.mainnet, + this.version = 0, + }) : hrp = (network == BitcoinNetwork.testnet ? "tsp" : "sp") { + if (version != 0) { + throw Exception("Can't have other version than 0 for now"); + } + } + + factory SilentPaymentAddress.fromAddress(String address) { + final decoded = Bech32DecoderBase.decodeBech32( + address, + SegwitBech32Const.separator, + SegwitBech32Const.checksumStrLen, + (hrp, data) => Bech32Utils.verifyChecksum(hrp, data, Bech32Encodings.bech32m), + ); + final prefix = decoded.item1; + final words = decoded.item2; + + if (prefix != 'sp' && prefix != 'sprt' && prefix != 'tsp') { + throw Exception('Invalid prefix: $prefix'); + } + + final version = words[0]; + if (version != 0) throw ArgumentError('Invalid version'); + + final key = Bech32BaseUtils.convertFromBase32(words.sublist(1)); + + return SilentPaymentAddress( + B_scan: ECPublic.fromBytes(key.sublist(0, 33)), + B_spend: ECPublic.fromBytes(key.sublist(33)), + network: prefix == 'tsp' ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet, + version: version, + ); + } + + @override + String toAddress([BasedUtxoNetwork? network]) { + return toString(network: network); + } + + @override + String toString({BasedUtxoNetwork? network}) { + return Bech32EncoderBase.encodeBech32( + hrp, + [ + version, + ...Bech32BaseUtils.convertToBase32( + [...B_scan.toCompressedBytes(), ...B_spend.toCompressedBytes()]) + ], + SegwitBech32Const.separator, + (hrp, data) => Bech32Utils.computeChecksum(hrp, data, Bech32Encodings.bech32m), + ); + } + + @override + BitcoinAddressType get type => SilentPaymentsAddresType.p2sp; + + @override + Script toScriptPubKey() { + throw UnimplementedError(); + } + + @override + String pubKeyHash() { + throw UnimplementedError(); + } + + @override + String get addressProgram => ""; +} + +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/bitcoin/silent_payments/payment.dart b/lib/src/bitcoin/silent_payments/payment.dart new file mode 100644 index 0000000..c04cf12 --- /dev/null +++ b/lib/src/bitcoin/silent_payments/payment.dart @@ -0,0 +1,242 @@ +// ignore_for_file: non_constant_identifier_names +part of 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; + +class SilentPaymentOutput { + final P2trAddress address; + final int amount; + + SilentPaymentOutput(this.address, this.amount); +} + +class SilentPaymentScanningOutput { + final SilentPaymentOutput output; + final String tweak; + final String? label; + + SilentPaymentScanningOutput({required this.output, required this.tweak, this.label}); +} + +class ECPrivateInfo { + final ECPrivate privkey; + final bool isTaproot; + final bool tweak; + + ECPrivateInfo(this.privkey, this.isTaproot, {this.tweak = false}); +} + +class SilentPaymentBuilder { + final List vinOutpoints; + final List? pubkeys; + ECPublic? A_sum; + List? inputHash; + String? receiverTweak; + + SilentPaymentBuilder({ + required this.vinOutpoints, + this.pubkeys, + this.receiverTweak, + }) { + if (receiverTweak == null && pubkeys != null) { + _getAsum(); + _getInputHash(); + } + } + + void _getAsum() { + final head = pubkeys!.first; + final tail = pubkeys!.sublist(1); + + A_sum = + tail.fold(head, (acc, item) => ECPublic.fromBip32(acc.publicKey).pubkeyAdd(item)); + } + + void _getInputHash() { + final sortedOutpoints = >[]; + + for (final outpoint in vinOutpoints) { + sortedOutpoints.add(BytesUtils.concatBytes([ + BytesUtils.fromHexString(outpoint.txid).reversed.toList(), + BigintUtils.toBytes(BigInt.from(outpoint.index), length: 4, order: Endian.little) + ])); + } + + sortedOutpoints.sort(BytesUtils.compareBytes); + final lowestOutpoint = sortedOutpoints.first; + + inputHash = taggedHash( + BytesUtils.concatBytes([lowestOutpoint, A_sum!.toCompressedBytes()]), "BIP0352/Inputs"); + } + + Map> createOutputs( + List inputPrivKeyInfos, + List silentPaymentDestinations, + ) { + ECPrivate? a_sum; + + for (final info in inputPrivKeyInfos) { + var k = info.privkey; + final isTaproot = info.isTaproot; + + if (isTaproot) { + if (info.tweak) { + k = k.toTweakedTaprootKey(); + } + + final xOnlyPubkey = k.getPublic(); + final isOdd = xOnlyPubkey.publicKey.point.y % BigInt.two != BigInt.zero; + + if (isOdd) { + k = k.negate(); + } + } + + if (a_sum == null) { + a_sum = k; + } else { + a_sum = a_sum.tweakAdd(BigintUtils.fromBytes(k.toBytes())); + } + } + + A_sum = a_sum!.getPublic(); + _getInputHash(); + + Map>> silentPaymentGroups = {}; + + for (final silentPaymentDestination in silentPaymentDestinations) { + final B_scan = silentPaymentDestination.B_scan; + final scanPubkey = B_scan.toHex(); + + if (silentPaymentGroups.containsKey(scanPubkey)) { + // Current key already in silentPaymentGroups, simply add up the new destination + // with the already calculated ecdhSharedSecret + final group = silentPaymentGroups[scanPubkey]!; + final ecdhSharedSecret = group.keys.first; + final recipients = group.values.first; + + silentPaymentGroups[scanPubkey] = { + ecdhSharedSecret: [...recipients, silentPaymentDestination] + }; + } else { + final senderPartialSecret = a_sum.tweakMul(BigintUtils.fromBytes(inputHash!)).toBytes(); + final ecdhSharedSecret = + B_scan.tweakMul(BigintUtils.fromBytes(senderPartialSecret)).toHex(); + + silentPaymentGroups[scanPubkey] = { + ecdhSharedSecret: [silentPaymentDestination] + }; + } + } + + Map> result = {}; + for (final group in silentPaymentGroups.entries) { + final ecdhSharedSecret = group.value.keys.first; + final destinations = group.value.values.first; + + int k = 0; + for (final destination in destinations) { + final t_k = taggedHash( + BytesUtils.concatBytes([ + ECPublic.fromHex(ecdhSharedSecret).toCompressedBytes(), + BigintUtils.toBytes(BigInt.from(k), length: 4) + ]), + "BIP0352/SharedSecret"); + + final P_mn = destination.B_spend.tweakAdd(BigintUtils.fromBytes(t_k)); + final resOutput = + SilentPaymentOutput(P_mn.toTaprootAddress(tweak: false), destination.amount); + + if (result.containsKey(destination.toString())) { + result[destination.toString()]!.add(resOutput); + } else { + result[destination.toString()] = [resOutput]; + } + + k++; + } + } + + return result; + } + + Map scanOutputs( + ECPrivate b_scan, + ECPublic B_spend, + List outputsToCheck, { + Map? precomputedLabels, + }) { + final tweakDataForRecipient = receiverTweak != null + ? ECPublic.fromHex(receiverTweak!) + : A_sum!.tweakMul(BigintUtils.fromBytes(inputHash!)); + final ecdhSharedSecret = tweakDataForRecipient.tweakMul(b_scan.toBigInt()); + + final matches = {}; + var k = 0; + + do { + final t_k = taggedHash( + BytesUtils.concatBytes([ + ecdhSharedSecret.toCompressedBytes(), + BigintUtils.toBytes(BigInt.from(k), length: 4, order: Endian.big) + ]), + "BIP0352/SharedSecret"); + + final P_k = B_spend.tweakAdd(BigintUtils.fromBytes(t_k)); + final length = outputsToCheck.length; + + for (var i = 0; i < length; i++) { + final output = outputsToCheck[i].script.toBytes().sublist(2); + final outputPubkey = BytesUtils.toHexString(output); + final outputAmount = outputsToCheck[i].value.toInt(); + + if ((BytesUtils.compareBytes(output, P_k.toCompressedBytes().sublist(1)) == 0)) { + matches[outputPubkey] = SilentPaymentScanningOutput( + output: SilentPaymentOutput(P_k.toTaprootAddress(tweak: false), outputAmount), + tweak: BytesUtils.toHexString(t_k), + ); + outputsToCheck.removeAt(i); + k++; + break; + } + + if (precomputedLabels != null && precomputedLabels.isNotEmpty) { + var m_G_sub = ECPublic.fromBytes(output).pubkeyAdd(P_k.negate()); + var m_G = precomputedLabels[m_G_sub.toHex()]; + + if (m_G == null) { + m_G_sub = ECPublic.fromBytes(output).negate().pubkeyAdd(P_k.negate()); + m_G = precomputedLabels[m_G_sub.toHex()]; + } + + if (m_G != null) { + final P_km = P_k.tweakAdd(BigintUtils.fromBytes(BytesUtils.fromHexString(m_G))); + + matches[outputPubkey] = SilentPaymentScanningOutput( + output: SilentPaymentOutput(P_km.toTaprootAddress(tweak: false), outputAmount), + tweak: ECPrivate.fromBytes(t_k) + .tweakAdd(BigintUtils.fromBytes(BytesUtils.fromHexString(m_G))) + .toHex(), + label: m_G, + ); + + outputsToCheck.removeAt(i); + k++; + break; + } + } + + outputsToCheck.removeAt(i); + + if (i + 1 >= outputsToCheck.length) { + break; + } + } + } while (outputsToCheck.isNotEmpty); + + return matches; + } +} + +BitcoinScriptOutput getScriptFromOutput(String pubkey, int amount) { + return BitcoinScriptOutput( + script: Script(script: [BitcoinOpCodeConst.OP_1, pubkey]), value: BigInt.from(amount)); +} diff --git a/lib/src/bitcoin/silent_payments/silent_payments.dart b/lib/src/bitcoin/silent_payments/silent_payments.dart new file mode 100644 index 0000000..d839c6b --- /dev/null +++ b/lib/src/bitcoin/silent_payments/silent_payments.dart @@ -0,0 +1,23 @@ +// Library for Bitcoin Silent Payments handling in the bitcoin_base package. +// +// The library includes essential components such as: +// - Core address functionality. +// - encode/decode address support. +// - Utility functions for address manipulation. +// - Generate labeled addresses. +// - Scan transactions. +// - Generate payment outputs. +library bitcoin_base.silent_payments; + +import 'dart:typed_data'; + +import 'package:bitcoin_base/src/bitcoin/address/address.dart'; +import 'package:bitcoin_base/src/provider/models/models.dart'; +import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +import 'package:bitcoin_base/src/crypto/crypto.dart'; +import 'package:bitcoin_base/src/models/network.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; + +part 'address.dart'; +part 'payment.dart'; +part 'utils.dart'; diff --git a/lib/src/bitcoin/silent_payments/utils.dart b/lib/src/bitcoin/silent_payments/utils.dart new file mode 100644 index 0000000..c43f0bf --- /dev/null +++ b/lib/src/bitcoin/silent_payments/utils.dart @@ -0,0 +1,112 @@ +// ignore_for_file: non_constant_identifier_names +part of 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; + +final NUMS_H = BigInt.parse("0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"); + +int deserCompactSize(ByteData f) { + final view = f.buffer; + int nbytes = view.lengthInBytes; + if (nbytes == 0) { + return 0; // end of stream + } + + int nit = f.getUint8(0); + if (nit == 253) { + nit = f.getUint16(1, Endian.little); + } else if (nit == 254) { + nit = f.getUint32(3, Endian.little); + } else if (nit == 255) { + nit = f.getUint64(7, Endian.little); + } + return nit; +} + +ByteData deserString(ByteData f) { + final nit = deserCompactSize(f); + int offset = 1; + return ByteData.sublistView(f.buffer.asUint8List().sublist(offset, nit + offset)); +} + +List deserStringVector(ByteData f) { + int offset = 0; + + final nit = deserCompactSize(f); + offset += 1; + + List result = []; + for (int i = 0; i < nit; i++) { + final t = deserString(ByteData.sublistView(f.buffer.asUint8List().sublist(offset))); + + result.add(t); + offset += t.lengthInBytes + 1; + } + return result; +} + +class VinInfo { + final Outpoint outpoint; + final List scriptSig; + final TxWitnessInput txinwitness; + final Script prevOutScript; + final ECPrivate? privkey; + + VinInfo({ + required this.outpoint, + required this.scriptSig, + required this.txinwitness, + required this.prevOutScript, + this.privkey, + }); +} + +ECPublic? getPubkeyFromInput(VinInfo vin) { + switch (vin.prevOutScript.getAddressType()) { + case P2pkhAddressType.p2pkh: + for (var i = vin.scriptSig.length; i > 0; i--) { + if (i - 33 >= 0) { + final pubkeyBytes = vin.scriptSig.sublist(i - 33, i); + final pubkeyHash = BytesUtils.toHexString(QuickCrypto.hash160(pubkeyBytes)); + if (pubkeyHash == + P2pkhAddress.fromScript(script: vin.prevOutScript).addressProgram) { + return ECPublic.fromBytes(pubkeyBytes); + } + } + } + break; + case P2shAddressType.p2pkhInP2sh: + final redeemScript = vin.scriptSig.sublist(1); + if (Script.deserialize(bytes: redeemScript).getAddressType() == SegwitAddressType.p2wpkh) { + return ECPublic.fromBytes(vin.txinwitness.scriptWitness.stack.last.buffer.asUint8List()); + } + break; + case SegwitAddressType.p2wpkh: + return ECPublic.fromBytes(vin.txinwitness.scriptWitness.stack.last.buffer.asUint8List()); + case SegwitAddressType.p2tr: + final witnessStack = vin.txinwitness.scriptWitness.stack; + if (witnessStack.isNotEmpty) { + if (witnessStack.length > 1 && witnessStack.last.buffer.asUint8List()[0] == 0x50) { + witnessStack.removeLast(); + } + + if (witnessStack.length > 1) { + final controlBlock = witnessStack.last.buffer.asUint8List(); + final internalKey = controlBlock.sublist(1, 33); + if (BytesUtils.compareBytes( + internalKey, BigintUtils.toBytes(NUMS_H, length: 32, order: Endian.big)) == + 0) { + return null; + } + } + return ECPublic.fromBytes(vin.prevOutScript.toBytes().sublist(2)); + } + break; + default: + return null; + } + + return null; +} + +List serUint32(int n) { + return BigintUtils.toBytes(BigInt.from(n), length: 4); +} diff --git a/lib/src/bitcoin/taproot/utils/utils.dart b/lib/src/bitcoin/taproot/utils/utils.dart index 2e05424..e49cc29 100644 --- a/lib/src/bitcoin/taproot/utils/utils.dart +++ b/lib/src/bitcoin/taproot/utils/utils.dart @@ -73,7 +73,6 @@ class TaprootUtils { if (xKey.length == EcdsaKeysConst.pubKeyCompressedByteLen) { xKey = xKey.sublist(1); } - final tweak = calculateTweek(xKey, treeScript: treeScript, merkleRoot: merkleRoot); return tweakInternalKey(xKey, tweak); diff --git a/lib/src/bitcoin_cash/bcmr.dart b/lib/src/bitcoin_cash/bcmr.dart index 9d97032..9899000 100644 --- a/lib/src/bitcoin_cash/bcmr.dart +++ b/lib/src/bitcoin_cash/bcmr.dart @@ -1,4 +1,6 @@ -import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +import 'package:bitcoin_base/src/cash_token/cash_token.dart'; +import 'package:bitcoin_base/src/provider/models/utxo_details.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; /// Bitcoin Cash Metadata Registries Script to convert uris and content hash to bitcoin output script @@ -42,4 +44,7 @@ class BCMR implements BitcoinScriptOutput { /// output value. always zero @override final BigInt value = BigInt.zero; + + @override + CashToken? get token => null; } diff --git a/lib/src/crypto/keypair/ec_private.dart b/lib/src/crypto/keypair/ec_private.dart index aaa506b..0abdb0e 100644 --- a/lib/src/crypto/keypair/ec_private.dart +++ b/lib/src/crypto/keypair/ec_private.dart @@ -6,6 +6,35 @@ import 'package:blockchain_utils/blockchain_utils.dart'; import 'ec_public.dart'; +enum BIP137Mode { + p2pkhUncompressed(0), + p2pkhCompressed(4), + p2shP2wpkh(8), + p2wpkh(12); + + const BIP137Mode(this.header); + static BIP137Mode fromValue(int? header) { + return values.firstWhere((e) => e.header == header, + orElse: () => throw DartBitcoinPluginException( + "No BIP137Mode found for the given header value")); + } + + final int header; + static BIP137Mode findMode(int header) { + if (header < 27 || header > 42) { + throw DartBitcoinPluginException("Header byte out of range"); + } + if (header >= 39) { + return BIP137Mode.p2wpkh; + } else if (header >= 35) { + return BIP137Mode.p2shP2wpkh; + } else if (header >= 31) { + return BIP137Mode.p2pkhCompressed; + } + return BIP137Mode.p2pkhUncompressed; + } +} + /// Represents an ECDSA private key. class ECPrivate { final Bip32PrivateKey prive; @@ -28,7 +57,7 @@ class ECPrivate { ECPublic.fromHex(BytesUtils.toHexString(prive.publicKey.compressed)); /// creates an object from a WIF of WIFC format (string) - factory ECPrivate.fromWif(String wif, {required List? netVersion}) { + factory ECPrivate.fromWif(String wif, {List? netVersion}) { final decode = WifDecoder.decode(wif, netVer: netVersion ?? BitcoinNetwork.mainnet.wifNetVer); return ECPrivate.fromBytes(decode.item1); @@ -47,34 +76,93 @@ class ECPrivate { return prive.raw; } + BigInt toBigInt() { + return BigintUtils.fromBytes(prive.raw); + } + + /// returns the key's as hex String toHex() { return BytesUtils.toHexString(prive.raw); } - /// Returns a Bitcoin compact signature in hex + /// Signs a message using BIP-137 format for standardized Bitcoin message signing. + /// + /// This method produces a compact ECDSA signature with a modified recovery ID + /// based on the specified BIP-137 signing mode. + /// + /// - [message]: The raw message to be signed. + /// - [messagePrefix]: The prefix used for Bitcoin's message signing + /// (default is `BitcoinSignerUtils.signMessagePrefix`). + /// - [mode]: The BIP-137 mode specifying the key type (e.g., P2PKH uncompressed, compressed, SegWit, etc.). + /// - [extraEntropy]: Optional extra entropy to modify the signature (default is an empty list). + /// + /// The recovery ID (first byte of the signature) is adjusted based on the + /// BIP-137 mode's header value. The final signature is encoded in Base64. + String signBip137( + List message, { + String messagePrefix = BitcoinSignerUtils.signMessagePrefix, + BIP137Mode mode = BIP137Mode.p2pkhUncompressed, + List extraEntropy = const [], + }) { + final btcSigner = BitcoinKeySigner.fromKeyBytes(toBytes()); + final signature = btcSigner.signMessageConst( + message: message, + messagePrefix: messagePrefix, + extraEntropy: extraEntropy); + int rId = signature[0] + mode.header; + return StringUtils.decode([rId, ...signature.sublist(1)], + type: StringEncoding.base64); + } + + /// Signs a message using Bitcoin's message signing format. + /// + /// This method produces a compact ECDSA signature for a given message, following + /// the Bitcoin Signed Message standard. + /// + /// - [message]: The raw message to be signed. + /// - [messagePrefix]: The prefix used for Bitcoin's message signing. + /// - [extraEntropy]: Optional extra entropy to modify the signature. String signMessage(List message, - {String messagePrefix = '\x18Bitcoin Signed Message:\n'}) { - final btcSigner = BitcoinSigner.fromKeyBytes(toBytes()); - final signature = btcSigner.signMessage(message, messagePrefix); - return BytesUtils.toHexString(signature); + {String messagePrefix = BitcoinSignerUtils.signMessagePrefix, + List extraEntropy = const []}) { + final btcSigner = BitcoinKeySigner.fromKeyBytes(toBytes()); + final signature = btcSigner.signMessageConst( + message: message, + messagePrefix: messagePrefix, + extraEntropy: extraEntropy); + return BytesUtils.toHexString(signature.sublist(1)); } - /// sign transaction digest and returns the signature. - String signInput(List txDigest, - {int? sigHash = BitcoinOpCodeConst.sighashAll}) { - final btcSigner = BitcoinSigner.fromKeyBytes(toBytes()); - List signature = btcSigner.signTransaction(txDigest); - if (sigHash != null) { - signature = [...signature, sigHash]; + /// Signs the given transaction digest using ECDSA (DER-encoded). + /// + /// - [txDigest]: The transaction digest (message) to sign. + /// - [sighash]: The sighash flag to append (default is SIGHASH_ALL). + String signECDSA(List txDigest, + {int? sighash = BitcoinOpCodeConst.sighashAll, + List extraEntropy = const []}) { + final btcSigner = BitcoinKeySigner.fromKeyBytes(toBytes()); + List signature = + btcSigner.signECDSADerConst(txDigest, extraEntropy: extraEntropy); + if (sighash != null) { + signature = [...signature, sighash]; } return BytesUtils.toHexString(signature); } + /// Signs the given transaction digest using Schnorr signature (old style). + /// + /// This method is primarily useful for networks like Bitcoin Cash (BCH) that + /// support Schnorr signatures in a legacy format. + /// In BCH OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY, will not be upgraded to allow Schnorr signatures. + /// https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/2019-05-15-schnorr.md + /// - [txDigest]: The transaction digest (message) to sign. + /// - [sighash]: The sighash flag to append (default is SIGHASH_DEFAULT). String signSchnorr(List txDigest, - {int sighash = BitcoinOpCodeConst.sighashDefault}) { - final btcSigner = BitcoinSigner.fromKeyBytes(toBytes()); - var signature = btcSigner.signSchnorrTransaction(txDigest, - tapScripts: [], tweak: false); + {int sighash = BitcoinOpCodeConst.sighashDefault, + List extraEntropy = const []}) { + final btcSigner = BitcoinKeySigner.fromKeyBytes(toBytes()); + var signature = + btcSigner.signSchnorrConst(txDigest, extraEntropy: extraEntropy); if (sighash != BitcoinOpCodeConst.sighashDefault) { signature = [...signature, sighash]; } @@ -88,21 +176,36 @@ class ECPrivate { /// - [treeScript]: Taproot script tree for Tweaking with public key. /// - [merkleRoot]: Merkle root for the Taproot tree. If provided, this overrides the default computation of the Merkle root from [treeScript]. /// - [tweak]: If `true`, the internal key is tweaked, either with or without [treeScript] or [merkleRoot], before signing. - String signTapRoot(List txDigest, + /// - [tapTweakHash]: If provided, it will be used directly instead of tweaking with the internal key. + String signBip340(List txDigest, {int sighash = BitcoinOpCodeConst.sighashDefault, TaprootTree? treeScript, List? merkleRoot, + List? tapTweakHash, + List? aux, bool tweak = true}) { - if (!tweak && treeScript != null) { + if (!tweak && + (treeScript != null || merkleRoot != null || tapTweakHash != null)) { + throw DartBitcoinPluginException( + "Invalid parameters: 'tweak' must be true when specifying 'treeScript', 'merkleRoot', or 'tapTweakHash'."); + } + if (merkleRoot != null && treeScript != null) { throw DartBitcoinPluginException( - "Invalid parameters: 'tweak' must be true when using 'treeScript'."); + "Use either merkleRoot or treeScript to generate merkle, not both."); } - final btcSigner = BitcoinSigner.fromKeyBytes(toBytes()); - List signature = btcSigner.signSchnorrTx(txDigest, - tweak: tweak - ? TaprootUtils.calculateTweek(getPublic().toXOnly(), - treeScript: merkleRoot != null ? null : treeScript, - merkleRoot: merkleRoot) + if (tapTweakHash != null && (treeScript != null || merkleRoot != null)) { + throw DartBitcoinPluginException( + "Use either tapTweakHash or (treeScript/merkleRoot), not both."); + } + final btcSigner = BitcoinKeySigner.fromKeyBytes(toBytes()); + List signature = btcSigner.signBip340Const( + digest: txDigest, + aux: aux, + tapTweakHash: tweak + ? tapTweakHash ?? + TaprootUtils.calculateTweek(getPublic().toXOnly(), + treeScript: merkleRoot != null ? null : treeScript, + merkleRoot: merkleRoot) : null); if (sighash != BitcoinOpCodeConst.sighashDefault) { signature = [...signature, sighash]; @@ -110,18 +213,41 @@ class ECPrivate { return BytesUtils.toHexString(signature); } - /// Signs a Taproot transaction digest and returns the signature. - /// - /// - [txDigest]: The transaction digest to be signed. - /// - [tweak]: Optional public key tweak to be applied when signing. - List signBtcSchnorr(List txDigest, {List? tweak}) { - final btcSigner = BitcoinSigner.fromKeyBytes(toBytes()); - List signature = btcSigner.signSchnorrTx(txDigest, tweak: tweak); - return signature; + ECPrivate toTweakedTaprootKey() { + final t = P2TRUtils.calculateTweek(getPublic().publicKey.point); + + return ECPrivate.fromBytes( + BitcoinSignerUtils.calculatePrivateTweek(toBytes(), t)); } static ECPrivate random() { final secret = QuickCrypto.generateRandom(); return ECPrivate.fromBytes(secret); } + + ECPrivate tweakAdd(BigInt tweak) { + return ECPrivate.fromBytes(BigintUtils.toBytes( + (BigintUtils.fromBytes(prive.raw) + tweak) % Curves.generatorSecp256k1.order!, + length: getPublic().publicKey.point.curve.baselen, + )); + } + + ECPrivate tweakMul(BigInt tweak) { + return ECPrivate.fromBytes(BigintUtils.toBytes( + (BigintUtils.fromBytes(prive.raw) * tweak) % Curves.generatorSecp256k1.order!, + length: getPublic().publicKey.point.curve.baselen, + )); + } + + ECPrivate negate() { + // Negate the private key by subtracting from the order of the curve + return ECPrivate.fromBytes(BigintUtils.toBytes( + Curves.generatorSecp256k1.order! - BigintUtils.fromBytes(prive.raw), + length: getPublic().publicKey.point.curve.baselen, + )); + } + + ECPrivate clone() { + return ECPrivate.fromBytes(prive.raw); + } } diff --git a/lib/src/crypto/keypair/ec_public.dart b/lib/src/crypto/keypair/ec_public.dart index e2d3272..8e47aab 100644 --- a/lib/src/crypto/keypair/ec_public.dart +++ b/lib/src/crypto/keypair/ec_public.dart @@ -1,13 +1,16 @@ +import 'package:bitcoin_base/bitcoin_base.dart' show BIP137Mode; import 'package:bitcoin_base/src/bitcoin/address/address.dart'; +import 'package:bitcoin_base/src/bitcoin/script/op_code/constant.dart'; import 'package:bitcoin_base/src/bitcoin/script/script.dart'; import 'package:bitcoin_base/src/bitcoin/taproot/taproot.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:blockchain_utils/crypto/crypto/cdsa/point/base.dart'; typedef PublicKeyType = PubKeyModes; class ECPublic { - final Secp256k1PublicKeyEcdsa publicKey; + final Secp256k1PublicKey publicKey; const ECPublic._(this.publicKey); factory ECPublic.fromBip32(Bip32PublicKey publicKey) { @@ -15,13 +18,13 @@ class ECPublic { throw const DartBitcoinPluginException( 'invalid public key curve for bitcoin'); } - return ECPublic._(publicKey.pubKey as Secp256k1PublicKeyEcdsa); + return ECPublic._(publicKey.pubKey as Secp256k1PublicKey); } ProjectiveECCPoint get point => publicKey.point.cast(); /// Constructs an ECPublic key from a byte representation. factory ECPublic.fromBytes(List public) { - final publicKey = Secp256k1PublicKeyEcdsa.fromBytes(public); + final publicKey = Secp256k1PublicKey.fromBytes(public); return ECPublic._(publicKey); } @@ -75,14 +78,15 @@ class ECPublic { /// toRedeemScript generates a redeem script from the ECPublic key. /// If 'compressed' is true, the key is in compressed format. Script toRedeemScript({PublicKeyType mode = PublicKeyType.compressed}) { - return Script(script: [toHex(mode: mode), 'OP_CHECKSIG']); + return Script(script: [toHex(mode: mode), BitcoinOpcode.opCheckSig]); } /// 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( - {PublicKeyType mode = PublicKeyType.compressed, useBCHP2sh32 = false}) { + {PublicKeyType mode = PublicKeyType.compressed, + bool useBCHP2sh32 = false}) { final addr = toAddress(mode: mode); final script = addr.toScriptPubKey(); if (useBCHP2sh32) { @@ -134,7 +138,12 @@ class ECPublic { /// 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() { - return Script(script: ['OP_1', toHex(), 'OP_1', 'OP_CHECKMULTISIG']); + return Script(script: [ + BitcoinOpcode.op1, + toHex(), + BitcoinOpcode.op1, + BitcoinOpcode.opCheckMultiSig + ]); } /// toP2wshAddress generates a P2WSH (Pay-to-Witness-Script-Hash) address @@ -161,6 +170,15 @@ class ECPublic { } } + /// toCompressedBytes returns the compressed byte representation of the ECPublic key. + List toCompressedBytes() { + return publicKey.compressed; + } + + EncodeType? getEncodeType() { + return publicKey.point.encodeType; + } + /// returns the x coordinate only as hex string after tweaking (needed for taproot) String tweakPublicKey({TaprootTree? treeScript}) { final pubKey = TaprootUtils.tweakPublicKey( @@ -179,30 +197,168 @@ class ECPublic { return BytesUtils.toHexString(toXOnly()); } - /// returns true if the message was signed with this public key's - bool verify(List message, List signature, - {String messagePrefix = '\x18Bitcoin Signed Message:\n'}) { - final verifyKey = BitcoinVerifier.fromKeyBytes(toBytes()); - return verifyKey.verifyMessage(message, messagePrefix, signature); + /// Verifies a Bitcoin signed message using the provided signature. + /// + /// This method checks if the given signature is valid for the specified message, + /// following Bitcoin's message signing format. + /// + /// - [message]: The original message that was signed. + /// - [signature]: The compact ECDSA signature to verify. + /// - [messagePrefix]: The prefix used in Bitcoin's message signing. + bool verify( + {required List message, + required List signature, + String messagePrefix = BitcoinSignerUtils.signMessagePrefix}) { + final verifyKey = BitcoinSignatureVerifier.fromKeyBytes(toBytes()); + return verifyKey.verifyMessageSignature( + message: message, messagePrefix: messagePrefix, signature: signature); + } + + /// Recovers the BIP-137 public key from a signed message and signature. + /// + /// This method extracts the public key from a Bitcoin-signed message using the + /// BIP-137 standard, which allows for signature-based public key recovery. + /// + /// - [message]: The original message that was signed. + /// - [signature]: The Base64-encoded signature. + /// - [messagePrefix]: The prefix used in Bitcoin's message signing. + ECPublic getBip137PublicKey( + {required List message, + required String signature, + String messagePrefix = BitcoinSignerUtils.signMessagePrefix}) { + final signatureBytes = + StringUtils.encode(signature, type: StringEncoding.base64); + final ecdsaPubKey = BitcoinSignatureVerifier.recoverPublicKey( + message: message, + signature: signatureBytes, + messagePrefix: messagePrefix); + return ECPublic.fromBytes(ecdsaPubKey.toBytes()); } - /// returns true if the message was signed with this public key's - bool verifyTransaactionSignature(List message, List signature) { - final verifyKey = BitcoinVerifier.fromKeyBytes(toBytes()); - return verifyKey.verifyTransaction(message, signature); + /// Recovers the BIP-137 address from a signed message and signature. + /// + /// This method extracts the public key from a Bitcoin-signed message using the + /// BIP-137 standard, and then derives the appropriate Bitcoin address based on + /// the signature's recovery mode (e.g., P2PKH, P2WPKH, P2SH-P2WPKH). + /// + /// - [message]: The original message that was signed. + /// - [signature]: The Base64-encoded signature. + /// - [messagePrefix]: The prefix used in Bitcoin's message signing + /// (default is `BitcoinSignerUtils.signMessagePrefix`). + /// + /// Returns the corresponding Bitcoin address derived from the recovered public key. + /// The address type is determined by the recovery mode of the signature (e.g., + /// uncompressed, compressed, SegWit, or P2SH-wrapped SegWit). + BitcoinBaseAddress getBip137Address( + {required List message, + required String signature, + String messagePrefix = BitcoinSignerUtils.signMessagePrefix}) { + final signatureBytes = + StringUtils.encode(signature, type: StringEncoding.base64); + final ecdsaPubKey = BitcoinSignatureVerifier.recoverPublicKey( + message: message, + signature: signatureBytes, + messagePrefix: messagePrefix); + final publicKey = ECPublic.fromBytes(ecdsaPubKey.toBytes()); + final mode = BIP137Mode.findMode(signatureBytes[0]); + return switch (mode) { + BIP137Mode.p2pkhUncompressed => + publicKey.toAddress(mode: PubKeyModes.uncompressed), + BIP137Mode.p2pkhCompressed => publicKey.toAddress(), + BIP137Mode.p2wpkh => publicKey.toSegwitAddress(), + BIP137Mode.p2shP2wpkh => publicKey.toP2wpkhInP2sh() + }; } - /// returns true if the message was signed with this public key's - bool verifySchnorrTransactionSignature(List message, List signature, - {List> tapleafScripts = const [], bool isTweak = true}) { - final verifyKey = BitcoinVerifier.fromKeyBytes(toBytes()); - final tapScriptBytes = !isTweak - ? [] - : tapleafScripts - .map((e) => e.map((e) => e.toBytes()).toList()) - .toList(); - return verifyKey.verifySchnorr(message, signature, - tapleafScripts: tapScriptBytes, isTweak: isTweak); + /// Verifies that a BIP-137 signature matches the expected Bitcoin address. + /// + /// This method checks whether the address derived from the BIP-137 signature + /// matches the provided address by comparing the corresponding scriptPubKey. + /// + /// - [message]: The original message that was signed. + /// - [signature]: The Base64-encoded signature to verify. + /// - [address]: The expected Bitcoin address to compare against. + /// - [messagePrefix]: The prefix used in Bitcoin's message signing + bool verifyBip137Address( + {required List message, + required String signature, + required BitcoinBaseAddress address, + String messagePrefix = BitcoinSignerUtils.signMessagePrefix}) { + final signerAddress = getBip137Address( + message: message, signature: signature, messagePrefix: messagePrefix); + return address.toScriptPubKey() == signerAddress.toScriptPubKey(); + } + + /// Verifies an ECDSA DER-encoded signature against a given digest. + /// + /// This method checks whether the provided DER-encoded signature is valid for + /// the given digest using the public key. + /// + /// - [digest]: The hash or message digest that was signed. + /// - [signature]: The DER-encoded ECDSA signature to verify. + /// + /// Returns `true` if the signature is valid for the given digest, otherwise `false`. + bool verifyDerSignature( + {required List digest, required List signature}) { + final verifyKey = BitcoinSignatureVerifier.fromKeyBytes(toBytes()); + return verifyKey.verifyECDSADerSignature( + digest: digest, signature: signature); + } + + /// Verifies a BIP-340 Taproot signature for a given message. + /// + /// - [digest]: The original message or transaction digest that was signed. + /// - [signature]: The BIP-340 signature to verify. + /// - [treeScript]: Taproot script tree for Tweaking with public key. + /// - [merkleRoot]: Merkle root for the Taproot tree. If provided, this overrides the default computation of the Merkle root from [treeScript]. + /// - [tweak]: If `true`, the internal key is tweaked, either with or without [treeScript] or [merkleRoot], before verifying. + /// - [tapTweakHash]: If provided, it will be used directly instead of tweaking with the internal key. + bool verifyBip340Signature( + {required List digest, + required List signature, + TaprootTree? treeScript, + List? merkleRoot, + List? tapTweakHash, + bool tweak = true}) { + if (!tweak && + (treeScript != null || merkleRoot != null || tapTweakHash != null)) { + throw DartBitcoinPluginException( + "Invalid parameters: 'tweak' must be true when specifying 'treeScript', 'merkleRoot', or 'tapTweakHash'."); + } + if (merkleRoot != null && treeScript != null) { + throw DartBitcoinPluginException( + "Use either merkleRoot or treeScript to generate merkle, not both."); + } + if (tapTweakHash != null && (treeScript != null || merkleRoot != null)) { + throw DartBitcoinPluginException( + "Use either tapTweakHash or (treeScript/merkleRoot), not both."); + } + final verifyKey = BitcoinSignatureVerifier.fromKeyBytes(toBytes()); + return verifyKey.verifyBip340Signature( + digest: digest, + signature: signature, + tapTweakHash: tweak + ? tapTweakHash ?? + TaprootUtils.calculateTweek(toXOnly(), + treeScript: merkleRoot != null ? null : treeScript, + merkleRoot: merkleRoot) + : null); + } + + /// Verifies a Schnorr(old style) signature for a given digest. + /// + /// This method checks whether the provided Schnorr signature is valid for + /// the given digest using the public key. + /// + /// - [digest]: The hash or message digest that was signed. + /// - [signature]: The Schnorr signature to verify. + /// + /// Returns `true` if the signature is valid for the given digest, otherwise `false`. + bool verifySchnorrSignature( + {required List digest, required List signature}) { + final verifyKey = BitcoinSignatureVerifier.fromKeyBytes(toBytes()); + return verifyKey.verifySchnorrSignature( + digest: digest, signature: signature); } @override @@ -214,4 +370,38 @@ class ECPublic { @override int get hashCode => publicKey.hashCode; + + ECPublic tweakAdd(BigInt tweak) { + final point = publicKey.point; + // Compute the new public key after adding the tweak + final tweakedKey = point + (Curves.generatorSecp256k1 * tweak); + + return ECPublic.fromBytes(tweakedKey.toBytes()); + } + + // Perform the tweak multiplication + ECPublic tweakMul(BigInt tweak) { + final point = publicKey.point; + // Perform the tweak multiplication + final tweakedKey = point * tweak; + + return ECPublic.fromBytes(tweakedKey.toBytes()); + } + + ECPublic pubkeyAdd(ECPublic other) { + final tweakedKey = (publicKey.point) + other.publicKey.point; + return ECPublic.fromBytes(tweakedKey.toBytes()); + } + + ECPublic negate() { + // Negate the Y-coordinate by subtracting it from the field size (p). + final point = publicKey.point; + final y = point.curve.p - point.y; + return ECPublic.fromBytes(BytesUtils.fromHexString( + "04${BytesUtils.toHexString(BigintUtils.toBytes(point.x, length: point.curve.baselen))}${BytesUtils.toHexString(BigintUtils.toBytes(y, length: point.curve.baselen))}")); + } + + ECPublic clone() { + return ECPublic.fromBytes(publicKey.uncompressed); + } } diff --git a/lib/src/crypto/keypair/sign_utils.dart b/lib/src/crypto/keypair/sign_utils.dart new file mode 100644 index 0000000..0b3f226 --- /dev/null +++ b/lib/src/crypto/keypair/sign_utils.dart @@ -0,0 +1,190 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:pointycastle/export.dart'; +import 'package:pointycastle/src/utils.dart'; +import 'package:pointycastle/ecc/ecc_fp.dart' as fp; + +final ECDomainParameters curve = ECCurve_secp256k1(); + +extension ECUtils on Uint8List { + ECSignature toECSignature() { + final sigLength = (this.length / 2).round(); + final r = BigInt.parse( + SignUtils.getHexString(this, offset: 0, length: sigLength), + radix: 16, + ); + final s = BigInt.parse( + SignUtils.getHexString(this, offset: sigLength, length: sigLength), + radix: 16, + ); + return ECSignature(r, s); + } + + bool isCompressedPoint() => curve.curve.decodePoint(this)!.isCompressed; +} + +class SignUtils { + /// Returns the recovery ID, a byte with value between 0 and 3, inclusive, that specifies which of 4 possible + /// curve points was used to sign a message. This value is also referred to as "v". + /// + /// @throws RuntimeException if no recovery ID can be found. + static int findRecoveryId(String hash, ECSignature sig, Uint8List pub) { + var recId = -1; + final Q = curve.curve.decodePoint(pub); + for (var i = 0; i < 4; i++) { + final k = recoverFromSignature(i, sig, hash); + if (k != null && k == Q) { + recId = i; + break; + } + } + if (recId == -1) { + throw Exception("Could not construct a recoverable key. This should never happen."); + } + return recId; + } + + static String getHexString( + List list, { + required int offset, + required int length, + }) { + final sublist = list.getRange(offset, offset + length); + return [for (var byte in sublist) byte.toRadixString(16).padLeft(2, '0').toUpperCase()].join(); + } + + ///

Given the components of a signature and a selector value, recover and return the public key + /// that generated the signature according to the algorithm in SEC1v2 section 4.1.6.

+ /// + ///

The recId is an index from 0 to 3 which indicates which of the 4 possible keys is the correct one. Because + /// the key recovery operation yields multiple potential keys, the correct key must either be stored alongside the + /// signature, or you must be willing to try each recId in turn until you find one that outputs the key you are + /// expecting.

+ /// + ///

If this method returns null it means recovery was not possible and recId should be iterated.

+ /// + ///

Given the above two points, a correct usage of this method is inside a for loop from 0 to 3, and if the + /// output is null OR a key that is not the one you expect, you try again with the next recId.

+ /// + /// @param recId Which possible key to recover. + /// @param sig the R and S components of the signature, wrapped. + /// @param message Hash of the data that was signed. + /// @param compressed Whether or not the original pubkey was compressed. + /// @return An ECKey containing only the public part, or null if recovery wasn't possible. + static ECPoint? recoverFromSignature(int recId, ECSignature sig, String message) { + // see https://www.secg.org/sec1-v2.pdf, section 4.1.6 + // 1.0 For j from 0 to h (h == recId here and the loop is outside this function) + // 1.1 Let x = r + jn + final n = curve.n; // Curve order. + final i = BigInt.from(recId / 2); + final x = sig.r + (i * n); + // 1.2. Convert the integer x to an octet string X of length mlen using the conversion routine + // specified in Section 2.3.7, where mlen = ⌈(log2 p)/8⌉ or mlen = ⌈m/8⌉. + // 1.3. Convert the octet string (16 set binary digits)||X to an elliptic curve point R using the + // conversion routine specified in Section 2.3.4. If this conversion routine outputs "invalid", then + // do another iteration of Step 1. + // + // More concisely, what these points mean is to use X as a compressed public key. + final prime = (curve.curve as fp.ECCurve).q!; + if (x.compareTo(prime) >= 0) { + // Cannot have point co-ordinates larger than this as everything takes place modulo Q. + return null; + } + // Compressed keys require you to know an extra bit of data about the y-coord as there are two possibilities. + // So it's encoded in the recId. + final R = _decompressKey(x, (recId & 1) == 1); + // 1.4. If nR != point at infinity, then do another iteration of Step 1 (callers responsibility). + if (!(R * n)!.isInfinity) return null; + // 1.5. Compute e from M using Steps 2 and 3 of ECDSA signature verification. + final e = BigInt.parse(message, radix: 16); + // 1.6. For k from 1 to 2 do the following. (loop is outside this function via iterating recId) + // 1.6.1. Compute a candidate public key as: + // Q = mi(r) * (sR - eG) + // + // Where mi(x) is the modular multiplicative inverse. We transform this into the following: + // Q = (mi(r) * s ** R) + (mi(r) * -e ** G) + // Where -e is the modular additive inverse of e, that is z such that z + e = 0 (mod n). In the above equation + // ** is point multiplication and + is point addition (the EC group operator). + // + // We can find the additive inverse by subtracting e from zero then taking the mod. For example the additive + // inverse of 3 modulo 11 is 8 because 3 + 8 mod 11 = 0, and -3 mod 11 = 8. + final eInv = (BigInt.zero - e) % n; + final rInv = sig.r.modInverse(n); + final srInv = (rInv * sig.s) % n; + final eInvrInv = (rInv * eInv) % n; + return sumOfTwoMultiplies(curve.G, eInvrInv, R, srInv)!; + } + + /// Decompress a compressed public key (x co-ord and low-bit of y-coord). + static ECPoint _decompressKey(BigInt xBN, bool yBit) { + final curveByteLength = ((curve.curve.fieldSize + 7) ~/ 8); + final compEnc = _x9IntegerToBytes(xBN, 1 + curveByteLength); + compEnc[0] = (yBit ? 0x03 : 0x02); + return curve.curve.decodePoint(compEnc)!; + } + +// Extracted from pointycastle/lib/ecc/ecc_fp.dart + static Uint8List _x9IntegerToBytes(BigInt? s, int qLength) { + var bytes = Uint8List.fromList(encodeBigInt(s)); + + if (qLength < bytes.length) { + return bytes.sublist(bytes.length - qLength); + } else if (qLength > bytes.length) { + return Uint8List(qLength)..setAll(qLength - bytes.length, bytes); + } + + return bytes; + } + + // Extracted from pointycastle/lib/signers/ecdsa_signer.dart + static ECPoint? sumOfTwoMultiplies(ECPoint P, BigInt a, ECPoint Q, BigInt b) { + var c = P.curve; + + if (c != Q.curve) { + throw ArgumentError('P and Q must be on same curve'); + } + + // Point multiplication for Koblitz curves (using WTNAF) beats Shamir's trick + // TODO: uncomment this when F2m available + /* + if( c is ECCurve.F2m ) { + ECCurve.F2m f2mCurve = (ECCurve.F2m)c; + if( f2mCurve.isKoblitz() ) { + return P.multiply(a).add(Q.multiply(b)); + } + } + */ + + return _implShamirsTrick(P, a, Q, b); + } + + // Extracted from pointycastle/lib/signers/ecdsa_signer.dart + static ECPoint? _implShamirsTrick(ECPoint P, BigInt k, ECPoint Q, BigInt l) { + var m = max(k.bitLength, l.bitLength); + + var Z = P + Q; + var R = P.curve.infinity; + + for (var i = m - 1; i >= 0; --i) { + R = R!.twice(); + + if (_testBit(k, i)) { + if (_testBit(l, i)) { + R = R! + Z; + } else { + R = R! + P; + } + } else { + if (_testBit(l, i)) { + R = R! + Q; + } + } + } + + return R; + } + + // Extracted from pointycastle/lib/signers/ecdsa_signer.dart + static bool _testBit(BigInt i, int n) => (i & (BigInt.one << n)) != BigInt.zero; +} diff --git a/lib/src/models/network.dart b/lib/src/models/network.dart index 4a863cf..ccd25fd 100644 --- a/lib/src/models/network.dart +++ b/lib/src/models/network.dart @@ -3,6 +3,18 @@ import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:bitcoin_base/src/utils/enumerate.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +class MessagePrefixes { + static const String bitcoinMainnet = "\x18Bitcoin Signed Message:\n"; + static const String litecoinMainnet = "\x19Litecoin Signed Message:\n"; + static const String dashMainnet = "\x19DarkCoin Signed Message:\n"; + static const String dogecoinMainnet = "\x19Dogecoin Signed Message:\n"; + static const String bitcoinCashMainnet = "\x1cBitcoin Cash Signed Message:\n"; + static const String bitcoinSVMainnet = "\x18Bitcoin Signed Message:\n"; + static const String pepeMainnet = "\x15Pepe Signed Message:\n"; + static const String electraProtocolMainnet = + "\x20Electra Protocol Signed Message:\n"; +} + /// Abstract class representing a base for UTXO-based cryptocurrency networks. abstract class BasedUtxoNetwork implements Enumerate { /// List of version bytes for Wallet Import Format (WIF). @@ -175,6 +187,7 @@ class BitcoinNetwork implements BasedUtxoNetwork { P2shAddressType.p2wpkhInP2sh, P2shAddressType.p2pkhInP2sh, P2shAddressType.p2pkInP2sh, + SilentPaymentsAddresType.p2sp, ]; @override @@ -245,6 +258,7 @@ class LitecoinNetwork implements BasedUtxoNetwork { SegwitAddressType.p2wpkh, PubKeyAddressType.p2pk, SegwitAddressType.p2wsh, + SegwitAddressType.mweb, P2shAddressType.p2wshInP2sh, P2shAddressType.p2wpkhInP2sh, P2shAddressType.p2pkhInP2sh, @@ -601,3 +615,76 @@ class ElectraProtocolNetwork implements BasedUtxoNetwork { @override final String identifier; } + +// class BonkcoinNetwork implements BasedUtxoNetwork { +// /// Mainnet configuration with associated `CoinConf`. +// static const ElectraProtocolNetwork mainnet = ElectraProtocolNetwork._( +// 'BonkcoinMainnet', CoinsConf.electraProtocolMainNet, 'electra:mainnet'); + +// /// Testnet configuration with associated `CoinConf`. +// static const ElectraProtocolNetwork testnet = ElectraProtocolNetwork._( +// 'electraProtocolTestnet', +// CoinsConf.electraProtocolTestNet, +// 'electra:testnet'); + +// /// Overrides the `conf` property from `BasedUtxoNetwork` with the associated `CoinConf`. +// @override +// final CoinConf conf; +// @override +// final String value; + +// /// Constructor for creating a Electra Protocol network with a specific configuration. +// const BonkcoinNetwork._(this.value, this.conf, this.identifier); + +// /// Retrieves the Wallet Import Format (WIF) version bytes from the associated `CoinConf`. +// @override +// List get wifNetVer => conf.params.wifNetVer!; + +// /// Retrieves the Pay-to-Public-Key-Hash (P2PKH) version bytes from the associated `CoinConf`. +// @override +// List get p2pkhNetVer => conf.params.p2pkhNetVer!; + +// /// Retrieves the Pay-to-Script-Hash (P2SH) version bytes from the associated `CoinConf`. +// @override +// List get p2shNetVer => conf.params.p2shNetVer!; + +// /// Retrieves the Human-Readable Part (HRP) for Pay-to-Witness-Public-Key-Hash (P2WPKH) addresses +// /// from the associated `CoinConf`. +// @override +// String get p2wpkhHrp => conf.params.p2wpkhHrp!; + +// /// Checks if the current network is the mainnet. +// @override +// bool get isMainnet => this == ElectraProtocolNetwork.mainnet; + +// @override +// final List supportedAddress = const [ +// P2pkhAddressType.p2pkh, +// SegwitAddressType.p2wpkh, +// PubKeyAddressType.p2pk, +// SegwitAddressType.p2wsh, +// P2shAddressType.p2wshInP2sh, +// P2shAddressType.p2wpkhInP2sh, +// P2shAddressType.p2pkhInP2sh, +// P2shAddressType.p2pkInP2sh, +// ]; + +// @override +// List get coins { +// if (isMainnet) { +// return [ +// Bip44Coins.electraProtocol, +// Bip49Coins.electraProtocol, +// Bip84Coins.electraProtocol +// ]; +// } +// return [ +// Bip44Coins.electraProtocolTestnet, +// Bip49Coins.electraProtocolTestnet, +// Bip84Coins.electraProtocolTestnet +// ]; +// } + +// @override +// final String identifier; +// } diff --git a/lib/src/provider/electrum_methods/methods.dart b/lib/src/provider/electrum_methods/methods.dart index f217aae..9ad7b3d 100644 --- a/lib/src/provider/electrum_methods/methods.dart +++ b/lib/src/provider/electrum_methods/methods.dart @@ -26,5 +26,6 @@ export 'methods/protx_info.dart'; export 'methods/relay_fee.dart'; export 'methods/scripthash_unsubscribe.dart'; export 'methods/server_peer_subscribe.dart'; -export 'methods/status.dart'; +export 'methods/script_hash_subscribe.dart'; export 'methods/get_raw_transaction.dart'; +export 'methods/get_verbose_transaction.dart'; diff --git a/lib/src/provider/electrum_methods/methods/get_merkle.dart b/lib/src/provider/electrum_methods/methods/get_merkle.dart index 7620749..0e35191 100644 --- a/lib/src/provider/electrum_methods/methods/get_merkle.dart +++ b/lib/src/provider/electrum_methods/methods/get_merkle.dart @@ -1,10 +1,11 @@ import 'package:bitcoin_base/src/provider/core/methods.dart'; import 'package:bitcoin_base/src/provider/core/params.dart'; +import 'package:bitcoin_base/src/provider/models/electrum/models.dart'; /// Return the merkle branch to a confirmed transaction given its hash and height. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html class ElectrumRequestGetMerkle - extends ElectrumRequest, Map> { + extends ElectrumRequest> { ElectrumRequestGetMerkle( {required this.transactionHash, required this.height}); @@ -24,7 +25,7 @@ class ElectrumRequestGetMerkle } @override - Map onResonse(result) { - return result; + ElectrumGetMerkleResponse onResonse(result) { + return ElectrumGetMerkleResponse.fromJson(result); } } diff --git a/lib/src/provider/electrum_methods/methods/get_unspet.dart b/lib/src/provider/electrum_methods/methods/get_unspet.dart index 0e36cc2..ad8966a 100644 --- a/lib/src/provider/electrum_methods/methods/get_unspet.dart +++ b/lib/src/provider/electrum_methods/methods/get_unspet.dart @@ -1,4 +1,4 @@ -import 'package:bitcoin_base/src/provider/models/electrum/electrum_utxo.dart'; +import 'package:bitcoin_base/src/provider/models/electrum/models.dart'; import 'package:bitcoin_base/src/provider/core/methods.dart'; import 'package:bitcoin_base/src/provider/core/params.dart'; diff --git a/lib/src/provider/electrum_methods/methods/get_verbose_transaction.dart b/lib/src/provider/electrum_methods/methods/get_verbose_transaction.dart new file mode 100644 index 0000000..8135358 --- /dev/null +++ b/lib/src/provider/electrum_methods/methods/get_verbose_transaction.dart @@ -0,0 +1,32 @@ +import 'package:bitcoin_base/src/provider/core/methods.dart'; +import 'package:bitcoin_base/src/provider/core/params.dart'; +import 'package:bitcoin_base/src/provider/models/electrum/models.dart'; + +/// Return a raw transaction. +/// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html +class ElectrumRequestGetVerboseTransaction + extends ElectrumRequest> { + ElectrumRequestGetVerboseTransaction(this.transactionHash); + + /// The transaction hash as a hexadecimal string. + final String transactionHash; + + /// blockchain.transaction.get + @override + String get method => ElectrumRequestMethods.getTransaction.method; + + @override + List toJson() { + return [transactionHash, true]; + } + + /// If verbose is false: + /// The raw transaction as a hexadecimal string. + /// + /// If verbose is true: + /// The result is a coin-specific dictionary – whatever the coin daemon returns when asked for a verbose form of the raw transaction. + @override + ElectrumVerbosTxResponse onResonse(result) { + return ElectrumVerbosTxResponse.fromJson(result); + } +} diff --git a/lib/src/provider/electrum_methods/methods/headers_subscribe.dart b/lib/src/provider/electrum_methods/methods/headers_subscribe.dart index 9dc0f18..a101f07 100644 --- a/lib/src/provider/electrum_methods/methods/headers_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/headers_subscribe.dart @@ -1,11 +1,11 @@ import 'package:bitcoin_base/src/provider/core/methods.dart'; import 'package:bitcoin_base/src/provider/core/params.dart'; +import 'package:bitcoin_base/src/provider/models/electrum/models.dart'; /// Subscribe to receive block headers when a new block is found. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumRequestHeaderSubscribe - extends ElectrumRequest, Map> { - /// blockchain.headers.subscribe +class ElectrumRequestHeaderSubscribe extends ElectrumRequest< + ElectrumHeaderSubscribeResponse, Map> { @override String get method => ElectrumRequestMethods.headersSubscribe.method; @@ -16,7 +16,7 @@ class ElectrumRequestHeaderSubscribe /// The header of the current block chain tip. @override - Map onResonse(result) { - return result; + ElectrumHeaderSubscribeResponse onResonse(result) { + return ElectrumHeaderSubscribeResponse.fromJson(result); } } diff --git a/lib/src/provider/electrum_methods/methods/status.dart b/lib/src/provider/electrum_methods/methods/script_hash_subscribe.dart similarity index 75% rename from lib/src/provider/electrum_methods/methods/status.dart rename to lib/src/provider/electrum_methods/methods/script_hash_subscribe.dart index 0a3b978..3f15f28 100644 --- a/lib/src/provider/electrum_methods/methods/status.dart +++ b/lib/src/provider/electrum_methods/methods/script_hash_subscribe.dart @@ -4,7 +4,7 @@ import 'package:bitcoin_base/src/provider/core/params.dart'; /// Subscribe to a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html class ElectrumRequestScriptHashSubscribe - extends ElectrumRequest, dynamic> { + extends ElectrumRequest { ElectrumRequestScriptHashSubscribe({required this.scriptHash}); /// /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) @@ -18,10 +18,4 @@ class ElectrumRequestScriptHashSubscribe List toJson() { return [scriptHash]; } - - /// The status of the script hash. - @override - Map onResonse(result) { - return Map.from(result); - } } diff --git a/lib/src/provider/models/block_cypher/block_cypher_models.dart b/lib/src/provider/models/block_cypher/block_cypher_models.dart index 09f994c..a237811 100644 --- a/lib/src/provider/models/block_cypher/block_cypher_models.dart +++ b/lib/src/provider/models/block_cypher/block_cypher_models.dart @@ -1,6 +1,6 @@ import 'package:bitcoin_base/src/bitcoin/address/address.dart'; import 'package:bitcoin_base/src/provider/models/utxo_details.dart'; -import 'package:blockchain_utils/utils/numbers/utils/bigint_utils.dart'; +import 'package:blockchain_utils/utils/numbers/numbers.dart'; class TxRef implements UTXO { final String txHash; @@ -308,3 +308,68 @@ class BlockCypherAddressInfo { ); } } + +class BlockCypherChainInfo { + final BigInt height; + final String? hash; + final String? time; + final String? latestUrl; + final String? previousHash; + final String? previousUrl; + final int? peerCount; + final int? unconfirmedCount; + final BigInt? highFeePerKb; + final BigInt? mediumFeePerKb; + final BigInt? lowFeePerKb; + final BigInt? latestForkHeight; + final String? latestForkHash; + const BlockCypherChainInfo( + {required this.height, + required this.hash, + required this.time, + required this.latestUrl, + required this.previousHash, + required this.previousUrl, + required this.peerCount, + required this.unconfirmedCount, + required this.highFeePerKb, + required this.mediumFeePerKb, + required this.lowFeePerKb, + required this.latestForkHeight, + required this.latestForkHash}); + + factory BlockCypherChainInfo.fromJson(Map json) { + return BlockCypherChainInfo( + height: BigintUtils.parse(json["height"]), + hash: json["hash"], + time: json["time"], + latestUrl: json["latest_url"], + previousHash: json["previous_hash"], + previousUrl: json["previous_url"], + peerCount: IntUtils.tryParse(json["peer_count"]), + unconfirmedCount: IntUtils.tryParse(json["unconfirmed_count"]), + highFeePerKb: BigintUtils.tryParse(json["high_fee_per_kb"]), + mediumFeePerKb: BigintUtils.tryParse(json["medium_fee_per_kb"]), + lowFeePerKb: BigintUtils.tryParse(json["low_fee_per_kb"]), + latestForkHeight: BigintUtils.tryParse(json["last_fork_height"]), + latestForkHash: json["last_fork_hash"]); + } + + Map toJson() { + return { + "height": height.toString(), + "hash": hash, + "time": time, + "latest_url": latestUrl, + "previous_hash": previousHash, + "previous_url": previousUrl, + "peer_count": peerCount, + "unconfirmed_count": unconfirmedCount, + "high_fee_per_kb": highFeePerKb?.toString(), + "medium_fee_per_kb": mediumFeePerKb?.toString(), + "low_fee_per_kb": lowFeePerKb?.toString(), + "last_fork_height": latestForkHeight?.toString(), + "last_fork_hash": latestForkHash + }; + } +} diff --git a/lib/src/provider/models/config.dart b/lib/src/provider/models/config.dart index 1ace97c..7dbfaa5 100644 --- a/lib/src/provider/models/config.dart +++ b/lib/src/provider/models/config.dart @@ -11,6 +11,7 @@ class APIConfig { final String transactions; final String sendTransaction; final String blockHeight; + final String latestBlockHeight; final APIType apiType; final String rawTransaction; final BasedUtxoNetwork network; @@ -20,7 +21,7 @@ class APIConfig { case APIType.mempool: return APIConfig.mempool(network); default: - return APIConfig.mempool(network); + return APIConfig.fromBlockCypher(network); } } @@ -48,11 +49,15 @@ class APIConfig { return baseUrl.replaceAll('###', address); } - String getBlockHeight(int blockHaight) { + String getBlockHashByHeight(int blockHaight) { final baseUrl = blockHeight; return baseUrl.replaceAll('###', '$blockHaight'); } + String getLatestBlockHeightUrl() { + return latestBlockHeight; + } + factory APIConfig.fromBlockCypher(BasedUtxoNetwork network) { String baseUrl; switch (network) { @@ -86,7 +91,8 @@ class APIConfig { apiType: APIType.blockCypher, transactions: '$baseUrl/addrs/###/full?limit=200', network: network, - blockHeight: '$baseUrl/blocks/###'); + blockHeight: '$baseUrl/blocks/###', + latestBlockHeight: "$baseUrl/"); } factory APIConfig.mempool(BasedUtxoNetwork network, {String? baseUrl}) { @@ -114,7 +120,8 @@ class APIConfig { apiType: APIType.mempool, transactions: '$baseUrl/address/###/txs', network: network, - blockHeight: '$baseUrl/block-height/###'); + blockHeight: '$baseUrl/block-height/###', + latestBlockHeight: "$baseUrl/blocks/tip/height"); } APIConfig( @@ -126,5 +133,6 @@ class APIConfig { required this.apiType, required this.network, required this.blockHeight, - required this.rawTransaction}); + required this.rawTransaction, + required this.latestBlockHeight}); } diff --git a/lib/src/provider/models/electrum/electrum_utxo.dart b/lib/src/provider/models/electrum/electrum_utxo.dart deleted file mode 100644 index 4e6e848..0000000 --- a/lib/src/provider/models/electrum/electrum_utxo.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:bitcoin_base/src/bitcoin/address/address.dart'; -import 'package:bitcoin_base/src/cash_token/cash_token.dart'; -import 'package:bitcoin_base/src/provider/api_provider.dart'; -import 'package:blockchain_utils/utils/numbers/utils/bigint_utils.dart'; -import 'package:blockchain_utils/utils/numbers/utils/int_utils.dart'; - -class ElectrumUtxo implements UTXO { - factory ElectrumUtxo.fromJson(Map json) { - return ElectrumUtxo._( - height: IntUtils.parse(json['height']), - txId: json['tx_hash'], - vout: IntUtils.parse(json['tx_pos']), - value: BigintUtils.parse(json['value']), - token: json["token_data"] == null - ? null - : CashToken.fromJson(json['token_data'])); - } - const ElectrumUtxo._( - {required this.height, - required this.txId, - required this.vout, - required this.value, - this.token}); - final int height; - final String txId; - final int vout; - final BigInt value; - final CashToken? token; - - @override - BitcoinUtxo toUtxo(BitcoinAddressType addressType) { - return BitcoinUtxo( - txHash: txId, - value: value, - vout: vout, - scriptType: addressType, - blockHeight: height, - token: token); - } - - @override - Map toJson() { - return { - "token_data": token?.toJson(), - "height": height, - "tx_hash": txId, - "tx_pos": vout, - "value": value.toString() - }; - } -} diff --git a/lib/src/provider/models/electrum/models.dart b/lib/src/provider/models/electrum/models.dart new file mode 100644 index 0000000..60298f8 --- /dev/null +++ b/lib/src/provider/models/electrum/models.dart @@ -0,0 +1,127 @@ +import 'package:bitcoin_base/src/bitcoin/address/address.dart'; +import 'package:bitcoin_base/src/cash_token/cash_token.dart'; +import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:blockchain_utils/helper/extensions/extensions.dart'; +import 'package:blockchain_utils/utils/numbers/utils/bigint_utils.dart'; +import 'package:blockchain_utils/utils/numbers/utils/int_utils.dart'; + +class ElectrumUtxo implements UTXO { + factory ElectrumUtxo.fromJson(Map json) { + return ElectrumUtxo._( + height: IntUtils.parse(json['height']), + txId: json['tx_hash'], + vout: IntUtils.parse(json['tx_pos']), + value: BigintUtils.parse(json['value']), + token: json["token_data"] == null + ? null + : CashToken.fromJson(json['token_data'])); + } + const ElectrumUtxo._( + {required this.height, + required this.txId, + required this.vout, + required this.value, + this.token}); + final int height; + final String txId; + final int vout; + final BigInt value; + final CashToken? token; + + @override + BitcoinUtxo toUtxo(BitcoinAddressType addressType) { + return BitcoinUtxo( + txHash: txId, + value: value, + vout: vout, + scriptType: addressType, + blockHeight: height, + token: token); + } + + @override + Map toJson() { + return { + "token_data": token?.toJson(), + "height": height, + "tx_hash": txId, + "tx_pos": vout, + "value": value.toString() + }; + } +} + +class ElectrumHeaderSubscribeResponse { + final int block; + final String hex; + const ElectrumHeaderSubscribeResponse( + {required this.block, required this.hex}); + factory ElectrumHeaderSubscribeResponse.fromJson(Map json) { + return ElectrumHeaderSubscribeResponse( + block: IntUtils.parse(json["height"]), hex: json["hex"]); + } +} + +class ElectrumGetMerkleResponse { + final int blockHeight; + final int pos; + final List merkle; + ElectrumGetMerkleResponse({ + required this.blockHeight, + required this.pos, + required List merkle, + }) : merkle = merkle.immutable; + factory ElectrumGetMerkleResponse.fromJson(Map json) { + return ElectrumGetMerkleResponse( + blockHeight: IntUtils.parse(json["block_height"]), + pos: IntUtils.parse(json["pos"]), + merkle: (json["merkle"] as List?)?.cast() ?? [], + ); + } +} + +/// txid, hash, version, size, vsize, weight, locktime, vin, vout, hex, blockhash, confirmations, time, blocktime +/// txid, hash, version, size, vsize, weight, locktime, vin, vout, hex + +class ElectrumVerbosTxResponse { + final String txId; + final String hash; + final int version; + final int size; + final int? vsize; + final int? weight; + final int locktime; + final String hex; + final String? blockhash; + final int? confirmations; + final int? time; + final int? blocktime; + factory ElectrumVerbosTxResponse.fromJson(Map json) { + return ElectrumVerbosTxResponse( + txId: json["txid"], + hash: json["hash"], + version: IntUtils.parse(json["version"]), + size: IntUtils.parse(json["size"]), + vsize: IntUtils.tryParse(json["vsize"]), + weight: IntUtils.tryParse(json["weight"]), + locktime: IntUtils.parse(json["locktime"]), + hex: json["hex"], + blockhash: json["blockhash"], + confirmations: IntUtils.tryParse(json["confirmations"]), + blocktime: IntUtils.tryParse(json["blocktime"]), + time: IntUtils.tryParse(json["time"])); + } + const ElectrumVerbosTxResponse( + {required this.txId, + required this.hash, + required this.version, + required this.size, + required this.vsize, + required this.weight, + required this.locktime, + required this.hex, + this.blockhash, + this.confirmations, + this.time, + this.blocktime}); +} diff --git a/lib/src/provider/models/mempool/mempol_models.dart b/lib/src/provider/models/mempool/mempol_models.dart index d891541..4f06041 100644 --- a/lib/src/provider/models/mempool/mempol_models.dart +++ b/lib/src/provider/models/mempool/mempol_models.dart @@ -56,7 +56,7 @@ class MempoolVin { prevOut: MempoolPrevOut.fromJson(json['prevout']), scriptSig: json['scriptsig'], scriptSigAsm: json['scriptsig_asm'], - witness: List.from(json['witness']), + witness: List.from(json['witness'] ?? []), isCoinbase: json['is_coinbase'], sequence: json['sequence'], ); diff --git a/lib/src/provider/models/models.dart b/lib/src/provider/models/models.dart index 11fdd0a..6ad78a3 100644 --- a/lib/src/provider/models/models.dart +++ b/lib/src/provider/models/models.dart @@ -4,4 +4,4 @@ export 'mempool/mempol_models.dart'; export 'config.dart'; export 'utxo_details.dart'; export 'multisig_script.dart'; -export 'electrum/electrum_utxo.dart'; +export 'electrum/models.dart'; diff --git a/lib/src/provider/models/multisig_script.dart b/lib/src/provider/models/multisig_script.dart index bff9f50..320430e 100644 --- a/lib/src/provider/models/multisig_script.dart +++ b/lib/src/provider/models/multisig_script.dart @@ -1,4 +1,5 @@ import 'package:bitcoin_base/src/bitcoin/address/address.dart'; +import 'package:bitcoin_base/src/bitcoin/script/op_code/constant.dart'; import 'package:bitcoin_base/src/bitcoin/script/script.dart'; import 'package:bitcoin_base/src/bitcoin/taproot/taproot.dart'; import 'package:bitcoin_base/src/crypto/crypto.dart'; @@ -70,7 +71,7 @@ class MultiSignatureAddress { throw const DartBitcoinPluginException( "One of the signer's accounts used an uncompressed public key."); } - return P2wshAddress.fromScript(script: multiSigScript); + return P2wshAddress.fromScriptPubkey(script: multiSigScript); } BitcoinBaseAddress toP2wshInP2shAddress({required BasedUtxoNetwork network}) { @@ -211,7 +212,7 @@ class P2trMultiSignatureAddress { for (var w = 0; w < signer.weight; w++) { if (i == 0 && w == 0) { multiSigScript.add(signer.xOnly); - multiSigScript.add("OP_CHECKSIG"); + multiSigScript.add(BitcoinOpcode.opCheckSig.name); continue; } multiSigScript.add(signer.xOnly); diff --git a/lib/src/provider/models/utxo_details.dart b/lib/src/provider/models/utxo_details.dart index 79238c3..95d35e0 100644 --- a/lib/src/provider/models/utxo_details.dart +++ b/lib/src/provider/models/utxo_details.dart @@ -96,6 +96,10 @@ abstract class BitcoinBaseOutput { TxOutput get toOutput; } +abstract class BCHBaseOutput extends BitcoinOutput { + BCHBaseOutput({required super.address, required super.value}); +} + /// Abstract class representing a spendable Bitcoin output, extending BitcoinBaseOutput. abstract class BitcoinSpendableBaseOutput implements BitcoinBaseOutput { /// The Bitcoin address associated with this output. @@ -115,12 +119,25 @@ class BitcoinOutput implements BitcoinSpendableBaseOutput { /// Value is a pointer to a BigInt representing the amount of bitcoins sent to the recipient. @override final BigInt value; + + bool isSilentPayment; + bool isChange; + // final CashToken? token; - BitcoinOutput({required this.address, required this.value}); + BitcoinOutput({ + required this.address, + required this.value, + this.isSilentPayment = false, + this.isChange = false, + }); @override - TxOutput get toOutput => - TxOutput(amount: value, scriptPubKey: address.toScriptPubKey()); + TxOutput get toOutput => TxOutput( + amount: value, + scriptPubKey: address.toScriptPubKey(), + isSilentPayment: isSilentPayment, + isChange: isChange, + ); } /// Represents a custom script-based Bitcoin output, implementing BitcoinBaseOutput. @@ -130,12 +147,15 @@ class BitcoinScriptOutput implements BitcoinBaseOutput { /// The value (amount) of the Bitcoin output. final BigInt value; - const BitcoinScriptOutput({required this.script, required this.value}); + + final CashToken? token; + const BitcoinScriptOutput( + {required this.script, required this.value, this.token}); /// Convert the custom script output to a standard TxOutput. @override TxOutput get toOutput => - TxOutput(amount: value, scriptPubKey: script, cashToken: null); + TxOutput(amount: value, scriptPubKey: script, cashToken: token); } /// BitcoinTokenOutput represents details about a Bitcoin cash transaction with cash token output, including diff --git a/lib/src/provider/providers/explorer.dart b/lib/src/provider/providers/explorer.dart index bfdcfdc..7f3dab2 100644 --- a/lib/src/provider/providers/explorer.dart +++ b/lib/src/provider/providers/explorer.dart @@ -4,6 +4,7 @@ import 'package:bitcoin_base/src/provider/models/models.dart'; import 'package:bitcoin_base/src/provider/services/explorer.dart'; import 'package:bitcoin_base/src/models/network.dart'; import 'package:blockchain_utils/utils/binary/utils.dart'; +import 'package:blockchain_utils/utils/numbers/utils/bigint_utils.dart'; import 'package:blockchain_utils/utils/string/string.dart'; class ApiProvider { @@ -11,8 +12,8 @@ class ApiProvider { {required this.api, Map? header, required this.service}) : _header = header ?? {'Content-Type': 'application/json'}; factory ApiProvider.fromMempool(BasedUtxoNetwork network, ApiService service, - {Map? header}) { - final api = APIConfig.mempool(network); + {Map? header, String? baseUrl}) { + final api = APIConfig.mempool(network, baseUrl: baseUrl); return ApiProvider(api: api, header: header, service: service); } factory ApiProvider.fromBlocCypher( @@ -45,6 +46,18 @@ class ApiProvider { return response; } + Future> testmempool(List params) async { + final Map data = { + "jsonrpc": "2.0", + "method": "testmempoolaccept", + "id": DateTime.now().millisecondsSinceEpoch.toString(), + "params": params + }; + final response = await _postRequest>( + "https://btc.getblock.io/786c97b8-f53f-427b-80f7-9af7bd5bdb84/testnet/", json.encode(data)); + return response; + } + Future> getAccountUtxo(UtxoAddressDetails owner, {String Function(String)? tokenize}) async { final apiUrl = api.getUtxoUrl(owner.address.toAddress(api.network)); @@ -138,8 +151,8 @@ class ApiProvider { } } - Future getBlockHeight(int height) async { - final url = api.getBlockHeight(height); + Future getBlockHashByHeight(int height) async { + final url = api.getBlockHashByHeight(height); final response = await _getRequest(url); switch (api.apiType) { case APIType.mempool: @@ -150,8 +163,21 @@ class ApiProvider { } } + Future getLatestBlockHeight() async { + final url = api.getLatestBlockHeightUrl(); + final response = await _getRequest(url); + switch (api.apiType) { + case APIType.mempool: + return BigintUtils.parse(response); + default: + final toJson = StringUtils.toJson>(response); + final chainInfo = BlockCypherChainInfo.fromJson(toJson); + return chainInfo.height; + } + } + Future genesis() async { - return getBlockHeight(0); + return getBlockHashByHeight(0); } Future getRawTransaction(String transactionId, diff --git a/lib/src/psbt/psbt_builder/core/psbt_builder.dart b/lib/src/psbt/psbt_builder/core/psbt_builder.dart index 559ff69..fae6a21 100644 --- a/lib/src/psbt/psbt_builder/core/psbt_builder.dart +++ b/lib/src/psbt/psbt_builder/core/psbt_builder.dart @@ -127,6 +127,57 @@ abstract class PsbtBuilderImpl { _psbt.input.updateInputs(index, [pubKeyNonce]); } + /// Finalizes all inputs in the PSBT and returns the finalized transaction. + /// + /// This method attempts to finalize each input in the PSBT. + /// + /// Note: + /// - If an input is already finalized, it will be skipped. + /// - If any input requires custom script handling, you must provide an + /// `onFinalizeInput` callback to finalize it manually; otherwise, + /// the operation may fail. + /// + /// Returns the finalized [BtcTransaction] if successful. + BtcTransaction finalizeAll({ONFINALIZEINPUT? onFinalizeInput}); + + /// Estimates the transaction size before finalization. + /// Optionally accepts a [onFinalizeInput] callback to customize input finalization. + /// - If any input requires custom script handling, you must provide an + /// `onFinalizeInput` callback to finalize it manually; otherwise, + /// the operation may fail. + /// + /// This method does not reflect the finalized transaction size. + /// For unsigned P2PKH inputs, it assumes an uncompressed public key + /// unlocking script for estimation. + /// For SegWit transactions, it returns the estimated virtual size (vsize). + int getUnSafeTransactionSize({ONFINALIZEINPUT? onFinalizeInput}) { + final fakeSignature = PsbtGlobalProprietaryUseType( + identifier: PsbtUtils.fakeFinalizeGlobalIdentifier, + subkeydata: [], + data: const []); + final Psbt psbt = Psbt( + global: PsbtGlobal( + version: _psbt.version, + entries: [..._psbt.global.entries.clone(), fakeSignature]), + input: PsbtInput( + version: _psbt.version, entries: _psbt.input.entries.clone()), + output: PsbtOutput( + version: _psbt.version, entries: _psbt.output.entries.clone())); + final builder = PsbtBuilder.fromPsbt(psbt); + final tx = builder.finalizeAll(onFinalizeInput: onFinalizeInput); + return tx.getSize(); + } + + /// Finalizes all inputs and returns the serialized transaction size in bytes. + /// + /// Optionally accepts a [onFinalizeInput] callback to customize input finalization. + /// The returned size is the actual size of the fully finalized transaction. + /// For SegWit transactions, it returns the estimated virtual size (vsize). + int finalizeAllAndGetTransactionSize({ONFINALIZEINPUT? onFinalizeInput}) { + final tx = finalizeAll(onFinalizeInput: onFinalizeInput); + return tx.getSize(); + } + void _addNewTxOutput(PsbtTransactionOutput output) { PsbtUtils.validateCanAddOrUpdateOutput(psbt: _psbt); } diff --git a/lib/src/psbt/psbt_builder/impl/input.dart b/lib/src/psbt/psbt_builder/impl/input.dart index 08f85e2..bbcb322 100644 --- a/lib/src/psbt/psbt_builder/impl/input.dart +++ b/lib/src/psbt/psbt_builder/impl/input.dart @@ -132,6 +132,7 @@ mixin PsbtInputImpl on PsbtBuilderImpl { /// the operation may fail. /// /// Returns the finalized [BtcTransaction] if successful. + @override BtcTransaction finalizeAll({ONFINALIZEINPUT? onFinalizeInput}) { for (int i = 0; i < _psbt.input.entries.length; i++) { finalizeInput(i, onFinalizeInput: onFinalizeInput); diff --git a/lib/src/psbt/psbt_builder/impl/signer.dart b/lib/src/psbt/psbt_builder/impl/signer.dart index 1bc7a8c..36e29ed 100644 --- a/lib/src/psbt/psbt_builder/impl/signer.dart +++ b/lib/src/psbt/psbt_builder/impl/signer.dart @@ -146,7 +146,6 @@ mixin PsbtSignerImpl on PsbtBuilderImpl { await signer.btcSignInputAsync(digest.createRequest(signer)); final psbtSignature = digest.createSignature(signature, signer); final sighash = digest.getPsbtSigHash(); - print("come here ${sighash?.sighash}"); _psbt.input .updateInputs(index, [psbtSignature, if (sighash != null) sighash]); } diff --git a/lib/src/psbt/psbt_builder/types/internal_types.dart b/lib/src/psbt/psbt_builder/types/internal_types.dart index 93e6440..17e84f2 100644 --- a/lib/src/psbt/psbt_builder/types/internal_types.dart +++ b/lib/src/psbt/psbt_builder/types/internal_types.dart @@ -150,10 +150,11 @@ class PsbtGeneratedTransactionDigest { partialSignature: PsbtUtils.validateMusigPartialSignature( signature: signature.signature, index: taprootParams.index)); } - final schnorrSignature = PsbtUtils.validateSchnorrSignature( + final schnorrSignature = PsbtUtils.validateSignature( signature: signature.signature, index: params.index, - expectedSighash: sighashType); + expectedSighash: sighashType, + type: params.type); if (leafScript == null) { return PsbtInputTaprootKeySpendSignature(schnorrSignature); } @@ -163,10 +164,11 @@ class PsbtGeneratedTransactionDigest { leafHash: leafScript!.leafScript.hash(), xOnlyPubKey: signature.signerPublicKey.toXOnly()); } - final ecdsaSignature = PsbtUtils.validateEcdsaSignature( + final ecdsaSignature = PsbtUtils.validateSignature( signature: signature.signature, index: params.index, - expectedSighash: sighashType); + expectedSighash: sighashType, + type: params.type); return PsbtInputPartialSig( signature: ecdsaSignature, publicKey: signature.signerPublicKey.toBytes( @@ -298,6 +300,14 @@ class PsbtGeneratedTransactionDigest { return partialSigs.first; } + PsbtInputPartialSig? getPartialSignatureOrNull() { + try { + return getPartialSignature(); + } catch (e) { + return null; + } + } + List getTaprootScriptSignatures( List xOnlyKeys) { final musig2Signatures = PsbtUtils.getReadyMusig2Signature(this); @@ -325,7 +335,14 @@ class PsbtGeneratedTransactionDigest { bool verifyEcdsaSignature(PsbtInputPartialSig sig) { try { - return sig.publicKey.verifyTransaactionSignature(digest, sig.signature); + /// handle bch schnorr signature + if (CryptoSignatureUtils.isValidSchnorrSignature(sig.signature)) { + return sig.publicKey + .verifySchnorrSignature(digest: digest, signature: sig.signature); + } + final verify = sig.publicKey + .verifyDerSignature(digest: digest, signature: sig.signature); + return verify; } catch (_) { return false; } @@ -418,8 +435,11 @@ class PsbtGeneratedTransactionDigest { {required List xOnly, required List signature}) { try { final tweak = generateTaprootTweak(); - return BitcoinVerifier.verifySchnorrSignature( - xOnly: xOnly, message: digest, signature: signature, tweak: tweak); + return BitcoinSignatureVerifier.verifyBip340SignatureUsingXOnly( + xOnly: xOnly, + digest: digest, + signature: signature, + tapTweakHash: tweak); } catch (_) { return false; } diff --git a/lib/src/psbt/psbt_builder/types/types.dart b/lib/src/psbt/psbt_builder/types/types.dart index 95fdf64..185fd6b 100644 --- a/lib/src/psbt/psbt_builder/types/types.dart +++ b/lib/src/psbt/psbt_builder/types/types.dart @@ -1,7 +1,6 @@ import 'package:bitcoin_base/src/bitcoin/address/address.dart'; import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; import 'package:bitcoin_base/src/bitcoin/taproot/taproot.dart'; -import 'package:bitcoin_base/src/crypto/crypto.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:bitcoin_base/src/provider/models/models.dart'; import 'package:bitcoin_base/src/psbt/psbt.dart'; @@ -180,9 +179,14 @@ class PsbtTransactionInput { "Mismatch detected: The provided scriptPubKey does not match the nonWitnessUtxo output scriptPubKey."); } final type = PsbtUtils.findScriptType(output.scriptPubKey); + if (type.isP2tr) { + throw DartBitcoinPluginException( + "Incorrect legacy scriptPubKey. Use `witnessV1` constractor instead of `legacy` for taproot spending.", + details: {"type": type.name}); + } if (type.isSegwit) { throw DartBitcoinPluginException( - "Incorrect scriptPubKey. Use `addWitnessV0Input` instead of `addLegacyInput`.", + "Incorrect legacy scriptPubKey. Use `witnessV0` constractor instead of `legacy` for segwit spending.", details: {"type": type.name}); } if (type.isP2sh) { @@ -191,6 +195,12 @@ class PsbtTransactionInput { "redeemScript is required to spend P2SH scripts.", details: {"script": scriptPubKey?.script.join(", ")}); } + if (BitcoinScriptUtils.isP2wsh(redeemScript) || + BitcoinScriptUtils.isP2wpkh(redeemScript)) { + throw DartBitcoinPluginException( + "Incorrect legacy scriptPubKey. Use `witnessV1` constractor instead of `legacy` for nested segwit p2sh spending.", + details: {"type": type.name}); + } P2shAddress addr; if (type.isP2sh32) { addr = P2shAddress.fromScript32(script: redeemScript); @@ -632,7 +642,6 @@ class PsbtUtxo { /// The list of individual TapLeaf merkle proofs used in script-path spending. /// Each proof shows how a leaf is part of the Taproot commitment. final List? leafScripts; - final List privateKeys; final List? muSig2ParticipantPublicKeys; final List? ripemd160; @@ -642,7 +651,6 @@ class PsbtUtxo { const PsbtUtxo( {required this.utxo, - required this.privateKeys, this.tx, required this.scriptPubKey, this.xOnlyOrInternalPubKey, @@ -661,11 +669,6 @@ class PsbtUtxo { factory PsbtUtxo.fromJson(Map json) { return PsbtUtxo( utxo: BitcoinUtxo.fromJson(json["utxo"]), - privateKeys: (json["privateKeys"] as List) - .map( - (e) => ECPrivate.fromHex(e), - ) - .toList(), tx: json["tx"] == null ? null : BtcTransaction.deserialize(BytesUtils.fromHexString(json["tx"])), @@ -705,7 +708,6 @@ class PsbtUtxo { "xonly": BytesUtils.tryToHexString(xOnlyOrInternalPubKey), "merkle_root": BytesUtils.tryToHexString(merkleRoot), "leaf_scripts": leafScripts?.map((e) => e.toJson()).toList(), - "privateKeys": privateKeys.map((e) => e.toHex()).toList() }; } } @@ -942,7 +944,7 @@ class PsbtTransactionOutput { "Either scriptPubKey or address must be provided."); } address ??= - BitcoinScriptUtils.generateAddressFromScriptPubKey(scriptPubKey); + BitcoinScriptUtils.tryGenerateAddressFromScriptPubKey(scriptPubKey); return PsbtTransactionOutput._( scriptPubKey: scriptPubKey, amount: amount, address: address); } diff --git a/lib/src/psbt/psbt_builder/versiones/v1.dart b/lib/src/psbt/psbt_builder/versiones/v1.dart index b0542af..bd74af9 100644 --- a/lib/src/psbt/psbt_builder/versiones/v1.dart +++ b/lib/src/psbt/psbt_builder/versiones/v1.dart @@ -23,19 +23,18 @@ class PsbtBuilderV0 extends PsbtBuilder { output: PsbtOutput(version: version))); } - BtcTransaction get _unsignedTx => _psbt.global.entries + BtcTransaction get _tx => _psbt.global.entries .whereType() .first .transaction; - @override TxInput txInput(int index) { - final unsignedTx = _unsignedTx; + final unsignedTx = _tx; PsbtUtils.validateTxInputs( psbInput: _psbt.input, inputIndex: index, inputsLength: unsignedTx.inputs.length); - return _unsignedTx.inputs[index]; + return _tx.inputs[index]; } @override @@ -47,19 +46,19 @@ class PsbtBuilderV0 extends PsbtBuilder { @override List txInputs() { - final unsignedTx = _unsignedTx; + final unsignedTx = _tx; return unsignedTx.inputs.clone(); } @override List txOutputs() { - final unsignedTx = _unsignedTx; + final unsignedTx = _tx; return unsignedTx.outputs.clone(); } @override BtcTransaction buildUnsignedTransaction() { - return _unsignedTx; + return _tx.copyWith(); } @override @@ -68,6 +67,7 @@ class PsbtBuilderV0 extends PsbtBuilder { } void _updateUnsignedTx(BtcTransaction transaction) { + // _unsignedTx.copyWith(); _psbt.global.updateGlobals([PsbtGlobalUnsignedTransaction(transaction)]); } @@ -75,7 +75,7 @@ class PsbtBuilderV0 extends PsbtBuilder { void _addNewTxOutput(PsbtTransactionOutput output) { super._addNewTxOutput(output); _psbt.output.addOutputs(output.toPsbtOutput(psbtVersion)); - BtcTransaction tx = _unsignedTx; + BtcTransaction tx = _tx; tx = tx.copyWith(outputs: [...tx.outputs, output.toTxOutput()]); _updateUnsignedTx(tx); } @@ -86,7 +86,7 @@ class PsbtBuilderV0 extends PsbtBuilder { _psbt.output.replaceOutput(index, output.toPsbtOutput(psbtVersion)); final txOutputs = this.txOutputs(); txOutputs[index] = output.toTxOutput(); - BtcTransaction tx = _unsignedTx; + BtcTransaction tx = _tx; tx = tx.copyWith(outputs: txOutputs); _updateUnsignedTx(tx); } @@ -97,7 +97,7 @@ class PsbtBuilderV0 extends PsbtBuilder { final outputs = txOutputs(); outputs.removeAt(index); _psbt.output.removeOutput(index); - BtcTransaction tx = _unsignedTx; + BtcTransaction tx = _tx; tx = tx.copyWith(outputs: outputs); _updateUnsignedTx(tx); } @@ -105,7 +105,7 @@ class PsbtBuilderV0 extends PsbtBuilder { @override void _updateTxInput(int index, PsbtTransactionInput input) { super._updateTxInput(index, input); - BtcTransaction tx = _unsignedTx; + BtcTransaction tx = _tx; final List newInputs = List.generate(tx.inputs.length, (i) { if (i == index) return input.txInput; return tx.inputs[i]; @@ -121,7 +121,7 @@ class PsbtBuilderV0 extends PsbtBuilder { _psbt.input.removeInput(index); final inputs = txInputs(); inputs.removeAt(index); - BtcTransaction tx = _unsignedTx; + BtcTransaction tx = _tx; final locktime = PsbtUtils.buildTransactionLocktime(inputs: inputs); tx = tx.copyWith(inputs: inputs, locktime: locktime); _updateUnsignedTx(tx); @@ -131,7 +131,7 @@ class PsbtBuilderV0 extends PsbtBuilder { void _addNewTxInput(PsbtTransactionInput input) { super._addNewTxInput(input); _psbt.input.addInputs(input.toPsbtInputs(psbtVersion)); - BtcTransaction tx = _unsignedTx; + BtcTransaction tx = _tx; final locktime = PsbtUtils.buildTransactionLocktime( inputs: [...tx.inputs, input.txInput]); tx = tx.copyWith(inputs: [...tx.inputs, input.txInput], locktime: locktime); diff --git a/lib/src/psbt/signer/signer.dart b/lib/src/psbt/signer/signer.dart index 14e60c8..84cf21d 100644 --- a/lib/src/psbt/signer/signer.dart +++ b/lib/src/psbt/signer/signer.dart @@ -78,12 +78,14 @@ class PsbtDefaultSigner SignInputResponse btcSignInput(PsbtSigningInputDigest digest) { if (digest.isTaproot) { return SignInputResponse( - signature: - privateKey.signBtcSchnorr(digest.digest, tweak: digest.tweak), + signature: BytesUtils.fromHexString(privateKey.signBip340( + digest.digest, + tapTweakHash: digest.tweak, + tweak: digest.tweak != null)), signerPublicKey: signerPublicKey); } final signature = - privateKey.signInput(digest.digest, sigHash: digest.sighash); + privateKey.signECDSA(digest.digest, sighash: digest.sighash); return SignInputResponse( signature: BytesUtils.fromHexString(signature), signerPublicKey: signerPublicKey); diff --git a/lib/src/psbt/types/types/global.dart b/lib/src/psbt/types/types/global.dart index 6ef4704..1b2760e 100644 --- a/lib/src/psbt/types/types/global.dart +++ b/lib/src/psbt/types/types/global.dart @@ -141,8 +141,7 @@ class PsbtGlobal { } /// Retrieves all entries of a specified type. - List? getGlobals( - int index, PsbtGlobalTypes type) { + List? getGlobals(PsbtGlobalTypes type) { final data = _entries.where((e) => e.type == type); if (data.isEmpty) return null; return data.toList().cast(); diff --git a/lib/src/psbt/types/types/inputs.dart b/lib/src/psbt/types/types/inputs.dart index 24d84d0..7afff35 100644 --- a/lib/src/psbt/types/types/inputs.dart +++ b/lib/src/psbt/types/types/inputs.dart @@ -9,7 +9,6 @@ import 'package:bitcoin_base/src/crypto/crypto.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:bitcoin_base/src/psbt/psbt_builder/types/types.dart'; import 'package:bitcoin_base/src/psbt/types/types/psbt.dart'; -import 'package:bitcoin_base/src/psbt/utils/utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; enum PsbtInputTypes { @@ -450,7 +449,8 @@ class PsbtInputPartialSig extends PsbtInputDataSignature { super._(type: PsbtInputTypes.partialSignature); factory PsbtInputPartialSig( {required List signature, required List publicKey}) { - if (PsbtUtils.isValidBitcoinDERSignature(signature)) { + if (CryptoSignatureUtils.isValidBitcoinDERSignature(signature) || + CryptoSignatureUtils.isValidSchnorrSignature(signature)) { final pubKey = ECPublic.fromBytes(publicKey); final mode = publicKey.length == EcdsaKeysConst.pubKeyCompressedByteLen ? PubKeyModes.compressed @@ -473,7 +473,8 @@ class PsbtInputPartialSig extends PsbtInputDataSignature { throw DartBitcoinPluginException( "Invalid PSBT Partial Signature type flag."); } - if (PsbtUtils.isValidBitcoinDERSignature(keypair.value.data)) { + if (CryptoSignatureUtils.isValidBitcoinDERSignature(keypair.value.data) || + CryptoSignatureUtils.isValidSchnorrSignature(keypair.value.data)) { try { final mode = keypair.key.extraData?.length == EcdsaKeysConst.pubKeyCompressedByteLen @@ -650,7 +651,7 @@ class PsbtInputBip32DerivationPath extends PsbtInputData { required List publicKey, }) { if (fingerprint.length == Bip32KeyDataConst.fingerprintByteLen && - Secp256k1PublicKeyEcdsa.isValidBytes(publicKey)) { + Secp256k1PublicKey.isValidBytes(publicKey)) { return PsbtInputBip32DerivationPath._( fingerprint: fingerprint, indexes: indexes, @@ -693,7 +694,7 @@ class PsbtInputBip32DerivationPath extends PsbtInputData { offset, offset + Bip32KeyDataConst.keyIndexByteLen)); }); if (fingerPrint.length == Bip32KeyDataConst.fingerprintByteLen && - Secp256k1PublicKeyEcdsa.isValidBytes(keypair.key.extraData ?? [])) { + Secp256k1PublicKey.isValidBytes(keypair.key.extraData ?? [])) { return PsbtInputBip32DerivationPath._( fingerprint: fingerPrint, indexes: bip32Indexes, @@ -1942,9 +1943,9 @@ class PsbtInputMuSig2PublicNonce extends PsbtInputData { : keypair.key.extraData! .sublist(EcdsaKeysConst.pubKeyCompressedByteLen * 2); if (publicKey.length == EcdsaKeysConst.pubKeyCompressedByteLen && - Secp256k1PublicKeyEcdsa.isValidBytes(publicKey) && + Secp256k1PublicKey.isValidBytes(publicKey) && plainPublicKey.length == EcdsaKeysConst.pubKeyCompressedByteLen && - Secp256k1PublicKeyEcdsa.isValidBytes(plainPublicKey) && + Secp256k1PublicKey.isValidBytes(plainPublicKey) && (hash == null || hash.length == QuickCrypto.sha256DigestSize) && keypair.value.data.length == EcdsaKeysConst.pubKeyCompressedByteLen * 2) { @@ -2041,9 +2042,9 @@ class PsbtInputMuSig2ParticipantPartialSignature ? null : keypair.key.extraData!.sublist(66); if (publicKey.length == EcdsaKeysConst.pubKeyCompressedByteLen && - Secp256k1PublicKeyEcdsa.isValidBytes(publicKey) && + Secp256k1PublicKey.isValidBytes(publicKey) && plainPublicKey.length == EcdsaKeysConst.pubKeyCompressedByteLen && - Secp256k1PublicKeyEcdsa.isValidBytes(plainPublicKey) && + Secp256k1PublicKey.isValidBytes(plainPublicKey) && (hash == null || hash.length == QuickCrypto.sha256DigestSize) && keypair.value.data.length == QuickCrypto.sha256DigestSize) { return PsbtInputMuSig2ParticipantPartialSignature._( diff --git a/lib/src/psbt/types/types/outputs.dart b/lib/src/psbt/types/types/outputs.dart index b5db135..f7e9c17 100644 --- a/lib/src/psbt/types/types/outputs.dart +++ b/lib/src/psbt/types/types/outputs.dart @@ -326,7 +326,7 @@ class PsbtOutputBip32DerivationPath extends PsbtOutputData { required List indexes, required List publicKey}) { if (fingerprint.length == Bip32KeyDataConst.fingerprintByteLen && - Secp256k1PublicKeyEcdsa.isValidBytes(publicKey)) { + Secp256k1PublicKey.isValidBytes(publicKey)) { return PsbtOutputBip32DerivationPath._( fingerprint: fingerprint, indexes: indexes, @@ -348,7 +348,7 @@ class PsbtOutputBip32DerivationPath extends PsbtOutputData { "Invalid PSBT bip32 derivation path type flag"); } if (keypair.key.extraData == null || - !Secp256k1PublicKeyEcdsa.isValidBytes(keypair.key.extraData!)) { + !Secp256k1PublicKey.isValidBytes(keypair.key.extraData!)) { throw DartBitcoinPluginException( "Invalid PSBT bip32 derivation public key."); } @@ -370,7 +370,7 @@ class PsbtOutputBip32DerivationPath extends PsbtOutputData { offset, offset + Bip32KeyDataConst.keyIndexByteLen)); }); if (fingerPrint.length == Bip32KeyDataConst.fingerprintByteLen && - Secp256k1PublicKeyEcdsa.isValidBytes(keypair.key.extraData!)) { + Secp256k1PublicKey.isValidBytes(keypair.key.extraData!)) { return PsbtOutputBip32DerivationPath._( fingerprint: fingerPrint, indexes: bip32Indexes, @@ -748,8 +748,8 @@ class PsbtOutputMuSig2ParticipantPublicKeys extends PsbtOutputData { super(type: PsbtOutputTypes.muSig2ParticipantPublicKeys); factory PsbtOutputMuSig2ParticipantPublicKeys( {required List aggregatePubKey, required List> pubKeys}) { - if (Secp256k1PublicKeyEcdsa.isValidBytes(aggregatePubKey) && - pubKeys.every(Secp256k1PublicKeyEcdsa.isValidBytes)) { + if (Secp256k1PublicKey.isValidBytes(aggregatePubKey) && + pubKeys.every(Secp256k1PublicKey.isValidBytes)) { return PsbtOutputMuSig2ParticipantPublicKeys._( aggregatePubKey: aggregatePubKey, pubKeys: pubKeys, @@ -781,8 +781,8 @@ class PsbtOutputMuSig2ParticipantPublicKeys extends PsbtOutputData { offset, offset + EcdsaKeysConst.pubKeyCompressedByteLen); pubKeys.add(key); } - if (Secp256k1PublicKeyEcdsa.isValidBytes(keypair.key.extraData!) && - pubKeys.every(Secp256k1PublicKeyEcdsa.isValidBytes)) { + if (Secp256k1PublicKey.isValidBytes(keypair.key.extraData!) && + pubKeys.every(Secp256k1PublicKey.isValidBytes)) { return PsbtOutputMuSig2ParticipantPublicKeys._( aggregatePubKey: keypair.key.extraData!, pubKeys: pubKeys, diff --git a/lib/src/psbt/utils/utils.dart b/lib/src/psbt/utils/utils.dart index 9547733..c410630 100644 --- a/lib/src/psbt/utils/utils.dart +++ b/lib/src/psbt/utils/utils.dart @@ -5,6 +5,7 @@ import 'package:bitcoin_base/src/crypto/crypto.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:bitcoin_base/src/provider/models/models.dart'; import 'package:bitcoin_base/src/psbt/psbt_builder/types/internal_types.dart'; +import 'package:bitcoin_base/src/psbt/types/types/global.dart'; import 'package:bitcoin_base/src/psbt/types/types/inputs.dart'; import 'package:bitcoin_base/src/psbt/types/types/outputs.dart'; import 'package:bitcoin_base/src/psbt/psbt_builder/types/types.dart'; @@ -12,6 +13,17 @@ import 'package:bitcoin_base/src/psbt/types/types/psbt.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; class PsbtUtils { + static final String fakeEcdsaPubKey = "0" * 33 * 2; + static final String fakeUnCompresedEcdsaPubKey = "0" * 65 * 2; + + /// 65 byte schnorr signature length + static const fakeSchnorSignaturBytes = + '0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101'; + + /// 72 bytes (64 byte signature, 6-7 byte Der encoding length) + static const fakeECDSASignatureBytes = + '010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101'; + static PsbtInputInfo _getInputInfo( {required Psbt psbt, required TxInput input, required int inputIndex}) { PsbtTxType inputType = PsbtTxType.legacy; @@ -113,10 +125,15 @@ class PsbtUtils { } if (!isSegwit && p2shRedeemScript != null) { redeemScript = p2shRedeemScript; - address = P2shAddress.fromScript(script: redeemScript); + if (BitcoinScriptUtils.isP2sh32(scriptPubKey!)) { + address = P2shAddress.fromScript32(script: redeemScript); + } else { + address = P2shAddress.fromScript(script: redeemScript); + } + if (address.toScriptPubKey() != scriptPubKey) { throw DartBitcoinPluginException( - "ScriptPubKey does not match the one generated from p2sh redeem script."); + "ScriptPubKey does not match the one generated from p2sh redeem script. ${address.toScriptPubKey()}"); } } @@ -415,12 +432,16 @@ class PsbtUtils { static List _finalizeMultisigScript( {required PsbtGeneratedTransactionDigest digest, - required MultiSignatureAddress multisig}) { + required MultiSignatureAddress multisig, + bool fake = false}) { final currentScript = digest.params.witnessScript ?? digest.params.p2shRedeemScript!; final multisigSigners = multisig.signers.map((e) => ECPublic.fromHex(e.publicKey)).toList(); - final validSignatures = digest.getPartialSignatures(multisigSigners); + List validSignatures = []; + if (!fake) { + validSignatures = digest.getPartialSignatures(multisigSigners); + } List signatures = []; final threshold = multisig.threshold; for (int i = 0; i < multisig.signers.length; i++) { @@ -428,12 +449,14 @@ class PsbtUtils { final pubKey = multisigSigners[i]; final signer = multisig.signers[i]; - final signature = validSignatures.firstWhereNullable((e) { - return e.publicKey == pubKey && e.mode == signer.keyType; - }); + final signature = fake + ? fakeECDSASignatureBytes + : validSignatures.firstWhereNullable((e) { + return e.publicKey == pubKey && e.mode == signer.keyType; + })?.signatureHex(); if (signature != null) { for (int w = 0; w < signer.weight; w++) { - signatures.add(signature.signatureHex()); + signatures.add(signature); if (signatures.length >= threshold) break; } } @@ -452,10 +475,12 @@ class PsbtUtils { static List _finalizeTaprootMultisigScript({ required P2trMultiSignatureAddress p2trMultisig, required PsbtGeneratedTransactionDigest digest, + bool fake = false, }) { final leafScript = digest.leafScript!; - List scriptSignatures = - digest.getTaprootScriptSignatures( + List scriptSignatures = fake + ? [] + : digest.getTaprootScriptSignatures( p2trMultisig.signers.map((e) => e.xOnly).toList()); scriptSignatures = () { final sigs = scriptSignatures.map((e) { @@ -469,15 +494,17 @@ class PsbtUtils { List signatures = []; int someWeight = 0; for (int i = 0; i < p2trMultisig.signers.length; i++) { - // if (signatures.length >= threshold) break; final signer = p2trMultisig.signers[i]; - final signature = scriptSignatures - .firstWhereNullable((e) => e.xOnlyPubKeyHex == signer.xOnly); + final signature = fake + ? fakeSchnorSignaturBytes + : scriptSignatures + .firstWhereNullable((e) => e.xOnlyPubKeyHex == signer.xOnly) + ?.signatureHex(); for (int w = 0; w < signer.weight; w++) { if (someWeight >= p2trMultisig.threshold) { signatures.add(''); } else { - signatures.add(signature?.signatureHex() ?? ''); + signatures.add(signature ?? ''); } if (signature != null) someWeight++; } @@ -527,10 +554,14 @@ class PsbtUtils { } static List _finalizeScriptSpent( - PsbtGeneratedTransactionDigest digest) { + PsbtGeneratedTransactionDigest digest, + {bool fake = false}) { final currentScript = digest.params.witnessScript ?? digest.params.p2shRedeemScript!; if (BitcoinScriptUtils.isP2wpkh(currentScript)) { + if (fake) { + return [fakeECDSASignatureBytes, fakeEcdsaPubKey]; + } final signature = digest.getPartialSignature(); final scriptPubKey = signature.publicKey.toP2wpkhInP2sh().toScriptPubKey(); @@ -542,21 +573,37 @@ class PsbtUtils { } if (BitcoinScriptUtils.isP2pkh(currentScript)) { - final signature = digest.getPartialSignature(); - final pubKey = findSciptKeyInfo( - publicKey: signature.publicKey, script: currentScript); - if (pubKey == null) { + final signature = digest.getPartialSignatureOrNull(); + final pubKey = signature == null + ? null + : findSciptKeyInfo( + publicKey: signature.publicKey, script: currentScript); + if (fake) { + if (pubKey != null) { + return [fakeECDSASignatureBytes, pubKey.key, currentScript.toHex()]; + } + return [ + fakeECDSASignatureBytes, + fakeUnCompresedEcdsaPubKey, + currentScript.toHex() + ]; + } + if (signature == null || pubKey == null) { throw DartBitcoinPluginException( "Cannot find current signer key in signature."); } return [signature.signatureHex(), pubKey.key, currentScript.toHex()]; } else if (BitcoinScriptUtils.isP2pk(currentScript)) { + if (fake) { + return [fakeECDSASignatureBytes, currentScript.toHex()]; + } final signature = digest.getPartialSignature(); return [signature.signatureHex(), currentScript.toHex()]; } final multisig = BitcoinScriptUtils.parseMultisigScript(currentScript); if (multisig != null) { - return _finalizeMultisigScript(digest: digest, multisig: multisig); + return _finalizeMultisigScript( + digest: digest, multisig: multisig, fake: fake); } if (currentScript.script.isEmpty) { return [currentScript.toHex()]; @@ -587,7 +634,7 @@ class PsbtUtils { return [pubKey.key, currentScript.toHex()]; } throw DartBitcoinPluginException( - "Unable to finalize custom script input at index ${digest.params.index}. Please use the onFinalizeCallback to complete the input finalization.", + "Unable to finalize custom script input at index ${digest.params.index}. Please use the onFinalizeCallback to complete the input finalization. ${currentScript.toHex()}", ); } @@ -644,7 +691,8 @@ class PsbtUtils { static List _finalizeTaprootScriptSpent( {required PsbtGeneratedTransactionDigest digest, - required PsbtInput input}) { + required PsbtInput input, + bool fake = false}) { PsbtInputTaprootLeafScript? leafScript = digest.leafScript; if (leafScript == null) { @@ -658,6 +706,9 @@ class PsbtUtils { if (BitcoinScriptUtils.hasAnyOpCheckSig(currentScript)) { if (BitcoinScriptUtils.isXOnlyOpChecksig(currentScript)) { + if (fake) { + return [fakeSchnorSignaturBytes, script, controlBlock]; + } final signature = digest .getTaprootScriptSignature(currentScript.script[0].toString()); return [signature.signatureHex(), script, controlBlock]; @@ -666,7 +717,7 @@ class PsbtUtils { BitcoinScriptUtils.isP2trMultiScript(leafScript.script); if (taprootMultisig != null) { return _finalizeTaprootMultisigScript( - p2trMultisig: taprootMultisig, digest: digest); + p2trMultisig: taprootMultisig, digest: digest, fake: fake); } } else { if (currentScript.script.isEmpty) { @@ -695,17 +746,36 @@ class PsbtUtils { static List _finalizeNonScriptSpent( {required PsbtGeneratedTransactionDigest digest, - required PsbtInput input}) { + required PsbtInput input, + bool fake = false}) { final script = digest.params.scriptPubKey; if (BitcoinScriptUtils.isP2tr(script)) { + if (fake) { + return [fakeSchnorSignaturBytes]; + } return [digest.getTaprootKeyPathSignature()]; } else if (BitcoinScriptUtils.isP2pk(digest.params.scriptPubKey)) { + if (fake) { + return [fakeECDSASignatureBytes]; + } return [digest.getPartialSignature().signatureHex()]; } else if (BitcoinScriptUtils.isP2pkh(script) || BitcoinScriptUtils.isP2wpkh(script)) { - final sig = digest.getPartialSignature(); - final pk = findSciptKeyInfo(publicKey: sig.publicKey, script: script); - if (pk == null) { + final sig = digest.getPartialSignatureOrNull(); + final pk = sig == null + ? null + : findSciptKeyInfo(publicKey: sig.publicKey, script: script); + if (fake) { + return [ + sig?.signatureHex() ?? fakeECDSASignatureBytes, + pk?.key ?? + (BitcoinScriptUtils.isP2wpkh(script) + ? fakeEcdsaPubKey + : fakeUnCompresedEcdsaPubKey) + ]; + } + + if (pk == null || sig == null) { throw DartBitcoinPluginException( "Signature public key does not match the scriptPubKey for input ${digest.params.index}."); } @@ -716,12 +786,20 @@ class PsbtUtils { details: {"scriptPubKey": digest.params.scriptPubKey.toString()}); } + static final List fakeFinalizeGlobalIdentifier = + 'fake_finalize'.codeUnits.asImmutableBytes; + static PsbtFinalizeInput finalizeInput( {required Psbt psbt, required int index, required List txInputs, required BtcTransaction unsignedTx, PsbtFinalizeResponse? userFinalizedInput}) { + final userData = psbt.global.getGlobals( + PsbtGlobalTypes.proprietary) ?? + []; + bool fake = userData.any((e) => + BytesUtils.bytesEqual(e.identifier, fakeFinalizeGlobalIdentifier)); final txType = getTxType(psbt.input); final params = getPsbtInputInfo(psbt: psbt, inputIndex: index, txInputs: txInputs); @@ -739,13 +817,14 @@ class PsbtUtils { txType: txType); } List unlockScript = switch (params.isScriptSpending) { - false => _finalizeNonScriptSpent(digest: digest, input: psbt.input), + false => + _finalizeNonScriptSpent(digest: digest, input: psbt.input, fake: fake), true => () { if (params.type.isP2tr) { return _finalizeTaprootScriptSpent( - digest: digest, input: psbt.input); + digest: digest, input: psbt.input, fake: fake); } - return _finalizeScriptSpent(digest); + return _finalizeScriptSpent(digest, fake: fake); }(), }; if (txType.isLegacy) { @@ -849,40 +928,54 @@ class PsbtUtils { index: index, input: input, sighashType: sighashType); - PsbtInputTaprootLeafScript? tapleafScript; - final digest = switch (params.type) { - PsbtTxType.legacy => () { - final legacyInput = params.cast(); - return unsignedTx.getTransactionDigest( - txInIndex: index, - script: legacyInput.redeemScript, - sighash: sighashType!); - }(), - PsbtTxType.witnessV0 => () { - final v0Input = params.cast(); - return unsignedTx.getTransactionSegwitDigit( - txInIndex: index, - script: v0Input.redeemScript, - sighash: sighashType!, - amount: params.amount); - }(), - PsbtTxType.witnessV1 => () { - final tapInput = params.cast(); - if (tapInput.tapleafScripts != null) { - tapleafScript = PsbtUtils.findCorrectLeafScript( - tapLeafScripts: tapInput.tapleafScripts!, - tapleafHash: tapleafHash, - index: index); - } - return unsignedTx.getTransactionTaprootDigset( - txIndex: index, - scriptPubKeys: tapInput.allScriptPubKeys, - sighash: sighashType!, - amounts: tapInput.allAmounts, - tapleafScript: tapleafScript?.leafScript); - }(), - }; + List digest; + if (isSighashForked(sighashType)) { + if (params.type == PsbtTxType.witnessV1) { + throw DartBitcoinPluginException( + "Invalid sighash type: Forked sighash types are not compatible with witness v1 transactions."); + } + final v0Input = params.cast(); + digest = unsignedTx.getTransactionSegwitDigit( + txInIndex: index, + script: v0Input.redeemScript, + sighash: sighashType, + amount: params.amount); + } else { + digest = switch (params.type) { + PsbtTxType.legacy => () { + final legacyInput = params.cast(); + return unsignedTx.getTransactionDigest( + txInIndex: index, + script: legacyInput.redeemScript, + sighash: sighashType!); + }(), + PsbtTxType.witnessV0 => () { + final v0Input = params.cast(); + return unsignedTx.getTransactionSegwitDigit( + txInIndex: index, + script: v0Input.redeemScript, + sighash: sighashType!, + amount: params.amount); + }(), + PsbtTxType.witnessV1 => () { + final tapInput = params.cast(); + if (tapInput.tapleafScripts != null) { + tapleafScript = PsbtUtils.findCorrectLeafScript( + tapLeafScripts: tapInput.tapleafScripts!, + tapleafHash: tapleafHash, + index: index); + } + return unsignedTx.getTransactionTaprootDigset( + txIndex: index, + scriptPubKeys: tapInput.allScriptPubKeys, + sighash: sighashType!, + amounts: tapInput.allAmounts, + tapleafScript: tapleafScript?.leafScript); + }(), + }; + } + return PsbtGeneratedTransactionDigest( digest: digest, sighashType: sighashType, @@ -905,47 +998,32 @@ class PsbtUtils { "Invalid Psbt input: Cannot mix height-based and time-based locktimes in a PSBT."); } - /// must be move to blockchain_utils package - static bool isValidBitcoinDERSignature(List signature) { - if (signature.length < 9 || signature.length > 73) { - return false; - } - if (signature[0] != 0x30) { - return false; - } - if (signature[1] != signature.length - 3) { - return false; - } - int lenR = signature[3]; - if (5 + lenR >= signature.length) { - return false; - } - int lenS = signature[5 + lenR]; - if ((7 + lenR + lenS) != signature.length) { - return false; - } - if (signature[4] & 0x80 != 0) { - return false; + static List _validateSchnorrSignature( + {required List signature, + required int index, + required int expectedSighash}) { + if (!CryptoSignatureUtils.isValidSchnorrSignature(signature)) { + throw DartBitcoinPluginException( + "Invalid Schnorr signature at input $index. Signature may be malformed or improperly formatted."); } - if (lenR > 1 && (signature[4] == 0x00) && (signature[5] & 0x80) == 0) { - return false; + if (signature.length == CryptoSignerConst.schnoorSginatureLength) { + if (expectedSighash == BitcoinOpCodeConst.sighashDefault) { + return signature; + } + return [...signature, expectedSighash]; } - if (signature[lenR + 4] != 0x02) return false; - if (lenS == 0) return false; - if ((signature[lenR + 6] & 0x80) != 0) return false; - if (lenS > 1 && - (signature[lenR + 6] == 0x00) && - (signature[lenR + 7] & 0x80) == 0) { - return false; + if (signature.last != expectedSighash) { + throw DartBitcoinPluginException( + "Signature mismatch at input $index: Expected sighash $expectedSighash, but got ${signature.last}. The Schnorr signature may be malformed or improperly formatted."); } - return true; + return signature; } - static List validateEcdsaSignature( + static List _validateEcdsaSignature( {required List signature, required int index, required int expectedSighash}) { - if (!isValidBitcoinDERSignature(signature)) { + if (!CryptoSignatureUtils.isValidBitcoinDERSignature(signature)) { throw DartBitcoinPluginException( "Invalid DER-encoded signature at input $index. Signature may be malformed or improperly formatted."); } @@ -956,32 +1034,67 @@ class PsbtUtils { return signature; } - static List validateMusigPartialSignature( - {required List signature, required int index}) { - if (!MuSig2Utils.isValidPartialSignature(signature)) { - throw DartBitcoinPluginException( - "Invalid Musig2 Schnorr partial signature at input $index. Signature may be malformed or improperly formatted."); + static bool verifyBchSchnorrSignature( + List digest, List signature, ECPublic pubKey) { + if (digest.length != 32) { + throw const ArgumentException("The message must be a 32-byte array."); } - return signature; + if (signature.length != 64) { + throw const ArgumentException("Invalid signature length."); + } + + final BigInt order = Curves.generatorSecp256k1.order!; + final List rBytes = signature.sublist(0, 32); + final List sBytes = signature.sublist(32, 64); + + final BigInt rX = BigintUtils.fromBytes(rBytes); + final BigInt s = BigintUtils.fromBytes(sBytes); + + if (s >= order) { + return false; + } + + final P = pubKey.point; + BigInt e = BigintUtils.fromBytes(QuickCrypto.sha256Hash([ + ...rBytes, + ...pubKey.toBytes(mode: PubKeyModes.compressed), + ...digest + ])) % + order; + final sp = Curves.generatorSecp256k1 * s; + + if (P.y.isEven) { + e = Curves.generatorSecp256k1.order! - e; + } + final ProjectiveECCPoint eP = P * e; + + final R = sp + eP; + if (R.isInfinity) return false; + if (R.y.isOdd || R.x != rX) { + return false; + } + return true; } - static List validateSchnorrSignature( + static List validateSignature( {required List signature, required int index, - required int expectedSighash}) { - if (!CryptoSignatureUtils.isValidSchnorrSignature(signature)) { - throw DartBitcoinPluginException( - "Invalid Schnorr signature at input $index. Signature may be malformed or improperly formatted."); - } - if (signature.length == CryptoSignerConst.schnoorSginatureLength) { - if (expectedSighash == BitcoinOpCodeConst.sighashDefault) { - return signature; - } - return [...signature, expectedSighash]; - } - if (signature.last != expectedSighash) { + required int expectedSighash, + required PsbtTxType type}) { + if (type.isP2tr || + CryptoSignatureUtils.isValidSchnorrSignature(signature)) { + return _validateSchnorrSignature( + signature: signature, index: index, expectedSighash: expectedSighash); + } + return _validateEcdsaSignature( + signature: signature, index: index, expectedSighash: expectedSighash); + } + + static List validateMusigPartialSignature( + {required List signature, required int index}) { + if (!MuSig2Utils.isValidPartialSignature(signature)) { throw DartBitcoinPluginException( - "Signature mismatch at input $index: Expected sighash $expectedSighash, but got ${signature.last}. The Schnorr signature may be malformed or improperly formatted."); + "Invalid Musig2 Schnorr partial signature at input $index. Signature may be malformed or improperly formatted."); } return signature; } @@ -1017,6 +1130,15 @@ class PsbtUtils { return sighashInfos; } + static int? getInputSigHash(PsbtInput psbtInput, int index) { + final sighashType = + psbtInput.getInput(index, PsbtInputTypes.sighashType); + if (sighashType != null) { + return sighashType.sighash; + } + return null; + } + static void validateCanAddOrUpdateOutput( {required Psbt psbt, int? outputIndex, bool isUpdate = true}) { final sighashes = @@ -1155,6 +1277,12 @@ class PsbtUtils { return (sighash & 0x1F) == type; } + static bool isSighashForked(int sighash) { + return (sighash & BitcoinOpCodeConst.sighashForked == + BitcoinOpCodeConst.sighashForked) || + (sighash & 0x1F) == BitcoinOpCodeConst.sighashForked; + } + static bool isAnyoneCanPay(int sighash) { return (sighash & BitcoinOpCodeConst.sighashAnyoneCanPay) != 0; } diff --git a/lib/src/transaction_builder/core.dart b/lib/src/transaction_builder/core.dart index 0606936..3511146 100644 --- a/lib/src/transaction_builder/core.dart +++ b/lib/src/transaction_builder/core.dart @@ -1,4 +1,5 @@ -import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +import 'package:bitcoin_base/src/provider/models/utxo_details.dart'; typedef BitcoinSignerCallBack = String Function( List trDigest, UtxoWithAddress utxo, String publicKey, int sighash); diff --git a/lib/src/transaction_builder/forked_transaction_builder.dart b/lib/src/transaction_builder/forked_transaction_builder.dart index 010f87a..a3c2973 100644 --- a/lib/src/transaction_builder/forked_transaction_builder.dart +++ b/lib/src/transaction_builder/forked_transaction_builder.dart @@ -72,7 +72,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { static int estimateTransactionSize( {required List utxos, required List outputs, - required BitcoinCashNetwork network, + required BasedUtxoNetwork network, String? memo, bool enableRBF = false}) { final transactionBuilder = ForkedTransactionBuilder( @@ -319,9 +319,9 @@ be retrieved by anyone who examines the blockchain's history. /// Total token amount to spend. Map _sumTokenOutputAmounts(List outputs) { final tokens = {}; - for (final utxo in outputs) { - if (utxo.cashToken == null) continue; - final token = utxo.cashToken!; + for (final out in outputs) { + if (out.cashToken == null) continue; + final token = out.cashToken!; if (!token.hasAmount) continue; if (tokens.containsKey(token.category)) { tokens[token.category] = tokens[token.category]! + token.amount; diff --git a/lib/src/transaction_builder/transaction_builder.dart b/lib/src/transaction_builder/transaction_builder.dart index 36ffeac..2512011 100644 --- a/lib/src/transaction_builder/transaction_builder.dart +++ b/lib/src/transaction_builder/transaction_builder.dart @@ -1,5 +1,6 @@ import 'package:bitcoin_base/src/bitcoin/address/address.dart'; import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +import 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:bitcoin_base/src/models/network.dart'; import 'package:bitcoin_base/src/provider/models/utxo_details.dart'; @@ -35,6 +36,10 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { final bool isFakeTransaction; final BitcoinOrdering inputOrdering; final BitcoinOrdering outputOrdering; + final List? inputPrivKeyInfos; + final List? vinOutpoints; + bool _hasSilentPayment = false; + BitcoinTransactionBuilder({ required this.outPuts, required this.fee, @@ -45,6 +50,8 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { this.memo, this.enableRBF = false, this.isFakeTransaction = false, + this.inputPrivKeyInfos, + this.vinOutpoints, }) : utxosInfo = utxos { _validateBuilder(); } @@ -77,12 +84,15 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { /// This method is used to create a dummy transaction, /// allowing us to obtain the size of the original transaction /// before conducting the actual transaction. This helps us estimate the transaction cost - static int estimateTransactionSize( - {required List utxos, - required List outputs, - required BasedUtxoNetwork network, - String? memo, - bool enableRBF = false}) { + static int estimateTransactionSize({ + required List utxos, + required List outputs, + required BasedUtxoNetwork network, + String? memo, + bool enableRBF = false, + List? inputPrivKeyInfos, + List? vinOutpoints, + }) { final transactionBuilder = BitcoinTransactionBuilder( /// Now, we provide the UTXOs we want to spend. utxos: utxos, @@ -114,6 +124,9 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { /// We consider the transaction to be fake so that it doesn't check the amounts /// and doesn't generate errors when determining the transaction size. isFakeTransaction: true, + + inputPrivKeyInfos: inputPrivKeyInfos, + vinOutpoints: vinOutpoints, ); /// 64 byte schnorr signature length @@ -223,6 +236,8 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { return senderPub.toAddress().toScriptPubKey(); case SegwitAddressType.p2tr: return senderPub.toTaprootAddress().toScriptPubKey(); + case SegwitAddressType.mweb: + return Script(script: []); case P2shAddressType.p2pkhInP2sh: if (isTaproot) { return senderPub.toP2pkhInP2sh(mode: utxo.keyType).toScriptPubKey(); @@ -358,6 +373,8 @@ that demonstrate the right to spend the bitcoins associated with the correspondi case SegwitAddressType.p2wpkh: case P2shAddressType.p2wpkhInP2sh: return [signedDigest, senderPub.toHex()]; + case SegwitAddressType.mweb: + return []; default: throw DartBitcoinPluginException( 'invalid segwit address type ${utx.utxo.scriptType.value}'); @@ -438,6 +455,57 @@ that demonstrate the right to spend the bitcoins associated with the correspondi return List.unmodifiable(outputs); } + void _buildSilentPayments() { + List silentPaymentDestinations = []; + + for (final out in outPuts as List) { + final address = out.address; + + if (address is SilentPaymentAddress) { + try { + utxosInfo.firstWhere((utxo) => utxo.ownerDetails.address.type == SegwitAddressType.p2wsh); + throw const DartBitcoinPluginException('Silent payments not supported for P2WSH'); + } catch (_) {} + + out.isSilentPayment = true; + _hasSilentPayment = true; + + silentPaymentDestinations.add( + SilentPaymentDestination.fromAddress( + address.toAddress(network), + out.value.toInt(), + ), + ); + } + } + + if (silentPaymentDestinations.isNotEmpty) { + final sendingOutputs = SilentPaymentBuilder(vinOutpoints: vinOutpoints!) + .createOutputs(inputPrivKeyInfos!, silentPaymentDestinations); + + final outputsAdded = []; + + for (var i = 0; i < outPuts.length; i++) { + final out = outPuts[i] as BitcoinOutput; + + final silentOutputs = sendingOutputs[out.address.toAddress(network)]; + + if (silentOutputs != null) { + final silentOutput = + silentOutputs.firstWhere((element) => !outputsAdded.contains(element)); + + outPuts[i] = BitcoinOutput( + address: silentOutput.address, + value: BigInt.from(silentOutput.amount), + isSilentPayment: true, + ); + + outputsAdded.add(silentOutput); + } + } + } + } + /// The primary use case for OP_RETURN is data storage. You can embed various types of /// data within the OP_RETURN output, such as text messages, document hashes, or metadata /// related to a transaction. This data is permanently recorded on the blockchain and can @@ -465,6 +533,10 @@ that demonstrate the right to spend the bitcoins associated with the correspondi final utxos = sortedInputs.item2; + if (inputPrivKeyInfos != null) { + _buildSilentPayments(); + } + /// build outout final outputs = _buildOutputs(); @@ -489,8 +561,12 @@ that demonstrate the right to spend the bitcoins associated with the correspondi } /// create new transaction with inputs and outputs and isSegwit transaction or not - BtcTransaction transaction = - BtcTransaction(inputs: inputs, outputs: outputs); + BtcTransaction transaction = BtcTransaction( + inputs: inputs, + outputs: outputs, + hasSegwit: hasSegwit, + hasSilentPayment: _hasSilentPayment, + ); /// we define empty witnesses. maybe the transaction is segwit and We need this final witnesses = []; @@ -652,8 +728,12 @@ that demonstrate the right to spend the bitcoins associated with the correspondi } /// create new transaction with inputs and outputs and isSegwit transaction or not - BtcTransaction transaction = - BtcTransaction(inputs: inputs, outputs: outputs); + BtcTransaction transaction = BtcTransaction( + inputs: inputs, + outputs: outputs, + hasSegwit: hasSegwit, + hasSilentPayment: _hasSilentPayment, + ); /// we define empty witnesses. maybe the transaction is segwit and We need this final witnesses = []; diff --git a/lib/src/utils/btc_utils.dart b/lib/src/utils/btc_utils.dart index afe34ec..bfa05b0 100644 --- a/lib/src/utils/btc_utils.dart +++ b/lib/src/utils/btc_utils.dart @@ -33,4 +33,10 @@ class BtcUtils { dec = dec * BigRational(BigInt.from(10).pow(8)); return dec.toBigInt(); } + + static String toBtc(BigInt amount) { + BigRational dec = BigRational(amount); + dec = dec / BigRational(BigInt.from(10).pow(8)); + return dec.toDecimal(digits: 8); + } } diff --git a/lib/src/utils/regex.dart b/lib/src/utils/regex.dart new file mode 100644 index 0000000..9508d66 --- /dev/null +++ b/lib/src/utils/regex.dart @@ -0,0 +1,40 @@ +import 'package:bitcoin_base/src/bitcoin/address/address.dart'; +import 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; +import 'package:bitcoin_base/src/models/network.dart'; + +class RegexUtils { + static bool stringIsAddress(String string, RegExp addressRegex) { + return RegExp("^${addressRegex.pattern}\$").hasMatch(string); + } + + static bool addressInString(String string, RegExp addressRegex) { + return RegExp("(^|\\s)${addressRegex.pattern}(\$|\\s)").hasMatch(string); + } + + static BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) { + if (network is BitcoinCashNetwork) { + if (!address.startsWith("bitcoincash:") && + (address.startsWith("q") || address.startsWith("p"))) { + address = "bitcoincash:$address"; + } + + return BitcoinCashAddress(address).baseAddress; + } + + if (stringIsAddress(address, P2pkhAddress.regex)) { + return P2pkhAddress.fromAddress(address: address, network: network); + } else if (stringIsAddress(address, P2shAddress.regex)) { + return P2shAddress.fromAddress(address: address, network: network); + } else if (stringIsAddress(address, P2wshAddress.regex)) { + return P2wshAddress.fromAddress(address: address, network: network); + } else if (stringIsAddress(address, P2trAddress.regex)) { + return P2trAddress.fromAddress(address: address, network: network); + } else if (stringIsAddress(address, SilentPaymentAddress.regex)) { + return SilentPaymentAddress.fromAddress(address); + } else if (stringIsAddress(address, MwebAddress.regex)) { + return MwebAddress.fromAddress(address: address, network: network); + } else { + return P2wpkhAddress.fromAddress(address: address, network: network); + } + } +} diff --git a/lib/src/utils/script.dart b/lib/src/utils/script.dart new file mode 100644 index 0000000..e6dc4a6 --- /dev/null +++ b/lib/src/utils/script.dart @@ -0,0 +1,36 @@ +import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; + +bool isDefinedHashType(sighash) { + final hashTypeMod = sighash & ~BitcoinOpCodeConst.sighashAnyoneCanPay; + return hashTypeMod > BitcoinOpCodeConst.sighashAll && + hashTypeMod < BitcoinOpCodeConst.sighashSingle; +} + +bool bip66check(buffer) { + if (buffer.length < 8) return false; + if (buffer.length > 72) return false; + if (buffer[0] != 0x30) return false; + if (buffer[1] != buffer.length - 2) return false; + if (buffer[2] != 0x02) return false; + + var lenR = buffer[3]; + if (lenR == 0) return false; + if (5 + lenR >= buffer.length) return false; + if (buffer[4 + lenR] != 0x02) return false; + + var lenS = buffer[5 + lenR]; + if (lenS == 0) return false; + if ((6 + lenR + lenS) != buffer.length) return false; + + if (buffer[4] & 0x80 != 0) return false; + if (lenR > 1 && (buffer[4] == 0x00) && buffer[5] & 0x80 == 0) return false; + + if (buffer[lenR + 6] & 0x80 != 0) return false; + if (lenS > 1 && (buffer[lenR + 6] == 0x00) && buffer[lenR + 7] & 0x80 == 0) return false; + return true; +} + +bool isCanonicalScriptSignature(List buffer) { + if (!isDefinedHashType(buffer[buffer.length - 1])) return false; + return bip66check(buffer.sublist(0, buffer.length - 1)); +} diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart index b0b050d..2bd2b02 100644 --- a/lib/src/utils/utils.dart +++ b/lib/src/utils/utils.dart @@ -1,2 +1,3 @@ export 'btc_utils.dart'; export 'enumerate.dart'; +export 'regex.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index ba7968b..3b6f604 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bitcoin_base description: A versatile library for Bitcoin, Dogecoin, Litecoin, Dash, BSV, and BCH. Supports P2PKH, P2SH, P2WPKH, P2WSH, P2TR, with advanced creation, signing, and spending capabilities. -version: 6.1.0 +version: 6.5.0 homepage: "https://github.com/mrtnetwork/bitcoin_base" repository: "https://github.com/mrtnetwork/bitcoin_base" Author: mrhaydari.t@gmail.com @@ -16,10 +16,13 @@ environment: dependencies: - blockchain_utils: ^4.3.0 + convert: ^3.1.1 + pointycastle: ^3.7.4 + bip32: ^2.0.0 + blockchain_utils: ^5.0.0 # blockchain_utils: - # path: ../../blockchain_utils + # path: ../blockchain_utils dev_dependencies: diff --git a/test/deserialize_test.dart b/test/deserialize_test.dart index 8594bf3..f713fd6 100644 --- a/test/deserialize_test.dart +++ b/test/deserialize_test.dart @@ -1,7 +1,21 @@ -import 'package:bitcoin_base/src/bitcoin/script/transaction.dart'; -import 'package:blockchain_utils/utils/binary/utils.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:test/test.dart'; +ECPrivate getPublicKey({String path = "m/84'/1'/0'/0/1"}) { + final key = getKey(path: path); + return ECPrivate.fromBytes(key.privateKey.raw); +} + +Bip32Slip10Secp256k1 getKey( + {String key = + "tprv8ZgxMBicQKsPdgBQV2Y9EVPSjAGhyZXArhwSzHwnV3FytzPRr8KCR8EKEpLeHbANAncgbc31a6QoXjBTARQiZ2h1Z2NgSCjFYeTqKpAN5Gc", + String path = "m/86'/1'/0'/0/1"}) { + Bip32Slip10Secp256k1 secp = Bip32Slip10Secp256k1.fromExtendedKey( + key, Bip44Coins.bitcoinTestnet.conf.keyNetVer); + return secp.derivePath(path) as Bip32Slip10Secp256k1; +} + void main() { _decodeTx(); } diff --git a/test/fixtures/silent_payments.json b/test/fixtures/silent_payments.json new file mode 100644 index 0000000..a54c584 --- /dev/null +++ b/test/fixtures/silent_payments.json @@ -0,0 +1,2766 @@ +[ + { + "comment": "Simple send: two inputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", + "priv_key_tweak": "f438b40179a3c4262de12986c0e6cce0634007cdc79c1dcd3e20b9ebc2e7eef6", + "signature": "74f85b856337fbe837643b86f462118159f93ac4acc2671522f27e8f67b079959195ccc7a5dbee396d2909f5d680d6e30cda7359aa2755822509b70d6b0687a1" + } + ] + } + } + ] + }, + { + "comment": "Simple send: two inputs, order reversed", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", + "priv_key_tweak": "f438b40179a3c4262de12986c0e6cce0634007cdc79c1dcd3e20b9ebc2e7eef6", + "signature": "74f85b856337fbe837643b86f462118159f93ac4acc2671522f27e8f67b079959195ccc7a5dbee396d2909f5d680d6e30cda7359aa2755822509b70d6b0687a1" + } + ] + } + } + ] + }, + { + "comment": "Simple send: two inputs from the same transaction", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 3, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 7, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 3, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 7, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6", + "priv_key_tweak": "4851455bfbe1ab4f80156570aa45063201aa5c9e1b1dcd29f0f8c33d10bf77ae", + "signature": "10332eea808b6a13f70059a8a73195808db782012907f5ba32b6eae66a2f66b4f65147e2b968a1678c5f73d57d5d195dbaf667b606ff80c8490eac1f3b710657" + } + ] + } + } + ] + }, + { + "comment": "Simple send: two inputs from the same transaction, order reversed", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 7, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 3, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 7, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 3, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38", + "priv_key_tweak": "ab0c9b87181bf527879f48db9f14a02233619b986f8e8f2d5d408ce68a709f51", + "signature": "398a9790865791a9db41a8015afad3a47d60fec5086c50557806a49a1bc038808632b8fe679a7bb65fc6b455be994502eed849f1da3729cd948fc7be73d67295" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: multiple UTXOs from the same public key", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + } + ], + "outputs": [ + "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566", + "priv_key_tweak": "f032695e2636619efa523fffaa9ef93c8802299181fd0461913c1b8daf9784cd", + "signature": "f238386c5d5e5444f8d2c75aabbcb28c346f208c76f60823f5de3b67b79e0ec72ea5de2d7caec314e0971d3454f122dda342b3eede01b3857e83654e36b25f76" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot only inputs with even y-values", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + }, + "private_key": "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + } + } + ], + "outputs": [ + "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb", + "priv_key_tweak": "3fb9ce5ce1746ced103c8ed254e81f6690764637ddbc876ec1f9b3ddab776b03", + "signature": "c5acd25a8f021a4192f93bc34403fd8b76484613466336fb259c72d04c169824f2690ca34e96cee86b69f376c8377003268fda56feeb1b873e5783d7e19bcca5" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot only with mixed even/odd y-values", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + }, + "private_key": "1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + } + } + ], + "outputs": [ + "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1", + "priv_key_tweak": "f5382508609771068ed079b24e1f72e4a17ee6d1c979066bf1d4e2a5676f09d4", + "signature": "ff65833b8fd1ed3ef9d0443b4f702b45a3f2dd457ba247687e8207745c3be9d2bdad0ab3f07118f8b2efc6a04b95f7b3e218daf8a64137ec91bd2fc67fc137a5" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot input with even y-value and non-taproot input", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + } + } + ], + "outputs": [ + "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0", + "priv_key_tweak": "b40017865c79b1fcbed68896791be93186d08f47e416b289b8c063777e14e8df", + "signature": "d1edeea28cf1033bcb3d89376cabaaaa2886cbd8fda112b5c61cc90a4e7f1878bdd62180b07d1dfc8ffee1863c525a0c7b5bcd413183282cfda756cb65787266" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot input with odd y-value and non-taproot input", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + }, + "private_key": "1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + } + } + ], + "outputs": [ + "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a", + "priv_key_tweak": "a2f9dd05d1d398347c885d9c61a64d18a264de6d49cea4326bafc2791d627fa7", + "signature": "96038ad233d8befe342573a6e54828d863471fb2afbad575cc65271a2a649480ea14912b6abbd3fbf92efc1928c036f6e3eef927105af4ec1dd57cb909f360b8" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs: multiple outputs, same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 2.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 3.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + 2.0 + ], + [ + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + 3.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + }, + { + "pub_key": "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "priv_key_tweak": "d97e442d110c0bdd31161a7bb6e7862e038d02a09b1484dfbb463f2e0f7c9230", + "signature": "29bd25d0f808d7fcd2aa6d5ed206053899198397506c301b218a9e47a3d7070af03e903ff718978d50d1b6b9af8cc0e313d84eda5d5b1e8e85e5516d630bbeb9" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs: multiple outputs, multiple recipients", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 2.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 3.0 + ], + [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + 4.0 + ], + [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + 5.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + 2.0 + ], + [ + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + 3.0 + ], + [ + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + 4.0 + ], + [ + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + 5.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + }, + { + "pub_key": "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "priv_key_tweak": "d97e442d110c0bdd31161a7bb6e7862e038d02a09b1484dfbb463f2e0f7c9230", + "signature": "29bd25d0f808d7fcd2aa6d5ed206053899198397506c301b218a9e47a3d7070af03e903ff718978d50d1b6b9af8cc0e313d84eda5d5b1e8e85e5516d630bbeb9" + } + ] + } + }, + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a" + ], + "key_material": { + "spend_priv_key": "9902c3c56e84002a7cd410113a9ab21d142be7f53cf5200720bb01314c5eb920", + "scan_priv_key": "060b751d7892149006ed7b98606955a29fe284a1e900070c0971f5fb93dbf422" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn" + ], + "outputs": [ + { + "pub_key": "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "priv_key_tweak": "2f17ea873a0047fc01ba8010fef0969e76d0e4283f600d48f735098b1fee6eb9", + "signature": "c26f4e3cf371b90b840f48ea0e761b5ec31883ed55719f9ef06a90e282d85f565790ab780a3f491bc2668cc64e944dca849d1022a878cdadb8d168b8da4a6da3" + }, + { + "pub_key": "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "priv_key_tweak": "72cd082cccb633bf85240a83494b32dc943a4d05647a6686d23ad4ca59c0ebe4", + "signature": "38745f3d9f5eef0b1cfb17ca314efa8c521efab28a23aa20ec5e3abb561d42804d539906dce60c4ee7977966184e6f2cab1faa0e5377ceb7148ec5218b4e7878" + } + ] + } + } + ] + }, + { + "comment": "Receiving with labels: label with odd parity", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "pub_key": "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a", + "priv_key_tweak": "51d4e9d0d482b5700109b4b2e16ff508269b03d800192a043d61dca4a0a72a52", + "signature": "c30fa63bad6f0a317f39a773a5cbf0b0f8193c71dfebba05ee6ae4ed28e3775e6e04c3ea70a83703bb888122855dc894cab61692e7fd10c9b3494d479a60785e" + } + ] + } + } + ] + }, + { + "comment": "Receiving with labels: label with odd parity", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "pub_key": "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c", + "priv_key_tweak": "6024ae214876356b8d917716e7707d267ae16a0fdb07de2a786b74a7bbcddead", + "signature": "a86d554d0d6b7aa0907155f7e0b47f0182752472fffaeddd68da90e99b9402f166fd9b33039c302c7115098d971c1399e67c19e9e4de180b10ea0b9d6f0db832" + } + ] + } + } + ] + }, + { + "comment": "Receiving with labels: label with odd parity", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "pub_key": "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951", + "priv_key_tweak": "e336b92330c33030285ce42e4115ad92d5197913c88e06b9072b4a9b47c664a2", + "signature": "c9e80dd3bdd25ca2d352ce77510f1aed37ba3509dc8cc0677f2d7c2dd04090707950ce9dd6c83d2a428063063aff5c04f1744e334f661f2fc01b4ef80b50f739" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs with labels: multiple outputs for labeled address; same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + 2.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + 1.0 + ], + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + 2.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ], + "outputs": [ + { + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + }, + { + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs with labels: multiple outputs for labeled address; same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + 3.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + 4.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + 3.0 + ], + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + 4.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ], + "outputs": [ + { + "pub_key": "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "priv_key_tweak": "9d5fd3b91cac9ddfea6fc2e6f9386f680e6cee623cda02f53706306c081de87f", + "signature": "db0dfacc98b6a6fcc67cc4631f080b1ca38c60d8c397f2f19843f8f95ec91594b24e47c5bd39480a861c1209f7e3145c440371f9191fb96e324690101eac8e8e" + }, + { + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs with labels: multiple outputs for labeled address; same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 5.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + 6.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5", + 7.0 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5", + 8.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + 5.0 + ], + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + 6.0 + ], + [ + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + 7.0 + ], + [ + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b", + 8.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ], + "outputs": [ + { + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + }, + { + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: use silent payments for sender change", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ], + [ + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqlv6saelkk5snl4wfutyxrchpzzwm8rjp3z6q7apna59z9huq4x754e5atr", + 2.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + 1.0 + ], + [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + 2.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff" + ], + "key_material": { + "spend_priv_key": "b8f87388cbb41934c50daca018901b00070a5ff6cc25a7e9e716a9d5b9e4d664", + "scan_priv_key": "11b7a82e06ca2648d5fded2366478078ec4fc9dc1d8ff487518226f229d768fd" + }, + "labels": [ + 0 + ] + }, + "expected": { + "addresses": [ + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqauj52ymtc4xdkmx3tgyhrsemg2g3303xk2gtzfy8h8ejet8fz8jcw23zua", + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqlv6saelkk5snl4wfutyxrchpzzwm8rjp3z6q7apna59z9huq4x754e5atr" + ], + "outputs": [ + { + "pub_key": "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "priv_key_tweak": "80cd767ed20bd0bb7d8ea5e803f8c381293a62e8a073cf46fb0081da46e64e1f", + "signature": "7fbd5074cf1377273155eefafc7c330cb61b31da252f22206ac27530d2b2567040d9af7808342ed4a09598c26d8307446e4ed77079e6a2e61fea736e44da5f5a" + } + ] + } + }, + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ] + } + } + ] + }, + { + "comment": "Single receipient: taproot input with NUMS point", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0440c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b22205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00150", + "prevout": { + "scriptPubKey": { + "hex": "5120da6f0595ecb302bbe73e2f221f05ab10f336b06817d36fd28fc6691725ddaa85" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + }, + "private_key": "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "0340268d31a9276f6380107d5321cafa6d9e8e5ea39204318fdc8206b31507c891c3bbcea3c99e2208d73bd127a8e8c5f1e45a54f1bd217205414ddb566ab7eda0092220e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85dac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "prevout": { + "scriptPubKey": { + "hex": "51200a3c9365ceb131f89b0a4feb6896ebd67bb15a98c31eaa3da143bb955a0f3fcb" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0440c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b22205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00150", + "prevout": { + "scriptPubKey": { + "hex": "5120da6f0595ecb302bbe73e2f221f05ab10f336b06817d36fd28fc6691725ddaa85" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "0340268d31a9276f6380107d5321cafa6d9e8e5ea39204318fdc8206b31507c891c3bbcea3c99e2208d73bd127a8e8c5f1e45a54f1bd217205414ddb566ab7eda0092220e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85dac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "prevout": { + "scriptPubKey": { + "hex": "51200a3c9365ceb131f89b0a4feb6896ebd67bb15a98c31eaa3da143bb955a0f3fcb" + } + } + } + ], + "outputs": [ + "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d", + "priv_key_tweak": "3ddec3232609d348d6b8b53123b4f40f6d4f5398ca586f087b0416ec3b851496", + "signature": "d7d06e3afb68363031e4eb18035c46ceae41bdbebe7888a4754bc9848c596436869aeaecff0527649a1f458b71c9ceecec10b535c09d01d720229aa228547706" + } + ] + } + } + ] + }, + { + "comment": "Pubkey extraction from malleated p2pkh", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "0075473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "5163473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187372102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d67483046022100c0d3c851d3bd562ae93d56bcefd735ea57c027af46145a4d5e9cac113bfeb0c2022100ee5b2239af199fa9b7aa1d98da83a29d0a2cf1e4f29e2f37134ce386d51c544c2102ad0f26ddc7b3fcc340155963b3051b85289c1869612ecb290184ac952e2864ec68", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914c82c5ec473cbc6c86e5ef410e36f9495adcf979988ac" + } + }, + "private_key": "72b8ae09175ca7977f04993e651d88681ed932dfb92c5158cdf0161dd23fda6e" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "0075473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "5163473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187372102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d67483046022100c0d3c851d3bd562ae93d56bcefd735ea57c027af46145a4d5e9cac113bfeb0c2022100ee5b2239af199fa9b7aa1d98da83a29d0a2cf1e4f29e2f37134ce386d51c544c2102ad0f26ddc7b3fcc340155963b3051b85289c1869612ecb290184ac952e2864ec68", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914c82c5ec473cbc6c86e5ef410e36f9495adcf979988ac" + } + } + } + ], + "outputs": [ + "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f", + "priv_key_tweak": "10bde9781def20d7701e7603ef1b1e5e71c67bae7154818814e3c81ef5b1a3d3", + "signature": "6137969f810e9e8ef6c9755010e808f5dd1aed705882e44d7f0ae64eb0c509ec8b62a0671bee0d5914ac27d2c463443e28e999d82dc3d3a4919f093872d947bb" + } + ] + } + } + ] + }, + { + "comment": "P2PKH and P2WPKH Uncompressed Keys are skipped", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "02473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187374104e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d6fe8190e189be57d0d5bcd17dbcbcd04c9b4a1c5f605b10d5c90abfcc0d12884", + "prevout": { + "scriptPubKey": { + "hex": "00140423f731a07491364e8dce98b7c00bda63336950" + } + }, + "private_key": "72b8ae09175ca7977f04993e651d88681ed932dfb92c5158cdf0161dd23fda6e" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "02473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187374104e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d6fe8190e189be57d0d5bcd17dbcbcd04c9b4a1c5f605b10d5c90abfcc0d12884", + "prevout": { + "scriptPubKey": { + "hex": "00140423f731a07491364e8dce98b7c00bda63336950" + } + } + } + ], + "outputs": [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + "priv_key_tweak": "688fa3aeb97d2a46ae87b03591921c2eaf4b505eb0ddca2733c94701e01060cf", + "signature": "72e7ad573ac23255d4651d5b0326a200496588acb7a4894b22092236d5eda6a0a9a4d8429b022c2219081fefce5b33795cae488d10f5ea9438849ed8353624f2" + } + ] + } + } + ] + }, + { + "comment": "Skip invalid P2SH inputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "16001419c2f3ae0ca3b642bd3e49598b8da89f50c14161", + "txinwitness": "02483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "prevout": { + "scriptPubKey": { + "hex": "a9148629db5007d5fcfbdbb466637af09daf9125969387" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "1600144b92ac4ac6fe6212393894addda332f2e47a3156", + "txinwitness": "02473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "prevout": { + "scriptPubKey": { + "hex": "a9146c9bf136fbb7305fd99d771a95127fcf87dedd0d87" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "00493046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d601483045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b97014c695221025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be52103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233382102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d53ae", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "a9141044ddc6cea09e4ac40fbec2ba34ad62de6db25b87" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + 1.0 + ] + ] + } + }, + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "16001419c2f3ae0ca3b642bd3e49598b8da89f50c14161", + "txinwitness": "02483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "prevout": { + "scriptPubKey": { + "hex": "a9148629db5007d5fcfbdbb466637af09daf9125969387" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "1600144b92ac4ac6fe6212393894addda332f2e47a3156", + "txinwitness": "02473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "prevout": { + "scriptPubKey": { + "hex": "a9146c9bf136fbb7305fd99d771a95127fcf87dedd0d87" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "00493046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d601483045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b97014c695221025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be52103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233382102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d53ae", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "a9141044ddc6cea09e4ac40fbec2ba34ad62de6db25b87" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "16001419c2f3ae0ca3b642bd3e49598b8da89f50c14161", + "txinwitness": "02483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "prevout": { + "scriptPubKey": { + "hex": "a9148629db5007d5fcfbdbb466637af09daf9125969387" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "1600144b92ac4ac6fe6212393894addda332f2e47a3156", + "txinwitness": "02473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "prevout": { + "scriptPubKey": { + "hex": "a9146c9bf136fbb7305fd99d771a95127fcf87dedd0d87" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "00493046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d601483045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b97014c695221025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be52103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233382102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d53ae", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "a9141044ddc6cea09e4ac40fbec2ba34ad62de6db25b87" + } + } + } + ], + "outputs": [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + "priv_key_tweak": "688fa3aeb97d2a46ae87b03591921c2eaf4b505eb0ddca2733c94701e01060cf", + "signature": "72e7ad573ac23255d4651d5b0326a200496588acb7a4894b22092236d5eda6a0a9a4d8429b022c2219081fefce5b33795cae488d10f5ea9438849ed8353624f2" + } + ] + } + } + ] + }, + { + "comment": "Recipient ignores unrelated outputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [ + [ + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + 1.0 + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [] + } + } + ] + }, + { + "comment": "No valid inputs, sender generates no outputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d641045a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5c61836c9b1688ba431f7ea3039742251f62f0dca3da1bee58a47fa9b456c2d52", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914460e8b41545d2dbe7e0671f0f573e2232814260a88ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1.0 + ] + ] + }, + "expected": { + "outputs": [] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d641045a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5c61836c9b1688ba431f7ea3039742251f62f0dca3da1bee58a47fa9b456c2d52", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914460e8b41545d2dbe7e0671f0f573e2232814260a88ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + } + } + ], + "outputs": [ + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [] + } + } + ] + } +] diff --git a/test/keys_test.dart b/test/keys_test.dart index eb985d2..de39bd8 100644 --- a/test/keys_test.dart +++ b/test/keys_test.dart @@ -161,17 +161,63 @@ void main() { netVersion: BitcoinNetwork.mainnet.wifNetVer); final pub = priv.getPublic(); - test('test1', () { + test('sign/verify message', () { final sign = priv.signMessage(StringUtils.encode(message)); - pub.verify(StringUtils.encode(message), BytesUtils.fromHexString(sign)); - expect( pub.verify( - StringUtils.encode(message), BytesUtils.fromHexString(sign)), + message: StringUtils.encode(message), + signature: BytesUtils.fromHexString(sign)), true); }); - test('test2', () {}); + test('sign/verify bip137 p2pkh uncompressed', () { + final signature = priv.signBip137(StringUtils.encode(message)); + + expect( + pub.verifyBip137Address( + message: StringUtils.encode(message), + signature: signature, + address: + priv.getPublic().toAddress(mode: PubKeyModes.uncompressed)), + true); + }); + + test('sign/verify bip137 p2pkh compressed', () { + final signature = priv.signBip137(StringUtils.encode(message), + mode: BIP137Mode.p2pkhCompressed); + + expect( + pub.verifyBip137Address( + message: StringUtils.encode(message), + signature: signature, + address: + priv.getPublic().toAddress(mode: PubKeyModes.compressed)), + true); + }); + + test('sign/verify bip137 p2wpkh', () { + final signature = + priv.signBip137(StringUtils.encode(message), mode: BIP137Mode.p2wpkh); + + expect( + pub.verifyBip137Address( + message: StringUtils.encode(message), + signature: signature, + address: priv.getPublic().toSegwitAddress()), + true); + }); + + test('sign/verify bip137 p2wpkh/p2sh', () { + final signature = priv.signBip137(StringUtils.encode(message), + mode: BIP137Mode.p2shP2wpkh); + + expect( + pub.verifyBip137Address( + message: StringUtils.encode(message), + signature: signature, + address: priv.getPublic().toP2wpkhInP2sh()), + true); + }); }); group('TestP2pkhAddresses', () { diff --git a/test/p2kh_tr_test.dart b/test/p2kh_tr_test.dart index eb45ad4..c394850 100644 --- a/test/p2kh_tr_test.dart +++ b/test/p2kh_tr_test.dart @@ -118,7 +118,7 @@ void main() { BitcoinOpcode.opEqualVerify, BitcoinOpcode.opCheckSig, ])); - final sig = sk.signInput(digit); + final sig = sk.signECDSA(digit); txin.scriptSig = Script(script: [sig, sk.getPublic().toHex()]); expect(tx.serialize(), coreTxSignedResult); }); @@ -127,7 +127,7 @@ void main() { BtcTransaction(inputs: [txin], outputs: [txout, changeLowSTxout]); final digit = tx.getTransactionDigest( txInIndex: 0, script: fromAddr.toScriptPubKey()); - final sig = sk.signInput(digit); + final sig = sk.signECDSA(digit); txin.scriptSig = Script(script: [sig, sk.getPublic().toHex()]); expect(tx.serialize(), coreTxSignedLowSSigallResult); }); @@ -138,7 +138,7 @@ void main() { txInIndex: 0, script: fromAddr.toScriptPubKey(), sighash: BitcoinOpCodeConst.sighashNone); - final sig = sk.signInput(digit, sigHash: BitcoinOpCodeConst.sighashNone); + final sig = sk.signECDSA(digit, sighash: BitcoinOpCodeConst.sighashNone); txin.scriptSig = Script(script: [sig, sk.getPublic().toHex()]); expect(tx.serialize(), coreTxSignedLowSSignoneResult); expect(tx.txId(), coreTxSignedLowSSignoneTxid); @@ -151,7 +151,7 @@ void main() { script: sigFromAddr1.toScriptPubKey(), sighash: BitcoinOpCodeConst.sighashSingle); final sig = - sigSk1.signInput(digit, sigHash: BitcoinOpCodeConst.sighashSingle); + sigSk1.signECDSA(digit, sighash: BitcoinOpCodeConst.sighashSingle); sigTxin1.scriptSig = Script(script: [sig, sigSk1.getPublic().toHex()]); expect(tx.serialize(), sigSighashSingleResult); }); @@ -163,13 +163,13 @@ void main() { script: sigFromAddr1.toScriptPubKey(), sighash: BitcoinOpCodeConst.sighashAll); final sig = - sigSk1.signInput(digit, sigHash: BitcoinOpCodeConst.sighashAll); + sigSk1.signECDSA(digit, sighash: BitcoinOpCodeConst.sighashAll); final digit2 = tx.getTransactionDigest( txInIndex: 1, script: sigFromAddr2.toScriptPubKey(), sighash: BitcoinOpCodeConst.sighashAll); final sig2 = - sigSk2.signInput(digit2, sigHash: BitcoinOpCodeConst.sighashAll); + sigSk2.signECDSA(digit2, sighash: BitcoinOpCodeConst.sighashAll); sigTxin1.scriptSig = Script(script: [sig, sigSk1.getPublic().toHex()]); sigTxin2.scriptSig = Script(script: [sig2, sigSk2.getPublic().toHex()]); expect(tx.serialize(), signSighashAll2in2outResult); @@ -182,13 +182,13 @@ void main() { script: sigFromAddr1.toScriptPubKey(), sighash: BitcoinOpCodeConst.sighashNone); final sig = - sigSk1.signInput(digit, sigHash: BitcoinOpCodeConst.sighashNone); + sigSk1.signECDSA(digit, sighash: BitcoinOpCodeConst.sighashNone); final digit2 = tx.getTransactionDigest( txInIndex: 1, script: sigFromAddr2.toScriptPubKey(), sighash: BitcoinOpCodeConst.sighashNone); final sig2 = - sigSk2.signInput(digit2, sigHash: BitcoinOpCodeConst.sighashNone); + sigSk2.signECDSA(digit2, sighash: BitcoinOpCodeConst.sighashNone); sigTxin1.scriptSig = Script(script: [sig, sigSk1.getPublic().toHex()]); sigTxin2.scriptSig = Script(script: [sig2, sigSk2.getPublic().toHex()]); expect(tx.serialize(), signSighashNone2in2outResult); @@ -202,8 +202,8 @@ void main() { sighash: BitcoinOpCodeConst.sighashAll | BitcoinOpCodeConst.sighashAnyoneCanPay); - final sig = sigSk1.signInput(digit, - sigHash: BitcoinOpCodeConst.sighashAll | + final sig = sigSk1.signECDSA(digit, + sighash: BitcoinOpCodeConst.sighashAll | BitcoinOpCodeConst.sighashAnyoneCanPay); final digit2 = tx.getTransactionDigest( @@ -211,8 +211,8 @@ void main() { script: sigFromAddr2.toScriptPubKey(), sighash: BitcoinOpCodeConst.sighashSingle | BitcoinOpCodeConst.sighashAnyoneCanPay); - final sig2 = sigSk2.signInput(digit2, - sigHash: BitcoinOpCodeConst.sighashSingle | + final sig2 = sigSk2.signECDSA(digit2, + sighash: BitcoinOpCodeConst.sighashSingle | BitcoinOpCodeConst.sighashAnyoneCanPay); sigTxin1.scriptSig = Script(script: [sig, sigSk1.getPublic().toHex()]); sigTxin2.scriptSig = Script(script: [sig2, sigSk2.getPublic().toHex()]); diff --git a/test/p2sh_test.dart b/test/p2sh_test.dart index 122efe8..dd64279 100644 --- a/test/p2sh_test.dart +++ b/test/p2sh_test.dart @@ -59,7 +59,7 @@ void main() { final tx = BtcTransaction(inputs: [txin], outputs: [txout]); final digit = tx.getTransactionDigest( txInIndex: 0, script: fromAddr.toScriptPubKey()); - final sig = sk.signInput(digit); + final sig = sk.signECDSA(digit); txin.scriptSig = Script(script: [sig, sk.getPublic().toHex()]); expect(tx.serialize(), createP2shAndSendResult); }); @@ -68,7 +68,7 @@ void main() { final tx = BtcTransaction(inputs: [txinSpend], outputs: [txout2]); final digit = tx.getTransactionDigest(txInIndex: 0, script: p2pkRedeemScript); - final sig = p2pkSk.signInput(digit); + final sig = p2pkSk.signECDSA(digit); txinSpend.scriptSig = Script(script: [sig, p2pkRedeemScript.toHex()]); expect(tx.serialize(), spendP2shResult); }); @@ -90,7 +90,7 @@ void main() { final tx = BtcTransaction(inputs: [txinSeq], outputs: [txout1]); final digit = tx.getTransactionDigest(txInIndex: 0, script: redeemScript); - final sig = skCsvP2pkh.signInput(digit); + final sig = skCsvP2pkh.signECDSA(digit); txinSeq.scriptSig = Script( script: [sig, skCsvP2pkh.getPublic().toHex(), redeemScript.toHex()]); diff --git a/test/p2tr_test.dart b/test/p2tr_test.dart index 5880a8d..e6f7d13 100644 --- a/test/p2tr_test.dart +++ b/test/p2tr_test.dart @@ -108,7 +108,7 @@ void main() { scriptPubKeys: [scriptPubKey2], amounts: [BigInt.from(3500)], sighash: signHash); - final signatur = fromPriv2.signTapRoot(txDigit, + final signatur = fromPriv2.signBip340(txDigit, treeScript: TaprootLeaf(script: trScriptP2pk1), sighash: signHash); tx = tx.copyWith(witnesses: [ TxWitnessInput(stack: [signatur]) @@ -130,7 +130,7 @@ void main() { txIndex: 0, tapleafScript: TaprootLeaf(script: trScriptP2pk1), ); - final sig = privkeyTrScript1.signTapRoot(digit, + final sig = privkeyTrScript1.signBip340(digit, sighash: BitcoinOpCodeConst.sighashDefault, tweak: false); final controlBlock = TaprootControlBlock.generate( xOnlyOrInternalPubKey: fromPub2.toXOnly(), @@ -207,7 +207,7 @@ void main() { tapleafScript: TaprootLeaf(script: trScriptP2pkA), ); - final sign = privkeyTrScriptA.signTapRoot( + final sign = privkeyTrScriptA.signBip340( txDigit, tweak: false, ); @@ -311,7 +311,7 @@ void main() { scriptPubKeys: allUtxosScriptPubkeys.map((e) => e).toList(), tapleafScript: TaprootLeaf(script: trScriptP2pkB), amounts: allAmounts.map((e) => e).toList()); - final sig = privkeyTrScriptB.signTapRoot(digit, tweak: false); + final sig = privkeyTrScriptB.signBip340(digit, tweak: false); final controlBlock = TaprootControlBlock.generate( xOnlyOrInternalPubKey: fromPub.toXOnly(), scriptTree: TaprootBranch( diff --git a/test/p2wpkh_test.dart b/test/p2wpkh_test.dart index be0aad8..4110d77 100644 --- a/test/p2wpkh_test.dart +++ b/test/p2wpkh_test.dart @@ -135,7 +135,7 @@ void main() { final tx = BtcTransaction(inputs: [txin1], outputs: [txout1]); final digit = tx.getTransactionDigest( txInIndex: 0, script: p2pkhAddr.toScriptPubKey()); - final sig = sk.signInput(digit); + final sig = sk.signECDSA(digit); txin1.scriptSig = Script(script: [sig, sk.getPublic().toHex()]); expect(tx.serialize(), createSendToP2wpkhResult); }); @@ -143,7 +143,7 @@ void main() { var tx = BtcTransaction(inputs: [txinSpend], outputs: [txout2]); final digit = tx.getTransactionSegwitDigit( txInIndex: 0, script: p2pkhRedeemScript, amount: txinSpendAmount); - final sig = sk.signInput(digit); + final sig = sk.signECDSA(digit); tx = tx.copyWith(witnesses: [ TxWitnessInput(stack: [sig, sk.getPublic().toHex()]) ]); @@ -156,13 +156,13 @@ void main() { ); final digit = tx.getTransactionDigest( txInIndex: 0, script: p2pkhAddr.toScriptPubKey()); - final sig = sk.signInput(digit); + final sig = sk.signECDSA(digit); txinSpendP2pkh.scriptSig = Script(script: [sig, sk.getPublic().toHex()]); final segwitDigit = tx.getTransactionSegwitDigit( amount: txinSpendP2wpkhAmount, script: p2pkhRedeemScript, txInIndex: 1); - final sig2 = sk.signInput(segwitDigit); + final sig2 = sk.signECDSA(segwitDigit); tx = tx.copyWith(witnesses: [ TxWitnessInput(stack: []), TxWitnessInput(stack: [sig2, sk.getPublic().toHex()]) @@ -176,7 +176,7 @@ void main() { script: p2pkhRedeemScript, amount: txin1SignoneAmount, sighash: BitcoinOpCodeConst.sighashNone); - final sig = sk.signInput(digit, sigHash: BitcoinOpCodeConst.sighashNone); + final sig = sk.signECDSA(digit, sighash: BitcoinOpCodeConst.sighashNone); tx = tx.copyWith(witnesses: [ TxWitnessInput(stack: [sig, sk.getPublic().toHex()]) ]); @@ -195,7 +195,7 @@ void main() { amount: txin1SigsingleAmount, sighash: BitcoinOpCodeConst.sighashSingle); final sig = - sk.signInput(digit, sigHash: BitcoinOpCodeConst.sighashSingle); + sk.signECDSA(digit, sighash: BitcoinOpCodeConst.sighashSingle); tx = tx.copyWith(witnesses: [ TxWitnessInput(stack: [sig, sk.getPublic().toHex()]) ]); @@ -214,8 +214,8 @@ void main() { amount: txin1SiganyonecanpayAllAmount, sighash: BitcoinOpCodeConst.sighashAll | BitcoinOpCodeConst.sighashAnyoneCanPay); - final sig = sk.signInput(digit, - sigHash: BitcoinOpCodeConst.sighashAll | + final sig = sk.signECDSA(digit, + sighash: BitcoinOpCodeConst.sighashAll | BitcoinOpCodeConst.sighashAnyoneCanPay); tx = tx.copyWith(inputs: [...tx.inputs, txin2SiganyonecanpayAll]); @@ -224,7 +224,7 @@ void main() { script: p2pkhRedeemScript, amount: txin2SiganyonecanpayAllAmount, sighash: BitcoinOpCodeConst.sighashAll); - final sig2 = sk.signInput(digit2, sigHash: BitcoinOpCodeConst.sighashAll); + final sig2 = sk.signECDSA(digit2, sighash: BitcoinOpCodeConst.sighashAll); tx = tx.copyWith(witnesses: [ TxWitnessInput(stack: [sig, sk.getPublic().toHex()]), TxWitnessInput(stack: [sig2, sk.getPublic().toHex()]) @@ -244,8 +244,8 @@ void main() { amount: txin1SiganyonecanpayNoneAmount, sighash: BitcoinOpCodeConst.sighashNone | BitcoinOpCodeConst.sighashAnyoneCanPay); - final sig = sk.signInput(digit, - sigHash: BitcoinOpCodeConst.sighashNone | + final sig = sk.signECDSA(digit, + sighash: BitcoinOpCodeConst.sighashNone | BitcoinOpCodeConst.sighashAnyoneCanPay); tx = tx.copyWith(inputs: [...tx.inputs, txin2SiganyonecanpayNone]); tx = tx.copyWith(outputs: [...tx.outputs, txout2SiganyonecanpayNone]); @@ -254,7 +254,7 @@ void main() { script: p2pkhRedeemScript, amount: txin2SiganyonecanpayNoneAmount, sighash: BitcoinOpCodeConst.sighashAll); - final sig2 = sk.signInput(digit2, sigHash: BitcoinOpCodeConst.sighashAll); + final sig2 = sk.signECDSA(digit2, sighash: BitcoinOpCodeConst.sighashAll); tx = tx.copyWith(witnesses: [ TxWitnessInput(stack: [sig, sk.getPublic().toHex()]), TxWitnessInput(stack: [sig2, sk.getPublic().toHex()]) @@ -274,8 +274,8 @@ void main() { amount: txin1SiganyonecanpaySingleAmount, sighash: BitcoinOpCodeConst.sighashSingle | BitcoinOpCodeConst.sighashAnyoneCanPay); - final sig = sk.signInput(digit, - sigHash: BitcoinOpCodeConst.sighashSingle | + final sig = sk.signECDSA(digit, + sighash: BitcoinOpCodeConst.sighashSingle | BitcoinOpCodeConst.sighashAnyoneCanPay); tx = tx.copyWith(witnesses: [ TxWitnessInput(stack: [sig, sk.getPublic().toHex()]) diff --git a/test/p2wsh_test.dart b/test/p2wsh_test.dart index 27229e5..91e6c03 100644 --- a/test/p2wsh_test.dart +++ b/test/p2wsh_test.dart @@ -73,7 +73,7 @@ void main() { final tx = BtcTransaction(inputs: [txin1], outputs: [txout1]); final digit = tx.getTransactionDigest( txInIndex: 0, script: p2pkhAddr.toScriptPubKey()); - final sig = sk1.signInput(digit); + final sig = sk1.signECDSA(digit); txin1.scriptSig = Script(script: [sig, sk1.getPublic().toHex()]); expect(tx.serialize(), createSendToP2pkhResult); }); @@ -84,8 +84,8 @@ void main() { ); final digit1 = tx.getTransactionSegwitDigit( txInIndex: 0, script: p2wshRedeemScript, amount: txinSpendAmount); - final sig1 = sk1.signInput(digit1); - final sig2 = sk2.signInput(digit1); + final sig1 = sk1.signECDSA(digit1); + final sig2 = sk2.signECDSA(digit1); final pk = p2wshRedeemScript.toHex(); // tx.addWitnesses(TxWitnessInput(stack: ['', sig1, sig2, pk])); tx = tx.copyWith(witnesses: [ @@ -100,13 +100,13 @@ void main() { ); final digit1 = tx.getTransactionDigest( txInIndex: 0, script: p2pkhAddr.toScriptPubKey()); - final sig1 = sk1.signInput(digit1); + final sig1 = sk1.signECDSA(digit1); txin1Multiple.scriptSig = Script(script: [sig1, sk1.getPublic().toHex()]); // tx.addWitnesses(TxWitnessInput(stack: [])); final seqwitDigit = tx.getTransactionSegwitDigit( txInIndex: 1, script: p2wshRedeemScript, amount: txin2MultipleAmount); - final sigP2sh1 = sk1.signInput(seqwitDigit); - final sigP2sh2 = sk2.signInput(seqwitDigit); + final sigP2sh1 = sk1.signECDSA(seqwitDigit); + final sigP2sh2 = sk2.signECDSA(seqwitDigit); // tx.addWitnesses(TxWitnessInput( // stack: ['', sigP2sh1, sigP2sh2, p2wshRedeemScript.toHex()])); @@ -114,7 +114,7 @@ void main() { txInIndex: 2, script: p2pkhAddr.toScriptPubKey(), amount: txin3MultipleAmount); - final sig3 = sk1.signInput(segwitDigitIndex2); + final sig3 = sk1.signECDSA(segwitDigitIndex2); // tx.addWitnesses(TxWitnessInput(stack: [sig3, sk1.getPublic().toHex()])); tx = tx.copyWith(witnesses: [ TxWitnessInput(stack: []), diff --git a/test/silent_payments.dart b/test/silent_payments.dart new file mode 100644 index 0000000..003d455 --- /dev/null +++ b/test/silent_payments.dart @@ -0,0 +1,213 @@ +// ignore_for_file: non_constant_identifier_names +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +import 'package:bitcoin_base/src/bitcoin/address/address.dart'; +import 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; +import 'package:bitcoin_base/src/crypto/crypto.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:blockchain_utils/crypto/crypto/cdsa/point/base.dart'; +import 'package:test/test.dart'; + +// G , needed for generating the labels "database" +final G = ECPublic.fromBytes(BigintUtils.toBytes(Curves.generatorSecp256k1.x, length: 32)); + +main() { + final fixtures = + json.decode(File('test/fixtures/silent_payments.json').readAsStringSync(encoding: utf8)); + + for (var testCase in fixtures) { + test(testCase['comment'], () { + Map> sendingOutputs = {}; + + // Test sending + for (var sendingTest in testCase['sending']) { + List vinOutpoints = []; + List inputPrivKeyInfos = []; + List inputPubKeys = []; + + var given = sendingTest["given"]; + + for (var input in given['vin']) { + final prevoutScript = Script.fromRaw(hexData: input['prevout']['scriptPubKey']['hex']); + final privkey = ECPrivate.fromHex(input['private_key']); + + final vin = VinInfo( + outpoint: Outpoint(txid: input['txid'], index: input['vout']), + scriptSig: BytesUtils.fromHexString(input['scriptSig']), + txinwitness: TxWitnessInput( + stack: [], + scriptWitness: ScriptWitness( + stack: deserStringVector( + ByteData.sublistView( + Uint8List.fromList( + BytesUtils.fromHexString(input['txinwitness']), + ), + ), + ))), + prevOutScript: prevoutScript, + privkey: privkey, + ); + + vinOutpoints.add(vin.outpoint); + + final pubkey = getPubkeyFromInput(vin); + + if (pubkey == null || pubkey.getEncodeType() != EncodeType.comprossed) { + continue; + } + + inputPrivKeyInfos.add(ECPrivateInfo( + privkey, + prevoutScript.getAddressType() == SegwitAddressType.p2tr, + tweak: false, + )); + inputPubKeys.add(pubkey); + } + + if (inputPubKeys.isNotEmpty) { + List silentPaymentDestinations = + (given['recipients'] as List) + .map((recipient) => + SilentPaymentDestination.fromAddress(recipient[0], recipient[1].floor())) + .toList(); + + final spb = SilentPaymentBuilder(pubkeys: inputPubKeys, vinOutpoints: vinOutpoints); + sendingOutputs = spb.createOutputs(inputPrivKeyInfos, silentPaymentDestinations); + + List expectedDestinations = sendingTest['expected']['outputs']; + + for (final destination in silentPaymentDestinations) { + expect(sendingOutputs[destination.toString()] != null, true); + } + + final generatedOutputs = sendingOutputs.values.expand((element) => element).toList(); + for (final expected in expectedDestinations) { + final expectedPubkey = expected[0]; + final generatedPubkey = generatedOutputs.firstWhere((output) => + BytesUtils.toHexString(output.address.pubkey!.toCompressedBytes().sublist(1)) == + expectedPubkey); + + expect( + BytesUtils.toHexString( + generatedPubkey.address.pubkey!.toCompressedBytes().sublist(1)), + expectedPubkey); + } + } + } + + final msg = SHA256().update(utf8.encode('message')).digest(); + final aux = SHA256().update(utf8.encode('random auxiliary data')).digest(); + + // Test receiving + for (final receivingTest in testCase['receiving']) { + List vinOutpoints = []; + List inputPubKeys = []; + + final given = receivingTest["given"]; + final expected = receivingTest['expected']; + + List outputsToCheck = + (given['outputs'] as List).map((output) => output.toString()).toList(); + + final List receivingAddresses = []; + + final silentPaymentOwner = SilentPaymentOwner.fromPrivateKeys( + b_scan: ECPrivate.fromHex(given["key_material"]["scan_priv_key"]), + b_spend: ECPrivate.fromHex(given["key_material"]["spend_priv_key"])); + + // Add change address + receivingAddresses.add(silentPaymentOwner); + + Map? preComputedLabels; + for (var label in given['labels']) { + receivingAddresses.add(silentPaymentOwner.toLabeledSilentPaymentAddress(label)); + final generatedLabel = silentPaymentOwner.generateLabel(label); + + preComputedLabels ??= {}; + preComputedLabels[G.tweakMul(BigintUtils.fromBytes(generatedLabel)).toHex()] = + BytesUtils.toHexString(generatedLabel); + } + + for (var address in expected['addresses']) { + expect(receivingAddresses.indexWhere((sp) => sp.toString() == address.toString()), + isNot(-1)); + } + + for (var input in given['vin']) { + final prevoutScript = Script.fromRaw(hexData: input['prevout']['scriptPubKey']['hex']); + + final vin = VinInfo( + outpoint: Outpoint(txid: input['txid'], index: input['vout']), + scriptSig: BytesUtils.fromHexString(input['scriptSig']), + txinwitness: TxWitnessInput( + stack: [], + scriptWitness: ScriptWitness( + stack: deserStringVector( + ByteData.sublistView( + Uint8List.fromList( + BytesUtils.fromHexString(input['txinwitness']), + ), + ), + ))), + prevOutScript: prevoutScript, + ); + + vinOutpoints.add(vin.outpoint); + + final pubkey = getPubkeyFromInput(vin); + + if (pubkey == null || pubkey.getEncodeType() != EncodeType.comprossed) { + continue; + } + + inputPubKeys.add(pubkey); + } + + if (inputPubKeys.isNotEmpty) { + final spb = SilentPaymentBuilder(pubkeys: inputPubKeys, vinOutpoints: vinOutpoints); + + final addToWallet = spb.scanOutputs(silentPaymentOwner.b_scan, silentPaymentOwner.B_spend, + outputsToCheck.map((o) => getScriptFromOutput(o, 0)).toList(), + precomputedLabels: preComputedLabels); + + final expectedDestinations = 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.tweak; + final expectedPrivKeyTweak = expectedDestinations[i]["priv_key_tweak"]; + expect(privKeyTweak, expectedPrivKeyTweak); + + var fullPrivateKey = + silentPaymentOwner.b_spend.tweakAdd(BigintUtils.parse(privKeyTweak)); + + if (fullPrivateKey.toBytes()[0] == 0x03) { + fullPrivateKey = fullPrivateKey.negate(); + } + + // Sign the message with schnorr + final btcSigner = BitcoinSigner.fromKeyBytes(fullPrivateKey.toBytes()); + List sig = + btcSigner.signSchnorrTransaction(msg, tapScripts: [], tweak: false, auxRand: aux); + + // Verify the message is correct + expect(btcSigner.verifyKey.verifySchnorr(msg, sig, isTweak: false), true); + + // Verify the signature is correct + expect(BytesUtils.toHexString(sig), expectedDestinations[i]["signature"]); + + i++; + } + } + } + }); + } +}