diff --git a/README.md b/README.md index dbba79e..3f71e5b 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,20 @@ should be followed. The recovery tool supports recovering from both types of subaccounts. ## 2of2 Recovery -Coins held in a 2of2 account need to be signed by both you and GreenAddress. -Provided you have nLocktime emails enabled in your settings, the service -automatically generates special "nLockTime" transactions, pre-signed by -GreenAddress but not spendable until some time in the future (the nLockTime). - +Coins held in 2of2 accounts need to be signed by both you and Blockstream +Green (also known with the previous name Greenaddress). +At the moment, provided you have nLocktime emails enabled in your settings, the +service automatically generates special "nLockTime" transactions, pre-signed by +Blockstream Green but not spendable until some time in the future (the +nLockTime). +To recover these coins, refer to the 'nLocktime' section. +In the future, thanks to a new type of script (CSV) the coins will be spendable +by your keys only after a chosen amount of blocks has passed. +To recover these coins, refer to the 'CSV (Check Sequence Verify)' section. +Note that, in some cases, it may be needed to follow both procedures to recover +all coins. + +### nLocktime To recover coins from a 2of2 account you simply wait until each nLockTime transaction becomes spendable (90 days by default), then countersign using the recovery tool and broadcast. The coins are sent to a key derived @@ -88,6 +97,33 @@ using your full node via RPC or online tools such as: https://blockexplorer.com/tx/send https://www.smartbit.com.au/txs/pushtx +### CSV (Check Sequence Verify) +Unspent coins locked by CSV scripts (OP_CHECK_SEQUENCE_VERIFY, not to be +confused with the file extension `.csv`) in 2of2 subaccounts are discoverable +by scanning the blockchain to look for them. The recovery tool connects to your +Bitcoin Core full node in order to perform this scanning for you when +recovering. + +You will need: + +- A Bitcoin Core full node configured for local RPC access; the node wallet + functionality must not be disabled, indeed the coins will be sent to + addresses owned obtained from the node. +- The recovery tool +- Your Blockstream Green/GreenAddress mnemonic + +To run the recovery tool in 2of2-csv mode: +``` +$ garecovery-cli 2of2-csv -o garecovery.csv +``` + +Enter your mnemonic when prompted. The recovery tool will print a summary of the +recovery transactions and also write them to a file `garecovery.csv`. + +If any recoverable coins were found the tool will display a summary of +them on the console and write the details to the output csv file ready for +broadcasting using the same steps as detailed above for 2of2 nLocktime. + ## 2of3 Recovery *Note for 0.17 users:* it is now possible to specify `--ignore-mempool`, which makes the procedure much faster (by using `scantxoutset`). @@ -141,6 +177,41 @@ If any recoverable coins were found the tool will display a summary of them on the console and write the details to the output csv file ready for broadcasting using the same steps as detailed above for 2of2 subaccounts. +## Liquid Recovery + +In the case of Liquid subaccounts the outputs are locked by CSV +(OP_CHECK_SEQUENCE_VERIFY) scripts. Which means that coins can be spent either +by signing with your default key and the Green/GreenAddress key under +normal circumstances, or by signing with your default key only after a certain +amount of blocks. + +Unspent coins in Liquid subaccounts are only discoverable by scanning the +blockchain to look for them. The recovery tool connects to your Liquid/Elements +core full node in order to perform this scanning for you when recovering. + +You will need: + +- A Liquid/Elements core full node configured for local RPC access +- The recovery tool +- Your Green/GreenAddress Liquid mnemonic + +Instructions to run a Liquid node can be found +[here](https://docs.blockstream.com/liquid/quickstart.html). Ensure your node +is running, fully synced and you are able to connect to the RPC interface. You +can verify this using a command like: + + $ /path/to/elements-core/bin/elements-cli getblockchaininfo + +Run the recovery tool in CSV mode: + + $ garecovery-liquid-cli csv --network=liquid + +The tool will prompt you for your mnemonic. + +Unlike 2of3, wallet functionality must be available on your node. Indeed the +tool will create transactions sending the recovered funds to addresses obtained +from the node. + # Troubleshooting If you find any bugs, or have suggestions or patches, please raise them on diff --git a/garecovery/bin/garecovery-liquid-cli b/garecovery/bin/garecovery-liquid-cli new file mode 100755 index 0000000..8d86944 --- /dev/null +++ b/garecovery/bin/garecovery-liquid-cli @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +# PYTHON_ARGCOMPLETE_OK + +import sys +from garecovery import recoverycli + +sys.exit(recoverycli.main(sys.argv, is_liquid=True)) diff --git a/garecovery/bitcoin_config.py b/garecovery/bitcoin_config.py index cc31385..b2ad620 100644 --- a/garecovery/bitcoin_config.py +++ b/garecovery/bitcoin_config.py @@ -10,16 +10,19 @@ import configparser -DEFAULT_CONFIG_FILENAME = "~/.bitcoin/bitcoin.conf" +DEFAULT_CONFIG_FILENAME = { + False: "~/.bitcoin/bitcoin.conf", + True: "~/.elements/elements.conf", +} DUMMY_SECTION = 'X' class Config: """Parse bitcoin configuration file""" - def __init__(self, config_filename=None): + def __init__(self, config_filename=None, is_liquid=False): if config_filename is None: - config_filename = os.path.expanduser(DEFAULT_CONFIG_FILENAME) + config_filename = os.path.expanduser(DEFAULT_CONFIG_FILENAME[is_liquid]) self.config = configparser.ConfigParser() try: diff --git a/garecovery/bitcoincore.py b/garecovery/bitcoincore.py index 504bccf..1df1f7b 100644 --- a/garecovery/bitcoincore.py +++ b/garecovery/bitcoincore.py @@ -5,6 +5,7 @@ from . import bitcoin_config from . import exceptions +from gaservices.utils import gacommon import bitcoinrpc.authproxy @@ -54,7 +55,7 @@ class Connection: @staticmethod def read_config(keys, options): - config = bitcoin_config.Config(options.config_filename) + config = bitcoin_config.Config(options.config_filename, gacommon.is_liquid(options.network)) return {key: config.get_val(key) for key in keys} @staticmethod @@ -75,6 +76,8 @@ def get_http_auth_header(config, network): default_rpc_cookies = { 'testnet': '~/.bitcoin/testnet3/.cookie', 'mainnet': '~/.bitcoin/.cookie', + 'liquid': '~/.elements/liquidv1/.cookie', + 'localtest-liquid': '~/.elements/elementsregtest/.cookie', } rpccookiefile = os.path.expanduser(default_rpc_cookies[network]) logging.info('Reading bitcoin authentication cookie from "{}"'.format(rpccookiefile)) @@ -105,6 +108,8 @@ def __init__(self, args): default_rpc_ports = { 'testnet': 18332, 'mainnet': 8332, + 'liquid': 7041, + 'localtest-liquid': 7040, } config['rpcport'] = default_rpc_ports[args.network] logging.info('Defaulting rpc port to {}'.format(config['rpcport'])) diff --git a/garecovery/clargs.py b/garecovery/clargs.py index dc6e847..56cc8a6 100644 --- a/garecovery/clargs.py +++ b/garecovery/clargs.py @@ -20,29 +20,35 @@ DEFAULT_OFILE = 'garecovery.csv' +SUBACCOUNT_TYPES = { + False: ['2of2', '2of3', '2of2-csv'], + True: ['csv'], +} + + args = None -def set_args(argv): +def set_args(argv, is_liquid=False): global args - args = get_args(argv) + args = get_args(argv, is_liquid) -def get_args(argv): +def get_args(argv, is_liquid=False): parser = argparse.ArgumentParser( description="GreenAddress command line recovery tool", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument( 'recovery_mode', - choices=['2of2', '2of3'], - default='2of2', + choices=SUBACCOUNT_TYPES[is_liquid], + default=SUBACCOUNT_TYPES[is_liquid][0], help='Type of recovery to perform') parser.add_argument( '-n', '--network', dest='network', - choices=gaconstants.SUPPORTED_NETWORKS, - default='mainnet', + choices=gaconstants.SUPPORTED_NETWORKS[is_liquid], + default=gaconstants.SUPPORTED_NETWORKS[is_liquid][0], help="Network the coins belong to") parser.add_argument( '--mnemonic-file', @@ -78,7 +84,8 @@ def get_args(argv): help="Be verbose", action="store_const", dest="loglevel", const=logging.INFO) - rpc = parser.add_argument_group('Bitcoin RPC options') + rpc = parser.add_argument_group( + ('Elements' if is_liquid else 'Bitcoin') + ' RPC options') rpc.add_argument( '--rpcuser', @@ -105,73 +112,119 @@ def get_args(argv): type=int, help='Timeout in minutes for rpc calls') - two_of_two = parser.add_argument_group('2of2 options') - two_of_two.add_argument( - '--nlocktime-file', - help='Name of the nlocktime file sent from GreenAddress') - - two_of_three = parser.add_argument_group('2of3 options') - two_of_three.add_argument( - '--destination-address', - help='An address to recover 2of3 transactions to') - - two_of_three_xpub_exclusive = two_of_three.add_mutually_exclusive_group(required=False) - two_of_three_xpub_exclusive.add_argument( - '--ga-xpub', - help='The GreenAddress extended public key. If not provided the recovery tool will ' - 'attempt to derive it') - two_of_three_xpub_exclusive.add_argument( - '--search-subaccounts', - nargs='?', - const=DEFAULT_SUBACCOUNT_SEARCH_DEPTH, - type=int, - help='If --ga-xpub is not known it is possible to search subaccounts using this option') - - two_of_three_backup_key_exclusive = two_of_three.add_mutually_exclusive_group(required=False) - two_of_three_backup_key_exclusive.add_argument( - '--recovery-mnemonic-file', - dest='recovery_mnemonic_file', - help="Name of file containing the user's recovery mnemonic (2 of 3)") - two_of_three_backup_key_exclusive.add_argument( - '--custom-xprv', - help='Custom xprv (extended private key) for the 2of3 account. ' - 'Only required if an xpub was specified when creating the subaccount') - - advanced_2of3 = parser.add_argument_group('2of3 advanced options') - advanced_2of3.add_argument( - '--key-search-depth', - type=int, - default=DEFAULT_KEY_SEARCH_DEPTH, - help='When scanning for 2of3 transactions search this number of keys') - advanced_2of3.add_argument( - '--scan-from', - type=int, - dest='scan_from', - default=DEFAULT_SCAN_FROM, - help='Start scanning the blockchain for transactions from this timestamp. ' - 'Scanning the blockchain is slow so if you know your transactions were all after ' - 'a certain date you can speed it up by restricting the search range with this ' - 'option. Defaults to the inception time of GreenAddress. Pass 0 to scan the entire ' - 'blockchain.') - advanced_2of3.add_argument( - '--fee-estimate-blocks', - dest='fee_estimate_blocks', - type=int, - default=DEFAULT_FEE_ESTIMATE_BLOCKS, - help='Use a transaction fee likely to result in a transaction being ' - 'confirmed in this many blocks minimum') - advanced_2of3.add_argument( - '--default-feerate', - dest='default_feerate', - type=int, - help='Fee rate (satoshis per byte) to use if unable to automatically get one') + kwargs_option_search_subaccounts = { + 'dest': 'search_subaccounts', + 'nargs': '?', + 'const': DEFAULT_SUBACCOUNT_SEARCH_DEPTH, + 'type': int, + 'help': 'Number of subaccounts to search for', + } + kwargs_option_key_search_depth = { + 'dest': 'key_search_depth', + 'type': int, + 'default': DEFAULT_KEY_SEARCH_DEPTH, + 'help': 'When scanning for transactions search this number of keys', + } + kwargs_option_scan_from = { + 'dest': 'scan_from', + 'type': int, + 'default': DEFAULT_SCAN_FROM, + 'help': 'Start scanning the blockchain for transactions from this timestamp. ' + 'Scanning the blockchain is slow so if you know your transactions were all after ' + 'a certain date you can speed it up by restricting the search range with this ' + 'option. Defaults to the inception time of GreenAddress. Pass 0 to scan the entire ' + 'blockchain.', + } - advanced_2of3.add_argument( - '--ignore-mempool', - dest='ignore_mempool', - action='store_true', - help='Ignore the mempool when scanning the UTXO set for 2of3 transactions. ' - 'This enables the use of scantxoutset which makes recovery much faster.') + if is_liquid: + csv = parser.add_argument_group('CSV options') + csv.add_argument('--search-subaccounts', **kwargs_option_search_subaccounts) + + advanced_csv = parser.add_argument_group('CSV advanced options') + advanced_csv.add_argument('--key-search-depth', **kwargs_option_key_search_depth) + advanced_csv.add_argument('--scan-from', **kwargs_option_scan_from) + advanced_csv.add_argument( + '--split-unblinded-inputs', + dest='split_unblinded_inputs', + action='store_true', + help='If any unblinded input is found, split the inputs in two transactions, ' + 'one with blinded inputs and the other with the remaining. ' + 'Note that, if one of the two sets does not contain enough l-btc for the fees, ' + 'the tool may not be able to create the transaction.') + else: + two_of_two = parser.add_argument_group('2of2 options') + two_of_two.add_argument( + '--nlocktime-file', + help='Name of the nlocktime file sent from GreenAddress') + + two_of_three = parser.add_argument_group('2of3 options') + two_of_three.add_argument( + '--destination-address', + help='An address to recover 2of3 transactions to') + + two_of_three_xpub_exclusive = two_of_three.add_mutually_exclusive_group(required=False) + two_of_three_xpub_exclusive.add_argument( + '--ga-xpub', + help='The GreenAddress extended public key. If not provided the recovery tool will ' + 'attempt to derive it') + + kwargs_option_search_subaccounts['help'] += \ + '; if --ga-xpub is not known it is possible to search subaccounts using this option' + action_subaccounts = two_of_three_xpub_exclusive.add_argument( + '--search-subaccounts', **kwargs_option_search_subaccounts) + + two_of_three_backup_key_exclusive = two_of_three.add_mutually_exclusive_group( + required=False) + two_of_three_backup_key_exclusive.add_argument( + '--recovery-mnemonic-file', + dest='recovery_mnemonic_file', + help="Name of file containing the user's recovery mnemonic (2 of 3)") + two_of_three_backup_key_exclusive.add_argument( + '--custom-xprv', + help='Custom xprv (extended private key) for the 2of3 account. ' + 'Only required if an xpub was specified when creating the subaccount') + + advanced_2of3 = parser.add_argument_group('2of3 advanced options') + action_key = advanced_2of3.add_argument( + '--key-search-depth', **kwargs_option_key_search_depth) + action_scan = advanced_2of3.add_argument('--scan-from', **kwargs_option_scan_from) + action_fee_blocks = advanced_2of3.add_argument( + '--fee-estimate-blocks', + dest='fee_estimate_blocks', + type=int, + default=DEFAULT_FEE_ESTIMATE_BLOCKS, + help='Use a transaction fee likely to result in a transaction being ' + 'confirmed in this many blocks minimum') + action_default_feerate = advanced_2of3.add_argument( + '--default-feerate', + dest='default_feerate', + type=int, + help='Fee rate (satoshis per byte) to use if unable to automatically get one') + + action_scantxoutset = advanced_2of3.add_argument( + '--ignore-mempool', + dest='ignore_mempool', + action='store_true', + help='Ignore the mempool when scanning the UTXO set for 2of3 transactions. ' + 'This enables the use of scantxoutset which makes recovery much faster.') + + action_gdk_path = advanced_2of3.add_argument( + '--gdk-path', + dest='gdk_path', + action='store_true', + help='Use the derivation path used in GDK until version 0.0.30.') + + two_of_two_csv = parser.add_argument_group('2of2 csv options') + + two_of_two_csv._group_actions = [ + action_subaccounts, + action_key, + action_scan, + action_fee_blocks, + action_default_feerate, + action_scantxoutset, + action_gdk_path, + ] argcomplete.autocomplete(parser) result = parser.parse_args(argv[1:]) @@ -189,16 +242,22 @@ def arg_disallowed(name): if optval(name) is not None: parser.error('%s not allowed for mode %s' % (name, result.recovery_mode)) + if is_liquid: + return result + if result.recovery_mode == '2of2': arg_required('--nlocktime-file') for arg in ['--destination-address', '--ga-xpub', '--search-subaccounts', '--recovery-mnemonic-file', '--custom-xprv', '--default-feerate']: arg_disallowed(arg) - elif result.recovery_mode == '2of3': arg_disallowed('--nlocktime-file') arg_required('--destination-address') if optval('search_subaccounts') is None: arg_required('--ga-xpub', '--ga-xpub or --search-subaccounts') + else: + for arg in ['--destination-address', '--ga-xpub', '--recovery-mnemonic-file', + '--custom-xprv', '--nlocktime-file']: + arg_disallowed(arg) return result diff --git a/garecovery/exceptions.py b/garecovery/exceptions.py index dedb705..42e70a6 100644 --- a/garecovery/exceptions.py +++ b/garecovery/exceptions.py @@ -10,6 +10,10 @@ class ImportMultiError(GARecoveryError): pass +class InsufficientFee(GARecoveryError): + pass + + class InvalidDestinationAddressError(GARecoveryError): pass @@ -22,6 +26,18 @@ class InvalidNetwork(GARecoveryError): pass +class InvalidPrivateKey(GARecoveryError): + pass + + +class InvalidPublicKey(GARecoveryError): + pass + + +class MempoolRejectionError(GARecoveryError): + pass + + class NeedMnemonicOrGaXPub(GARecoveryError): pass diff --git a/garecovery/formatting.py b/garecovery/formatting.py index ab971eb..0b7c725 100644 --- a/garecovery/formatting.py +++ b/garecovery/formatting.py @@ -2,7 +2,7 @@ import collections import sys import wallycore as wally -from gaservices.utils import gaconstants, txutil +from gaservices.utils import gaconstants, txutil, gacommon from . import clargs from . import util @@ -132,9 +132,13 @@ def get_nlocktime(tx_wif, _): return format_nlocktime_string(current_blockcount, wally.tx_get_locktime(tx_wif[0])) def get_coin_value(tx_wif, idx): + if gacommon.is_liquid(clargs.args.network): + return '-' return btc(wally.tx_get_output_satoshi(tx_wif[0], idx), units) def get_total_value(tx_wif, idx): + if gacommon.is_liquid(clargs.args.network): + return '-' return btc(wally.tx_get_total_output_satoshi(tx_wif[0]), units) def get_bitcoin_address(tx_wif, idx): @@ -189,7 +193,8 @@ def get_raw_tx_column(): where value is <= 0 are shown as dust. """ def get_raw_tx_or_dust(tx_wif, _): - if wally.tx_get_total_output_satoshi(tx_wif[0]) == 0: + if not gacommon.is_liquid(clargs.args.network) and \ + wally.tx_get_total_output_satoshi(tx_wif[0]) == 0: return '** dust **' return txutil.to_hex(tx_wif[0]) return Column('raw tx', get_raw_tx_or_dust) diff --git a/garecovery/ga_xpub.py b/garecovery/ga_xpub.py index 3fdecfc..5846873 100644 --- a/garecovery/ga_xpub.py +++ b/garecovery/ga_xpub.py @@ -1,14 +1,19 @@ import struct -from gaservices.utils import gaconstants +from gaservices.utils import gaconstants, b2h, h2b import wallycore as wally from . import exceptions def get_bip32_pubkey(chaincode, key, network): - """Return a bip32 public key which can be either mainnet or testnet""" - ver = {'testnet': wally.BIP32_VER_TEST_PUBLIC, 'mainnet': wally.BIP32_VER_MAIN_PUBLIC}[network] + """Return a bip32 public key for the given network""" + ver = { + 'testnet': wally.BIP32_VER_TEST_PUBLIC, + 'mainnet': wally.BIP32_VER_MAIN_PUBLIC, + 'liquid': wally.BIP32_VER_MAIN_PUBLIC, + 'localtest-liquid': wally.BIP32_VER_TEST_PUBLIC, + }[network] public_key = key private_key = None return wally.bip32_key_init(ver, 0, 0, chaincode, public_key, private_key, None, None) @@ -18,8 +23,8 @@ def get_ga_root_key(network): """Return the GreenAddress root public key for the given network, or as set by options""" key_data = gaconstants.get_ga_key_data(network) return get_bip32_pubkey( - wally.hex_to_bytes(key_data['chaincode']), - wally.hex_to_bytes(key_data['pubkey']), + h2b(key_data['chaincode']), + h2b(key_data['pubkey']), network ) @@ -55,7 +60,7 @@ def gait_path_from_mnemonic(mnemonic): return get_gait_path(derived512) -def gait_paths_from_seed(seed): +def gait_paths_from_seed(seed, latest_only=False): """Get the paths for deriving the GreenAddress xpubs from a hex seed, rather than mnemonic This is an alternative derivation path used with hardware wallets where the mnemonic may not @@ -81,7 +86,9 @@ def gait_paths_from_seed(seed): # For historic reasons some old clients use a hexlified input path here - generate both path_input = chain_code + pub_key - path_input_hex = bytearray(wally.hex_from_bytes(chain_code + pub_key), 'ascii') + if latest_only: + return get_gait_path(path_input) + path_input_hex = bytearray(b2h(chain_code + pub_key), 'ascii') # Some clients use the master public key instead of one hardened derived from it chain_code_m = wally.bip32_key_get_chain_code(root_key) pub_key_m = wally.bip32_key_get_pub_key(root_key) diff --git a/garecovery/key.py b/garecovery/key.py new file mode 100644 index 0000000..0ad188e --- /dev/null +++ b/garecovery/key.py @@ -0,0 +1,170 @@ +from garecovery.exceptions import InvalidPrivateKey, InvalidPublicKey + +import wallycore as wally + + +class ECKey(object): + """Elliptic Curve key""" + + def __init__(self): + self.__prv = None + self.__pub = None + + @property + def prv(self): + return self.__prv + + @prv.setter + def prv(self, prv): + try: + wally.ec_private_key_verify(prv) + except ValueError: + raise InvalidPrivateKey + + self.__prv = prv + self.__pub = wally.ec_public_key_from_private_key(prv) + + @property + def pub(self): + return self.__pub + + @pub.setter + def pub(self, pub): + try: + wally.ec_public_key_verify(pub) + except ValueError: + raise InvalidPublicKey + if self.prv is not None: + raise ValueError('Cannot set public key if private is already set') + + self.__pub = pub + + def sign_compact(self, h): + """Produce a compact signature for (hashed) message h""" + if self.prv is None: + raise ValueError('Missing private key') + + return wally.ec_sig_from_bytes(self.prv, h, wally.EC_FLAG_ECDSA | wally.EC_FLAG_GRIND_R) + + def sign(self, h): + """Produce a DER signature for (hashed) message h""" + return wally.ec_sig_to_der(self.sign_compact(h)) + + def verify_compact(self, h, sig): + """Verify a compact signature""" + if self.pub is None: + raise ValueError('Missing public key') + + try: + wally.ec_sig_verify(self.pub, h, wally.EC_FLAG_ECDSA, sig) + except ValueError: + return False + return True + + def verify(self, h, sig): + """Verify a DER signature""" + try: + sig_compact = wally.ec_sig_from_der(sig) + except ValueError: + return False + return self.verify_compact(h, sig_compact) + + +class PubKey(bytes): + """Public key""" + + def __new__(cls, buf, eckey=None): + self = super(PubKey, cls).__new__(cls, buf) + if eckey is None: + eckey = ECKey() + eckey.pub = buf + self.eckey = eckey + return self + + def verify_compact(self, h, sig): + """Verify a compact signature""" + return self.eckey.verify_compact(h, sig) + + def verify(self, h, sig): + """Verify a DER signature""" + return self.eckey.verify(h, sig) + + +class Bip32Key(object): + """BIP32 key""" + + def __init__(self, extkey=None): + self.extkey = extkey + # TODO: handle missing private key + + @classmethod + def from_b58(cls, b58): + """Create a bip32 key from a base58 string""" + extkey = wally.bip32_key_from_base58(b58) + return cls(extkey) + + @classmethod + def from_seed(cls, seed, is_testnet=False): + """Create a bip32 key from a 128, 256, or 512 bit seed""" + version = {True: wally.BIP32_VER_TEST_PRIVATE, + False: wally.BIP32_VER_MAIN_PRIVATE}[is_testnet] + extkey = wally.bip32_key_from_seed(seed, version, wally.BIP32_FLAG_SKIP_HASH) + return cls(extkey) + + @classmethod + def from_mnemonic(cls, mnemonic, is_testnet=False): + """Create a bip32 key from a bip39 english mnemonic""" + try: + wally.bip39_mnemonic_validate(None, mnemonic) + except ValueError: + raise ValueError('Invalid mnemonic') + + _, seed = wally.bip39_mnemonic_to_seed512(mnemonic, None) + return cls.from_seed(seed, is_testnet) + + @property + def xprv(self): + return wally.bip32_key_to_base58(self.extkey, wally.BIP32_FLAG_KEY_PRIVATE) + + @property + def xpub(self): + return wally.bip32_key_to_base58(self.extkey, wally.BIP32_FLAG_KEY_PUBLIC) + + @property + def prv(self): + return wally.bip32_key_get_priv_key(self.extkey) + + @property + def pub(self): + return wally.bip32_key_get_pub_key(self.extkey) + + @property + def prvkey(self): + k = ECKey() + k.prv = self.prv + return k + + @property + def pubkey(self): + return PubKey(self.pub) + + def _derive(self, path, flags): + derived = wally.bip32_key_from_parent_path( + self.extkey, path, flags | wally.BIP32_FLAG_SKIP_HASH) + return Bip32Key(derived) + + def derive_prv(self, path): + """Derive private child key""" + return self._derive(path, wally.BIP32_FLAG_KEY_PRIVATE) + + def derive_pub(self, path): + """Derive public child key""" + return self._derive(path, wally.BIP32_FLAG_KEY_PUBLIC) + + def sign_compact(self, h): + """Produce a compact signature for (hashed) message h""" + return self.prvkey.sign_compact(h) + + def sign(self, h): + """Produce a DER signature for (hashed) message h""" + return self.prvkey.sign(h) diff --git a/garecovery/liquid_recovery.py b/garecovery/liquid_recovery.py new file mode 100644 index 0000000..06fad7c --- /dev/null +++ b/garecovery/liquid_recovery.py @@ -0,0 +1,237 @@ +from garecovery.subaccount import Green2of2Subaccount +from garecovery.key import Bip32Key +from garecovery.utxo import SpendableElementsUTXO +from garecovery.ga_xpub import gait_paths_from_seed +from garecovery.mnemonic import seed_from_mnemonic +from garecovery.exceptions import BitcoinCoreConnectionError, InsufficientFee, \ + MempoolRejectionError +from garecovery.util import get_current_blockcount +from garecovery import clargs +from garecovery import bitcoincore +from gaservices.utils import b2h, b2h_rev, h2b +from gaservices.utils.gaconstants import CSV_BUCKETS, LIQUID_EMPTY_TX_SIZE, LIQUID_OUTPUT_SIZE, \ + LIQUID_INPUT_SIZE + +import logging +import wallycore as wally + + +class LiquidRecovery(object): + + def __init__(self, mnemonic): + self.mnemonic = mnemonic + self.seed, _ = seed_from_mnemonic(mnemonic) + self.master_xprv = Bip32Key.from_mnemonic(mnemonic) + # only 1st one as Liquid does not need to be backward compatible + self.gait_path = gait_paths_from_seed(self.seed, latest_only=True) + + def get_utxos(self, outputs): + """Get utxos from a list of possible outputs""" + core = bitcoincore.Connection(clargs.args) + + version = core.getnetworkinfo()["version"] + if version < 180101: + raise BitcoinCoreConnectionError('Unsupported version') + + # using a descriptor with CSV is not possible + scanobjects = [{'desc': 'addr({})'.format(o.address)} for o in outputs] + result = core.scantxoutset('start', scanobjects) + if not result['success']: + raise BitcoinCoreConnectionError('scantxoutset failed') + + # add info for unblind + for u in result['unspents']: + blockhash = core.getblockhash(u['height']) + tx_hex = core.getrawtransaction(u['txid'], False, blockhash) + flags = wally.WALLY_TX_FLAG_USE_WITNESS | wally.WALLY_TX_FLAG_USE_ELEMENTS + tx = wally.tx_from_hex(tx_hex, flags) + u.update({ + 'address': u['desc'][5:-10], # stripping from "addr(
)#<8-char checksum>" + 'noncecommitment': b2h(wally.tx_get_output_nonce(tx, u['vout'])), + 'rangeproof': b2h(wally.tx_get_output_rangeproof(tx, u['vout'])), + }) + + # unblind and match keys with utxos + utxos = [SpendableElementsUTXO(u, o, self.seed) + for u in result['unspents'] + for o in outputs + if h2b(u['scriptPubKey']) == o.script_pubkey] + + logging.info('found {} utxos'.format(len(utxos))) + return utxos + + def scan_subaccount(self, subaccount_pointer, pointer_search_depth): + """Scan for utxos in a subaccount""" + subaccount = Green2of2Subaccount.from_master_xprv( + self.master_xprv.xprv, self.gait_path, subaccount_pointer, clargs.args.network) + logging.info('subaccount {}: start scanning'.format(subaccount_pointer)) + + start = 0 + utxos = [] + while True: + logging.info('subaccount {}: range {}-{}'.format( + subaccount_pointer, start, start + pointer_search_depth)) + outputs = [] + for pointer in range(start, start + pointer_search_depth): + for csv_blocks in CSV_BUCKETS[clargs.args.network]: + outputs.append(subaccount.get_csv_output(pointer, csv_blocks)) + + new_utxos = self.get_utxos(outputs) + logging.info('subaccount {}: found {} new utxos'.format( + subaccount_pointer, len(new_utxos))) + + if not new_utxos: + break + + utxos += new_utxos + start += pointer_search_depth + + logging.info('subaccount {}: stop scanning'.format(subaccount_pointer)) + return utxos + + # TODO: transaction may be too big, allow to split it + @staticmethod + def create_transaction(utxos): + core = bitcoincore.Connection(clargs.args) + + nlocktime = blockcount = get_current_blockcount() or 0 + is_replaceable = True + + balance = {} + estimated_vsize = LIQUID_EMPTY_TX_SIZE + inputs, used_utxos = [], [] + input_assets, input_values, input_abfs, input_vbfs = [], [], [], [] + + for u in utxos: + if not u.is_expired(blockcount): + blocks_left = u.output.csv_blocks + u.height - blockcount + logging.info('Skipping utxo ({}:{}) not expired ({} blocks left)'.format( + b2h_rev(u.txid), u.vout, blocks_left)) + continue + + asset = b2h_rev(u.asset) + if asset not in balance: + balance.update({asset: u.value}) + estimated_vsize += (LIQUID_INPUT_SIZE + LIQUID_OUTPUT_SIZE) + else: + balance.update({asset: balance[asset] + u.value}) + estimated_vsize += LIQUID_INPUT_SIZE + + inputs.append({'txid': b2h_rev(u.txid), 'vout': u.vout}) + input_assets.append(b2h_rev(u.asset)) + input_values.append(round(10**-8 * u.value, 8)) + input_abfs.append(b2h_rev(u.abf)) + input_vbfs.append(b2h_rev(u.vbf)) + used_utxos.append(u) + + if len(used_utxos) == 0: + return '', [] + + logging.info('num used utxos: {}'.format(len(used_utxos))) + + policy_asset = core.dumpassetlabels()['bitcoin'] + if policy_asset not in balance: + raise InsufficientFee('found assets ({}) but there are no fees to spend them'.format( + list(balance.keys()))) + + # TODO: allow different fee rates + feerate = float(core.getnetworkinfo().get('relayfee', 0.00001)) + fee = round(feerate * estimated_vsize * 10**-3, 8) + + map_amount = {'fee': fee} + map_asset = {} + for asset, value in balance.items(): + value_btc = round(10**-8 * value, 8) + if asset == policy_asset: + if value_btc <= fee: + raise InsufficientFee + # FIXME: consider trying to avoid floats + value_btc = round(value_btc - fee, 8) + + # FIXME: ideally we should accept either a list of addresses or an xpub/descriptor + address = core.getnewaddress() + map_amount.update({address: value_btc}) + map_asset.update({address: asset}) + + # use core rpc instead of wally mainly to delegate random number generation + transaction = core.createrawtransaction( + inputs, + map_amount, + nlocktime, + is_replaceable, + map_asset) + + # Note that if an output is unblinded the following call removes the nonce commitment + blinded_transaction = core.rawblindrawtransaction( + transaction, + input_vbfs, + input_values, + input_assets, + input_abfs) + + return blinded_transaction, used_utxos + + @staticmethod + def sign_transaction(blinded_transaction, used_utxos): + # TODO: use a wally_tx wrapper + flags = wally.WALLY_TX_FLAG_USE_WITNESS | wally.WALLY_TX_FLAG_USE_ELEMENTS + tx = wally.tx_from_hex(blinded_transaction, flags) + + # All sequence numbers must be set before signing + for index, u in enumerate(used_utxos): + u.set_csv_sequence(tx, index) + + blockcount = get_current_blockcount() or 0 + for index, u in enumerate(used_utxos): + assert u.is_expired(blockcount) + logging.debug('signing {}-th input'.format(index)) + u.sign(tx, index) + + logging.debug('signed tx: {}'.format(wally.tx_to_hex(tx, flags))) + return wally.tx_to_hex(tx, flags) + + @staticmethod + def test_transactions(transactions): + logging.info('testing {} transactions against mempool'.format(len(transactions))) + core = bitcoincore.Connection(clargs.args) + result = [] + for transaction in transactions: + # TODO: once core allows it, pass all transactions at once + result += core.testmempoolaccept([transaction]) + logging.info('testmempoolaccept results {}'.format(result[-1])) + # FIXME: consider filtering the unaccepted transactions instead of raising an error + if not all(d.get('allowed') for d in result): + raise MempoolRejectionError( + 'One or more txs rejected from mempool ({})'.format(transactions)) + + def get_transactions(self): + """Get one transaction per subaccount which includes at least one recovered utxo and it is + able to pay the fees""" + transactions = [] + for subaccount_pointer in range((clargs.args.search_subaccounts or 0) + 1): + utxos = self.scan_subaccount(subaccount_pointer, clargs.args.key_search_depth) + if not utxos: + continue + unblinded_utxos = [u for u in utxos if u.is_unblinded()] + if unblinded_utxos: + logging.warning('Found {} unblinded utxos.'.format(len(unblinded_utxos))) + if not clargs.args.split_unblinded_inputs and unblinded_utxos: + logging.warning('You may want to create two transactions with ' + '--split-unblinded-inputs') + utxo_sets = [utxos] + else: + utxos = [u for u in utxos if u not in unblinded_utxos] + utxo_sets = [utxos, unblinded_utxos] + + for us in utxo_sets: + transaction, used_utxo = self.create_transaction(us) + if transaction: + signed_transaction = self.sign_transaction(transaction, used_utxo) + transactions.append(signed_transaction) + + if transactions: + self.test_transactions(transactions) + + logging.debug('transactions: {}'.format(transactions)) + flags = wally.WALLY_TX_FLAG_USE_WITNESS | wally.WALLY_TX_FLAG_USE_ELEMENTS + return [(wally.tx_from_hex(tx, flags), None) for tx in transactions] diff --git a/garecovery/mnemonic.py b/garecovery/mnemonic.py index 8dbc57d..52aac1e 100644 --- a/garecovery/mnemonic.py +++ b/garecovery/mnemonic.py @@ -1,7 +1,7 @@ import wallycore as wally from . import exceptions - +from gaservices.utils import h2b wordlist_ = wally.bip39_get_wordlist('en') wordlist = [wally.bip39_get_word(wordlist_, i) for i in range(2048)] @@ -18,7 +18,7 @@ def seed_from_mnemonic(mnemonic_or_hex_seed): """ if mnemonic_or_hex_seed.endswith('X'): mnemonic = None - seed = wally.hex_to_bytes(mnemonic_or_hex_seed[:-1]) + seed = h2b(mnemonic_or_hex_seed[:-1]) else: mnemonic = mnemonic_or_hex_seed written, seed = wally.bip39_mnemonic_to_seed512(mnemonic_or_hex_seed, None) diff --git a/garecovery/output.py b/garecovery/output.py new file mode 100644 index 0000000..569a8cc --- /dev/null +++ b/garecovery/output.py @@ -0,0 +1,92 @@ +import wallycore as wally +from gaservices.utils.gaconstants import get_address_versions, CA_PREFIX + + +class GreenOutput(object): + """Base class for Green outputs""" + + def __init__(self, key, service_pubkey, network='mainnet'): + self.key = key # ECKey + self.service_pubkey = service_pubkey # bytes + self.network = network + + +class Green2of2CSVOutput(GreenOutput): + """P2SH-P2WSH-2of2-CSV Green output""" + + def __init__(self, key, service_pubkey, csv_blocks, network='mainnet'): + super(Green2of2CSVOutput, self).__init__(key, service_pubkey, network) + self.csv_blocks = csv_blocks + + def scriptpubkey_csv_2of2_then_1_fn(self, keys, csv_blocks, flags=0): + return wally.scriptpubkey_csv_2of2_then_1_from_bytes_opt(keys, csv_blocks, flags) + + @property + def witness_script(self): + keys = self.service_pubkey + self.key.pub + return self.scriptpubkey_csv_2of2_then_1_fn(keys, self.csv_blocks) + + @property + def witness_program(self): + return wally.sha256(self.witness_script) + + @property + def redeem_script(self): + return wally.witness_program_from_bytes(self.witness_script, wally.WALLY_SCRIPT_SHA256) + + @property + def script_pubkey(self): + return wally.scriptpubkey_p2sh_from_bytes(self.redeem_script, wally.WALLY_SCRIPT_HASH160) + + @property + def address(self): + script_hash = wally.hash160(self.redeem_script) + # FIXME: be more explicit + version = bytearray([get_address_versions(self.network)[1]]) + return wally.base58check_from_bytes(version + script_hash) + + def sign(self, h): + """Produce a DER encoded signature using the user key""" + return self.key.sign(h) + + @property + def script_sig(self): + return wally.script_push_from_bytes(self.redeem_script, 0) + + def get_signed_witness_stack(self, h, sighash): + return [ + None, + self.sign(h) + bytes([sighash]), + self.witness_script] + + def get_signed_witness(self, h, sighash=wally.WALLY_SIGHASH_ALL): + """Produce witness stack assuming CSV time is expired""" + return wally.tx_witness_stack_create(self.get_signed_witness_stack(h, sighash)) + + +class Green2of2CSVElementsOutput(Green2of2CSVOutput): + """P2SH-P2WSH-2of2-CSV Green Elements output""" + + # FIXME: consider adding a seed/master_blinding_key param + def __init__(self, key, service_pubkey, csv_blocks, network='liquid'): + super(Green2of2CSVElementsOutput, self).__init__(key, service_pubkey, csv_blocks, network) + + def scriptpubkey_csv_2of2_then_1_fn(self, keys, csv_blocks, flags=0): + return wally.scriptpubkey_csv_2of2_then_1_from_bytes(keys, csv_blocks, flags) + + def get_signed_witness_stack(self, h, sighash): + return [ + self.sign(h) + bytes([sighash]), + self.witness_script] + + def get_private_blinding_key(self, seed): + master_blinding_key = wally.asset_blinding_key_from_seed(seed) + return wally.asset_blinding_key_to_ec_private_key(master_blinding_key, self.script_pubkey) + + def get_public_blinding_key(self, seed): + return wally.ec_public_key_from_private_key(self.get_private_blinding_key(seed)) + + def get_confidential_address(self, seed): + ca_prefix = CA_PREFIX[self.network] + return wally.confidential_addr_from_addr( + self.address, ca_prefix, self.get_public_blinding_key(seed)) diff --git a/garecovery/recoverycli.py b/garecovery/recoverycli.py index 0a5d506..99c3e57 100644 --- a/garecovery/recoverycli.py +++ b/garecovery/recoverycli.py @@ -4,6 +4,7 @@ import wallycore as wally +from gaservices.utils.gacommon import is_liquid from . import clargs from . import exceptions from . import formatting @@ -11,7 +12,9 @@ wallet_from_mnemonic) from .two_of_two import TwoOfTwo +from .two_of_two_csv import TwoOfTwoCSV from .two_of_three import TwoOfThree +from .liquid_recovery import LiquidRecovery # Python 2/3 compatibility @@ -44,8 +47,13 @@ def get_recovery_mnemonic(args): def get_recovery(options, mnemonic, seed): - """Return an instance of either TwoOfTwo or TwoOfThree, depending on options""" - if options.recovery_mode == '2of3': + """Return an instance of either TwoOfTwo, TwoOfThree or LiquidRecovery, depending on options""" + if options.recovery_mode == 'csv': + if is_liquid(options.network): + return LiquidRecovery(mnemonic) + raise exceptions.InvalidNetwork( + 'recovery method {} is not available for this network'.format(options.recovery_mode)) + elif options.recovery_mode == '2of3': # Passing BIP32_VER_MAIN_PRIVATE although it may be on TEST. It doesn't make any difference # because they key is not going to be serialized version = wally.BIP32_VER_MAIN_PRIVATE @@ -57,15 +65,17 @@ def get_recovery(options, mnemonic, seed): backup_wallet = wallet_from_mnemonic(recovery_mnemonic) return TwoOfThree(mnemonic, wallet, backup_wallet, options.custom_xprv) - else: + elif options.recovery_mode == '2of2': return TwoOfTwo(mnemonic, seed, options.nlocktime_file) + else: + return TwoOfTwoCSV(mnemonic, seed) -def main(argv=None): +def main(argv=None, is_liquid=False): wally.init(0) wally.secp_randomize(os.urandom(wally.WALLY_SECP_RANDOMIZE_LEN)) - clargs.set_args(argv or sys.argv) + clargs.set_args(argv or sys.argv, is_liquid) logging.basicConfig(level=clargs.args.loglevel) try: diff --git a/garecovery/subaccount.py b/garecovery/subaccount.py new file mode 100644 index 0000000..c7c713c --- /dev/null +++ b/garecovery/subaccount.py @@ -0,0 +1,35 @@ +from garecovery.key import Bip32Key +from garecovery.output import Green2of2CSVOutput, Green2of2CSVElementsOutput +from garecovery.ga_xpub import derive_ga_xpub +from gaservices.utils.gacommon import get_subaccount_path, is_liquid + + +class GreenSubaccount(object): + """A Green subaccount""" + + def __init__(self, xprv, service_xpub, subaccount_pointer, network='mainnet'): + self.xprv = xprv # Bip32Key + self.service_xpub = service_xpub # Bip32Key + self.subaccount_pointer = subaccount_pointer + self.network = network + + @classmethod + def from_master_xprv(cls, master_xprv, gait_path, subaccount_pointer, network='mainnet'): + """Create Green subaccount from the wallet master private key""" + # FIXME: handle branch somewhere else + user_path = get_subaccount_path(subaccount_pointer) + [1] + xprv = Bip32Key.from_b58(master_xprv).derive_prv(user_path) + service_xpub = Bip32Key(derive_ga_xpub(gait_path, subaccount_pointer or None, network)) + return cls(xprv, service_xpub, subaccount_pointer, network) + + +class Green2of2Subaccount(GreenSubaccount): + """A Green 2of2 subaccount""" + + def get_csv_output(self, pointer, csv_blocks): + """Produce a CSV output""" + key = self.xprv.derive_prv([pointer]) + service_pubkey = self.service_xpub.derive_pub([pointer]).pub + if is_liquid(self.network): + return Green2of2CSVElementsOutput(key, service_pubkey, csv_blocks, self.network) + return Green2of2CSVOutput(key, service_pubkey, csv_blocks, self.network) diff --git a/garecovery/tests/test_bitcoincore_config.py b/garecovery/tests/test_bitcoincore_config.py index 9687eff..ac740c9 100644 --- a/garecovery/tests/test_bitcoincore_config.py +++ b/garecovery/tests/test_bitcoincore_config.py @@ -184,3 +184,20 @@ def test_too_old_version(mock_bitcoincore): output = get_output(args, expect_error=True) assert 'Bitcoin Core version too old, minimum supported version 0.16.0' in output + + +@mock.patch('garecovery.liquid_recovery.bitcoincore.AuthServiceProxy') +def test_too_old_version_liquid(mock_bitcoincore): + """Test Liquid asset recovery""" + mock_bitcoincore.return_value = AuthServiceProxy('liquid_txs', is_liquid=True) + mock_bitcoincore.return_value.getnetworkinfo = mock.Mock(return_value={'version': 169900}) + + args = [ + '--mnemonic-file={}'.format(datafile('mnemonic_1.txt')), + 'csv', + '--network=localtest-liquid', + '--search-subaccounts={}'.format(sub_depth), + ] + + output = get_output(args, expect_error=True, is_liquid=True) + assert 'Unsupported version' in output diff --git a/garecovery/tests/test_data/liquid_txs b/garecovery/tests/test_data/liquid_txs new file mode 100644 index 0000000..e69de29 diff --git a/garecovery/tests/test_data/raw_tx_1 b/garecovery/tests/test_data/raw_tx_1 new file mode 100644 index 0000000..92a2a9f --- /dev/null +++ b/garecovery/tests/test_data/raw_tx_1 @@ -0,0 +1,2 @@ +020000000102082b30cf538a47dd3b4ade4a59f849146f40839a320fb50643f3cf94ad7331c90000000017160014897e69d4c420a47a07479a732a42d213a7885159fdffffff03cc86ca865f3bb48a7a80f635f824ec88253a1655c6b925d10109914284963f00000000171600147fa8ebf4a2a2a37c2f899bb99abc3ee143cefaa6fdffffff030a5f0103e2c5b332ff766d489536fb57fad18e6f10cd23198721a85513489602bb097afc5314ca6352160ab736af7523a236a0f18bcbfe1e79f3e688368b35469a7e02ec7f8c3f26a6c4275fb92a2237476cbb31754b4f9ff0ef592f8564147811f5ef17a914d2924bcb2ddd0874fa87268ca53417ad102e8115870a2abb7f3bd51fc9406f07efa711dd95bfca680fcadfdef186b38455c6d1a30d85083b0a294ec44c8de4064773415cff13c49bbf626adf798365d1d86b0482aa342c029a20293ed662a36dc2abba585bc23d828a4ad90ff902b4cb61673e40082cc73517a914c900fefd0bffc5c0ab4836a56350730701b6f63a8701230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000147e00000400000000000247304402205a2463e133341d46d5aeece69691bab6178175a9be26e3eca76c1415fb97fdaa02207440716335ac73980e892d9abaf0e410103a4b68f5ef2ed6b72dced40f60f7eb01210326b8e4dfcef507671f0f2bea357f0eaa423b620310da5ffa97dafe3e72c40a4f00000002473044022074739135ccec7749ae1309e5e652b24032a310bd9341be87ad4735a381a4b41f022050c8e7462189f82dd9c567fb5797b4af445bd90578a7d6d65a176aea38c63e1a012102f2f5698e8d4bbd44cd82e5fec65521f1468f5037da5f6e04049e6144bac6b82d006302000378da8e57ae43354c58652de5ab66bdef0f49b18f7b12ff7b0afbcdd4c7533d5cadf41764b9e3df932fbadf53c83b12624a8697c63bb352550ff8440415258d14e2f4edb008f7a327b8b0233ec8b5d184a9718793e5f64e91028e7f9a1290648dfd4e106033000000000000000148f9c901c9feefdc753e200d95aad754c4e8cfc2fd707b5de35ffd18d9a63859a909c912720a631687acebb65d56c670c993c4c9f6702a7cb2d5f6eac530ea9d15ed5b5789ec6976825787a2f9ccdb0c820c9300cb25f8d708dffaa8d825ca30e5367542692186a2bb80b13a3d6876ab2a540da6afb771ddcde5ed1fd4aa4fc886df00230edd22c95a4bbe415ccb1f9cdb80389c7e3af9931738b534263ddfea33bfca9496eb20b6269b2e9cd20d93518124373eac149f3e5995b828289e8384dcec903a273733b24a8a8e5415cd82b06d1e2bdaf0c18ccd2a6bfbdca5c173f2093dd7ac78754ef00effe225f6962e81ef8f3048056d48543150e23cc33387aad36ef7cb37d25300a3b4ad28c744726a8fa8ce9d513bb5e4401f7f14838156e2f22dfefa6ce5e9c9c2c73732e6e3beb02e535250c4795b6b58e0d8afa9725af0e513c4d98776f4087df03f6870c920c14f1e786e4c7dfdb0b17d2262b3d39eed1b6be3609504f5334a711c880d02a7845f91e79f2688b1ed24fbd1fbe5d5ef5f989ca858130f2cafb23f0580f9b79df754f4ef221a006063e4c1d37c7dbfe4f91fd7efc9a2adfa03e48b382e6660be2202c6fb379c34009540865152cafa33e804964b1d8b55269a2f07f061eb83805680cd3872006252d4e67521955c2327ad02859ea29ab0758e6fca7d7507325931e6cb7aab29ff16dbc5821d5f967d3b65049dfe4b398ec6d6048332fcff76a1a1036bb94712ecf16a84d64ecce3ec03e778e8c3809485376ae85bcb891af5e18db8417b9d7fd267972ab7b6759ea58f821ba350b962d367515268f228cd49ebecec70acbc51aeeb6aa75233745f12289679455682425c9918b9f852f78de636c4060b62170d96b0402b1f94029f37bc9ec7efde3d2611cff4e1d718ae74d9925ff9d5790a1c2246ebe23dad54322f5c8e6c3cd29a9c4739473c8ba3bf9a5d6fceab82bf5b148d008190b213924b86a4f7d488541778be58ca7c52e5a66ddfc1ea3901be56dda9bc81bcbfe60a18aa906cc2838f91d04f8f1a8fbd44b386bad2d79c192909fc71f081718ac36371b9160c0f74accdeefec93bf8381276f0f4867e4246010072b02478908c19c8a633dcf815b1aacd9952b37d05646df94679f5b0c06a115582930c8f9055b13355c646f0762b48a7e32f6c68165b1822acb158d68ecd8a9dc03e36b0d3c69307156700d1dc9c28c1539ef8b6c9f742ad24b2fd69b4c74b3d2cb5a71ae863456dbef8dc79531925f3da8f67d7c5f24adc0d68e62933ea6783b6738ab375c15ea483fe17f640970b7fe014b77f0068a5970ea1e3194f88bc3b075f5658c03ea95bd5d1d65aee854d320b69ad983171c335acb5abbaa49beb0f288aa656d1384af538428099750e3865bb8179591e20634603474a4a8495e2687907f23cbdcea5b33397f78f39989b9c2d63baa4f8ccf1c5f7b3f91ee2500d1db712515cc57464124a8da53ed1c11b03875e7ca18af3d044dfd39dee092371702e4c519a6d9015bc5b05588f6f84da9ef758540a42afc5a5a875cbb77f96735ece50ef28a9ec3eb66afcd9b76ff56772e02db08255c5517be0c4fb59be791ab6718b10e1d3f1b6decf0f65fff57d5c0cd94a83e6873938c10fac159aced0bb85aaeff67de10900cf71ead07400a079cab8ddc1a858d73b275acc32726bdaa5b166aa01acd59112bd6270ce9c971c0df34fb8a59bcc1fc6e334b2de4df43612f810b1bc414882cb999f3d425782a81871582730e9e810d93c6704fb008adb114e6425478755f57f86e529d432e441e01ddaa9ee7759057015e3fed50683566cb50bdeb35a27c3bbdd6ca2a91e9baf0e82542501387460c9f5cf17cff23ec8a34879eae104c99d81205cb3651a72c7cbbd32af0c6cbcba811ef1388aa68b5fed2ba6c27f0bd7e0ded85aecb8a09e52ba660ad1a9ec647ccf8c8058bd080d9ffd379bab840370fdce99b969ef617f09fef500fa0f1d8887f88a94fdb38452060cef400ce1ccff89cdaf54d38418eae0aaf274f221840d2eb87923e7b8d1915ae3e85cf27a4e3af3701284af0b9811147eee2d593e131e779d0ada67c1d81864bc8580e5e423fa80578edcd274d5295165236a366a4fb494a1b7206b919712781f81ed6a8177fffa1c8eff0f47c9b28fd38aa680c78eb7fb754fc80d1b461d24c2cd93338c8f19b7f01e6dd5a550c7eab19c98e14d0dc5286b47aa85763ffac04f7f8263b0a0da9cc9be0670813440a4d003cab98ab8ac0ad1de419e6cea087a1ec144ce9023143f26379ae1f28f05e793394602c0a4054441b310a869eab9e5204bc70e83b6a6bc0f4842ea7b772bf1471ec89b549b218ab2e17c2ce9d54e796ead72af68f5df60eef602af550ec7f15563a36cbc3708f9ef9eddb3cf6b24754e13dae93f4489894cb1249f3d5e80c1b076fdd2d32a53daec64f656f322a31155c82f25cd07629a53392afc9c4117ada1fb8094461034f52c608c4d6457c1b01050c82eb98da2743bb404a5ae239f41c507d886f3f4a0716e426aa159854212dc46ce902d6ec2f40742d5a49bcabea3dca6eb6ced157797b479ee4139938fc0dc501504ee0a9f369f294f76c9509edb53f9307370c437354ac325717b3e8f5faf01c35333a1615fee82c9c736887f39579b5adf0df665a69db7b525e34f8fcc6fddfa7a91c72363d9933045edd184b79a045551de182cf7a2bc15c0f781cb11391c4b7f9689ae7d6facceb5574132c4f53a44404c7603803b007b0859c841d4a5d2fc5ac12ff7d0a61ae6b4bae83426ce92b0aff875bc45f32981e71a87daa26447a6993c9ca1f88836e51f718ca149f22f3377ec7392de1400993450917439b9c206715aee2cf85fbc8a6bed50fbba9e900e38cb36a278ddfc01acad0b9866a1fd28ba1328fc42d2a7113356ac856f777bb78cb1b92e4809897daeaa34505eb7ca4f56100a6eacc60e6e6371d56f9b9c5c860ff8244a625bd1e50af9dd3d2d7d88e7880af71c83092f4ecfb78c72585c673c88823b74c581a9d7eb60136599fb700194409f07de723e91f03d54ba5662e96e001c74633071796a7d44f5de8999f4730e2cd5c68869203470ee0d1c07aa16e0f269374150681160d0275848784bd5e14093653a392bd7856b5b7f8858bf85b5e07d2d558ebbc488aa73cbd298251a7721b3dc7495ec471f580f5e233593e50cf22fae41587052752de78a214111f5492a918ec05896063ac5d52312c33786039e3c64748a1819880d6d0e91974605c9eabbbbfbc4095eedb588a9cb66b988c4d0a9e85488f494df36504e2c1e1b94dab57d6b8cc24763efe65c2485e27cc0bb54ccbdc68109abcc62aaf6b92ad5f31aa74a95cd11bbbd1d708ae91f200d105bc7bcf1755e5020f8dd5dce4b270dca4941c32814fe5aecac44d5f666b1cb15ee0a74c3628539ced268e10d299be4513cfb315c66482b358bd0ffba2d36c859efcf1ff976f71daf18a941f588563b60656aea05d7b08e8c754b6bf085e5c12c24ba37dff0424f91974992cf097069123f664703c6aa3eb83e4396386e669c9ac1e7824f21d4109bcbf3c0f1da4ebe2ff63b8f2baa176b77961002086f589d6bc63013114f4d7c7d7437e579bcb5932749b25e90c2b7304749232032c034e6fd6f05b59697a76010c3aa7ba27d34cde1b4c205e2b5c1ab617d815d5d6a200f38da2e3059fae4b4e4233abdf58bd0f43df52290ee3381fc18f5f92422286646b67dc319b091b2e4eaa1f7fa9eea64056e11f6423a2c3ad30de48802ffa1951e09dffafbb27bda3978ef8a3d8ff8f38e5a3b1f5dacf96a2a1869b54b0218569e1881ef01b4a4bee4d8332d50697cc312b74d5cc221d249e923973921955efe7a0ea15d2007d563104451eb8d6f58938b057b832854ba29237f198eba48f7914b200b2e1d4dd9163635a75458ae5852ea161971884e327a6b055cbadc3e0c9999823e9a4cc961008c1df830aafdec890b86f9173c21cb04ac882bb4f79340c728cdfd98dce3008452a39e450d5d180f32a29f172c94ec808382deb721ed3ced45f5e92c1e953a263f2dd909dc9475cd993a9bbd47664d96c9d6c6968ec748639e73bbbe858bc96df6ab9c9f20579b483816c10780d52b941aaed49ac5dee89cfce0e3d57b63ac291c5451d1daf8b652b300335c97ea666c5e21a3378a3e2f495a5f5fe77af76b178b268c896f8ffbb9994c63d5cf8e9480e88d0dac64e40715510be0d6827d61ce7e71251ba3f74e7a54b58faef57d4581ef7ffcf45bc6d96c4ac62008705cc5f6ddc51cef1006e977860b12a5eaf975319e41f2cc299fed052d710deafc42a89d381f507107def5e9abe9f5d704ec1e49e0d91cfcc7d9bff1de251909ca0dc24b6e7ecc732c3eb4d9188e0aac012320e3f0833321bf8db7fab3e1f7f8994454d773e53734d87d21d891574da0fb87c3dba664835c111b002dfd0cceec02f3ecb732e78effd6c5c18a222fba4a608283b67d2a99e5c061ffd41490def807cf1c407da3dd71549a3a4026178b9d9ade373ecd3438b57bcb56a628f634938830d41d416e7bafd091f583d88fbf5e662c4713b98d61dfc1f603fef110992ff3119ab022516a829ea9adb19a06183ef05858ebfdbc4a611f538166845e68d69a1fa5cc33fd1ecbe8af04c0d2080eee1d49bb8e694faa563b5ba731686f0a22a707195075a72a3852adc1b96cd5e3f9ea8c30540ea286790a8ab0d2c5b00f3425840d67d986474e44090255c3e21d47dcd40fd13f17561800a60be27f1a74b2228d0cfaaae9a903184152710e6af20a7540486530ca3c8bb5992004cebed0de05b0a9814e6874f05a22256c3a62d7c1fdd4013bb9673941e605818b60f3324c5f14826eabea530c48bf3217589e8b6195689230699405d8f1a64138609489a4cf03d612429726e2b6107a85d1d1dcbb4efc0b24b2f239aa03a777b8567ac7ec08ee8db8f504e820984856def654342307fe9b534f4a6c2aead7edc59309dbfc48604fc76a960694688f12148b40b28b5d5875d2d72e2bd82b64801051f71102d21530499b2d4aab43c783f22ddcc3fd2048b7a23e8c29885265c57e38b7916dae92f122b9d1006c0bc38fc365aad2cef5d659bcd0a23ebc41fb242f497eba1c946de18b0d76d07302117dabe42414c95ccaeda0d9810432e23ccad65470236a9b1ab83f1ff1f382342e4a7e3a5d68576094e51358a730f3363e3ce85f12df6f64c854cb69a93ebc82d9a111c7d67db105b45198e5b90346b8d85740196dcb4286a4b4d559b6c2244368a59c7a4d60a4b9dac4a0ebecd5a35a5265aa4b6f016172b96335f8f28a7d45e6d6fccb505d0675ecb0c59c46dee5406bc5db3488fddb938d7d689f244e0fd1aaac00e5dd6ec4e3b4221f3143f1345798e176ff675ec40c57fe57f924a10529ec84238ea68dda373db1f6db85bb9683ccb257f432df435e8bf9115aaea9eac30a6f75649768539bfa056beefbcaa645966ded3b75c63512965dfe29bbbd91fc5d967aca16b38d2fa718ecf9e10f2b3c460952fcd487a387dbbf9b0a12079006029adbde22e3db3d895db01d5769bdecd81f552858e5512d2811815e5edd9e4a9e91a0e204f8a09f85d28249279744e3ee2c521ca6b90af8ac1dda1a91befcd5cc757655ceda605f2eb941c699a041c67ecae2225d475acbd5489fdef86a27b783534d96aaab03dd491e4961095cc0840663608929f02653458e8466ed8721411e822d56f5b56c2d3f07b70de4048e7f112c36191aed996b6d974f5837d4d920f69e7b4d0f849479ed0db4b841058b317f5db636b13af2c705c554fc2abccd4be0da5b630bb96ada3846aefb13f351e1fa2f66da626302000309827f336bb7771055c7c5f9b9c1490b1defb290482f2ebd9b9bdd3020d9c2ceba5dbb1e4b1a4e05346e92d3459bcc2ea5a80004559e8311c3d92335495dee16e943b934414dc9cf7bdbcc189c530c854a29ab0d5f874a2693b9304b66ab83d8fd4e10603300000000000000012a063e00cd34ab6478141107ba6ab9959891ee6b9e1826d9ed0362785bc55b227c8b858d9247bb1d0473aa312ed7ec3ae455a2fc8184dc8323e1f7dcecb05d12a50df604a64f3418cb96615fed3db31ff7844ebc3f9ecd57e1ad408587976f1ed33bfb564f783eb5974cbed793683ed3be6294e62fd39b6f735aaaef72fdc4e49831e3ec62f85251ecaa48d18f07ef4d159190df66ce49566cbf5f8a932e1cdfe6f9671f481bdb04a95b607413bb18af534b7ee813bb988392ac1b4155a4626c7b8ae4905b3488f95a51fceae2aadd24ba0f3a315ad30dd6acae295e3484cb9a2fc86160b578d12b0bf8f5ed442529ecaad1b8c920c57c14a1e4dcd85da0979d8111120f6e41362ff3e600c8c3f90b5fa1330bd6ffe1977e9d694c1a1167fd6e1a59e399ce946693b9c73ae7eb185a5a7bfd88c48112b0b918263204c039970fff002daa28868829d0aa367b54a125c4a991b512d521f3680ce0ae764fffc8bd2cd78b0c1bde4716140b7f1e5a9c245e4edf5bae50de5ef0698046b47dd76b66051e1b504cd6d2fb6adba6ddb062d4b07b99b1b1af4fccd4aa33b02618a0d7e0e458800be218ff42930452b77f6a7ec388fc69ab4cc486a7cad1a5d61f4ed8c36620755e157a887ba172857feb8445950d53972cec8693f8eb445e45fb5e6b3ca76c52c2fb213b03e125a6846be4ac415ec351f58e601a9e1b0041925a073db295e8ae7cd1367c051744aa65f7d35f82f201a61b7cd9decbbf5d1142e327e64baf73edf0471cd24a8b291557dc62dec8d6dc621c4ca9bad89c0599dc5843a62ecbce9418c17544cf8b12365fcdb59492b25b9d3c4b18645842c2f697de19019dabb523435cade4d6b1ffd580a8f43c391992c29e4d2c14fda86f0413bb97b0a4159111eb62309f80ca133e355d71a870d45023c1d3c01a1dac6762171cec6ab4b1c361dbcc99c5b57cf66cb204a67b9709305ac608a77277e78eb878be3822524d16c4d42ef4338f5af0527dd1ed4f0d81c5b8977d9ed84d85690583ae46b69c8eb5263fe93e5ed6a3ef5b27cc2e9e79611727ec3cac69664dc6aee14973fe55e9b35c0975a9f816d92e26441af05016ec29d7c475d2d4a186a370103e0e6dbd354ce06769c093217b0c170d2eed25cb13a685dc3994e3f0d85c24cf3b61e12c91b5edc3fe3c2c41ccbfe8d316e51d3db800435d505a9c6a5e2bf4d9539327717b0089095f0fcce18c918c7a5567aa455f97a98cf2fc090649e3ad1901c7fa0e4ba626ff804ee7b5d74d1ad438a161a12779e83799e48dfd14905b0be9c50f16f784235b901e0589c3a628f8b3c0b978c19a3f7f80a7b6bb8beb9af1f4448080d055e9e3a3a35fcb57839dc9a144276b2316009beff221d1a9ca1a4108b8172f88d9af51a6bc3c0b436397e992b81b1e8beef14e0739991ebdf4139c27d211a75fd8e7d3e4c16db792c638aca875f2285ee1dfd0683c0abdc571afc3036615514d983c76ec204514c8f5b156acc8d390e5e9f3bb3390f6cb96128ceab66d5136e07558b756d22b2370d13f9910be77382677019ee1b359c9cacf780a86a80ac87492b0456b02bf10edcad0c957fc6599cfdf42f81a6f1f59214367813892750c0e46127be973e5945f2efe588613be4e7b4ef854242f4b0a4f243fa6169c8facd748b2bfc90d461a21bbb80f05da837dabb4ddbf5de2678b0c8821d190c9ceac28adf53d612d285cde514e41f6c1220b48042032d9cf694fed553f4e70e4f6bf621e20fbef079399a4e579d9815896ecf5e542c228b32dad6c9c7de7abc53598af27451618994f9eb2b87cae494b3bee6ef13ab38588311891fc2a482dff6575ab969bf1c3b34e52695d9e538f6ada972d4aa30a3a65d856a3ca6afe525414927420d40e31f51af2b84b663481d3e72c1973b06b469174d5b31b73c0b682ae629ce197ba4c9b22ea5bdf92e5cd9096015feca920e71cbad8afe2f3542812dfa060c834cd7ecfafadf2f403575e04546bf5bc20bd599ff0ea8b1a432b39095ab8865fd620723190b6eb2e11d706f5b77f853345956da0ac865fde5d0943e5b9c7b130302a4ae59a0ca7a2b43f5cf288940f4191bfe628d39f7d603aab825e61e5dbabbd3011678e8339c344e2910f0bb6e11434be64e5075b3e3d3644c7afcb1e5bb049b6d696fff37666fec7567806180e8e53ae7aa7317b5b006b0220908c3c578fd29ea0f09a51a9973504faed26a00da9e47507fc99814f1e6572b5ce8f7565225449711d98bddfcc36eafafdeb24016d4b8c0e301c501c8b6826ac460c4d2e92fe4d09b6fe7dc790bdfb7b178369ca9ea324f3d141f6d859e37f420a5ed8df1d8f249b9817c22248bc2be1052905e8ac7dfa66e04b3be8f81c6625782c36c41070af518dc51319ac8d2adb467b33142729f24842142d7f68fe282917e486efbeb4cae698d03964e05b80a1f63895fd19679617cd50ba663167030b4a413c27d5bbc6b3a4707c507faaafaf04744fd200827580d3306c20f1d106eddaabf72defdc54124ea6d9cb69facb71327abaafc3a142d78fe94389aef85a7d5c64a5d35edd5305e0e9ae64722f072ce598fa736b1bd04fba9cdeb04dec50f1d9a63ecbd0b85421a54c4db4cae005c5d89b4692fbe709d37d0d7b423549eceaa0a13f41140020558bcce0fee817acfbc365dad92e3d511426e355ee7a48e44a042cd57b9ea07bdf4c216ffcfe71027fb0081000eb58c69c5ff4601458b2df3febeca607032cd0e6615088462b84cb059eefb2b3fead8e51a075a775d98b955952dfd83895b37d102b0ea525071f64219d558c91c139e50bce0b00e969cac9cc02027489eed3c2862f5a1d687dfbb9a31c2606fa8d7d268533b18cea97138db62e329fe41036d8045619ae1cd2685fef1b470c249d0860d4b8bc7d4a14489e05e7ecaec41d39d8d5fdaba23f5949dd3383cfcdcd1de1312cbf5aaf66b58fa3da29e7fc25540dc257ecb93db7baa47d4ea740e75fba2d87ff6c862ff6acb5d7e6dcd91ee5246e34808ade43ccaf530a9def36ab0e74a6f153ed679fbdd847c8f3950311bcf036f7423e2c53f3a8412f994337af0c0c47c897765944d05c49354e6967bee186f1d0b3a3b88481650b5c625524978f68424a36836903848f19b5a6fe79ce98537e1ee454bf5df3a76b1a55dd125d2dffb064e21496083a6adf1207558f8803e642f58efb7efac20a90882052effca144255e36459a7b97b949d57e859daef33a4a1f77b0e01471c0f2396c84d0d722574290e24d427f773a0380e0904c8b87ee5f38e1c64135c2885a655b26c02d6d6f247ef770d6b9326b530e2108193111ccf99586ec0528584f54e49ae120fab84f1dc936534641b89664e9a1a308de5caf58320d287f11019f9131299f16e237491b00882bd6ca78a030e5003b453fe82584f30d3bb67c213ab7382e42e45479f08b3146098c3651ea0ba244a127b7c920d07b8d9f2444b63a50d0fc062fae3387910322682795dec74d3b730c7e365b1f8c35205559124f4f1a65462b4cb1ed4dbe1693d613c64709050118d471aade2b5b21a25a55bfb9d005c2c9875dc553f90ca66e3c73ef2a092c0abe5505bd3305a9cc3bdc4fe7e1caf9bdf9b8cc42f991824389c0645421dae96728f70d5b8dd02f9586ea90a99a6d1439bca2ee46ef7d3ef900c3e41efaa57a4231af34cbc68b3ef3cb393083f73dd1480b8f6bb5cecb5da779fa2aece4011497ae088375b73f75cb1e712cbd43550af881ab7135767a84a55bf91f211529342f9468b1c0a4e3f4fa294a15eaaf38929524dc77db043a954fdd8bbc24f069f706d120682baf69797cecd5e3ec33469b2a9a3bda5cbd8b24142c93cdb98b31683b2ae41b51550319303500e26af9ddd2324ed0930898375b2718ab37b1e093c833a33dbb1f109fb7d6426e027379ed32a62f92b8bcadee8221c4df146dc5fd3d847add717e4a07918aec2c56feb0ca72a4132c4a022d89719ff9ea3c16c7c5297de30982c6a74aa1be384e28a6173c7c2c3c70f5baf68defdc921501d0a57dd5b9503ee79cdbc7883e515da0c57b640fb84841b97c1ba7920a7c2f3ffdf8df7714553f3cc5894a61a842047081acbac01c921dc9a158f301a8a49a08f56b6948e34274d110832c6527587adb6bf623433c3da28f2f6bc1950890678a217a832c6457c6019052a2f1288736e32b5a95561420489d3464ff6a339da041f57984af46c0780d0354e610461908a8ca374d9dbbf2b0c6d3426d8270c74f287ffc37c127af5fe82f343d819a2ac44d77317c36b180b24e14ba60f2ef6185db1dc432004a8e82e602bee22568ef4d9462c2ffe6188d6bd60e8f58058539cba27578fbd119e19ed9fa8bf433f2a8fba79b3d2764531ca9ce4e866871a38a9f6a6f61f7ec19fdfe59a05a522e1d53e658d1d6e9ddff0a15731c742d4a6dbc53a449d4dc8363409fef15838395b715f95d409770aac16ed348db83cb4f59c08782594cac25ddc8b17d3dba96926c6915ddfeeca2c6bf02208d64395a665374605234bf1125311e5d9c8867cfbac94ea615e90b0ed5d2bc2ed30c93f9f7a30eee5e49fc547110eb0eced6a1d9e1d1fe8572250efafdc7a1f9e79ed6b889c2bce3ba1ee0dcd322d85ce4f119b3ca2acf339c6f1f33f350c1b708637ef196eb16a26f901b37d970f8dc8291b4cd2c3d2b42395b54aeaaf5688922b1101fcb8089b42ff8c3645165cb2f46543b143a650d267ff9590ce3d5abdd866509d6c7ce654c3c7541b55e501904c4b59c961579ba04d4edaf6d4d5a655568b98a9e77b316b26acaccecdeb9fa5231e4f87bbf03ffa79f85524ed8d80e4d40d0326c013b5c114ffff9e7dd72574f77445e3fbbe5d177d38e0fb4eb06097e25fc66458857b577d40023b2d091ea31c71c7b2ef46151cd639d3af162f78a7b935636ddd9cf106ad2b877cfc4a13286f7620da9436a5a43fdbd4d3bc5e5dba5233eb8d79ffda6a7ce98b2fdd323a69af752d1d2a7de7905190d4b22e938643f4959ad45f65b56b21939fc584109f6ba8ed42c74f4b0ab5e062fe9b601a244abd4f69426aafcb89e0a77e50d07f73058727b7473dd188c20b0bf9bc820fe97c2f7593bfddd847746b69c366e1c67a9df40d42154c4ca8e2bdd81fbc403160ff12492e7841bd345a612c7a93b86d0d1929ed8a00ddd598fb5d2c45f74bc1388f3ae396b87c53dacc1c03f703ce038d2a0d3703bd66ddee0bab699ca62fdbea4728863c805968d6e53ba6da9dbc7b54d7706338567dc392634c474eabe275095e026d936ed7ce6a887f8a95db2a332d2dfbf225390116c64e70444b103901f6d307ce1ecd8641b314115a5fa90988517e690d85311ad76b258c57d163511c14a28cd91dcbc110d66172a7641b25294ba260bb98f663d064bf219595f47c47199e9d34e7c6da39e6a3b3746fb36b223512381b6e8a737b0e89710aa303ae42f8ac206c5710f405705bd608c43513ab0854c29ebc3621559805c115d7ccc3d963a3f35faa0d636c18aa95c909073c11ffffe04a113f807b96883d197151f25f2e4757f06f1ef73827546d4dfc8a0362ed18e4c7e3a67d304e297a49ad9bcbf626cb70a3768445ae6d9865920fb22951aa78e6c1711c904e7a19fde2814ef35300957350a8dd3e867f27240ceaae2561d1f374380be698cfc7f574548fcc7e45238c8d3f00fe89024493e83492524f21646ae5a9c8f874ff538cb9790fd494643f2c5d06d7fb49684355061fb58c635d20ebb390260581151b3bed7297c8cfc8f9371cd0695ba2e1f12ea0a0d41e7f034ec0d4d02406f508c29792137ed9581caba4ebaf09a8670f1e39024593295c0056cb46309eed0a9eb0000 + diff --git a/garecovery/tests/test_data/raw_tx_2 b/garecovery/tests/test_data/raw_tx_2 new file mode 100644 index 0000000..715fd2e --- /dev/null +++ b/garecovery/tests/test_data/raw_tx_2 @@ -0,0 +1 @@ +020000000102aaaffe35aa96cc59832d3509dde61a91fa10151e6302bcc7a8cad2dcb025a8aa0100000000feffffff03cc86ca865f3bb48a7a80f635f824ec88253a1655c6b925d10109914284963f01000000171600142d2263ec9931a1de376edb65eca9dbea98ef43c9feffffff050bba366e88398a23eac2a78fc74945316650d874f9966885dde8c787dfec4ba13a08fcebb24b4b79ae24c46aded9848f1e04eb6343f8c2fdacbf8f9005d0634e166502e0d4ee8e4a4df49bf3659f0164b45d07e716f80a5e7773088e9a06fb0de8912f17a91444aae2eeb5f562e0c7d49275e2f8d8355e2b9c9f870a087dda002c1ea48845494c5ac02418f414d45db836e00937a798b85961bfd0e4098cf5415bb24ba440e0cf263f8953acc098a2bfff7790aafc5fcad0c21de945ca03131cd28e76ddc55166adf270d17536d9ae44788b23c1d435a03ff139084a0cc617a9141cb61f7cf905d8a9c367d1fe1bd6ae330b51a677870a05d405a85dcd6e18f43ff27f03cc0dc966e5695aaf88d5005ee25df4b4c6157f08dd32cab0d056442bdb3fa0dffc470abf4bc169a50d1312cc6ee721a7298cd0d302ce19d0f6ffb29fa49a7bc4e1696cb03e0b6b4640a488e013a7042c4111676fad17a914fdbd477728bb5d9a7e9f91ae429817584a9f9e0c870a264983e992c3eb2bf597906b2177e2f84e878e2cb72ccaa064b3338df48add8c088e052a5862f98f93d97d4716ca67735db088d1a5644dfd48579c377e42e200320312d05c00c9b318bb451a80fa4dd653c50e4830cdc1bdad038b048aef1a5034e317a914fea08fb6f71d0553fd193178bc6ecb61cb49b4a38701230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b20100000000000026f2000008000000000002473044022030f74156fe90d820735c1fbe2f450a4a96cf6150591345804c25bd0c3764e2070220233afa4c28cc51b30fdac8e0e278ea20a1a438f713f42ba2af4641619c4b62c8012103a7b57c348962e3a92440fb736e37afa72a7e56763f6de0b39b5053ba5bad914400000002473044022045469ab1ccd610bb4f079480f60db0e660fd034ba8b0bbb0270d63b7bf1e623202200209a81c847963725267f63524048905bb419e1c729367c0356a9f75417c12680121037708a4695d1b39512ad2853a745439d27b9c0d8701c4c8238d31e9edb741795200630200039f020a6f7ccdc63d31a506ca23ced5246be31b0abd73e166f458dd7bf8d7e63bc90358a37a938c289c6d5521a5524f49dce70cb913f73ebac32c2b3fd62f068974217ecdf89173f636b4b975e9b59f0d91ac4e8d6f8f1843d82cf6252987178dfd4e10603300000000000000010d932b01ea66ff14312a3032700509ef62d661f99c28fd438b8693685f723b061db99af507c358b877d01415a64339307878f293f2ad6069edc71a1639c319506bc5cf65d1870dd7f89b6370404b6e3d770a05e760f4ed9d8b9ef55398ef5e52b540bef804e841a1331c67eaec2438a3535019a8b4b06561c75d958bebf382cabfafda0005eef621c652e031ec66e6dd7a4573d590403218aec39cd68197cc80a835dd23de93935f6943d0dc4a7e2563ec8f30af999400ffaf2697f46312b58dbb9ed21945df05d9bac23c52367b0244fe1e102b27a6493b6c512012562326cbb2094891469a06c214c5a68a506e257c15a38112f9cdd55c8d93ff8f215d415e3fb5f33a686cf8bceca4b0752fed3888cf6c403a0531c561be8ee4a07b871a0ca35f497190bc642e4a43a53c9fd9d19127d8840d95db2c5fc2069872ed9ccf63b06fc212ab9746ed3abfd0de3d15e74d2845a17ae1fa2f1d773f9b63eec2b20e20155da886f81b672eb27e5be5a587f54e29055a79302a888869d9c7a5010b3f460dc4d333bbdbf623b8c96b7e7c8cb0489604a736e5aa041f174383227bd405bcaea23b87ee275b23556d552d0c05da88919bd9ab71f50f9aa1b29dfcb5ca130749a74fdfbdf2b5c5d27e91f6640c0583ec6a688251eec8501138d2387b62ead60951e97d123ac5365803487c29c577eeae8e2ab0fa0afc065dbb5ae84b3ad3f3c7ee8460cf7b1734fe2dec7f7a7eb71b7efa467d37d4574ec785b786af06572ad52a04ce3290166567d0c3e66a6afae9df1a0a73688d6085c2c5f3a7e138997047fdcd680f13b0eede82dbff31580a42ef0834f4dab9f19454d304a570049164816b43b6d94921c48e2f8ef3b47a116f1f890ea462f690c4cc4cbac10ad7db794d504ec824bc998c6b2fc2231c62f39a7b61b8fb02f6438ab7c522fd0e11c0a595241358f93acb092a3292e7135a94d0d42c33368e39e956fd5ca8e8e0a4a1892963678a50d854c1b608dcdefa7a15b78d6376095820d19f3280cd8b1049be4c060e3141c2330d2ca3404131f50311efd5e70acafd907c36c321b8a8808888289df987b10ad4a8293ee560ed7be71a97eeac0b31b263288d986fc62d8eb289a643626419a08956e9379fcea3abeafb051813ad4e8aee54a59a23f8b98960012bf88553db8182f0764a3d452a49db36179cbea4af56d460606685f27f348fd818e42316680f64a95e164a25208eafc7990416daa24f9ad771fd5dbabc475b04a24c737b44ef3251dc11b0815d1cb74525d3e4aedd305fcedcea5f13ace60202b0cc721fee69af31ac5c7cb1c9d7c9d2b7913468bcdcde82076a03acbd753c786aa144e1da841ece4a5ca9773130a32c3a6e9c1a8391b0d5fc0bcba9fb16d0b8342f1312225f3ce7d8c798326916fcf40beb9a7c3e80b814f35aa227d4d79b4cf4722cef9ecfb7b83514d372aa279c3d9c6585dee9f4508976bfb68f2ffd9f649024b376032059aa32b625487e501ea44d17d38408e465064721cbe5c82ba6490a936561736c7a0483e758f52be70f6a4fe9759c3b3a7a12a3bbee14e8279b0fc43820ccfc8c58739de2052221e6628d45716d2e633cc1a583e2233d5d1b9bceb96f297d28f094852ebc818dc4407a0b22a237483a0490af5098b26ee0ca6e8c88e450356f7bee964edafb48dcd9940abd1bdb0df83daa0732e8567e94bb4897c0613af2e04ac344a65530d2f4f0bbfa9b350369cb2e85acd183ea11dd8d9e3a8a4f36a83e0a541c2f277c478b6bcdebeb0791459c0128684738a764e241c6b810122724b789e60b033cdd3322fc30ad85d0b0247cf665b80bc96557f85d85e8673ec8b048f02bc56336d965b3c450a151f1ffddf10c7ea20bcf43bbf39d5f2cf0e88172783989ed8fef66dbb9a90e053ee80a907edfa1803da959fa8eeab9a757f60127d5fac3af176dccacb9b992c7692d3d240eae1c0011d5560525c32591859b7109e7f5a3116eb367d8aec0d4412af392936851ce216b4337e6b3f75324276f4a4b617e2009c43090885371381d7eb8b6a88e8b48b66898e6119c47259ffd8c3578b9019d42fddf6647ac5b88b1f251cfe7a0640fd36ab3720a3cc27505d160514acfd7ae87075902b14347aaf7d0d64c48a1acd6c3c4ae0199e1ca012ac41895a473daad7ebf4350d84c20d6aa1928981cc0034c4917e770bb9afee1b396637d893f59dd6513e77b05d35410ba4c68a4b154a43803d9c21affa13021f09aa3eddb0cb7ffdb7dac58ff021305225fd7542587558c1bd0ab9d8e333990039b9138dc5c1a2633083e87c8dae8b89061d178a4d22d11dfc7720847a7ae6e7faddfb50ead246464f44699be8db597de01cf37bf97247d7a3261e7b3fa09cf1f0fa2cd74e526fa2f71ac3fc07bec58a85e4f7b39a45197b515fbc097e891f450c319fc2bc6cbc01e01bf00dc607d9c384767a4a9adf7e40e8efa1098b5ccca7a30e5591aa129cd13a5f738a55a5884d1b680cdafead070a5345b87bd388373566f51497b579d9ee36b0d953afa19eee30182132cba1d499cbc8cae6c3622f4f21986d27ea44c32ba5b7417d084c00cc46d3fc3ad9d16e6eac1e10f9cfedd62b66139c27d6bd0c16ea30caab827c0fe62afcfc7273ce04401a4efa94a450119209ef51eedfc1c1e8ee545aa572954140ff43b783699772a97a7f684a759eddf201f5a822b655683ab74e169f574522544b534f46066c776c1d447b4574da5d6fbbfe552ef719f2291486d78b47e4509371ca7d8472eb93c213c8c381fa0a756625fde5603af551f9a652fcbb9b2c2d87f26d486d60ca5f2129b97da7777310c5d1d94306947ccb521dea2bdbfbc282182037d259eb2547fe7ae68efbf708277fb19ee95db68c2b5c7507d4d3b34e1c34f958da23592d0563c6e1d08a0bf81a1f9e495e7455a93270dca623c99cd473658e9203881fd53142e21084f7f5b09997a56ec97ec526a4fbe0a9c90b6f048822f50d50723c9faf5b4b8aff3d4287329eef697f16c0e7802f5d7b3e93d8901862bde0659037471a172ac5d52e67f008515ccf3d0d91577b3a8ef977a8597e1818195ce541311fb6ed3cc5ee40b0aa1010ef36d13e963201b08675d6b88287f5957b55d7ab30dc54454cb1186610f3464a70370ab35b98891b404f8c2e64a688dc8c1e2f2f5d26e2e8e883b22267fb29152e0f9c187267c5e0f8321d93f0d07cd2a4e699c84af139735b40806116537f78eabfd92d3c4dd3723ee2fbb48a486dbbefb5acb456db1ea10c8e30befd4a649c846491a01e198e74d705141cf06f8db7c2d3e19169df50527dda44e55beca93eb21a3d4e4a178cf973eaaf0a3ad11d3317c5ce68a7ed102e35d2d3988143c795e42823f0fce126627793bc82e7c15bace78870b3a9f23017f94e68676e829dda1c18e45cf4f306a0e99c658b2814d801954098fd1a5e54113ad503ea6c584428ba7b3e0cfdc0655f571bb93ed99cfe06e7660461490b20e99c31aca96f664c85761a8f18b88de8980b54aca2bc4342736a56d25314ed5785b8f4518f2f81496281dddb5704278e937cb72a2d70e88abe6732a972071b3d6ae738315f8871bafc9b1172b9c87d4d26eea3028d654594b4d5c41e841a16275dfd7167a8bf8ff5d9d7c14c78807746352462ec3f7cdb8b9eb3c26b08425d97a8f82b51eb2c9d5bb2fd07d0b22700c367421600f667fa738fefbbd67fb850efeb86eb7a66fa55e1eccb5c3be27dfce83fe51c6c80e6b4cda0f3b289dd77e95a9cd61291e46086394bb0d9fba55476fdd64911e46fbbb2dc03eab8c52ad9726b9d6c88e8df509b58e77b0699b6b47708930a8a7095726f24b264009764ca208d4ad88d706a57e100c769709173be8866693960d4386198eab1e74f687927432c06ed9008c33bd841426dfea7c8ffb650fdb1fb42d465230621e076d6246bca0030faad3feb8e03f33976085a8984eb02ccb4e3c983e67277984761ce7e2f57d09d8c3e81ca86573f950304c9bb0e52596d28e28e6777e51861395549b6d5556a3d0d88dad09b27c12d6b8bb1b01c402eb18548daedfd39243b76e833612b08286c3a744925fb4a59fd39e3b455530600deb2ab958089113373eef2e6d5e649792f14b8867dff60c834d7e2f44a79a3934a7f60c3db5345aecfd08416e56b6207f40bda54871226d5534df1d619b506f702c4ca2d808a42f0ce09f831de203989a126dc9682f68e99ff0f14d055cd8360744d309aaf84cc439b274b59e8ec8bd2a785df06139e504c5993cb1ec265dc456d62f2b44d3105d23722a68d1750ccd4d6590dd3f70ba805236c12e5097f6b5d52273b2a2dda9fcaf6d1f1504bdaee5b38a1ecd335abdd8ab947cbfa1b719baefac022771e7799c86bafecd218159eb82135a3adb7a5c2f490b44f8e0a1a00133ec8cc98e1298bb6f8977f4e9a851b2f902f761123f065d17bc576cd16fb1a2bd010db4c54d16520a720140a7925a2d423c5aeddb0f005d60e4c7887ce4143307efe900f98e16f4de5de4b19ecedf914bb8bdda600d27687ceb582456bb3cc817c0fc379155a59e551cab85b1ddb29137b0a9db991351b81b1bd614026561f13b3403a806c5c475c7d6d79151d3070b721dc727f9d20f46941209cfe39f1833c89464b61b82d12647c6247ccbc8f97bd1db1014688ccbc6c928360d5f29304d0e4d081576c7bc4157ba0bfe587cb97408d23a19d97ea35c6f66d65c7b04d8ea411e0ab4edc264cdfdf6e041d5468295f88c21f1f770a00aa146dec2d8acbeba310266508f231919bd31da0f8d682d28004af2fa4fd339a6f77f3c27ead2281470d1d86230f90e69cf6f35b995a35575a84428a95c28f0d45b2230baac3b6b003c491c70c7f8303609717df8abb2a9508db80a903befb99cf3de0f63f2d56a06e3ed811c976f10970a144f6ed519c302cda232ca872b21960b04994cff97a1e9511a18be975489e4bed71d3c839e96aed0ebf3ebf04ac163fd5662c97dee82203519992f764543043ee97da4d011d207ea2a64f4237efa2a24677862b48ed3e44b98ca9898d8dff291e3bf746b0bcae46b83a7d167321a5ac0d8227957adcf781eb9189729d68255ea6ffe7c6dd6808e022a81d3b077fd7b6285e6d27d6c164e8eebd6b26ab7a6c88450bf2f792ea52fb0d7029e52aed44ff37c6059cdc6ac2b34dd7fa8751567ad535e8c4f2b68420230e91c49a141b0e12eba19cbddb5020413c79aad5bbea89399fbd818f803c3e8496fb6d5c047ca73326bba1fa68c45f6b2b3a54551842d34799e4076b16e8e2d0e8053e453417b8d54495615ee22c5c186bee14ca10ccacd801c8cb7d2bd230cf1c78f71bdba28d2ec10bcd1a81e7072e1913aa9675abbe8a962144a78dba16368e2f8ce376a5fb9b4a04a51d6d5f9c718fbd0decb585034a91f482014a0ca86358d02f2ebdfefee1b75dc830def15da31e654ee7039d5a2d1c8e4cf9e6378f0354e5fe84956dc4e54c607577df2dac517ba8b094a09691097fbe209bb398ddd6a1d9d0750c847dadc078a2eb2f96f8a56a098dff9787f9ac7afad3c161d81966f9e0ff1a7174a706422e38af2965ab20d5ff5fc7f64830a4f911be104ab73550e8fa3c9563c1c3533084850f5b4f8dd2d55c1de66c0af36172c36e8246115ce52be6d3cff9cf20ba72bdfb6c584dade749e240e9ab4c3d1b8a471197556d5aa336a0c0bd98fcd228a1acd8ae63f6ea12da1486852c6c39c094e1d2476ca5d6fb6db8134c21b6f8a54931a85a3cec74a7a050ed84cb4eaeca09f9bbfd9eba88903bcf1dace60cadc421faf14a7112a746b5fce0b916be3d2decb32caf7067f5297e9ddcfc5ae2c4960693ae6b50163020003b0c36e3008ba9531218f4d148aaa348d045e26415237ab009408a7928b386a8d52945ee8986ba36063c68bfce2425c9ad573cf3f6a885e8dfb77d7693244c62cab249225a092e3d590b9e20746df7ef36dac40e728392ca769f492f0cdd4b418fd4e1060330000000000000001513a6f005bfe3ee767fe38f0ba3f930fab4c44c533a4348d4512f5ce3b7fe1bb1f1887ca4bac604e51ddcd3a49838acca64f847dd67f7a048627d29e32800e5588bbfb3453501233de0e4e90b7e792727e33ab5d56c06da1c6c182a3f69f96ad796881f8899160aaca26fb2930c4b1a0996fc33b9e02d4a86d42aa253804fec592f5ddb295db44a6ba1d0af68298d0e4787ae89e0a8cc5af627edf60fec1d3ccede8336be8e0caab6682039b7e81f5bf82cd55558f781003ce72defcf45ffb8d3646347c5069be128e105b5e17d4c49ae83099f1e78e69db98c2f06fb5db36f07dd1581bd675b01e590879a8c004f86032ecea042a0b6aa898fca7729dd1365dd03abc658e1ce1bbd79268aef5c659b5339a455a9f4002daea38394d92a6f114b2c2111ba6fc9c4cf76aaf3e4ca19bf74ed593c63b5cc2921f657d55909c77170474df42a93da8566334bab664c4a475fc59a5a3d77921675c68e74dfbbb0151539705db219b1a778b87cc4025a0271c1fc6f47e0703d3aec21b6d0c9c37f97c952dba7f3f68c250823606a23ab1e08e9375c836fae88017d8c41cf9c407308c1aafd06ff94e49eb6fba8e3974acf3d150f15f3c375c9f062ec50253c6e4f564f1062382c6ee04477036d5ca015657d45f96ed743bd51be0fda8903a92262b9afa586eebc8f7a9f04972434303de8b4d3f8c6122315aa83d415b9f6084c26771ebbd111674aadcf30db70c0718161fd8587900a7d346a8db15f3a92f10d489b68ccec782b6c02344a84ea204dfb11e0741e144c187eea4e66a0b077ea1e830f0b256466f8ca9b64e77b538d2b91044c80a27c6b034d77cdb85d2c7781c356fb9cd455fbb7a287bfd0a5202a78c3328144503b040d09a9fd2d08bee78063a6eb7b000bd443c9caf4489b0ba9ed5bd325bcb46c40568985f71d4fb0a396cdea8d6c63b662df40b23e25efaa1702225bb8597bcf9db26138e627556bcf34572324af8882da25c8c5bc8c0b9e8cd92267f90e10f515aac320748c69344861e7dfcd83c07f28e0e0769381886312ddf2627094728b5e7118c81929833f25a985eee0eab383fd149a374f583239058889e9e93693e7b23a75f1574ed413bae8d87710474e56ca4b14252b21d96c0f77556fe61746f2b0501aab057a1514dc6eaf68a74887b3e6c323e215f569f76546fccf57597133bc234c07f508cd89f9cdc4ae30f42541ec91e68cc8b9ed5a3dba2ca920f8d57c87e9baeca70dae1397e5a2bc6ffbed9c28a67dffc80108b73bfd999373d23917cb1fa3afa90ca047d5d0938db97a693d3a07ee2a2f5f0d93a8371a6e4f1ebec6dcabbaef4a15403b55a32900d3f3cb3f1cd90bbb45ae9f768245643d8e41d7a1cfed37e9819cda387aa7b0434013a90291137dea25605b93ab1af7f61fc79af64197cf898c3c697b701a12532a75675f4340dabc3d53535cfbdda15e29798ee2434e77a25468e931c69f6b319ab25c3237fe95ed78fbc955664900e267efd78c77f7576dd4f875f66dcc353780aef587b3f52b533159ddd7c7ca4582770e7ce607a63eba3ef7911c392af1dbb2dd45ca71bc91c598f8af6feeb3c8369353b95824d7d124d9a3c6ebf65e64aa5cb5129221c317f0676f4f2075c0c72ee7a1f1135b4a70325fccf98f0cb8b38a5dc3637b715972ec9c0582fbc749c49576ad4da6771c5c695b9d43d0697d7b956d40e85e4a99bd59b3afbe5e324f82a24b7d1e0e141bd7f9bdc293e0b6bb9a271b83e09aa53735c04b4f82f6514172cbebc0ce645b082731ffc2ece10a84fa5d7ffd4dc47b0597421158fa17e598f36f14601f9d1ef05acb18eb2e977e1c375d5fbdd559ae887d687fbc0fc48192a6e78c21b6d1e120f73125c9751912aa754e6abd69f9d05585ab30c2d12505ef5069e89c6e34b7dca4e8f04315e997efae4278ff9c9974c6dbcc888d451613f2b068c23e7812fbfbeb27b7a8395ca01ae212775ba9c1beaee459a24caee2f35ec97d2d2143a012dbd53b266cb056a76cae7ec8a7e50dd8e658b9c0dcb416d2dc17d50aac598c98a99f11e8a7c19155de99aba231900a35ba262d87a777c962a776d8c602e28edcbf9cb6e0724a5225478f2ba48e0cf47cba26ee9617f448f837cf66af2743cd046afb907458791664544b9a4aab888ec5a677c3dcb665983c1001a5ec262de3fb0557d7c1f0547f923689fe0a27d0fdc7ca72e7413944a0b1687eb1790fe825a04dc31035fdd2ed342470dfdfd8e643707c2533f149cbc2942dde0ed765f5f7627594cb670c85257eb56f3fe8673a88a9a3ceb5ee9ff4203b11e648839e28926a374c9803183e85ef47ecabfe38e2935dcee9ca6f6fc1394194c14db280336ca144f915779039c3d6c481ffb7da02ccf7bd075be138799b437a3b0e714804dde711f935381432a835560fba4c0437f95d5b6caa11c8646306b8c7e7498ba5dfd61a429349702ec8dde485e6aba01b76d86633e4a635ed1c1e769c2a63706f6dcb3f94c532f5bf1e40799050584df37e8207dd46cb93c5367eb3a5669372c235a20c92ba40a17826f9dad3551a268a8d98cfe4e4987fc633a45f9744311195251b65842ffa457fa54e446b05c58f2dccb8ba09c7b3013a811656fca9e53fc2e687e45234185212f462feb82e859097de33bd8b0c2386354a45dbea59553aa9216c7b028386e72b4072a545b58b2340aadbabde02ee624c54a11898c0019bb1a35d17af88cc0137ffcb814a1d92e87bdf4762c9a4802aaa4386d79a29a5d23062c03ef1a2735878a26a96963849ead860c55efd7289222bb0ae430ca6f5eb8b08c46f3aad521081f99ec50a79f7be53f38f492c6a5dfc8f4874554dc40b028cdd34f228dc9eec34c9509cd6b871218d4b99e27d85ae1dc5afe05a8bb3c3597cbad32c87de103cbe7f0f68ac7eeae62eb936bb4d807fe2037fb4cc6e0858a9a519d48a6177c69158010c9524ae9271e995e594db8ce6da7743e5e24bb99074981232f5bcdac811f1a7a9407f659b1f3d7a472c61df95a72ee326b6e4f3a7e2ba6741db32e04850aa31b71f9eb6b5382a5c18ac15310d3e805c8fc1d2a0000a394b9aee04aab9ee761a77390f37e0d1c74329661a2a4f7a90f51028a289547d1978f6536187e6837e8fbb735e6233b714b4afd9dfea1eb3e869a4c08ce0c5500f809a46d51cd94bd140a8b5f11509ff78a442bb8f25c7ea0163ff017fc5f15ab8597bb0f6c2210e47d3a59187c5f2d9c59b1526811487dd17caf76e3accea04eb7dcba794a33bc655d67490ff83d36fe27fab13a6ecf936c3965262d3649a071d8c1cbe68a47c32e5689a05b207f3de2ad227bef70bafb18ceca2524f87b4208ccac98d66de9363d18be2f01a25ce7a0af9cde9d84dbb4656573846e3a53515493189112cc36929f742e0a6bef59011020d527184b65393393af08beeaf97eac0367f879ceba0ffdc7482aef88e2cce664cb8e82fa10ebefb369824534730d75ec80e618f35755b515ba90e0d09d030cbf4784d7b3e17bbdc1db8c75e29bfd0f3b8338d3843b88c2c52b5edecfd150d8b236b193f96c01b6fd8bac5ea46250f5dde813946db10491a6afb6eda4d95a6a629e9b868a3317260e25f7bc09d85c03cfe8d36173ece66fd7a57dae7d8c2fad7a70ecb5288da6c9bf420ca8efa4b45ba1f37c630c5f0389044043d63f9338575bc4bea8f6c0ede99da5669f83ade1046e95c5147fb638d596570272618887a7947153d7f4a0e795f057a16c3d47c7027c437c245457b1761cc511cb86e714fdf75840a7765277381b16de73a1a1080c7cc7ef9a6c40d3cc870dd7bdbef006f494108cbbdbd791fd8d20f925b6463cad93b6ea01fcec7b6817147272363cdaaaa33d95f635c6f26ca062d9e24485ad7b24f20be0825ba10fdd1d62dd134e2a79962947b0059af628caf40b190efdcfda2d6c918466c2977747631663fa8f3c8a3dd6f38d65b9e256495867028c7947200c03c79191a7be5ffeaff7ff117766826bd6bd07ccedf3eaf88b43ad3203c23fe68d73dc3685f9218ccdc88d8c51f7acec720067e7f1feff25bdfda115bd4783d970a8ab971e65f487dab9033312b2f3d94b436f149837f4e9bf1b75e2e847c5d1d9635eb132ebe9dacb78c7d775fca6e75855a16fa369b25397c5c2cdb4ba9c59aa9b907b3ae87752ad467dfdd59da588d043a6fea3cfc6d82a863d1c6bc4e55399020e29a1b21be24699c4450f4710e9a709d6775e2f94d05b23fec75299e495027ea857e93d3f16df49b5c8f8c7ae7d3e9dbc139a415942066d31ac35fcee38085aa81d1c5ab444c9855f747a3760c45da47f4a3a8674a835ca8bcaf12d7d6fc8323344b08b5a36afcad18c48db6b423c5282d7423ae846fa35fe4b881f5da8ad3b51bbe9b8c6d830758c6b4448e2d165219ef94117b31a188643de1080de3e03ff8b67c513e7847c6040bfe403b161eeb6162fccc16c2109e617930cb25d9634a885bac89b2d23ca2b89d38b458fad8a41c5830bb55dfbeae9a4cae72b24bb29c633ff6fad8258d52eae4616fcf9bfa4cf25af91fe61432e2cddbfb87cb100851a66d505de0a1ff1e0d1e5664174b884d5895405a5f6febf9ae6f4bc54683c322d879c5559dcdcd2bf79defd58f706a23d7a9bc7202444a73d00d0d1ef13d5c68ccea69ba2289616e91d02a59f38ef1e6f9e2e5a3e0bded27f6df4d9313356b4faff6b07700441ee4e819af4982427b68affec4823fd78f55b742919a6e731fc07b599b404b8c9c3f0217e43ec02289cc8be7b7ad83b11a36758db07af499b3ee6aec2652156028e3940baacb1df856ee826af2af82857a3100fa2ad2d58371254a42d4c03e7546878d77f1e8a736ccf26a96fcead83e78c23fbb906b9d25f81db3fa8d2847ec597c1e77b37b5fab10e8a823838bcff9b527dbc32c1670c5b3e871c3d589a7df0e1a2002494b1cc55858973106c1cfb2a666cda77e29ac6132e41b0461a60f36818dc130b0969be98b9bcdfcb388786b8949de493b86d92b529dd9b723d5a9f299d4206d37ae62155f9b6f610f14cf858930e6a67d1a0ff86e75d66c4d34772ac36dd8c06972f0e24762336a1def3555cdae0837e8a2f70e3e6eaf6b47081cc1ff70dcbbbfff75c5543a9c148c1526ebcbc085dd957e57f79cf7b6a10a2616cb42a46675e4bf3e494b822794c91fbdfedc5eb3fb1a69d3a4276c59462c2e4c1320d93b76436657009f88170a17b689630b48d07573e5130023254c18521de7dc53be676774c594bd0cf385c66190e18a3124ddc10cc8c6608dd823d332752f5cc4c7d40b2f4fda7324ee57c600aa3208c92879553b1bc2f349d34a1ad075dc7b652190ffcac6c1ed2122a0e2d17952b8992feb384f39c7d17053b4fba092a51c8e0a5c1bb84dd43de6a545e86854b9e64cc2389747ba17c1a5edecd3ada5de64f4d048ea5e9e1d679f38e3b34c464c3e343a5ea33c5861729416912b4faa585122b20749be40b6b41d43981f99ad03cdd4fd7c2c61bd2d891f96904b5bf0a55abca50bf87da092698e9d516d6141431e6d4698c54dd22b38e7ebee5f40b0be9c73de85697d5762a1587363df9ece9dc1f902757f324c20fa379f0f467995e40da39cdac40c59e0a6a3fd7c6b3f9b4ca762523bd9be2d1d39a0137d804de6e3141fa64ca7a9a9ee876b4f084e596d12428358b5d362b79dc6beda967ba5b3ea2619b0f394e35dd3488b45f6aa94fbf125a54be93b5b08e45db52e3f40acdf888b6d34a760f9f73109a1f5f21366290120ef8efa546fd669b179a6fd197b55b90bf8959101333a71b7b5dc1e6d113eb7bc660269d1ba762000e855145dd6927b48df755138f7e3aa8d1fe28630200034043d37f8bedf2ff91a276480fb837749a1630e424dd46503e5ff1f7a3880fc7a35228230b0bda09b73d49661440e9960a7860e8a381946a9013b05e72cb06b94fe644035af2ed46bfa8f18f7c00e50a15b341cde5bf6388df3624d4aaafa597fd4e1060330000000000000001664f6000a5f470354dd049e61219733a1635123dfa155693dc933b9415d889a79d0572b631e3ad7851dfd552ed3165b4d2afa00d0a17cc60b34f7f9cb54ed10ce6abab8013824e7b66a1ddb2340f835141f89c5ea599062d4a0416efa29848498083f0fe0199c3ca07dcf6e88c59fb72ea76f1c871c6dbf34e3974313c1500f3c19bf0ef75fca226d34d2a5e4ac9a67b675a470c8c78c6a14f42ce6422d76b5e0515ea279924c417c344658652cb808675300900d828fb023216251d6861fb6ea3d97c6222e9b42b1c7cfa56caafcd94fc04bd50c097f1b6531cdd5962588bd823d9a572e09caeec7d30a2341ffb59c5a375733770bea6a1553d21e85a9352455ec45fa6c076a524ac10b25a3fe5886563a877f2bb50c7426ff371151b6d6cccc910eeb006058d0c2c2eb4ecf070a3a56444d4d3e210da94ad2fe5ddc6adb125b0e4b93622a0023cf515f9cbe37fba2ec38dc922d45582f15fce91ea08dbc9a3524d93f19e966fce5a1d0d7a83f108d116f32fece965fdd7767a21a81697229d08f6d8efaec3a3b98fecb2e6abd63f5e07c99b2fc44a0ba92e5548140176a4cd0e841afb18b0debe50b17890e43c92dc570489713c17c7d9d335b162d4a946b51c9dc2035dd597a42ccb1eb8b18286ec56c7ebe0ce7285d9b773b621fee089f5d238e6920b1a02998b3525326a83ad07042b95f3e8987b5c4740aa178f593a66fcbebc07c1a603f50b0d807091eb4951a804cd6d0675cda775304fb99491dd6fe483dd209f6b0dd188151277d444109d5b48cf5a76243dc67dcdfb338daae52bb6ca081a32cf7dc9343139e13ddf4b118b0cce9501c2b719fc804fe2ad1e39a46ea894671be42400173e5fc34762f99f20e7b6782afd143d8ad3bd07a73dcebdec4e950dfd233e0f368aeda1101c8aaf5f1c7d4b724801e6fe02dbc1799a37e30bc7e2223849ed004a8b599a371e04a2fa06f409f843e0f062c675efb539f52b42927b5df8862f76f6593095f7cb6165d451d5853f6fbc01b5455bde2fa07b95d62b7a327c4f7f691b090161e714cd3a900b6e6753327c1b4ba02c71c6a8f79cdcb6bc6bbaf2bdd4f5ccd467021ad783b253518670e7b06d3ccb74731b10cb09cce33fbe655d042f7feb14278ffd343a4e2a8559b8a187a8d2a717a66e7037b4735596dcf97254813085abff603190b323013fbefd8bdd36e4ac619e801ffc633a11531a24af6b2635295ffce53d469cd9f764a6d59e09c820acf8a317de4e4f95913ca60a99671ea80a0f5cb69386a47b88b52ec71b9b796488f0b2c0831fd3a69689db39d7c3f11bbb7a1a4acb99e55c4920d1012e116afadad920d1f82a40496d1043f7d1bcd8ce67f488ed7a4d8e332449be9d4bd5c1ed3949cdca51642c9f38459c6c54bbf5c53f594c1662e52a705536ab6857084766238a48d55fa4b1803ec761da29b18193a9e8ce5a941138e9abe0db19b747bd4c51fca354a600e6a6c21b860becb97cd27465f3524a3fc564c25e26f28adfef071ae7588d0614d6df44e35120e291ab06757a22020eda73edf37ddec98278f4aedc4dcb30a3ba17748c0f9fe6e62cb4c46639ffd052ea21bcef9cdc651a642bf3cd80619fc9d20866631147094324c86a191e70f6b81a693e64ffb32892b953993d463c83219c83c569b96bd1d478e678f9db7b9e5d8c9f78f78e0ade06a948c3e221f0a98d6c6e130719815112ad00a8c383cc2da0524354afe76c7deb19b7b2286bd87a780a523b24c8f42f553d20882eef8010294b5d470e2d4ba7ff4e980f1506886e4ef99ff3166c7385d2cd134770dd89e9af6b08a8cc317bd6a9d4eaa5be84cbd7ed23c15578db64f6dd567782b8868fdfc4f6db4856b31637526d62156c0520a2a3369f4308d3ae45adc646488f4c666b7c1c8489bc3acf4c04aa3d94b80fe8bbd9c07d6f7b35f41dc94651da94e729073a8eb5f51b8482aaae43a00628e911b77689d4ed89f4e0d74c0c2afc568bc083abba7a6f3fdb409ad0fd36935c8746100e3014a1d2789dce45722118e6172fe94cc07c002dc76aea025f4331d207a8665493b79a28e98d388ea7997bd2f3aef4c1189aa107dd34a48647c6d35dd71dd28c3ea21712a4fc5739ee0f341cec55699ef32090030eff3c060fac365ccd55a91ad367e7da736834cebe55cd76d811dd12e8de05ac63b2877f86ba67f64c56efc1840934728e17c22f703177a765ede18e9277414c18f793cd5d56c6eaa4e24c82094c257b38d535a8957fecd60d6e2feb52d6f401473f0a25f777cad5a583c60f9bd96da8822a466689da2c0e6a62dbbe88fada099352caa387546b058ebcc6fd3d1ebc92ca1a4e456558875fc0dcb0c77e135d06e9fcfab7643d7b512b6f4397e8a71b437aec1558551aa87cc7bf12dcb465b981d11607a84ff0bbe3540644d4d68207d33ddb8bfb0a544b2b718e2eaf0bfe5469573469d6e08ec2cf05bd87818883765a6b38a0a55c2bbdf51a66d5aa41e00f765182afd52cdb99a31542f3f3313073a122acf039ad5349248bbd9b1da7104286f6ec4c95f2acace1d742d34215c8c6d41dba6f637f497e5f68dc3fa00dbdfa252aa2d6f51699cff4f6772c6650d81bdeff5382b47a77aea31b8df5feba797814e70bc3170fce8a05a23f6d13a59bc29419cac049c8660b8587a1d20f7ecc67dd93b0914858247a020b182f72a1e2c5d5a17d932afa302eba8d53526fff933f2cd1b83747aba8e898d4ede9577ad5f536b31185cb7903184d2e03a2dc0b04369efcd6f22aceecdcbcfc1241114eb06df57d367dce4640f567c92058c1f67f0d950c91105e73b4f5c1972744f8f3cbd39bdb361b5108b81162e7f42fa57babe4eea3dffd9801f42f55d980323d03b9e49dd2c830274a8b62132cdf77bb64744275c7991fde35f930c87d7c9fe8287827f7fd3195e31f1c3e7a4c67117619cadd8ef5e844d0f6fb83e2f50255ec5d212edcbe2e19399a926d722065ff880bf09bcdd02f62cc37c112d706bd4b75a8c86dbf83811ec4a3d9bafd9c92ff39f646ef28d1a1cdc09845ef27664a20a0a2d9cf5b68e74a70918ba6f24e6abc480446cc351616844213a320ad289545318df492a5876be46b31560ace334c5198d9dbceb250f200fca1075c017dfcf371a1ff518665847481b2ee679b6b6adc07215e97b60d5a4501c460873298d3b2a0a7644627a13f8d9616103d435682e8f5986b1d144f05c829a2149c59d1629c966409f74c67702deada5f3fc4c6768f9df088d410752a6393e76fb2fd668cc796e2bf5da1c0d9c42099384e1d33ef7ad08c08d36c9677ddf4a2341d70c4b21cdd0c70d6e931fe9d979d8595a49c40e71d5d33218a388ab6dd0094b5cb59240337b61c219735a00ef13885e9e9f93a75c56a1ba3ef1ccdc38cc0fb96b0d3d2b91250ed519b2a7243386603a5d7985973157fd1d5579785ec83ba11ec638931e17662d515e6842920a42f1a4446c829bbffe7c81871d40bf8a3a916b576a2f3f8c5331e38aa00e2579637969d1682ecdd316ac53e099e31ff75bddfc3576cb0c370dbf42238e25e57f4331b268c70ae1161f50f701d1850d5f59fd614a3da5b405ca9ed58e43de47f78c7ef597cc735eff762ce8dd5690217767c5d1a1b4b6af623cb185cab559d8a6dfbfab824350517af91ee6f95573c7a6068139212cc05cf108a04847db905f7e03429b3c91e9c10d91308e8da2e482e92225c85919a4f1af2ecfb2021dba73d3df425f60b84c4a1547e92c836a132d7acb5ff4fee5b98ff793f5d73e1d7de46bb624557d1fb69bcba38d250334be7a4657775f54be3bad11466eb602fb52de00bd344bf0ea79a781d6eff4c6629685e144525f2e7136462bc42572e88d243e3ff03cb157a229345db606ffa332ff8e0c112dc9dc8e6d35add0ce30c16da17d9f6bd76451b34f82e35af40012736e5b2a74c9681506fb2919822658f2809a7074cbd410193fda2c92740459f879878662c79513fd768dd7feb4a9debf016404fea604901f93280e518186f23463d848d1c07580c74bd03fb6e32f5bc824e27be1efd2b8e2f8eaac3902f399decf98bda58d4d021d2a64ebb9d4328f27986b46c96a94012eea774bf96c7bfb18374f52fb34bbfafa08955e209e5d24ccd5b4083bc782ef87232fc820d6652d56e1c8cc6e4b5bdceaffd6d6313a7d0be4a82ad57d4e5fbc0740f15fc218489055d8cccabedee267a7a5a4c8ca84e272ac378b538dae3291204bbb09913c80e195f41207f794b8b9d871c7c229ee3c7c51073bb4f69d477b2c8ee96412d6e0a192c3bef0c3ae93b0f8e2ef666e54ebc9c2380e610de1a9354e41c112a8aa15546f61dd1fac8d2805a8b5955e029f8621258ed10b78739ac893da2dbb82964a7426059cf573f97c42e56bc3db5f4aca9dc5fd02803bf3f05cd517b702baf61c3abba8e21381fbe72fdece730faab23af61deb68fd6330cc262c75467b44e293a8917b5e97d25c2024c4cb9afa62a2334976ce9f0b7544ee63c61deb30497cae5a3b5ca7777f3abb1c062c23a8482032e3408b30a1fe935f32c26f6fe4e3bf13df032b0827507680d1cfe6486e02f93f59e7d6fa3c79a926b779dd1af036f3326e2c59f8607ab984e3441035806018fa10fcc80b2d93c03b9fdc8fbca5578ee12fe353e2b1697a7e8068f579253ad437112e8c55ebd538ccc61467f68e2a306d766aebed25c345614852b8d0ec02523d088ee33ee321108293ad5661a94b24dd6aea3f4dc0e2e11b3eda8e76a89c3505d2f7d30b3246cced1182986a2d34ef4971ac6f165102ae4ca95b3b1a233ce42766ce2aba4d08c88c25e6eee95d081f0962259c7f4c3a2a07480e28c0bf773239718037daf1189d4b69b0eae95e94c8d3a888dae60f4064e1003d0652eb8d001ffd67dd3da1d8c945cf5fddcd2244160a720835a3cf661dbab4f00acc41e81b7e2d845bb9a53938fa645e2d625b78ef6963221f976ff98bcf4d6e55e67e4e6b285ca859a1ff2a4b366ebb1e20d47655bed1d3ef7b6229d2817e1e4ad8087f9f890a6852c702a7d3f86f322c0a7512e59cdffaff24f02b73fe31e9bf32ba6775a20a7d8923a28574776bc0c34d0e8a95492c21faf8b1ce4fd6f56a348d38ea4c6b0f219c77de2bb949a0747c650ab5308f21cda05185f67a33ec20571fe0ef34ac3b47805a23eaaaf82a8bee176804235dd32e9f89da4dce09aae645fd47d8a003e4ce6d2d9eed482cd35f6d1e988a5f27a5d62869e5895c2124e1f7302baf0e53b50786e880860ef5777c2adcb19d45a51a909a640dc513ed4ea8edbc3d7473aa3ac2786736f0b6f2e5f29ca5b80abddeadf98b845c95640e7e34b06a835a3912bc5ce49ccce4af02814b177d138433d7bac6227b96a2fd0f15e861d0be07025ce01468da04bd1d21d8b01c1cdadc0fe54b7414e0358dcc8140cffdd7b60be7f5a9b67dd5dfe1b9a368015d99c9cbf5e2da4d6e1f37a1e0e805c6f511006b7fe05a5e179ce4a53811d9905f8285b7ee243f222ff091cacfc6bb2a81e99bb98fa679c671b297fa46f420bd26f638400798a141da0f72e40e667f279caf37215cb4221d56c3a5842001f0c70bed5f01471a1d58c4c5b5e4df6071fbfebbb130660a399543ea5f8a05b22b5d28b24b4271d3b9e7d52b855fa52f27da60469ebdcb520c19092ffae3f3364511570f3ac4475fd1a792c7c72a0cf25260c8bf5465ab4e183314944a59d91e7975ad8f6ea3f9ede3a726af01b774ffde691c997949ddc21cddab60385e163f58875ec172829ae5c6e3f34e08815743052736eec9130209b4edf549ff5bb8c55d8379b2928e2abf1e3188ffbbd2ecada155448d737f686ef37a1a42e63020003925cf6b944ae85e013a7886d001f56764e6858fac3d7bbbe56d78335b93168dbbe9ab649b7772f5ada0b4a3c1759f4d24a3527bd3ec25a0b7c48b5483b263c5b08002053b65d9726cea285dd3f2faa7c4440f770398e3fff5d8471d2208537effd4e106033000000000000000171ee9d01baef9bbfc62b70de71f72e9416bcd822f58c0f3b237915eb3c470a8b7a724d654ab8ddac04a7085242012bc594d952cf6461ea8c9d7ed71dead430e74cbb3f90739ef319b1981cb5b1fb9b11c60059777448871b5be6bcf1eae2ad8582a9da3027e52b1e939d35ff713427b716ea71154f8a80f3c246b3360022eb529f0ad710ceea865fa66778e5b6669ff52fc47c49d55ab0ce887bd993fefab543e91486cb60e1efd0a033d6dbe043ac8ac19e1bc741ac499d8596c5cefc7246f4bbcb7e3d20eff6aedc75b244bf605d3ba9c624abd72c429bf3f0f265ac10bbc7ab52b6b14f6f4e4bd2a788bf91bd6a9734d2a70265d0776bbc81f2fb575396ec20eb1512df2e3766c81915f60e1fb51c9cf65627fd96b5a9e8c32cdaa3d607cf4a5d6f8d98c6d34fc97694d10968ed50f10cc65e66e8bdccb346e25bf25764613c23858f3248f109bbe136f5fed7cc9d0bae44cbb56416ab6a76ddf36683d0c85422a84e6714300884439082a7a2487864e21f9b8c300a00caf60e0891943e40ad923a66d158351f013e2d3d3c73ab0201c6fdd4402951da2b2239680334bbbbd9a0077652f5aa7c49ed349eb537fc32bb9dd134ac620048abcb9d33e9577da842e5b06e0e14021753b69aa112ed26c9cdd804d5dc139cdd2f72b104c36f3c16d4974ccf42af8fd9dbb209696a56b1ec5523e225a673bccd419703604eeda7910d3aecf97ea18c7551f859e8f68762a82fb493c4cb536c2bbcd600c8e7d1f76c960e74bb5c5b454a416f33a86a74431352fca2cd587dc6218612304eee32c4caefce9e26e73ecb2db84c08414c5f6355d8fd0a4d4f7423e3d6418543f2d66d2667b75f46dd77c4ed3ee2fa2f9ac747292fe97fbb623bd8d5b7ecf6a71ecd4f8696ac57f81a76c348faed1bab982e6f1029d93514e6e6517e78eb3d99506766325b02f32bd663373810b60dea10587db9acbb74bd2cec4157b9574ac7128ddda1f91fe237585043540d6f1ee29e492af5d60015077cfc764b8ebc1c965f039e71a342d4918c3d8dd8dc4d4c8a116caefddbe66041d1643115bde11784d2405782eaaafd9ce28c168b84107398f6e497b6268275a59f0f53bb70d5a433183fd5a5afc744c73f048e7c5d3d59ba5c8d457eceaa5d7e108f8f861056d10e41bc885c74558fdad95aebadf7214c049a85191ad8057af2697757f43f5fa8b4a80ce0120151883a7b696518f1d9592cc2ea7e88b4e5cc70193fe0f8364530c1f84967c2ff5d1d509c3144840801bf2eeb3b8f5c100afa0b1239e96e51cbbb80bb118490507f706e9188a2a3c126d1028d7d462efdee3308e3537efe7759ccd41ede1d12e5aea114e90c36a953164b3e1cc2d638b4eca06d81c01563883301a37a142a2ddcabbdbb07ee375ca4e97e1e4914023f17b329f6238f3eacf67435c2b51cc38c41e32acb0df0017f8b7ee6b5ec038a14bd0318931e3cab8ae5e9253c1c740a59320b980247ddb51f41d557352ef51a60e7aded98da1b56c0be7a4a4675916fb2450044cef72e5cb27bca0842e56d5acbc23590de30b719a35e59fe99c4c9e10ffab1fdebd2fcf8f8ea56ddb8ea3eb4a1ca0c21cc3d144d02d028c9aa8c3e43db5671f91894701af4c5faa09994d8729de5314d8f7a0c931436b6a9426d6df8c446971e0cd186cffa005aeb0e40a35b8d908d88cbf7dc15bacc299ff56b73b5cf9f79d93c6f55fba372baa58075390d7c7bd4bcbfa7084236b0bd252006a94c4af8c4285ef489249d66d4b0204007b20daa56c8456a2b8ee1af9973b3abf92e9d6f11e0ea3243d3d706d75c4d639700d1a0f246a9d9751f6a6b7af1cdcaefe331e3b30dee647fa5aef10faaa52d41417202642e1c516a9e300074b00802ef0fcc6026b6f761013870f3df4f267fa1717aa146672b2d916e0c661a57473afeab2f8fbe99f6a3c7db2de88978254938ae396286114262238ba5fc3be579c96b7dacba21c35183fc9adbcbc819d7fc128874636a1c9c45cd71c1827aec336885eba3399120d48f8564e6b77821c01cdcad48248e192b251dad294d89400a99238c869392c6e000a5284e4008fdd33053d25b83a44202ec1a642020de01dcefc28250d6439efd2431d6945d7f16e97c25bc4d87a111449d508285ad950fcb60176eb043974fd37f2a1e9e8e6b766d30daf9cc0bbf6a6701b49e9b202166e11d4941814795ec81d12b721559e467b8f175a46b3a8581c09626b47169e166421424037713f60eaff3ed1054a5db1df02372be8e8d31fa708f43ce4341b33e10b360ab0d1be3b69f19541b5e39848b24cd5f9dec07f59af03b8203862b20e3130de44a1f152a0844ec5936c62c655386fc260e160cbe8ba88aa9e154c411f6b5892591daa1f52e6fc5fc6b99bcdbb492eb81175a79e8bf872f48e4207d4048deb899a2f18a7def40a8b9d316b7b7dce3a5e8b6cd097cf6674a4749202410ddc6ccf6ed58025e09842851e832f7b1451e89d698757f873f0e19d56e07b7886d708c9680da98e96bd47e5b46f59a4b743ac3fea3d5b03e906fca966d24ea2cd8c691fe9a4919b8e825da8c3031463daa6ebc4b3298dc5298f683461f876c5b5d5f9715be05e95c879c5cbb9f2a824ab1c4171f5e3467322d5b91e72b1fd7bccc979af0d0f318c641a90cb00d923046311c78a7202fff3b7b4590999b196fb95f9d28a70bd0b84cf4fca634d19a770af5d623ce11c1fa1dc95091fac827f4ce706f5f85e27a20980c82cfc5c2b4e80e1d7c052c2ed98697198f3f3145c41bcaa50fa70a32e0e6ca17987212a6e98da58d1eeab5440bcac7b37a7cc1c0cf33630f2d52203a09f3ef83d9303d56ec9b8cab183993a3bc2c0333400f7f1123343cf2a8a9b6a67a30ea895b76e3c7a6003b6639cb5041fb740675785a09037aa2a8288e705397bc0809bdd1127b8b5e679d1625b8eb3f09fa60ae013f69a9798648c689cf2de668649c6a8f9c693d81ae734ba463cb8813a705188eeafbb1f76b54515e7f9e2a588f545f3f85a2dce451125be2e02fda0a648b9b681d49956bc08c3d164073872aa2ffc85a96b4d93c6b74ce19fa311b2f19a37d7daa901d97289be8d2398c5ca4eb10242e4b97ffa0053043742a75e0bc0d9a2b9dc022b261aac753fa3774b46b871a10d623845f0f470096bac5bcd0d4758a45f91ab40b7c3dac512e5a88715789cf1bda6e2e4c0db1f73a57fe760d9667ba19ff1661b8a3e168856ff2cb03b58cf09cfe9861b734a8bde1f3c0e3b6e1207ba0f5b5433841e1df8c98ca630512eaa8a73cc93a840faed0900d894e829c314a8a6f3ea8edf9448a7d553e5a4037a8e29d423f0f6577ff4fc1e91885727d7443570b5ac2bae36a3a3dee5bc85223e16619213f9b0bfc91222bba1cd9e5b282b47f262a24c9d5f8a5fb7b5aa585b3bd8c9ba4d279d4e38ffbcd582493dc2884c94cee6271fa6d76c248aac6db081fa028cd33e01854b6dcf429f72d1b7b4bff902fb0dce599e03677ef9dc6f640a2d199c1096fb8559e9694e152be22cca17bf092600c52103584f028baa0cd56a31de96acd5a4454b9cb1c5ca4af6ccbaaa184e6e18db1f35ae9eef98a989a2b55a51d482beef7d65239f3705c2dc0b7383a884be83f552973b99265b6c614ca31dd421b3976e62c0052e261820c93f4d67c09e7b13be1a619fbc7c6b2cf349fe710ed136970aa4534c4b02c04eac70ede7624aef21cf4dde255147a6cccae370256d39cfb32b8734ed35f2a3a00bb087192b476ee39f8b89573110e09a9a07e3fe49b8cba178f641743914e5f5087568c967888727f3f5b0158e969b3b50731d97eaeb4a0638f22e37102eebaa9e868c6ce0fe7aca26868042237d772d219db0924e2d36b592d2c854b7c1e464863828bf0c77be80ffdb1a5ecd094246d431f88eddd042872bf0bd76d365ac105fd58dc2020c290c414aca3c5ea6b7b90978a0265a5dd23ba1395c0ad38ff0a71ead1fef84d3e6b0f8927fc5db94790c1b48bcd4b5c80090e08667c5161a5c8cd9d1f2e38fc41a0b72558fa6f312a225e4f626bab83dacbef4a8dee2bb2154850639e627911666063e6c707c6767ab7fb61c0c1f70d148ae5ff0ff3ad8719470139880baebf44f2052be60bd98c379174a5555e72ae52dc445e5edb83c75c2aa7007625c862a146bdc2bd3fac0847a9aa956e0513191665c0dc1894c47e03d30807ca2f7b9bc01273ae35a5bb91d78a09354a562cc45e305b8bff6871e89cb14c605a193f1542ee2c71a4c99c80363b6834e10e5edbf62c128564a9dc560319cdc3d3a4eb1f1249278f5d39ba56fdeae85163b2e650eca02e067e01a5559df6ab86e5b02fd0a42f970aa73dcc79fe957c56a3f678b15c1d805dc6f10b28a10f7f6baf0ad263e96d1413bccd595226b89a5f9f70a4aaab303560e900e614be319052c65ccb0e620c7e9a20dde26ef60f761a434702315f42f3bd8b2eef6e237b91f884310207e9c5102e5c253f519370d1453377953bb195287e536586c0f4223c8ef30668cff06f4d87c460f3ce82cf099b0afa5527462c8d63c8962e563bc2712eadb4aa4cc3c9c3332f5c7ffc0794c8ecfecaa1f635619d2746fd14b274a91b7a79f815bf9db9230cc675a617efefbe5086e28e07663e579eed869377f6ecbabbde46e625e679012fb624bcd0edc7ea3968ae406f04b0d95c8fe1147ce230494c503d24e3d02f2dd2bf7cd8264762bb07e8fb53ef933875353477270b4cdb4dec109e01c705b38101d6f4012c9fb1eb5578a13bfa8b543ecfea46b0cca56a6cf64a6aa19fed18fba5f432eb54cc4d7e3c5834996fefb88c7d84a97999965b74f374152e618c78cde9576d05feb7165a8073bcae1feb00e74753611bd27c0dd44e4e44a24d6604d785a4efcd8b65243c6ed2348a1b7ef434079f7f87626edda6c1dccbf569fa882320b4c6b119678bfc202deedf2006833c3d3cded164601b05d09147bded298cad63b652ada81255f89f81958ecc8d9ece9f3ff9df8b52dafa76a1276b37fcd02d3b5d8acb8a9309bcd37f6e90cf4724848120e4eae9198bacad026cefb9f3b47d43b98ce7cbaf4146761c6ec9cfd002dfea8606c138919b2c72c02ebea777836d30eadc8d07c2db19c7fa2a8bb8ce4f58b04938baafdf9fc9004eb1e67a43cd453eaa58c5ddcd06db635cf96c4e9f776557183c069cc8e2fc39387c97a5172139e55abecd0ac47e4855ba1286134c08eac8083286953230286fda486a2c1593910559e94eae530d1f07f63d97bd22965a5d728e11f103d55e96bf5c2c4c6a02846dd4d6ea038546406b0975a5df4de199aa7e304f853cde561b78d76174e2021e532e93eda453ae2321c044eeb6149470b5c4579de4c0633369380ad43cde75bffd903cf96ba251e587874ded2bfc41938410d2a7f014cb459d02bc0a50da5a25e09c5184af4194b7f8910954e9882ef4a04700e358ca47aeb58480f920e0995359bc7d1eea9b3462929d2a6619c7e64f2f29f090b403ba3b6702e7821c859f007d3f9abbb718dfbb3d43291e3a44d3e8310c13f84d74ff5496bac79116ef43ccaeae371a9aec8e74a9b8ea55d21b3f41346cb581bacbc218e0f121872f824c750ad59c6654fbe89e2dd975b5055093879f5ade32742f9f4fa717488063f02043eec8db1fcf194c60e97f5b673a170fe54e26b0eb8c78ec0a5956453cdac666953b66741f071e19d87da79dee4426febc49a6a17968002dc2e0a44550b231cb6c3d18fe260a5791e53e92d8a101b6a00e99a376d159258e89aa45e8911f0f51b81fff436ae62126045ad2cd0cfe25f18009747ae6aa70591d3708ed34de8cc32fcf44466a6d5415d45f6e3a76e5ba6e0000 diff --git a/garecovery/tests/test_data/raw_tx_3 b/garecovery/tests/test_data/raw_tx_3 new file mode 100644 index 0000000..c96a533 --- /dev/null +++ b/garecovery/tests/test_data/raw_tx_3 @@ -0,0 +1 @@ +020000000101c34eb8dffcb1188713dd46c1ae8b0aa5201ed6c25a4cf0cd6fa4be99ea7b68280100000017160014083e4ff8bae55ee98bfe9ef9cd146f9f37272218fdffffff040ad8d4ba795b5c8bb4b9fd49515813afa3328eb392c4a197cd70debf6997fe2f6709754c21f5e2f1e6f0afac287c9e43f76c5262e2d880d78c42cd98b8eeab64276d03d98703dd81f4387256ecddd96b85df1839dcbe1666602adf24d107ffa974cd0217a9143b342bc797467ae910e95fe513334a50773c82b88701230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000000bebc2000017a91400d74ffae0ab3b1d61b11e0d621302863183e9a3870a20224400d4dfbf7f374cff3113f0939030860fecc1ba696ffbc7372474f6dc62092b1b3e159ea3f164ca4bc59a7e4ea81828fd1bc2e2adad18c04d9d1c4aa89fec0230bae2e7687ea610287baa8b3112990d9e16979d95fc07e9c74b3bab11943d8f17a914ff111886ffc1320133c476f9989256a4fe7574c68701230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000142e00000a00000000000247304402205ecbb3e6d5f94ade8bb4e870015fd0826e496d44a9345b43ccbdcf777f42094502201d53318858b7e833f896fff0f09d0f1100215326c7c257e64cdeafc190b6c692012102d4341fc7f1d3274c6ab3491de83c3b74926c9dad309cea49ad678875caeefcc70043010001ec8bbb39fdb00830cf323cf1c237f818c66b28af0fbdb8a4ea13fcb2310366700f0313b836aaf8722337c758481862f5eea142e8f4c9d759d22cd4f8fb7a44d8fd4e1060330000000000000001c6b9c90168649c57bd64d1b9b361afd3b5c5a70e339702865da2da8670e3db05844eb1708dfb7302107d594909c0132b749ccf4e580e7e8bdc44e4a5a7536402097de3befa1c4472b96fd51da4498196e9d851995b758a67ac4ab20bd64b88fff88938d3bedff0bcd3ddcc536ad238a976695e376e821f9db809248501cd5a13231fafbb74c12ea22d514ec61a97044bed81daf3a38e7ca9b429d0b77279c61f87e3385f6a282bd0f43ab780b5f97fc4da04d5869f5987261a1fde65168876bc620329bd594981cffee584d9990161982ade37b44b5454ce902eb278968cc0ca10329922c1e7cd553baaf15d7648e7b3ff459e355d07ac077c956765be09c309dc6882ebf893845966698da2b265b4ad49d0f54ecbf150115de830a237e306ab499460f4fc572b1ef798aa7799dc57f053ec7ce2141f397b83c454ad2f0b6ffec213c51fe3c41409a7c063cd7e9149b9a02e936e1befe61cb17b4f08c9c88dfb1f47cc2f4fef0259b5f041f0dc02e90e9c8576c23f8d285e2b1f9747bfc7d529bf410f9da6a8790c531a2bc7375d057767bbf9307ca9d03c7938d4a3f218006c423d39fd5d8c68ad975fddcae3bc8e52ad3aacd5710cbb92ec7b9da6601955133ec2affaf2c043254363ae1ed6d000bb1179fe53992ce36e908947e326b2d49f9fb8cfa57bc6f84903b12752f0653b85ba685990c6b9ddd72d59cbaac6ac5ac1144a547b8b1d88f2a5822aa6ee9c05705854b1842cedabbb35a940dd100dc490fe7c8fe64a23fcbea1e71b566447adb1a5ee2fae26a327a16f9b05aaed754b4056abcf07ec80ab7c11819bcef1be45fa655c9be014e17a97b2f6415ac7445f5ee75e8309973390020f9ab626afa3a1fb4c06a6e536e26bd23e6505d52f9704a2f15a149ed5daf5796b5a2a1260783b1d4efa09b87bf096b4a854218c23869b73364dabdd087df8047075f4811f5e75640b8003c5a9df2d81cfd63b7d0364898393cca61d04b485965565e62a367cdd036516ca9936546bb0635196c3f37f4527502efeef19d3d3bc852a63f645b738c0a8d470164edf14ab0d9f22baead253d42687110420827ffc6fdc903c0a5c0ca7044edd39c405b741c0afbf2fb4febdf187b336a49dc407e059798f7a5b8b42393b0282f384a40aed1ae87b67e39c89364b9d695ae65755c4e32769ef6c9fb7e3442a08fae7c510a4552b8daab0aebcf94ef238e3b98da29442029696cbc53e78452ec629c4c83f8689ae7bc053f2762c24d67aa46496ee1bb20c696617c27f18742a05ab1ddeaf2b0bb28edc4a05f6136cb3e585ffcbbdae4fe253b6d3c27b7cc492795cc94129af0880f7ef335b7edafec2d2648922b8de065bb08ad0b2478522100d0320f155a2c4ce88924bca88b4557eca3f591827ddc95a393f7f21032a1212b4c0bbb8be0b0abaa2af29477ee0af12faaf17f072e968eabb1c4930e17bf0fd7354defa91ea72f34086a67477a28506a969c2746afb807ca388580cc040f0628fc2e767b2b1783b275f5b8c1a0f8a432257e3e2d8f6d73cb21fed916b8b321031e19c44fa18ff223aeed155a78c67a84484de4392741c2b17fc2e64dafc9bdcbc610f16c665e7bf73d074a952f1a3576a2adb9d7a30cf64d7457221e3a594b3c9f3d541232e019027068baf437d41e42edb53b754ebba49c4227ee2c47ae855b2378e2284c183020891f41f32090008cad03b3dde2afacb903ef5abee53012e9fcb9e31d7641730b730eb3f581ee24a84755ba390462399be104ade025511dbcd990b24da3405c83da9e19b1fed29ce2db15e24e3fda4a5570c60801d8f3d15ab02678a6098673342dc2f8962dd5de02fda1d09cd8028c9636bfe5aa205ab66863a1bdb4c732d0e986d7626beafe4a3cf92354bd203c0ef8347508ce2f6143334ce7de701872322cbfbb20e58f30df302fa8bca8c63ecb618d903d47054208298ef0212073177175b2af541d39287a616bfb226a6909d5d312e4ebd956525516f53b4da3c40647f391dd42e9682274a48e697a3c67872194af2a48ff19bf883c1f77beb0b447b1ade48531a6aa2634ef29433dad4c90f0012e1fda27b4358ed243d538de9779a9f4c8033fc548c38ca4226531fa670d13f90ef936e758e63508cd08855415145ed84840306fd3405e18db19a47fa69d7491056e4f1d308c6dc83dad06f712b76290da28e7439a277a991a8a7cbeac48b373bfc3c12ecd2a7059379d9646ad000c385b581376406e10ddc5ce07c9c01a15ef01bad8979c18420c9ffef69cd694a7573dd1dcbe3883bfcc9a7c0e98c0af5df62b89da1a1d52a802472fb34a7f74f9bb6351ea97561c7582f4a086fc98afe849564eca5d4099c06d766ef24ff912ae2245ffdc2637166a70e48e890828df8a1a8b433c4e1597737c0fb2e9a7ba0e890659f6bdbae426583f3d3c59dd42e31293a70942f0e71fa47cdcd0d136985743c4761bbd7ee70395ef17c81591dde4bb5357098b9891f498ae888d886b801e4f4cb3d8d7f2dd6fc720c647175b271b31a1e8622d16aae3ebf9ff2c7dd0abfd98fb4968df584d362022cfacba0f9cad1253286d96ef7e1709014637c0b538443fcc1ef5fe64d8fc12dde6c6a178fc6df9102ae84ef6720f7c0e4f3fd7fdd8d3a5fa4fb432c242cec1f85cb0fe0447177822c45ae68f06b3604219f97644b131c65f5b3e95025306a0e7e1c5a7910908fcbb1ca0fd6868c60cfc1e71f88948c924447a08554fe0bc025234a3772f56d2c5878479370b22e7df3c10d6f358709323d6338d7702eb40f188d4c40dee9412cb42e5c00d363d2ebd2af6fc48b1f5223c751996207bccb0f8e579d1717fa2bbbc2024eededcbfd98f8e454c1f87a3b3deae0e161bf482ed4d595c777132f500220fba58c42566a72afc5a1e7f2420212cf8affd873eea6289ada2fb383abb3886e457ad610d16256b041a0c2ab84a43d9eb8bf3c6c03b90aa4af108b9689a3b34c6b9a3dfd210436bbdefffab2fcecdda2e5dfd81bfa8df67d54fd11d7e4573856286993171ee2aa57db0c0faf9fd59c3a1c7af24e9640d27574f79e50df8594e4356c86121aa747c5f1559a2f1f3ef155be02564531bcd4ad39ef02ff4d038d5c9752bbb1e19e3ea61276fceaf1a30e5968f88bde276071125aeb430a6a8c065ed8244742c74e4705e1dceab37ee49cab81bb1925f33b7bc4f519444b53af3a0956b597a4994030a87a3bc56afe19b3fb1bbc2c359aa0c6812778a043de96efbd536b309cc287959e84eced3a1560a340df9bf7df77f298d63df52b6d88deee796caf6a6c4ea4465aa5538d4f8ad6b80cf4d0aa85792ffe223b7dbb681cba3e6db13c3aa0e3e400498abbadbbd5a7f95b32eaafe0d21c5c240c0da1ccacc874f8b84fb9c63bf5335717bf3a3c54be7edd997d371440eac936e47035e490a26e7afa1ce084ec9abe2d5d4039f83ee78573d7766341a8b50638aa027e52f5aa787260f8301acadd3b79d4f4f328b00223c60d58bd47f995fb505116adfd0d3090f966fe1f795d48a101d707c355ea04fe2750ea90daa56fe776bad4687f3ff37164e430cb748eeb4b6e3bd08649923ccded4dab656feabf2dcca15351c55221850eab67b26d8647883b292ef16bba193ec503c2d0c9389900aa0928247afb0dccbb00492cf73717a6dd72e6a188c451bbaab8e9523c19cca6ca9b97c39d78bc8a303349ae705bc40368ad8d5ef937509971ebbc960bbc285921cfc7dcac742542e9cd030c8a4f3c51f89b878b466b4ec13bbfd308968456410a7600b6f11190d6acd843c568cc88fbab99785afdb4bf03ee15fece3399433f1bdf3f78b174cfbc81efe12694d553efff5547a79df36a5cc4793decff6c0bcdc6c1f4d77ba4df95f78b88e448c065c4947a6ab1dc356e948c744ad4973974778667e023406cc8077cad249362392a79050114b05e12cdc35a28931662977216789c21f08ac40e432f00cff8eec26cf96bc185e89382e282f2f01fc77ca883b7ae5677c42c5a68e1d248d3f1f45a55ecc88c68246706f03e9789645aaa1b9d188012812af4dcc7c1656f90675897955e69b75de942710f3e719362435c6cc7921653ab7f7a820a5a2a440021a7401a4d932646790d31cbbb730b4d3df2b2495fcb61e0611170e099b6692b0bfb7665674396bd32cbad632b6b0dd706fec1f15a654c35fcb9db22ce5f2b895b4ac79169bd4cce32e2c53570d316c7032e756a413b00025752927c1116d3d8d0e786e7bdd38b3f2497841f9440d66630c3e5e555d1a0df270bcd1ee2380332d3fe8bc5af03ccd5b4494337ff203554838b973ad54b0705c73492473a4ebebb5a30cca1507040ad615718d747fd966b26d06a26de340454a6178e2a4e84e8e63742f97ffc3b839afedb66649627dc50099ee86cc2940816882372dccf7b33bce3fc95ef811ce4b8380f14d0e8f220ed0c329c85c591b0f29cf49342c874bae0a5288b947231a6dd50f140ee140746121b5fe182971f547bdde8f9b091146217275f253b8a2b3634a1b175b4a4bfd8cb3f6a2bc130814bee7f4850af541385ff7cc916e08f2f72618bf798a6146411f9c2dcc3bf51ce6b508a47d10cba15a9d6a46a47024fc32b716317509a05de67be00c19e20d510d0c4b49e6e196d55283f3cfa72d38e2ce68fbe66515f7f6e0496cd6ab38e0f6197e0b6f5cb0f9bc955d5f0945641ae5305e64690d45fc9be4406a4a10499c153ebcb5a9f9dcce56da8cc39fd5a3a1af3e184d8e84dddf45ba77f606e53d8a504fb324b463f26cf1499b0aeae06f513df0aa3428471bff37c2126c7e0172d1b6bc429dda33ceb7cc0543b8a87da0b105fad5acd0a2ed86ff39f74f66bb7f6a538cbb2b048dc019b7aaa29f622ef79eab3ea64b08228578aacf34a39648206caaab6f1187414f3f17a3db73e17d378101f6327ed5f79363d89e99e1d2e7914da284b52613a1b13e0da3ed5b15c0bc591136a27ccfd72fa9d6ceab0141ca598695999d9f7b7ca8516456c8391422a3bf4a06487a2a9b564ec7d595c5a346027c81f71d43fc4be9602389d83bee086c0ca6202b4d9d42a2aed083b1c7ac3f11b1c5d961d1ffe7997a53fbfa17a17fecdd15d4f31c9dcfac075fd2570f4f9cf50b18ff698b3a1a3275211e782ca995a5d39f785dd8337c76899ac78fb451c69caeca25948bb9d060b50fcbd9921a973ecca4c5f4e0576c53583909c8120098cafd8a2f64eb50f10b2993d21ab5b4e7626b1805bd57de265f8351425924ab20d779a8c74ede177c116f2743b0b4b463cf2433ff8736def517d5a7c2ddffa737e26a709d4318302b4d81164b425f85155a2fc71f9a2259872cd48b1dc1b5496ffa04f71448d04a44cbe0fb61b434e7b212d181eaa3853c63931d538e7d2e4c882ad1bab62e3e28ab235410bfe0ec44abf0b54b6e93ee914504a4903d52dd137a2f1462113af460870ba2cf2d28f849b307a97c960416f69149793a32a4b217b486ec684104ef89d9afc6c26cfec863191cfd0de01651e0ebf28d2ddaaf2b73b547eab9d3fa71ba43a156212aab6d06e1d9436bfd2ab544f492180e871e2b08a7c0a4f9db1762e57bbad9fc12cf1fa6e4e76e3523a542f21917366025a5be09cb6a4974b989d10561d340bece7f14828eb49d674865b7b719e78fe593b2c15ec292e4e8ea8a08554598ccc2ffc74ee3137192c0d3fcc3350e30830a46dc64a971a3ef23003117f6d378eb48a101ef3e3ecd0c9da77b557dc6e42a587a94cb18987b033dc9061ea3f1ddf03a09d0004811c3d75321ee7b40b3863904b00c7be50d8953b6d148c2a3237345141d945231878ab7ac7361049c86b7c6857209149ce032fe6165f680000430100011dcbf58c6a4e190c8491f226f467f29e0a8966532f2be5846a271cd7a1d452f2682c34df09cdd06fe84e8c1add4d2618dc62447aee12abbb399cde2fe6670756fd4e106033000000000000000104b89200037ad68dd1fddeac3fdabd1aaeca5bb02d2ac4d9fc9e84da2495721abe414db2b1259cf012ee43ae4316529fc4e603523cd08df7407b7505dd75fe21dff26c051cce1e0c2366e7d9a51a3b0540340830d20959560552aa02f7ae5ae56ab692d8dd1d33c6ee62ba0b3763fdaa9cd09fd7640057b942a832fc671731e1c7657d3aafa9b122567795f6765a520efbe9a4c1b879dbd80ecee9f284a8b0043dc5a6371909d13e7def08622f881ffb2fa847e5d574bb58889087f24e7fe8f8d7aadb122a14c406627c70916bf060d5bea9afd99a036956ffb06c1f158612920a6faeb6ea5401db2afbc479cade817013766a08cd5e3e236b571c471ccfb4d8d18549edbe04ce78b337f56d4d60ef94e36198daccfcd7f38aa15c8b80d8caf1190f7c11a439f3b3d94502242cf545103514838341d46aba97be7d603afe85217ca839b19b9505e9ce0205d681e1269e8436673ce00f5e94134353b009cb3a5df9a0e7be9371e930a257ec2302280e01734006ce1b273babd8014f37fd8726739acee0e32d56fbde8e4d374d32fe2b6e3b9b2ad953371149d58249c338b06e665499d0d124bfd3521f0c529295a6ddc7b91394f3aca1b8261350c35033ad7d711e2950e57a80a6706228490e05cce4f78670881668686c4300c568795804976c5595797f72bf9b6b4f1566d3306fbb6e5c722908f55b1ac7fa252fc2bde0b619964c32d6caa44de1ad4216bb31cc6c4bf0827412ab1f6f59da14364a6f3851593501481ddcf8e558a76d7f80df903fd20cab0f0a38fd570f719f905de87e5410c4801bb8349eb37e94787c9e8365812b73a8c6c45199a7aabfb12307de02419a587da056ed44a7923102f3ddd8498c2bc96333a7a85a793618a7a218825371506a768be5397d7e2a7b831c2d1314a82e2ba8780158ad1710d98bc52f33023b6f75d136547cc654729dbfa850efbe436263eabbb7a545d5a54536489d4cce9cd1a9341157d8d446dc461572b90c81b474a2c321a40b542026bb9b4748ae902579b8d3da4b3ba87226660c7747e4651fddee9ccc19e8fca6d1e5e65bc3c634c52fa3764b03f4fd5386fc21c05cbb770051a6835e39132f3c18682c70a1129a6abe7a69dacb1dd70dfb7106b885aa45b5f22e3a5ff4c0c32fa08fa618ec3393bc60656f9a14ce11e773c2bf09a7269e62deca004f694281ea306905bb39442541e066cfa7f4018aa7f5482247e1d17490a4179666d7cfe6664e3a3cb47c49ae419400fa30077229da574a5177da0151ddcfd26b5c4759753f6c387cb5f098c90c3334e9fb21280d2f7e5601280c54360894bf1d9e05a45551ac77cfd8c9700a082533a085f28581089652ee54c7fd317295f378ab6bb386fde820230dd0784f4c60ae348db8dcf1193361b5174f724efe03a50d77ec2013db37eba671f70cf4b67557c4c6f29cd9116f28132f0653e2105e23e503a1d04517d6d76568abea0fd5ee521f6061d524fcfb7259917ca0b19ae4b5ee342d32d6a9e87c2341eaa7e601e28f489f472e95e58b6ea1160b39f1c4879dd52cff1fb2981eec95591dd3327680bbb734aa91960c2a1362c83624f806a3b63b60f3453e8a0fc368fdf3f87cb77e9e105684a5a39d0ea4649b056872af09b989aaa0b1ea4b95a40290dc62b87458cf6605cee8bc96b8232084022995a38db2f989946e33a9f4b83668272f556f733d255a7e1ffb862b58c01d7b47812bbf390fdca1fa4f2b7012598fa19dd3962fbf1377a3c8a08ded3e44e214c3dfdcb3aca66e754426d3eb6db1dee08e7dc738f90646c8f678b452cbb621dd5e5d7eab55bbab84f4ae241970047e98da366a80a632a8583275e1e62e10ce88d7a3e1c96611c5c4fbad17971bf37d4fa8ff3075e51cb54cf3d639c05253f01c1005d7c93394d55cc65a29e2747d5a070def820f5701d084a2c12061009a7581cc6c1e033917697c5b082c5d1c088052d02fb77c112dfd4f23b4d30afa945723eaada2e9996fa4d9f8cb03a68ffc70586609eede80905ff6a6b3c8c5ac8e52711dddbc2d6daf9143edd8591b8b53deb88114b04b55a7ed85bdfd6061147ce2dfc15895d92880122f01cc646437f4035c075d32dd62fea001fa3f8a1590291b9a9ed89a59539b773021d5f63fdedb0d1efdbbe5b01ddf4727cccb935d781ccda75d392eab1eff42fe5dc8751cb58fe77d1811b0002e6c08ca6597fb87153f63870d599f09c9291828f80a0165139d04be5388926f6309db2ef97e9e0546e18d1cd4f38fc6ae55cf261957db68c6acefefde19f9e71a58fb2588087f8ecc732573b64e0e40a5857176d1178a9ffebe9c26c85332ed4cae3eaf5eb273b6c07a8268fecf139816d150bcbbccb53c764233567387f022197d93e87eaf8bc282f85b586082f775ec9d7387c4e940fdf285623d901f6e4d955d8362d0cab2d42dc1239923c09e06e5ac94182d7c05e06846ef4eeefb9e9f7bb19e918440805c36640d0d9b37a702ab84fcbbba29a2627daa1eae3ea0e83fc98918fe214d6db99d2825403a015cb05da45ea4122b18a22c3e611a406210458ce98de1ced9c6fb600db36142b2cb353c30342dcc8360694f5c60989ee6e953c74fc4e79774bee4fd5e44d377c95e7cda5df05806878a48fb79d204977a81e8222242216b6032dfe15d077c34fcc22ae32cb58880e79b90a48068bfad291bf4846dbabbecda0e0d39038f7cd277e64ce21027c3438554dd6e8508f62f3151a330dd6c98d3918691e386a323a67240c106fc40205a5315841f05ea80180574df46654c64a7940c07596e80b75b866287c95eef0d97180efbfeec5cd766cb66dcd4549aebfe38ae8162ea8ba9e3d73c1e67092d9ef22158ec9da8afe738f0fe682ef5bab53c62fe6ef6ad7c7e5fbbf951170d7eb49e5bf1e4978092548aee30dfb6cba15616dfe114ea267a4d63891101091f13aba1d409a6579c2966a36d53e52e3eac8c26b341df5910a8dbc2f983ed852741e5a13e5c2e2ff7a899c35fa1b127c70525723ada18e3fb3eef4fec676be2a0e5ed605728d12f4d9446e5362add5f956a1c32d09735e2da65678e9414a90a4ab7fedb79b4e506a7d619e72420c74397881ecc4cd66e5a022b4248d595fb19beb868888baf106cad3668788a0db4fc88f7480a9b09aac7a1816c4879c5ae4d6ef0ea1fa019e5a767302336460ec86a70372cd7198b758a97b75b755ae60b72e8a8025a74b1e3e0dfac3d29241d76e01b92853dca4132531423fb9c1016185bf1bbbdf9f1e4580f4a02cb1634cf0e7ddf4c003bfb9e433646999b840ed177b798d11acf61ae36a5aed6a440c9d1463f823e9f7acea90ec5004f0f8240f618ceecf7c1a12d8316ec68fd15228696a637d98b3820b4e40dfca06d8cdc8b7e751e767b6c2e26e6a2be5b19b76ec82d5f7d771e1fbbae91eb80741fcaee51d4ce36b1832bafd65532b73a57d4776db542c073b73934d9a507a6bda2f0de535e3767eb39d4dd76eac80696d30eba5c21b3951e15c37f1ed68c92c99199ab7584097fcca421a43164afe2710ec84c7adce520ce77b3b7635e37a7f2de8bcf2365a815b25edb2a0f43bf26343f686892650469542f79405261d04e40e59f9cd98f4f4229eed0930bb10bf6b4693c8850af3eda1976b26b7ddfc77eae8d293c3fbfe2cfc8f0b4216aa3dea537c39e855e2081eccb9145688c2eebb578a376df734cf859e42685f97eb6b46f1449e0521ce339feae55f66e905b818a9d9dcd3d1215e5f3250ca6716dded97878abdd4afb973933d6e3581dc475d6a4d790da1e238ccbdfb9f14a5d35fc46d740a2868f3a4a1c4e449c59509a786e614ec66a1dedf97f2d46efd345cb5b7c5bc2b7b161428ad157e371ee3c0fb95c6518425757baeaf038889554aa59205ba05769c5903def94659ddfd018e30af61c8b54f003c93fd22f4be3e990f258dba788d5f840966814d5bc7072c08c825b6bd757d59134b2b3e2cb6f4f1b5c008a357e11f9ca2bce43c708d74c88d71b2f2942d9fa8119a2385a6f9aafa582411e383d4696c90faf5a355186d360605ca70cf5960580fec9032ecf55ee98739cfbfa39e8fa780f8030b394b58fdf364d8ac325475e2fa1654d6d46f619b79295448b2b88bd58d4128f40f17042ea5a4c8101399196782ce655aa37ac6831431b1a65fb8ef3eaf5d8df9aa701c9daf8cd213adc9069d984ea75fa0ce42021dcae7bb759fbf9973903e30ca6a2da8208c8161f9e7d608b50b50fd25153e61668a0db55fc5cc3d0201a4c7acf1fd5715de8ae3056432bdaa22b8803e74eef71d786e0415c5e56ad2efdc5b49119a22287fed20c1ac0aa6ba03531ae6e4001e033c1593bbf440f57de7482ec6161d4787129a4bd284f603236844f571bde1d77eb5d60d16baa9d468040a4d921fd06971c31dd41239970aafb38cc6fad13258756967cdccf1527a16be51d426f0234282b98a7a029ef1f8e232b60d904f0ca12caa451c7bb609e5fd43412107b90bd142ee10c9faf08c44a6d6680af27b70e08c6c96cc5e69b1b5eade4232adca60914d4b4ff7e990546a7abfad5f51688eed2a4193685b2e0599a751be9df6e74cf63617b5eada9c3964778b33af133f5c3fdbb4f78ca3dd7b9f300a7a43f595847782c1585e86380e299eee0bcb1797bb52f3b6803a6f66c76bacb962d57409096286c696828f3062344bca991be0945049c0172756c5eb74189bf31a454d1415ca5d535e78a31cc201ab9f7bc73e4f077ce87ad6bd1f6faee6ca9cf78e2e60af4269375fcfa324ec538d9408eab7b3a6f3ab17a76f4f0d6f1d66243730aa5792ad7323135960379912d37e73b101021fe379a8122ec564a257dd44d01f06063eb3fe50da227b8c06a42b1f26db96f91416a0f4bafa4f97763f8710b088a94a273539103afffa2c0e4ebf80ad4db3277a9645b608229614746f1b4cff58d98baec99737bb709f25340300412c6c0e80cbc4e29a349276b68043bcb752d422100d23c42b87b1504d07da5e8a6d96c897de1f8465b6788afd3eead8b2aaac4dad6a578b861aa76fa4dd2ab811907e2444cd748ee88a9cfec12629183245638d02e756126ab3d0758bfda776179b3697c93f50d01622a76debb27f24b0c0196b674aeaa67fcb47ed5bb58301f10d49557d732d8bbfa0adbad85f18397c8116cd92209a7c2092b4a2b53ad638a88da3284762f836d0b705aa01c9c8286bd2265807ba79b4b1da69eef30c8cfea2fdd066a6b5db8a94c41c009f0a2a507cbf6f649d67a2de811c11e171b02e8ea2a011cf9b80ee8ccbfef41e536218269e276b4a211ae3cd65d1c668216775ba9dcc6f3d8468edef27454d8dd581641a4a5716ba9b09082664ad0b7a6fba3297b5a2f9722618313ae3af16f209b18f3bde3ce954f21ef2e3ad4bdc0a0c3fe480360152eb1074f417906041c7dbf2575571b4214ca66aa1ba8575c286065e50fe8ad3978401ad6dfe70bfc32be310ef0ca849d39eb5c1e6c3f1ef002a6eb0a91421c2aef3f0a9c6a95c27170598b4f2da64877bd0fe0fb172396b866d47921d1010b00b3ec3d2d8aa17a5ece843dd046ea8712d257be69cec8cedb9f7a43d22f6e39d2a696e39a757931406d99352246ba82f4dc06e65bc8d21617927d3820e5d0dbd210d8d3da1fe7162d32fd9319dd1d748d3d5e6ccb0d6e8a557265b754eb14f89b3d3f300a9d09614b6337f550ae81e9eed38f6b49d2b08903b6a598c2ceca3d1e7af261d0bdf51472346db3a37411b8a74f1203650ce0da0f84fae596829d35752e2ccb821b6e3f21d7b2a11a000e405570a19de7156f46002ca098c55d5543270816fc48f90b43a8d472d9ffc472e16203d631910000 diff --git a/garecovery/tests/test_data/signed_2of2_csv_1 b/garecovery/tests/test_data/signed_2of2_csv_1 new file mode 100644 index 0000000..7e49053 --- /dev/null +++ b/garecovery/tests/test_data/signed_2of2_csv_1 @@ -0,0 +1 @@ +020000000001015be10f9d8d5669a9c81d2163309b1ad292c4b8dc5f1555e41d605af20ed7b50a00000000232200201c671f617cbf0f118196168615df4dea30097115db821eb5ecc5f5842452eb439000000001978501000000000017a9140000000000000000000000000000000000000001870300473044022041fa7e705df63c468c23e2fdbec492c7933650c808e1fdd888e83e41caaa500002204ead4b3eabceb9465e4ea0637eb00255814700b2c8cd3441cf2c5ce614e4e0f6014d210285ee1b46a8a29c514416b6ce55aab24c0a5d2a761e1b80ce41fdf66940da4265ad2103d5f6d4bef080bd00c0b3e9011a566aefc83c1e30962f0ffe6684e1022658e6aeac7364029000b26890000000 diff --git a/garecovery/tests/test_data/signed_csv_1 b/garecovery/tests/test_data/signed_csv_1 new file mode 100644 index 0000000..8b92378 --- /dev/null +++ b/garecovery/tests/test_data/signed_csv_1 @@ -0,0 +1 @@ +02000000010117fc53aecffaabbec168a0b6ae790a457272a661eba7797199217cf260c0a1d50000000023220020390a8d0dfbc07955837d8b89cab401239430a6dd96594430ffc74899b4670c7f90000000020b76eb2ec412158630898a5a001df763be7a4d8810a3a0a88659993311340327ca08fa3520ccf5fe52d1af9f85e83fed7da2de967fe7193d760f78b3a2a95275301e0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179817a914d7559651b007d4971de281f81347653fa8cf9e008701230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000056e0000900000000000024730440220047c197c89992d7643ddaf10b2d478605658d686ec9bcf689b4c67d52aaa8f9f02204a879dea2beaa3b14b7eb86ae1149797b8629a6121227a07aeca35bdbd2085d00150748c632103d5f6d4bef080bd00c0b3e9011a566aefc83c1e30962f0ffe6684e1022658e6aead67029000b27568210285ee1b46a8a29c514416b6ce55aab24c0a5d2a761e1b80ce41fdf66940da4265ac0043010001706bafeb28aa5ff816b9121629f31d6133b0ce9b3e8ccd524999c84b08a29fb5aecb7b235de3ab1a24a3695cb4a86fb8ba4caf6089785723a084bc72e56b7f39fd4d0b602300000000000000011f8800cdc88750649670abb2b24b0fbe79311dc9640dbc94c92ac5c33d208a6d4a33182d884871b00b491b79bbe5487eb11757ab0a292c387bccf2bf28c9ef9cd9c4a969a57c87e7554a4969efeddb7a56e89f59b6277bfdf6901817f35277e72c61e13c0c8f927487110cec85e6dfe4d52dace96efdb46fa6002a70488af72abf0c445d42b7c6ac6f1b011f966aa78a8d11ff7a66cdffe4b326bf430c1d6744d77291b0883597bf557a4b19942273c5e1c485528369e588b38e12259309f8d5ac34bb4d9c318602ecd50a2fcc5b117cc3532e5f203d44f559bf94e9e6b247cea34521479dae1ec0e54fdc31a47ada4fa4cd6fd3788ac8c37c3141b1a9f72f1924e4f684f979089eb3d1c36f72cbf0bb5525c9c6c6e7457f718729944593ffcf8e46afd73b2f6412741129520669ecbeec37d03ae6e9d1fb5cca1d1912b9488bd1c709425d17dbd74d9e1238bb8ab6afb7b7f29848fd700ec6fd530c86a34dc2e993f4f44dbdf3580cddf8aacd29596ee0efe2154afc8052f121331fab1847f511cd4d4905d374398d8fd36e4c2910e44771768ce216c64c6837e12a11d1909c7a0d40a6bb1e14bc3e1173674a8968bbce1b2c6f0ebecf50e572574030e178d24b53f1171a38a3ad11b5c5787730c3dba5428365c88503671d0fdb25aa56d0df428cbfa981fec94cd2d8366533dc07d5aac6db08a5730966eaecbbe24cb9c3ffa66c7ab4593b68eca1df0d688f2159205f0acfd9f869e7ffd6ca2b0c46f9e4923ec4a0073064158ec2d9d6ee3eda7b84f8cc4ce7c95ad2de51496bcaddae710e23e46f3d6405161e985e8a4394d21b392f77c7c34029603696131d6c83f2f256134d3379a07a246d6aa45213c6f05874552274fd1bf6842ee2ecef8a94d698d1b13478ea1996a06cf02ec8180b1174e187b438370d1d14a8aa6d1bd9382358053db7617afc369287e527e08e3b8199af439f184059b6e13ea4598f4032a97c932768b675a7508e1f4dae102eeeedb3c8f0c51a3994a9ee5c68ab33e146d8b2e69b00ddebd00d5ac26b5245a1d16fbfdb24db457bf65b356e09f70b49db06e302299c04fa287b49e614b4556d4c79de306792461404a3d03ff0aedcf5f6eb81f9139511575521229e86bd1b78f0cdb4621953b7a580505a9ac98d17b48cb01ac1337378bfb654e8f0051beffa5e595c3cc421c4f27bb4fbe4216e951a2203950407f81eb5b38d3eb893b8674191184457ec22b5b9aa100744e16b6e2649d21c16b30399cca163494ea1e1bc769bdd4b728de85be9c343161312cd18db903f557972993224929b8fb0c9ad8c13b4b089e6d8d5f3ecad9ccc783a727130dd79bf990c1ce8c8924a2a9f75be2bd878785f01d48716c179673b9a5aac3481a9bb20e9ef1e8ace7072fef2ce5fc0cc071f65f508a64af1b1c84d542710a108b592ed6e3883957dffe0787bfe325c941834829628dce38c1e5b8d73e835531605f0bac616163252df5807863c3b76263666f26bd7a22e50050c36c45ec48da8379a5983a68a37958ba97303e81db40a6dab3f5718e49549d279d278b2363d7c87f4b99befec5fcdc9dd04d4536b903101383c2ea3760dcadd57332605269d719fe0318dd5bf9580275391422a8cfb729841a0a1764db9f96e5dfe637a70a469de1dcc7be97ee9a7ba814d072e3ca40e0db16a1cc1ff5fe5022be11582b853de419b86b7fd8374f89ced46a89d76b4a13d59d68166ec3d313497168bd896e7a38fe48ebd278d710da792289205748e081f0b36168b2d635c7b6dc03facf32a4be176aefb82c7956d91988388edc406000610fcd7adc081d489b1efca27b24e056e334edc9da6523c3e13da2135d9b057bb666e3a8c629f608a64b6eafd79f55e440526d78e305fa666f6fe6992de510da922b9439866a48a30b9f1170eb901bec0a878684f7b16a68603494366f831a05065ae80d37234dbe00291dbedffe1e9e0444400337de404bff6a27d6dd5f2544df911dfa44ad975de31803708306a99e73b37d713d082ed49b925480128f8c2f217419dcaf56d37c2cbbe42e1571ce3c302b151ef1ba5adc21849bb60755da06957a68357fa73db65cd5e24bd583d046a837d2d7f80a980c7a432a32b840a7c7518674ad508d074f6c56ac1d7b83783b83e6668f34220f964c4afc4bd385f59ca9f37cdf493a1636b17de6220dcd0acf59aa5fffc4f59e7fecb51e0d24664b67e0576d29c775367468f51339e3f2c3c173e1dccbc657055c956ef306413e930554aaffaa309458fdce0b8881be8ca2ef6529f42556e74aa84d2b9467620e71cd50f740c6da75d9d1a63a428f96a6a8cf6741c1c9014b0f97c3d09174e7e0ece5c669bd479a94766665f61546ca995d3e474127c6427383c0bff0303e1c0219b6f7d07d181e9844665e268adf933449d524796e99371ae1367d9c0f068fc5db5648b672824d5c163f639fe489491899137c9e2f2ea42fe005f64c40d77c30e1bcfde957d538b83132b9bdafd3fb03519100af367663291f1f3874398e10e1e68399d0b2dfbec1d987d3bf1f0c726e92e83f80920da97691234384d9d8330f1c0176cb7861c3073139c319d0f7733b224b9e4e592577326bd39496f3b14e4e63cb5827b7ff454cd159e9b401fc2959d079dc7afe527e7daaa1882e77e19d24e13754be5e1b04b80ea60f7bf234ef2621293590901f7c6af2973fbf0382ad5873e70237fe51e7e37e678efea5328cf68d165c66bbaa79f4432a4415fe4da9742c2b7b5dcbddbbd012f5a36f8c69a5404074461f45658d6692c89a2b324e12471810cd9e9a30571a0872d48f53831f1b1b97c10721d000d5ff15a534ede09a305517849b33cfece3bd4c2b4e934f054e551c69fb649049bf7699a2d70471d67bb09ec25bee03e904d616ca4c0c9521f3ee746e0843705a3ca17e19a3055ef9a25c4e63016c94adfc09afb703f1af6b73844f2e1117a4980e94bd669e8070b91043e2ad825773bc2b3fe7eac1762aed72a943d21202b17f866615b687bb2cb0a7db571594e3a122f29dee8a5d2fa568582d9d1999894dcf87d4dd962d1a13d13ed8e9b606b24cee5f651b4c74ee2b7a6e89455f51ee628084ec5e804bdb083f55709871b067db5a2320e5c52f156a26d9c0dd417bbd7c8caea14c70b9321f1ea2d4ccd4307902cc5aeb1dc33e946d5c7ec53e51d9b32dfd7843b05e154770c676460aa923f08b33351295f1f794b1ba147e14b2a671ad537e2fab82e554a6572fd4260bfef3984a7745753d5e149dc4c0d5adc7080aca5efb92f3d05e78ccb603dbf9dfe98d89eee1c527f560bb67d493b6b4725367ed6336ac10988c898c3215a49144c197029d319f564cf66adcdb389f3c4df2d2dded62f0aff89bd7fc3d43c1338ab9b5628c33afabb151eca078135af240e069c5a3fec61e932f5793b1f19ede2efb6b6b78b8b2e97c71053517a19d83e8d1864336ad9973f7256a76014e5eca70bca90222f5d6c337765214f6e6c6e2e74832211185dbc03a913fe6eddcc2137ec818e6806cf8bd779c3745b85c66a1ecd2c7b7006d7e5d925240c8f3051f81c20d66a076caefef9f7bcf58c793d34948fe3dfebab7ccd801a0b22462da0a70cdadfef13d532e50260c802755afce9923c6d7cd9c6e668c9b512c528fe74a074ea3ef8f88477ed98b15bd260e0a3114942a481364ec9051067d8ad0e8d01d8d5d368b5fb2bf9335ac064571630656b5bd3f12de21e4a28ff3ca7c39abd184873381d51dfea534227a729a48e660b2125cbe56e25b5249491240ec726787af18bd23510aa487339f198fd54337f9cebaf9e6d1f5ba0c94200e43c8873b659dc8164a5dbb514696418a3fafcdf2895f5733610d79ef6bd901eaae2e3da3cab653b254b3bbf34d0d3a0ae2b3da97b5c3f2e524d3209f224a3bb81713bbdbc8149d545297f4b90bdec446da18c43b767fbc38fed5325dcc845c3e181cfe604c269697b501cce719269db488fe979525a512913091f7cd7d4638dfa7a5709ca5bd00ecb439368c01eebe369f24e488f3bf0350b3e839686f80000 diff --git a/garecovery/tests/test_data/signed_csv_2 b/garecovery/tests/test_data/signed_csv_2 new file mode 100644 index 0000000..9beaa70 --- /dev/null +++ b/garecovery/tests/test_data/signed_csv_2 @@ -0,0 +1 @@ +020000000102c34eb8dffcb1188713dd46c1ae8b0aa5201ed6c25a4cf0cd6fa4be99ea7b682802000000232200200944c046b6768fc362e9e2e394193d88c12704a4bfa2811887b724ad6107155590000000c34eb8dffcb1188713dd46c1ae8b0aa5201ed6c25a4cf0cd6fa4be99ea7b68280000000023220020a1664254fc19834653911603d6aac8d294afd4662d384de37047b1b6bce4752e90000000030b76eb2ec412158630898a5a001df763be7a4d8810a3a0a88659993311340327ca08124fe0080d81fe95ee23d6a87c716be240c45ffad62b0c8ec326c3ec94d8c5700279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179817a914d7559651b007d4971de281f81347653fa8cf9e00870b607661340404294cf30caa2cf6299963c8ada3d787eda8cf3c16a7257c06f25208320a28a2519aa48849bb9cc3e6f9b702e1e98d36acfadd6df67ffe62b238f7270279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179817a91457d2b3591fe27661d0a785967126abf9ed19999d8701230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000000ab400009000000000000247304402201fb44fe2a964c77d73e0a2da1b4503dae0df691f7283f0c3ba38110f40da3c0002204450cedefd38370c8363f29d21542da1649c970a294d494eae42e25d3e16a34b0150748c6321032e41ec9a335c00da06d259b63667d6a628d9c5cb385ad4ee03e58edfa5851c00ad67029000b275682102e167a7a904364b23dbb856f14aa507e187e38a8d7d7311291ff2882ca1dc58c1ac000000024730440220303546b2f1e9a5580f515d06d0db17cb0c1aec366ab3b3ceedad6ccb2dcb780402203f42cc7999930a2201df2c575441947af29c8c2290d50a3fc601e7db19ecbd410150748c6321020ce4ba1397d25be82fc0fd7ec6a9227d9be3d24f5b2e028ecddb845de83d19e7ad67029000b2756821027c04383f91ac56b442cf7e01cda5369b56491e5a6b1491fc6a38343432a30011ac00630200032a4de60d0def29cb3516dca8a6cd28ab60d82e480e7c1aab6e7b0a459959c18effdb7b8016d2a097f3eb6113f2c200c67c0b73d3ea8a46bc7c228ecae6c06d3db1ca4f03aa08d512dbe0c4d8a323323e65257354e6061fc10d5e610c2beda35efd4d0b60230000000000000001f66d01fea6c031d02af3bfb542d248f81fb1cd1851068e8061a9605a6f6b19ed00d1e4231a7c614cbf8d525f1c0b646e0788630c7f1dfdce397279b3612fe5f9885b76dfee4503a8f734c2f49fcb237e6f0b3c965e5be881fc8cf566d5ce33e6c3da6364ffaac229d769359fed693cf3a170ccb6e4f7e80eab9ee9ea55e9b37622a9c175a114382254e5ce79daf44f5bd0ac46e567d49a685a58cf70ad45388a9376b238cc70c00e2702353a76c903ab919fd323c5ac3f3814b565571f93aa215ef7bce495925ed4bc6037964188c3e8842b9740576ec89d25b7faa5705dfd828bde4081a915d67f42de0d5ef6d3e7c9456ddda3d527ef6c9c2399c1ecb2cf848d2367f4786811b7529902b4c58c3785435f8eae10cb8dd6a27cfca9c3194d50d2f79629340651fb285cee7675122563b3ba58617a9e1bbdd6545f82b2e2abf6e0ffd6188b576cc96a06261735bbe2a90c24cefbf2cc202534ead2f865bf6751880d51443b12846ba96bc5fc0551ddb978b900ff523aa83a1b90ea725e6b392434f4e8ca2b0a3e0e79bebbae94ebdba81972738e5e63d633c5231a5921094fec828dc321db3991653e1395c524a7e355f92409ba50e45c357b3f675c469c4b1bb57519458a805a3ce5e3cf1c2d0562ae70cb3716e7b5ddb9efebdb47c277b14e57e85609c0f87e3b67d953227815fb1939e5e643546e83eab4e23e359eac1d9bc7ac83b0a0a7ae5a0efb3dd73103d1c897a216a29ce684674673103eaf4e1a957b861609992e480b07e42b75b34f6b5f6a4dd6701cab82bbe538cdfe345d34df6102ccf0f5a89439893ba7c5e4ffebe1f51867d45c54396da1c61daf3556813ce39963e2c1504fe2671e7d6612351b7682787a253ed4e164ecefb6e78519e7afd1f9f620dc17b5edbafba62ec9c8551620094b7360f11108d41aa976dbdd9a81e36deb9094b332dc7dffb58b964edefd495d28f9f12c57f78dd4208b155d3718384c017809bddaad95d84e2582885783da3aaa555980cd9565d728ce7bc2d5807795e7cf01eb4c23ff848009220e8e394b3596429f7fc803d12e3b26f02f3b522e386987a135b15a15d1e073f317d55c456d17002348aab334fd50aea75d63a9fd1e23c5952fa97570337084f73445d281d17c93a724c01f281470c8d181d16128a46f76d7c7725f1d3cc9400da4370f3181fdad905783a166bd11ed746822c6f8663ae6a9de0e62a6ed9f112e80dd76ee8ed5e5bc18cc9225a6205ddf05e06a9782c76fc621c31697dc8285b7442a742d7fe92406d4749c16f7cd993ea5ea8308c578bd4aa2516ee471e85e0a93a53d005377ee6b91514b6e3e9267c070b171483176112101a4b4dd1c2d3bff67b63a21e66a42a681c9216d9013e944bd7c87822449375794843f4ebac0678b0baa46537faa7198743e5edf260ee154c8c5c5f58b3be2eece160cfcd5a6f17e9621626da13b2d440591aaebeb21a354787dcdf5c46dad9e18c9810abd9a4d5bf7972d1bca2bd1e57cb9585284a6c2c7f166f8821119b24dcdae7b70ba34dd61f3f52ab8c337ad3fdc628996eeeec9071a1096a99ac85a570791406feb991ebfe5d0a8435ed49b7f166dba5e6e850a01f0edba29af2db7479ff2d3f4cb1302e1c408cbd4fb1d7369c2cb32799ba70983ce0a602482583e92b7d15c9afcf2e04a0acd89ddb99c37eda9cfc31929b0387e61ff4fa0b4c9b374431d6681833e9bf5ada62a1c9b486fbdcdb1f006615de04199185eb1000e4333f0d53cca19819115c02d8647c4d28c620864dfc8f41d3c82381aaf947aca8904869e4bb050b0d57d8014712a9870d72c9a1832515c6dafb97c1497e7dd5970df39389287c6af6d447dac6cd23426dfb8002a75349a4fab0b33c658b0a233b27147a91a6de0c5fc21ef8f2f5177b3652ac5f3c85eea95c5958ac23f53823c661a9c4184675ee7f6655f62a463fd165e5c860f17a26283a187e53452daf624da5825afc82adf56adec1040d787719105ed197a269427dc6d905a5801e9c3473c161633bf60e7a4d141560a869f8a0f8dbc75cf178068e64cc2d66103ec7573a622b52559b7d0d5b1ba145197501d21983bbc763bbd42fc53085c662a3f461dff39800a6e9e47bedfdff3d499a226514e1e149a1604a31da0a2f6f30532af6d8cb98270cd773d3a556220de09a6aa5fe266b0c25f277feb085db55f3ad70d01a7dbd715daaf026baa3311a52e6d8fcf44bcf2f7e1bbad245a41e83df2d8f40f3fdda42936e4bd080d447c51ffbabaa6c06115b8dbe8dad16ec5ac65a14202f4e5e7c495f380fc6dc8356a02f47232b07f1ce518871ee0c68001d343529f0eaaca7941cadb3ef2d729cb9df6102912eef786e9c08d604de8301e32b5056ba715a88f1135cd34ebd08fe7cb9baa1fc83f3dc2a06ee0eba741b262c5de6892158a018ca36effe464aca8b9e2cf27b489d73a51c84a11a9e4f8bdfa6d8b677ee8cb53b426689f1c663a1e4e70c6301865afc8c3f85101991fd04c16ca248de114ab521b75c6473571331894545c28e6e6a8cd77f25ab24a27ea2f53707be1d20cb3c9f7463e2da679ed1abc19b24d5626548ccbe228906c2dd2d81028960d484143d2f10525b9f62b65093bd485816304666780a31ad7113b9e571eb8d1829d0632a19be8dbf2069178462f7ed08c561211dc01f72628b7af0c0365a82561934e736b24c318dfc476571d42b53cfbd7e0570626d740422250d1f28a88519489e8587752b968b67237ad150b143d4d3a2d32284f071f8b4cf9b0817829debc6cc390d2e9939a92852bf44cb5b33296439934da3c7a149fc881830d006d88a0c2136d9419e23e46446cfad8b5c2caf54b59836d34a9d07f47c5ba7bebb53da05b9b6037beeb14da9ce23a4aa34d5dae045aa22dec8009821963d61d42481d42a66e921d29927d1ece4730c2ca216257e7ea265e3b7db5d961ee7a581d1b172de17f4a7682acf0fad873fd25e123896e1304e675739984d8e1202a3ae14b2ba343cf3c052246f9224f7b010b2d35a7382834c057a2eb47f32d42afeb5d6afab8e08d9888ca47c4d1ca59cedc27d1f3e901e8f1f1ff41b265f34f12754ed9cd21b7078bb35794a02dd867dde923214d858070ab2961b71523bca2251ff91913124a0ca2e109c17af141f50ee1d15292523be77bd0d0b4821462ecd4e6a79f00266463d9ade006c7ded6271a74f477535a8609d89748ac529ed18c0e4935c6b49c4642dbf73e9df04423f8aa15dcffec680636cf45a9b3fbd384f7ade6d145545a26acf7e170d262668bd995a0360a8263b25ad5c430572b2bae9320b5c06f35dad53ef24e1f198e17cb4e45ec3fcbe1f3dc67b1462704c1d3a7f77108ccc11c45f3a0732ae86252e2850b7a309260ba3c1d070e543319ba342eb2ff8118487cf05320486d3fcf4f253885670acfd82814e09dc3b7e01606ab3c8ee08c819779c09de257a9ff8438cc4a35cc0946722a40b15b73608c7685f932c12b6504be93344a89870b27b388309046d8c1a694dea0f77b9b9a17ed762e3eea3b3efd2cdfe3bd591fd241a7c968bd650950a3a3188d3cecb9b74dbb6e6c7aa31f95d9f022b391afc154c69827195f241af9abf097d4097bbb1ba509fcbd4d55a1dc6dc0235c9c27449f7519b4c8cc298091f077121e83dded6ccd7a6d0f8770b2fe81359fbe1e4d78a98c7bcd03df7a7bb87b18376895316c59a16d4a4aa553ca481d53c74987eda41b570fa0242a08ff7956e3f081e7ec7aeb03bf49cc23c0b9dc1fe5d251ff2bdad928da6551406162c98961a69b3e4bb9db022d2979f7118d911c2a4790847496244da4dba97309504a533e494168ee6e121da7cd24f3aef410ec8e04d669e89f031e43f6aff5563844c930660c10341945a9943abea630d19e5adda1e6845e3691d2451f673b55569b92436944de21055ad05b2c8d36d72d6e72e69cbb6255980caaf8318c08f6cb26b59e3ee3f5d57f25ecd9b369ded8819d77461468f34254a310b478bbebbb12869aaa915861fcf2146766b207e190daee664798f3d882bf6302000332995d8f1e68209404e38f2392e49da50cb6cd8f30c2cd3c9de1b84b9e3f608f86e998ddc58e659de2df432947231e72c8e18cb251db683d0e82896cd2cbae474138d5d49c43398a15ee5b4864a5d33dea47521ba1ca045156278afdfb057aecfd4d0b60230000000000000001d24401a2c7b65e1b97e2b25909e9e8c791ade0ba1db03c94245588f8993a016843a6f03951b59446c7403e729b490b4490f6fea9a4df4399d5639a236c5e5b1eefa34fe84a770c31f7bb71ecca501e1d2557bbed0e82ee05a58935110d62ddcedc8d2aedf48d1d5acc6c555717fe0a6cdea4a6c17435543cf8258682ae30edc77bb1e9a0e939db53af0b13eb9e38bdf4b48c0c46e52e5d2a4eef72de4a539207348ab35ff28dec0adcaac51e31922e0de2a175b9c3f4cde1eff115c1333a339e9254d16e96c9ae110254a84b17a73f10b8733270f4d4416a95a6bd223b45a38972672752a565c64189f78ac8ea75004309c1892d347ed34360d327d0f48b6d72c0d36d8f3856a4614b254a24a19736676b4218938a9882c89905673fc889e1ceac03cf4bd4232bcc6a017369b5e4ce566d25d19b068db7aac0b6f95b9d6722ed2bb0443eb2df9f1d9b812c3cdc351261495b87b6601a4121e8af92257edcd3232f1e2cf754b155920fbb843b680a9df4fde94e39e8ed529abea8b8f5f317ce95c90e2132858a1a3e6ba8b7c83f94adca9fb2471dd0cd6c5c4b2862d67d05d9f7791ab8bec86f8b1f9eea33eb0d7a022ac645acffcad6ce806dafa52ecf94d31729b550257e92fd7a47de82e58b5c57a7391cd4e621cfbb6531ed73a0499a08d5520e5265fa31ade49dd63623f93fa2cdac2c46f273ef252fd018d0af268932e90ea84739fbc5e5f7a4e4dcbd52d72559e3ea16f014cbfa1e0cb9ab2884bed325582c1ab7ea3ab6211a047b405dfecb2d83ee91fce0bf92f1f28600e4d3b114e1bc5eb5208eafadaa7357e9c47a452a93da05baef987ba5fddb9aed90015027e10274728484f13277b42f2634798099edb9f3e9b6b48c04c6f34b79fa9047f67929381e2d245d3a331a7199f717579ffad08bb3124d5da23e13bd10a88601c2fc08251f9a03d1e07ef0d966b5b0c6f49f996091047b1e9b44f45afac71c5745ce6e009a4a1bd5cad802b79a1461ed602be68473668a0e8052eb1b15bce8a3f6392c1c6f63347cfc67f88f1ef33ebeb3494a82d71ada289ac9e77d6c61ff756df05b2b0f532c00960e3ec2b3c65574c45e9040c441adedc5da61a8a4dee0783f888e58caf2164113300f10173b9d3bc2adf9f658dabd22d520fc84f7248a6a14a4d7f0d17f161c66fe1efb05b2a1ef4b5961c66797fa892ada71b07c680d0a398051ecb6660dff9898bb7ecff19a7fb081aabf36f76a326fb803032cbca4019d21b06f69744c7f18da1bef17b1602553b0a1175d4672d364d28d72e3ec1108bd4bb6c07a0b4735bbd9a0dfdd93738d6424f5ec177eff0d04bef90360b415b1f7709e930adaae0f19d800be75a739473b566b8d8082cf4245f023382ba30c24a1b12c09d4ec30a6031ea098ce6e380d769c20aa50a417f55ed29eab75e30198c1e90317513c20bdfa49abf485d2245c532bbbb3a487833be4f83fe75df315cc5c01efd28d9fe0cdca1717af4286e8334f59f7d8656f6ddc2dc05c5adff19831cf43611c810cf1173936f0cc5576e4dee338dddd11bdfe663a28c87dfbfaa0c8c736dfa61e4ed7b6a0860dd8a834ce8c362510a042a95211f3df789a27ccf73967457c876565ef820f67a7c8bb7c3d0ee7011dac27c0f85512cc83c5f1379661f8dc5235af57b656b1e92019af30f18a628a1031e0aeea79086cf3014fa8a1c8d75b8af7c651ccded46aa72ad8914ce6a6543bc2415a6bfd0c80fefcd06c3f44745fa0e08018de482adfc8299e298f0920af4f45b30d6ecda680841ee3fd1db7a5619ac53518d7db6ce326f2a16c0ffbbc93f11d56ba0e6180e617bbd30c14712993804866fabeffc869493067badca9d52e66708f33398eca72bfc08c1fed457101faf777de2a3a48a9edef91ca77521f5d9a74fab4e315c47e5c4cd9386eff138d7fa3bdcaf1cc85d1ab65f8ae00f1715b621082c03e241360b9afa0a508b8f7ad54ad346feaec4c034afdec3de069dbf4a967dafd7cc7a77288223ad42ba361bd631fb07d44682c9cb6f22fa2628b1ff7f6d974768d2ecd4731de3ceedc5456515058bcab0e6839a470cfef8d66024ce7ef4d5838465106553053e02aa84ae22b63773f6ec35acba1ae6e4be063e0fa7e324c27b1c6c50ee3e6d59f3708cc924b7ad6f3772a1e1970cb2e2e3b257eace96ef1c598311cb4b73bcc7672bd5a609b9576631a03b16b1a854bf995866f1a3dfe87a3e1a71998217f99803a21c038a9cad0e21851db1c1a94a03fcc30e41b1ad6508d503e48f4a3888db565354ca2c98dcc50295b1357e75d3fc06977c89d30ed4326f8586b73d0308a0287dfaa8eb98d7aa0867532d695a757298b4161312006962dad7aae4c90356e83aa4be2ebe278eaf75409c384deafbb300c13ae818300f8b7b0667073ef7ad6184422e6059bd6b28f1c4c7ab0ae3383eecc6d7b06899e34ce777e98ca4a78c3cd6df8021f3c8f321534136984e2ab74d512940870fb7a8457f08316a620331cb96b32b75c573d3c7b9dc3c712196782a40ab21957c82a32db012ce50fd8d5bf9133b5e9f01953b29f676f844cd5e835b89d7c3b4fdbccc333e22d986e8d08ca05cacd2319fefb6fbc5b29bceffd40adc0a6431cfdbb7ab540ae97f8255634d08da33d669674741d023546dba303b1b78c45c37a0c816f84a2810e91afce6407e7ccdca053e4a9a02c44261235d055c02e0639a47478d90298fde8c0b68e8dde3630f43a17207aeccca989cdeed162e18af82bfe772272cbdc45c506d1cdca18ed4188ca16ea5fedc68fbb20b0511c2b5bfa93d76942a88e43d102e2281823e5d6e26004f156b49dcba500fb1c55da49003657e831895d7e8fbe5305b880a467a6066a1608ebbf870ca402e8d8f15adb3c162939e5a42aa13338ced7dbbd179dd0cd8a76b9ff32975ec2b74d542e7304830604f13a62a59879464d912293817f0f2e52a33a6e0575b3e85ffaef9b0fb93ba49806b43a40fb56c1a3ffee21e753cd759c8b77fba4240496af3cf14246d601140f7ac8c0100fd638ac786e85556d160b767627a1e3be553283d6dd43e8fedaf091cf7d1cc175b716929d28fdbffa1dd6f20b61377809156527d935304af87848cf0599866e0e1ffef2125da8d86780e32b4e078b81f8da0f2062d076eba7fb508a87db2d9b30437e7442b682cbec35cd0e98eb520ddbcdac232647796ef0939068f0e21c24d8ed22b32dfc11c7f233202c40d24f280834562a88f163e4502b49dc8f58630f47cec2c31e37c60c6c45c6430d79492e2dde68c81cae23aeaa4336a0db39beb71cafced4c139115a27967f167680aade70580c3d5701a159bdf4e4e51537f998c63aa576959397366db18d4037d8ea1a14ab43511ed1755eccfa4f995255dbdbf00b2699bd53ac4d1b03611f74eec47b92331060defbf6044d60ac59b4017b9be04e485847f6a68ac65e75f5bb4ec876f1aae8b62cad25ae70b3b7c15ee8a3407596704db0471ddd19c8d37acb22a44741c648bcc1d76acb404a354d7f202904d9d39f62c06fc735d24fb7c84199d56162f71aca2ece3631a7c3fa24cb17b9a878b553151266c54fd11fba8452db36cadb81b2ee0e25168a6343b8306a5c4ab98dbdd6861c12ce760038a3d8d26766f608c37d2f8bc112e91de33fb70557b0574fc25f7b4fdc304c0779d915a007551789e59863182e7cde9b3759cd0f351bcf5066edfd94073f34a9329d4c097b64060a34ff42697195bbb9d8d73a911ef8e28e14b3d9020d7b28d31c171763075bc9481955e7cc9fdecf5d06453e0f08ad48fc62217c5ebfee17f911bd13071af7ba65d003a6b84e455b233baaaf4bb6f04e2971e1d610dca4cdbe636daa809d4ee4058279de60c9054ed9842c80dbd97b6bd2f5040bd8112fc75b435512f02b6cab22db12451c8785865c321f7a62a36e2a935d4b72b659ec39e7c4952da6bf28df67ab1a2b363f091b49d167b4d681f0eec72f312598699fe7ea9a2a0f62af661a1ad93c0c9677277aa800916cf3bdd61d9abbe7fdd2f049e5995a9d74944160783c840000 diff --git a/garecovery/tests/test_data/signed_csv_3 b/garecovery/tests/test_data/signed_csv_3 new file mode 100644 index 0000000..08e2184 --- /dev/null +++ b/garecovery/tests/test_data/signed_csv_3 @@ -0,0 +1 @@ +02000000010259162fe573834ca72e67e835aaed6cca621a17ff134f3a7dad5ad00b4dc4818400000000232200209d98d9e892ef9b09fbcf61956361b801134cb21db8e54062fc65038778953fc19000000059162fe573834ca72e67e835aaed6cca621a17ff134f3a7dad5ad00b4dc48184010000002322002069749b29ea31ae0af3d76d8f026ee30e37bbb26b774cd4f9a9973b09f96acbf390000000020b76eb2ec412158630898a5a001df763be7a4d8810a3a0a88659993311340327ca09cb3e24ce9f38f5996873ece2a980b9de03c17bad5c5d389d28c230a1287b84d50279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179817a914d7559651b007d4971de281f81347653fa8cf9e008701230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000060400009000000000000247304402204c414a42b5aff163ae5a2cd629bf1bd31fb723c0fab6f871e459276bfc919437022036a5bc80aaa73ccd7f948124f100b3b2891a0b7e1d21f80d99d9a8ea886dbf1f0150748c632102337ecd4c3e979e0132ad1ec7d7f4e1f2af45e13b09d83fa3bdf763175165922bad67029000b275682103dbb0fc1c04e58346be52ac64a4f906dedc1a4bb4714ee782ccf789a9cf7ea8e0ac00000002473044022063ad069986a85d6eedca84a779c965c68d48bb937ce9f37ba3edf614075d6c3b02207241be2712b282c5ac9b7afdb24494ec105eab0ff2812e5db6f96412385289e40150748c6321028e69f32e21432dfedc3a7c154ce6adfe388a1a2b73505313c88a37fac8279910ad67029000b2756821034234d0923d041f1cb4380308930ca85faba19bb7ddb94b5d1bc7f41ebf844f98ac006302000342e78cedb23e3f2686bbd3a8d021694d22511644dbcefa23b16c44a8bf20ec3ecbae7badd5b1c7922d8b7f5951019585f51e9f8e960e585c392984ea1bbd57c76917faf6b9e4238baefedaae38d4165dc6b9dd7473f716839aa6d71eebbc148efd4d0b60230000000000000001d87e000f75c066ca86e552c64015dc3327552db1f9be41073398aaf96afd3c5a5034de4f493dfd2e6af68d139af07304ceb4937772374d5372f2af1e9d7d80fbca33fc5902599809cb63b82fffcdc7da0b4bf55cbdc4dfb0d7754e00bdf1945f426183b2b1993ed9bfd244ae0eeaf27b754d296ee37f08796e8df12aea7f4ba624d8062f76a34cf4909097d9cc12d58a536c03a755f17b2b4ac46fa608fc8d64ee0cd6c43025adc6ee533ca95ee57fde7aaac025a75916f392e74cbb414a4eeeb89b491b3a7755077c57979d7e923030e02a33c6118106f9725fce44748e9e82b1a0c15b29a0dfb6ccbb018161ece52b419934594a9de79598cca1210fc1fccee6da4e4fc818b6416762619ee4887b2412389fd33eacb2c9b831ce36d7b9b8bd8b5aa3a613509b657aaffa3b3a2d6814e31a2cc7b2ff548a6dee5b3f26d313b42c0aaebebbabfcc57908c03696d9242b611d3b43e3ba4445ab25b2eb783ba79457847a90a03ddfb58e261d340620c0f9f5d4d092196be91cc15198221a50ea7af5700f80df93b03ed2af4cb80b36bca653434addcb989f0ce6cbacada28314e1c2accae7035b5d090ca3880b2261c75d0c043415b480a937c5b844b2d5285ff96c0b6fe9dbe5fd9e2a0ba73cee3b86a10c3d1f8ee40365e3d425425dc3f001a1cb5877f20d869fdf76acea680bebbbc33189e4ecfd71583710672e5ce6cc00e817fb46d24e774a69992dd91b7ecd2c2cf3c0f1caa4c4ae86028481742926e3ee524a171def874ae1b204001504a3a166641b741df011c8b5888d9f86ccb27de24b08d85c84560477b3798a82fbc51cb317fa01a5f35d2a954726715d165e888ea1cf552d4711c5fab00fa17fd6b4ed49a32ed2d5ba14eda80d99b1ee8ed8190866b63ac44a4ba2fd3f471511d78eb00f947e8242e181c65b4cb027f920fc02797808c5c1998b6c8559b7cfb40d8d5437722d1a10eedadc2a55cd5c775d8ed6942e4dd258e01f7636bd1ea6e9cb15105f99a6bbca1844cde54d34b8ef0cc9ff5483d7777c45d7c0ad0fb30aedd6d5ebf05c3495c18831a0245d18047a323e41258a21e9b3ed1dc6ed533d227f95801f026b01d44465084f551b1cda1f7a1e4c862408361b7b4598ae4485c4d99b253ff9a441369d02a4498c8ede3ef5d6bbdeac7eddcb5056cec28526a0caf28e51d977f169bbbb4edcc7578e86ecdd7e9d7fec959ed5976556663e58e8a142f7ba32cbcadc4654509fbacbe257958f09b01b0e0dd35e6f8cd1812b284570ae9eb30bb326d8a91323ed7cb16b06c76091b4550bd35c8fcc607b89e745d0c249d0a5cff51d9b5d58ece2ecfb8f17a51d4ec99fb354cce660c86846695b06c751edf4f4d8575086245c76e419097d34e4a9994eff1c902a55bc0b0e82aca915d4a2e6ecf4566aecbbcd25dc3cf8926303d04be5c84b9dbe5395037d2bd40e606495c897f88f349287d31e30af16e8978862e9330f255c2acdfc48f6a2f22b09996e2f28cfc39fbf19c616aa4a0ec310b9f4289c24daae49aa7a207bbfa058d018107a9895dcc9ced6707c18dc677171fea05762f74cf700ef9f3b88f6af2db0561e98e2868c27d84a6b143bb0c0a682d81f299dbd15c8bcfbc242c7319dc9e5035a24fa437ebc6ebb70ef7ff26ba69b10d845cf1852e78bcc1cb3e24ea43c7b41a71c19f50f85741dbb7c68dc7f8e3411564d1e3a201eb2a7d52dc1084aeae2c9c903eb9bab83f348196e609c94c79840f53e55b6ac793bc0585821e8a54ec9e6fc48e6626f173ee183e77d4436a9f147e22e40ed871e7e6ee8152db26e46a6374ee68374dea1a05390940d1405b1b14336dcc327dd54f116ae9635aea2a12613de45b098b5b08ad64edd9287835c3353e1a800e966e1c20b77e4fd8196433fdab6b391a9714da571a121b81259da15dc34c9d917e60cb424594004e0490d037641fa2196368173978414612d0c1f8dadc7a0ffe509d0efc85368ab6700ec56210e925609f65155e8ac56ad621f5005d57cee87376cf2ac860e44baf2cbe1ea5cc227a53183fb89b55334fd07d5a4c96e83da9a5ff243de1c533b00cc77a315c5f426609dc2e2223d0f2027d7445dbed4e4c3cdae4a2ad4c57bbc6b24a6b2f1e75621e4d7110d1f375275b057ee293154730b5f523439373fa05dee948207260e8d45c89c781b520e88a1b527c9c93ae151f8c3f337c1b5beb86259c0bb676b1638f348fd65bea2d40b66c141ff6d092320c84d25063533a48447bfc94fb9c79dba4a1e331cc7c6759d6e7f1ff8cb252b06210635896dee752b013b2beab3dd23f3ec07d42a72925a209c3097ed909537960d01c904bdaa78c5022eec6debf0b2cfbc1a1957b17834335290253a454c2949995a277d2356313af3abeb81d6e19e31f97305c77272c20f371cc29c48f07678f8fc52a8e5d3428c01c118ba54b890e9584aec95e188103c997e8120f2d1369fa4bfc5d38bcd7e6df64b8235d0d74c08fc5153620f602529880bdcee2b6f06c8f0f3b3ca759a36985a582bb4ecc15fcb5f8cea07f30dfb8727231fec191e1efe694d9bce233ee9043cb2eea0370075d1e744fe582ae1e9884a19c80e66981e186c703d82dd6d4c0094cd53cb16f83fc615ad65c27bb01b8789c6892645de166234c09ddfdff5fbd52a6a761c3536e4cb3968ebaa820226c84e5b15878a7ea89dfa5fa457413a22a76d910de466860070cb252fd0aaffb9d83e852485a0625dd077aabdbee573b05d7c07dad2b312a093041a23a83f2b8b2a6d66af5cb06f6e72656a6bcacfb5d26cb2f9f7ede0ce4162a33f27451292372987fefae70525d2c9dfc91ed7880d6f095e47a9dd4b6241808a1b156e2a3ef7bfda4c73c939a0c0783dd0450176fc6873776d0fa31a6367bb51b1daa70676f197c471c21ea03fceafbd86609fc8a8e7ee80620c1b3140eb28a6ba1279cb1c342a541aa17ea4a8cdaa5b58df76ef0c8dfc2123af4e50be6035394bcd60fbfa18c65afa19e4544f128ec77eb90574ff79443f59624f98b46ea822abf5f1955c391fbbac64ce5ec871cd7f52bfcad355b06fd8e6019f35404d36a8afb1248d31fb7cd25e363812aaa5046066ec1bae3a5553b4d25e72c8c7b2f24b883953f2d4715974d1eebaee1df548a3bce8fcc6fba8fb0de3ff9e4c4c62a740ed0a3a90466b7a14036e600c6c49fcbfd3276953be7fb9dfd6eaf679d8d18f8760d3474e8842bc8e530a4c397779bac2356974b5245628dc9bfedab3d29a43f979c6ec5ce43f065ca0997fd3cd02770965632005da8f84da32c6c38a1b858a276c322f6c818120b6ac2d4fb4d2e75e486e5df4a9a52068cc652779bc20020bdd965cb0a9ec0ff110bdfcf3db08d7ecaf64ecabf583f29c84d7eebe73c54d33b05daa93315278cfa325566a73700fa70def602ac84990f04a6dd741eb3f2fbf20d560396bf257f6f1fa5cea6df781b1c6bd58725c4f73303c66b6e373dd7def92240d73c197d3e37c201a5c3f8bad5d55a80a33071138e26bae6c95c59179a419bcbef45ebd40a770739a76b83963af5ccf6be4efad3711b223ab623e0c753ac1ffaac0624b8a058c67df371512f0a174c77e07116267afe14eaefa58132329074da45fcbfaf5d08271a2ad7dcef2d0c373c42acf6ce815ed87760384d8bc9edb60607d25422588ba59f260913d2919decce4c70b4f06c361fbdfb7b799c57e0866680d34e13a8d0d8f7daa6f395aa669ce472e22e992dba8b0413b551a067f7c6a416d4ac87124b9ff1293847a314180a91de8776c7c2579e9d8fedc99166fceab7e1ef7bdc92201cc9de0dd816f6364206c57050985816da17c11d91d71b58f7642245a600e258e5e319a6e37c2ceb9f13fd380b325a728301e885139b242bd4dc3026f5b8c4d081bbbbe0b6b4b936a1579fd8e73a0c50bbd0539ab45212a48b1b8d6727f59078a4f7a4020d08405696a2453fbafb75c7cf0276a40af31d7c7d0943a20db270a01e16c5fce87011bb428fcbba590101538b7d9550dd2571f4beb74a5ab9f708022baf0d776fe12dfe978eb4aea8a0000 diff --git a/garecovery/tests/test_data/signed_csv_3_split_1 b/garecovery/tests/test_data/signed_csv_3_split_1 new file mode 100644 index 0000000..e04674b --- /dev/null +++ b/garecovery/tests/test_data/signed_csv_3_split_1 @@ -0,0 +1 @@ +02000000010159162fe573834ca72e67e835aaed6cca621a17ff134f3a7dad5ad00b4dc4818400000000232200209d98d9e892ef9b09fbcf61956361b801134cb21db8e54062fc65038778953fc190000000020b76eb2ec412158630898a5a001df763be7a4d8810a3a0a88659993311340327ca096d7753fd7f386b51c82ff2f3a52b039a8c221778486b9ee5efbae8ec168676890279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179817a914d7559651b007d4971de281f81347653fa8cf9e008701230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000056e00009000000000000247304402205f4592914eb594520c55400513bf2e630d477dcbc3cf09bf603bb0e907485fbf022072750bfb60bd0d21a614e8346f8f185aa5c0946a4387637a223331a649cb3c1e0150748c632102337ecd4c3e979e0132ad1ec7d7f4e1f2af45e13b09d83fa3bdf763175165922bad67029000b275682103dbb0fc1c04e58346be52ac64a4f906dedc1a4bb4714ee782ccf789a9cf7ea8e0ac00430100012e42967c50c38ba0a5bc346a43f79e20bbebc1d09b47e14cfd02555de8b2421b398b6e06193de0730b1aaeb08116567bc2f5f9aa3edacf60321c640df793d7e1fd4d0b602300000000000000016c0901bbc9890ae9619b68316085af1b5868b2657ec9d189addb08925aa92c16028320ff9d85137f541501d1f91cc0552ce43d8f65fbcec69215b76585aaed91934fa8efb8837ecf2951f5a523d8ea7f186a27145eafda59735e7088f27b03ebf4123156b97cbeb43d2feace8d8ec3533f35f12958970c05cc056bc9c6c0bd37c47d078dc5d8f9e8ed91ae2d218c5ee310d60e0bcd2ecb59497820de059fbbbedd0b539a7bf5361c7a3cec557b823b1b679acfcdb5a6b7dd120a172e310565078f0d4ce6a6a40011a918e5e973247816a77e33ca4c226d9eae9aa825968484d4ec30dd2d2235ba93c7f169aaad8cd33abd81f87c2ba1509cf7055f1b841891253093b6a9866c4f6b19e73ea624f129632379b2bd4374adb2be2ef0db6cb15d8b8b01f6dfa1566bd4f220c45f8b22698d4bac681e3ab630a057fa03cf216e2bc03212e85237b4b04266e407057b75635eff99f452cbdf134e6a722bc19a7d1bf249f67fcab9c97c55857de97a9524ad13eba38b29d927ceda677768df3773c73da33cb250427b1cb626c6ea6b8edb3b19928f1ade882ed4d532b1d3abdbadc8d737aae1abfdcc1951ca3841b39e10f8ca369c1c507808ab31e8c05bc11a65ca00f47ea3bc58c3ca3c3a04cbd04d517d35efc8255aa4ac317fd92f72f3f7d6343b131800806c501643d241859e77faf62a4af04a762f5dee6db7bd3451f207d696a3d22830947a67b949275fb36eacdb2728062126b19ef6cce7e469a1489b4572cc7d7b8d6913a52912050f20b55255d8f0d00b6244a0a81325f2bf783d0f72d2ab7d96a5fe064c6689eddf1f0fce9e227331d90ef11b01967ef33237c88f0ea6e5f7672abc7da8d7e567ef1b4edd85bfda017eb2cedef4b49b0c6d8e623290047745a76ce2fb4118bdcb4f93df2501756a971847b2389bb135d8c6df7e084d6c518dc961a85037a01358212ef77a2050a5ea325e94b007ac1be9990ed0e12b6654e3acdc9ebce2d96f40059149e095239d3dc2de7e5311961a9e5f298576584b26eb313ab4c799ee83017b59c36136a5ad81e60d121c1dd848c34584b184cfbfc79498e1104a929f8d8513d4285e07acbeadff10f82939fd2792e43a0321a230995fb3cfd627ce39f4118b343dbeca4d4dffc16d14ae28ee856aae5c447c089326df28be5cdcdf9760578b66c8af13e7feea06218a330170249a75df46c6c9d3827088ee77d1c1e192b3ef685a2527235b5a8e9e1b32219e3f65bf346b548ca5d7623b88031e07843e6beff67f951550a70f08101e47cca9a08d7cfdf9fd48ea11614d118948c0c1f64c8b8882efe1aad7d27909abb65956f1b18a1f781d3244b0a401565ab89c6604f41918637ec083a003d7b1713757bcd97d7f518c2ffe9194a4e37ad90687912b299dcb1425e9f904f4befed566cdfb5b2950827e738bc0889694c9faeb3702d0ff47bffafc705f9cd8b70825a978895fcf7016bc8a494453e3df0b9516464753205816f1bbb9c1316d3c278e404542724ec21e9cd6dd16614d39b7438bea9c039c9f3ce8aba5b922d61418e3e2d94d3fc1a5c8fbda832287e2ab7191fda9e4798a18e4f680b24ea6392492b7b0dd8e106a4d5f4df3a322b0a9dea2a9cb70a64a148db233f4a366b5e861bad15daa4da5beff4ff5aa7437d8d8f8cb7543f2a54d3aaf351569001c7027b61cec8f5932b43f4a479afcb982d775d287ee8f911701a69215fd8fc00eb058354dfdef2b92d152ddc09f374d6dde4a6190484ec8a991a9f0fd4c9d4605339943028227b33d5f024ce2f820f05f82f2329231ee3003b5d3f0462fa9d1ee99ce5f883a70f1a95a6ee9faed740832df6d8d6a9f3c6b35d7ead964e15f11bc374fbb13cd78f7a42a4e6158da42c6761caa00d47868a8be80c58f0416280db4466faf15c6e9509c2596402fde7ff72cd9d5553dd8bfe2a18eca7693e772530feceeab382bec28c9188eaa8ac72b71faf964f8c0323ff9a085328da8def28c081f906c5adac58b7760b8a1303f11e4ae7fa157720e63f559ae305263676d7b8ea2f92d7f5325a1dbcd5a27e6e8aa9cf6986f933aabac67d42051d8723e210c1b11adbc46ae1bf19579fffdc4f52b304909ba8a49296b42cbac128b239058037b34408a41b47079d0156b8036bd8a9c7adae80526c601f8c8558c6052f544390565ed51d8d89e7ea91adbd48280005ddd1aa252736f190750ee2ecd761b3ecfcc46a95cca9c5943773f906cd82950aeb799f1531e0211572ba4d7685c649ffa3d975b4c57a341ece737132596f30d8434eadcd9f29fd4cc5935f273a123b5edca91b851989c06dd0881506d02fe2bd69fe6a9e5cf265edf83318ab11c1deb23848d62cd9698f293fc441ae3f29470a629a56c550b856791f9510c0f1647da58dc9f233c3a3798ee1a32a1b01f12b52acc8db3db718bafac29d62cef26322a372bc73aabc56936d7413acb7c1b31eb67e8a356c10e4cde43ef03fa25be13830a4b56a08ba7ed6150f5736d6075af4f117bc0c94f22a34b3154d6d03be4f8821769026443f6d18af113155c356d3190fadcbad606dd70136a0e87fa4dbc971e9b63236147f593609ec76fe154ea9db84e341d2f27d6100e7e072206d7fdb41ab2266054dad36ac85c925477e0c40fb3a53f205ef519622eb7119ac3c905250f698b6fcac50717f05e703b264d9e77ed981f573ee1a1824c55b27a06f499253b171696d29105d1ccf3ac82994ae06789c16aad8f68d11ec9c9e2eb9d5c8ba87e7797bf5ce340d30b9b2053eb6da97cbfcf077dd5b97db080cdbe87b337ecbfe7a398a57043da98010b40290013b99b6ff04dc2235b3b67a7844315029cd914de430e0780b88d94e326695a25d807dcd85f39cf7a005d9f82d8ae1db8ca51246368939fb1acb7bfbcfd2ee7ce34825be5e00490219033867a816c5916ae4e3a02eda0be743fc17c9a681f7dc63ddc6f0677f29f01ec38e0ccbd5aa4ae7b24b261397de6204b18f604c79779797753d778eadb8f48706414ec1299145a1f368e51e3eec1fd7b87c0962746c688ecb692deac53ff0c14fe6254fc02b64aecccd47242534018a3e01e9eefbb326ae12fc5315d0c9a634f80d48517589292ab0ae9344fe22abcbab5ad8cbbab59a690e08ff229996b61951071f83bb51da38318c56d949f2e0017152731bd061a02e6ce5c1b9b1b39de2213a58dc14b7ee0291288d0efa5e654e0dc2024144ee4851adbcf57065146d26fab0c41d491dcfa4eb279ab25e696f247db077fecea47157dcf25a989ad6707cc4947ff8c72eb521e5c5d13a7ce4d1919952f258fd5d4c2964945729c723582ff4a336ef7c96b9ae72963781f19e28c16a164c5f6326758cab84a52897b6190a7eaa1efd2fc185ee2ac307bd8d59a300d868b2b7a3f6a3aece46fba32c50ce5e6343c461982543b9a0a3a792df601e5296e58035cc22f53b3757188ab093748e07c05a5866c834e267e44149cedbb18336d013ca636a0c9e2699e960fa2da58b8e799812e9df65a5f8cf70ed62f9d79e942485fbba2ab37f798bb438648bc5c2c452f71bb0884bd1b5b85b25f1504a197e94ce5915d4a995c62c9552c7a9e5a38768ea3e8a6276dc129b6d32b5fc229f74592770dd16784f1332967d190d0d29ca74d391328a80ea5ff276da23caa4194d3874df945c929bbdedbbffef346577b0725b406cdbbd76b272f3613dc4cedc7d90a6f24266d6a18551fd2dda19551081b4fe990c2c4d48358716c5da6a78292f3b2c808ceceb1bf5e1209c685297da96fde3b32dca65339fb32d9901a38afb4a51592fbd8a99c4d7bd75a21d6bc91cd572169bfd2698cd362e80b6c36bd909afe9a8010f2b97a681630b4e4ec291913bcf2a070cc9103c4864340aa4564d12ec1073b32b6ab04ae8e4d0d20aecda2d63d20a45b4919bf98ca2c7552534d108f13fa30b53ad4d8cec9dcb9c62fe0eab148b19221bd85a45c8435371d88831ed0c284c6318419d069114dfa12fbbc06cbb53e6c37c5dd44d2ede30f2e3b3b99255aa0be762ce7d0d9e7a782f4954414e70000 diff --git a/garecovery/tests/test_data/signed_csv_3_split_2 b/garecovery/tests/test_data/signed_csv_3_split_2 new file mode 100644 index 0000000..76dfb7b --- /dev/null +++ b/garecovery/tests/test_data/signed_csv_3_split_2 @@ -0,0 +1 @@ +02000000010159162fe573834ca72e67e835aaed6cca621a17ff134f3a7dad5ad00b4dc48184010000002322002069749b29ea31ae0af3d76d8f026ee30e37bbb26b774cd4f9a9973b09f96acbf390000000020b76eb2ec412158630898a5a001df763be7a4d8810a3a0a88659993311340327ca087ee239237ee1bef5e1c85c19543f9211e4b935f68b275ee9a3b51a505d2762590279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179817a91457d2b3591fe27661d0a785967126abf9ed19999d8701230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000056e0000900000000000024730440220667f817bab5e4545299a60aaf6a4472a7f1ca8944df6337eaee71bc7c7cfde5d022072bea3e7aa88d4a3bade7ed0e8073717b3376e50fbc0a35b8eea3b34434456110150748c6321028e69f32e21432dfedc3a7c154ce6adfe388a1a2b73505313c88a37fac8279910ad67029000b2756821034234d0923d041f1cb4380308930ca85faba19bb7ddb94b5d1bc7f41ebf844f98ac00430100012d0fde1d8633f3ab9921ef497fe8fc4136f275e89b98c3df56e475cbd3e320253c30b3c9294bbfb701a0ce51eb5ee38ebc78bcf7e05acb8a1a7c4b2c0b70dc67fd4d0b60230000000000000001df5b012b9b34c716d4709d8cf7f3ac3c2ff0dfec63ad35ab0c631f98a677423dec6c7f50a6e2128608e8bd9af6798ace4fc50479e9ef9c537851cd8fdc7f3073711322551e6266dcd9658ee02f2283cc687e5948f9868fb01d441225a3ef886e157304e74c5fa427edbe7c0d44a945aae4e4994327126e97e0e402a9b1e82b4f1faa66ae5a65ef45f768fa08cf831c7de5e85b14acd4499261055a877511766acc09e5f9b16095f0eb5cf030092af75756986fec1ad06bebf80bd94bfe64e1920d7e8feab7846df5562aa66edf621db566683bab9512f4f67a62d250f0c207279e4d8009b8878c4d1c86f5934fac244cb5218a0149bd7e76036e3dad1aa6cb4d4c172b3a31883636945dfbfb9bcd5f083f7c9a66f2fdf9f8bda9390819aeecc36d02dd8bc6137cc255d54d0db545f0ba18c097694cc9af1b861c9a639a20c2210cda48ffc0e1b39d8669591bbc6777f20cd86742bc62bc4e9593c5ada85754a9ef5678ebb9aaf7753bdec40ea753f61bef5a536321ec0a504d2fad51027cf57e46413a284536f83f577abbfc65df1a758b61498664b91499740554bc442a1235bf0751cf74f45d76b097a808dcc3bb454dd542cc1f1f4ac9a7705fb49c5b7ea91ce299430b6c702ca6071cf59d9972c87fb7db39c83c347f9c1f4b5696cf629f872ae2f5d84764075be7f9b6e2d266ee71ef439660a0593803720a27ff374d3716cdf5e8673ad00649aa55a2c9bad854f7469309db1a27570d916be14b1630a0987657a95a152a95ac8d985782d5a983c38ab235e681dd62b1715403e9120cc7663edccd9cfe86a557bb1d97444e38358031240181564c186b4ce7a938ff375a61127845ef2db1a1cc9bae1378e5a1e4a031876580d354a71524fbf7d7bc7f1dcca8a8bb88b6c944f14b832f57430a6c81b1e1e570aa5cc01e625937694eabf24b81f695c44b22a7442d9722c25353e431765496d01a2e903cb8f4ab345d0dc211228f90752da080a13e29cd557e9e5bbdd88ecafeb46cad6c7cf63b69f1d717e1bf956ee8abb07f395bd5fa5b1a0bbfa836606a8381bf69d2a1227b0c17d1be4228f4c7dcc61975ff1b27315e14e622d8eda07f7b21a75ea06a0f16683b809b0dd6d5a8a8182a1cdb4f834fc7096dbda780346bd28d21027d1998a3a66e823acdecdb246723f8c468eb44645324c04489e9e592281bd624dee9caf38cbd7b81f81e632c226b7d478470961b65702f6ca7707316b4e23f3beefd9f3c2a102446262faabe35ec99b55b00bf62e8399c3d553127221292dd7f189779b8212f6b411eeb79b95d63ed21c45723dad93c702bb2ef10ecb5b8af5ff3305b62c4409aa7f891066530e384f599c1028b99abcf8d3ec2920ba5a69f50f8732c8d14e21e6110b00604f8abf22a9fb9524025f27132389db7b4c3b03722a8705a193ae650564fe5c102a440d72325a667154a0a30bd3583cce05f53a8ad742c81ade10287f1633f35bfdcdf82b3f6e16785631c18579b08672854bcd561110cef635e8d7f6c1a642580f3a8f9b75caa3b03eeedcd3c9b3d26534e54c3ce14a58c400032ff2b64ad5a282e0e4c78985603d07fd83c3d42a5c2c3e83ef276c4d1af580cee11f6bc7d2ab09f47d8e974e3674000ce937b82b49090c89fa50908cbb2f2dd0ef3044d0af2ceaad6da8e524665b4c41ca3626fe8725ad3e665e307083e15903bbd29f147deda5cf1e2ca20becb8db9bf03e3f321e65f5d9036b0279f1664a6bb3f715eae72f36aaf54ccfb882f4f327571d9c0ded784a81a88d3feb1981b2ae462235695c7b0c6565a5bd178bd841bd6538008fd7e2ef0bc3241a444e98585ca3fcf7af3843cdfc396ca516c8ddd048a193d62f48ca93da2f2e88c2d3a00fd04d3d15d31b7562725638e456aecfe2525a1527a43e6c746516c8e4a10e3098e8bacda7981f0b2652e5cc4752a0cf0e1f5dc8ad6772e2d1cc05aaec899851ced7ef0119cf1b407fa63ace569a6ffc1b71bc4c8c269651968c37b1da8687e199f83304f0c52347a1570ae6c9c7f7468dc2f537d36bda23de6e69223465d38b0228e14e87dacb0aaa74413346b4db18d10d792144ee2fe332027ff3cd517c864925ed894e92fc02f4c9e6fff9917f305b2c7d0fddc16058c3f4a862ac2ae44337933411e9e5f22eb34ef032231144af215e652cdc23319f29f6b2ad75f198ebe442181fda8c8bb2ceb4b109f18b4f0a3b95601d2f989de23600b566b841423a345e9ca59290ac36f1165d366f57df95b729fa51c94437c072a24ed132bce8a426e26aab1f449c5a046a3d42a6bf94293a1caa2efc3a2615e9f130ebb1d92e9af6d2a37681a9fe28445953a1cfc17324b0ab5c1d19e801995b841ee46ce44430868acfda5e01887d9101ac0cb426ab496385392f9906754f1cdbe2126cdf281df30db190cbcb51d4a2d837d91a995b191debed7f91f840bc33f46502bfbf3401aea06e113123b9d3515fc98b38c2e3fcaa3aa6cab4b0c6ce2155e19b7de5ddf4949c5429b7febf74129f4b32396f63e520b76b1cddca09511ef50f763522e8182449d9c19a619b0d1a8c7cbf124bf5a969e39b029d5d7d0cce2ab1bf9209625cbeb4c00b398f616049c82d7ba64628f0bca7b87ed6f724ac084c0d9841edca2e4bf9cec3ce044d3a767381d2f7d27461af723e65582bbd01ac932276a1764c071185e87d229897e276f5adae959a2c98598ab945f97c3cc0dcc3c82c591c015c2c1a4cdae58b5b0158e7445908cdc7d97539d1e27a108b20c29dabe5334be697a38cdb1f9622249c3b9d2763a5ac689c16344e5dd5a9ee0e51e7bec95a89369ddd1de0e184789220f19bfa17a14c5f4587e5e5f1fddc21cc1d8443c463d72fb3756d061113e31c0f8c6cd75a7d0654d4d2af7eab1393d1302a755ca1c2a33d8a6b90aab77d468d24837faf31fccf9177b6a80571d68999eeffe4b17ebd4704b837867a01ae2295b4f1b0d87f2fa3bb2534329906504d78ffb91899830b500a96d9c9b4a4b2fc060429d27a76d7e5163c4c9106a82a8fae93fb12afa8d710d75ae82d6cb4abc32ab075a7d7f3d460ef718cf41553ef2b61ae9b163fb80301c30c5da6b49832f848b93ab8bd3aa738d68fb1bda6a5abbc089c7aef99de3ca14027a884ef5e8fa89155a017e88d2c405ec686213c0d25f1e25c8afdfb2d8d06db1d5b23a123960f916479baa3a31f5efff1141008705e02a6771d8d2910877454704c276e247a090c2ffc35e49322841f180b4fc461913889878476e71817f3f1ba9d1833a2791b56a49079e7837c871c100eb53a1327d282eb4736de03ea5d48899114698e8281068762604fb7514b02039eb0340b5776d554cf74ced62b9ee1276bfeba2f7dc2de89f2971ab464676f40196ef0e11c51e60757a6f22f349c8f22240bfb1ff580813d159417d5779eca9f5ed9970148789eb98adab885fa5eeed33e2f59ba758f19a5893ff76b876e21bdd565716959a8c0097580ab5fd45e5ea97b33c44eee4e08485516085c0a4f63a561080985b44cad00360aaacf37d6a5aa0167b5337f8e827164bac9bf314b2e5e856e2931b367e694ea74840644e9bce83eb60afabe79c53ed6e3ee6e62430b087f463ba8cd8c062419f9856370d833bd0a1da3c04a094aac985b1bc48f339d710777cd00a155fe3f303e854614589d688fbe32514eec95bc3066f58d71fa02b4609a5da237dce15ffa98e12dfb82abf058db50da0ad5c792676db7327b534c46160a17523f90f8885346b2b20df455f1c97bec8477dd200dc47777a845d4108e134ac392b591a4a81185c980386cce30e9b911fbe4e957ccba48bf9079b4a66052a009cfd8b8848a4d81ec5206607b9ecaa2e2e2d5b6a0c22e7ae4667b6af5e9abbc3ba00c63e906438f2ef92604c18e69c3c2b71aaa59451db4e40157011b5cc2ed5eb0bca1afa1da2f8f15eaec6abbaa69ae1bf998122e94d0da1b00e86b5286059b4dda05890e6e49179c9dccbb77ca5b6a22a996b5b59b9e1f4bd558591dd2ee4646d6bdc1959d218a990146632e6fa69396fdfee7b0000 diff --git a/garecovery/tests/test_data/testnet_txs b/garecovery/tests/test_data/testnet_txs index 170b1f9..5d0e1d0 100644 --- a/garecovery/tests/test_data/testnet_txs +++ b/garecovery/tests/test_data/testnet_txs @@ -2,3 +2,4 @@ 0100000001dc3f1f69739082361216f858228ee19e39ffadb22de0201ca9ee6cc83d382d31010000006b483045022100b635bf496499f03aceda59b5879a73fbb90208096f887b40b1a8b4319c296e0a022000b37843dbaa3ea3da1bbe6be3757a7ed51eb2e1c5778de6c4b0ddef8133bed1012102242381f8b58a4e173327f701fa9342d24cd99d80c2a60a1d3bc3800f59a6b258ffffffff0280a4bf070000000017a914ab85000633ae44082c689cb07b578c4d81f99a5587cb4c421c450000001976a914093cc1bb39c19264ea29f713ce6d7ef67c73a35288ac00000000 01000000013797d4cc7274fe4815107f1d9a05df53ba5a7b6c7aafc934daefd058acebf1e8010000006a47304402200f7a4c1ad416414361eecbdc516c1e0611071ae5a6a180b969f7e4d02e8d28870220397ce191ebc0dd11352370b6e599b64c9aaa398ae96bb55e2420c86f3847c2f30121031e2e1ef881723c78ed119b928adb9296c604da159127ba634c91d36496a2f585ffffffff0280a4bf070000000017a914636320f726fc0307151a417a0fe19816db46f9f687fc74d2aa110000001976a91415ec7bb620c8d01479fac51a79d392f580a3fe1188ac00000000 01000000011e9663053b75b8e1a3a2f7245443b4c222037d04f4c0e7ab4a2a969e17879e8c010000006b483045022100aa0f44559604f737a286359e576128394996f54017ca8d8e8a1048f4a755b1100220732a45f4d9a84c9e49a5bebee0980bedc40b3c29d03803c395d213da21622c760121036529d92a1fb85faa51c25aebdb6f546d84b82e38982a7b36bc176860e454baa5ffffffff0280a4bf070000000017a914b4bdc72ddfbefe34da8cc3a34e290b17b58b6d58874939e1752d0000001976a914581834ec7efb234f8f96a13afaf1071f2e8d7dea88ac00000000 +020000000100000000000000000000000000000000000000000000000000000000000000000000000000ffffffff01a08601000000000017a91458ce12e1773dd078940a9dc855b94c3c9a343b858700000000 144 diff --git a/garecovery/tests/test_liquid.py b/garecovery/tests/test_liquid.py new file mode 100644 index 0000000..3514c5c --- /dev/null +++ b/garecovery/tests/test_liquid.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 + +import mock + +import garecovery.liquid_recovery +from garecovery.clargs import DEFAULT_SUBACCOUNT_SEARCH_DEPTH +from garecovery.tests.util import AuthServiceProxy, datafile, get_output, parse_summary, \ + raise_IOError, mock_addresses_liquid + + +garecovery.bitcoin_config.open = raise_IOError +sub_depth = DEFAULT_SUBACCOUNT_SEARCH_DEPTH +key_depth = 20 + + +@mock.patch('garecovery.liquid_recovery.bitcoincore.AuthServiceProxy') +def test_csv(mock_bitcoincore): + """Test Liquid CSV happy path""" + mock_bitcoincore.return_value = AuthServiceProxy('liquid_txs', is_liquid=True) + mock_bitcoincore.return_value.getnetworkinfo = mock.Mock(return_value={'version': 180101}) + scantxoutset_result = { + 'success': True, + 'unspents': [{ + 'txid': 'd5a1c060f27c21997179a7eb61a67272450a79aeb6a068c1beabfacfae53fc17', + 'vout': 0, + 'scriptPubKey': 'a914d2924bcb2ddd0874fa87268ca53417ad102e811587', + 'desc': 'addr(XWYe1FwJci5gXvjzK6qBoYi1SfPwporjMz)#25yupueg', + 'amountcommitment': + '097afc5314ca6352160ab736af7523a236a0f18bcbfe1e79f3e688368b35469a7e', + 'assetcommitment': '0a5f0103e2c5b332ff766d489536fb57fad18e6f10cd23198721a85513489602bb', + 'height': 0, + }], + } + mock_bitcoincore.return_value.scantxoutset = mock.Mock(return_value=scantxoutset_result) + mock_bitcoincore.return_value.getblockhash = mock.Mock(return_value='00'*32) + mock_bitcoincore.return_value.getrawtransaction = mock.Mock( + return_value=open(datafile('raw_tx_1')).read().strip()) + # output not expired yet + mock_bitcoincore.return_value.getblockcount.return_value = 143 + + args = [ + '--mnemonic-file={}'.format(datafile('mnemonic_1.txt')), + 'csv', + '--network=localtest-liquid', + '--key-search-depth={}'.format(key_depth), + '--search-subaccounts={}'.format(sub_depth), + ] + + # Raw tx + raw_tx = get_output(args, is_liquid=True).strip() + assert raw_tx == '' + + # output expired + mock_bitcoincore.return_value.getblockcount.return_value = 144 + + # Raw tx + raw_txs = get_output(args, is_liquid=True).strip().split() + assert raw_txs[0] == raw_txs[1] == open(datafile("signed_csv_1")).read().strip() + + # Summary + output = get_output(['--show-summary', ] + args, is_liquid=True) + summary = parse_summary(output) + assert len(summary) == 2 + assert summary[0]['destination address'] == mock_addresses_liquid[0]['unconfidential_address'] + + +@mock.patch('garecovery.liquid_recovery.bitcoincore.AuthServiceProxy') +def test_asset_csv(mock_bitcoincore): + """Test Liquid asset recovery""" + mock_bitcoincore.return_value = AuthServiceProxy('liquid_txs', is_liquid=True) + mock_bitcoincore.return_value.getnetworkinfo = mock.Mock(return_value={'version': 180101}) + scantxoutset_result = { + 'success': True, + 'unspents': [{ + 'txid': '28687bea99bea46fcdf04c5ac2d61e20a50a8baec146dd138718b1fcdfb84ec3', + 'vout': 2, + 'scriptPubKey': 'a914fdbd477728bb5d9a7e9f91ae429817584a9f9e0c87', + 'desc': 'addr(XaUtZ2PMPTcKnVLHuRwp5REzTwyzjFLjix)#25yupueg', + 'amountcommitment': + '08dd32cab0d056442bdb3fa0dffc470abf4bc169a50d1312cc6ee721a7298cd0d3', + 'assetcommitment': '0a05d405a85dcd6e18f43ff27f03cc0dc966e5695aaf88d5005ee25df4b4c6157f', + 'height': 0, + }, { + 'txid': '28687bea99bea46fcdf04c5ac2d61e20a50a8baec146dd138718b1fcdfb84ec3', + 'vout': 0, + 'scriptPubKey': 'a91444aae2eeb5f562e0c7d49275e2f8d8355e2b9c9f87', + 'desc': 'addr(XHcKWb5ap9kani16sRx5C6mkWFxnmNyxAZ)#25yupueg', + 'amountcommitment': + '08fcebb24b4b79ae24c46aded9848f1e04eb6343f8c2fdacbf8f9005d0634e1665', + 'assetcommitment': '0bba366e88398a23eac2a78fc74945316650d874f9966885dde8c787dfec4ba13a', + 'height': 0, + }], + } + mock_bitcoincore.return_value.scantxoutset = mock.Mock(return_value=scantxoutset_result) + mock_bitcoincore.return_value.getblockhash = mock.Mock(return_value='00'*32) + mock_bitcoincore.return_value.getrawtransaction = mock.Mock( + return_value=open(datafile('raw_tx_2')).read().strip()) + mock_bitcoincore.return_value.getblockcount.return_value = 144 + + args = [ + '--mnemonic-file={}'.format(datafile('mnemonic_2.txt')), + 'csv', + '--network=localtest-liquid', + '--key-search-depth={}'.format(key_depth), + '--search-subaccounts={}'.format(sub_depth), + ] + + # Raw tx + raw_txs = get_output(args, is_liquid=True).strip().split() + assert raw_txs[0] == raw_txs[1] == raw_txs[2] == open(datafile("signed_csv_2")).read().strip() + + # Summary + output = get_output(['--show-summary', ] + args, is_liquid=True) + summary = parse_summary(output) + assert len(summary) == 3 + assert summary[0]['destination address'] == mock_addresses_liquid[1]['unconfidential_address'] + assert summary[1]['destination address'] == mock_addresses_liquid[0]['unconfidential_address'] + + +@mock.patch('garecovery.liquid_recovery.bitcoincore.AuthServiceProxy') +def test_split_unblinded_csv(mock_bitcoincore): + """Test Liquid unblinded csv recovery""" + mock_bitcoincore.return_value = AuthServiceProxy('liquid_txs', is_liquid=True) + mock_bitcoincore.return_value.getnetworkinfo = mock.Mock(return_value={'version': 180101}) + scantxoutset_result = { + 'success': True, + 'unspents': [{ + 'txid': '8481c44d0bd05aad7d3a4f13ff171a62ca6cedaa35e8672ea74c8373e52f1659', + 'vout': 0, + 'scriptPubKey': 'a9143b342bc797467ae910e95fe513334a50773c82b887', + 'desc': 'addr(XGkHDNKrBbAKzE1xEgnGJPdwtH2u7Ag92C)#25yupueg', + 'amountcommitment': + '09754c21f5e2f1e6f0afac287c9e43f76c5262e2d880d78c42cd98b8eeab64276d', + 'assetcommitment': '0ad8d4ba795b5c8bb4b9fd49515813afa3328eb392c4a197cd70debf6997fe2f67', + 'height': 0, + }, { + 'txid': '8481c44d0bd05aad7d3a4f13ff171a62ca6cedaa35e8672ea74c8373e52f1659', + 'vout': 1, + 'scriptPubKey': 'a91400d74ffae0ab3b1d61b11e0d621302863183e9a387', + 'desc': 'addr(XBRgnKBGGbStGTqqYohhup7Z7mhNwLU8SS)#25yupueg', + 'amount': 2, + 'asset': 'b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23', + 'height': 0, + }], + } + mock_bitcoincore.return_value.scantxoutset = mock.Mock(return_value=scantxoutset_result) + mock_bitcoincore.return_value.getblockhash = mock.Mock(return_value='00'*32) + mock_bitcoincore.return_value.getrawtransaction = mock.Mock( + return_value=open(datafile('raw_tx_3')).read().strip()) + mock_bitcoincore.return_value.getblockcount.return_value = 144 + + args = [ + '--mnemonic-file={}'.format(datafile('mnemonic_3.txt')), + 'csv', + '--network=localtest-liquid', + '--key-search-depth={}'.format(key_depth), + '--search-subaccounts={}'.format(sub_depth), + ] + + # Note that the mockup node does not exactly replicate what core does, thus the transactions + # created may not be completely realistic, specially for unblinded transactions. + + # Raw tx + raw_txs = get_output(args, is_liquid=True).strip().split() + assert raw_txs[0] == raw_txs[1] == open(datafile("signed_csv_3")).read().strip() + + # Summary + output = get_output(['--show-summary', ] + args, is_liquid=True) + summary = parse_summary(output) + assert len(summary) == 2 + assert summary[0]['destination address'] == mock_addresses_liquid[0]['unconfidential_address'] + + # Split unblinded transaction + args += ['--split-unblinded-inputs'] + + raw_txs = get_output(args, is_liquid=True).strip().split() + assert raw_txs[0] == raw_txs[1] == open(datafile("signed_csv_3_split_1")).read().strip() + assert raw_txs[2] == raw_txs[3] == open(datafile("signed_csv_3_split_2")).read().strip() + + output = get_output(['--show-summary', ] + args, is_liquid=True) + summary = parse_summary(output) + assert len(summary) == 4 + assert summary[0]['destination address'] == mock_addresses_liquid[1]['unconfidential_address'] + assert summary[2]['destination address'] == mock_addresses_liquid[0]['unconfidential_address'] diff --git a/garecovery/tests/test_recovery_scan.py b/garecovery/tests/test_recovery_scan.py index a8aa968..57d20cf 100644 --- a/garecovery/tests/test_recovery_scan.py +++ b/garecovery/tests/test_recovery_scan.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import decimal import mock import wallycore as wally @@ -77,3 +78,85 @@ def test_set_nlocktime(mock_bitcoincore): output = get_output(args).strip() tx = txutil.from_hex(output) assert wally.tx_get_locktime(tx) == current_blockheight + + +@mock.patch('garecovery.two_of_three.bitcoincore.AuthServiceProxy') +def test_recover_2of2_csv(mock_bitcoincore): + """Test 2of2-csv happy path""" + mock_bitcoincore.return_value = AuthServiceProxy('testnet_txs') + + estimate = {'blocks': 3, 'feerate': decimal.Decimal('0.00001'), } + mock_bitcoincore.return_value.estimatesmartfee.return_value = estimate + mock_bitcoincore.return_value.getnetworkinfo = mock.Mock(return_value={'version': 190100}) + mock_bitcoincore.return_value.getblockcount.return_value = 144 + + args = [ + '--mnemonic-file={}'.format(datafile('mnemonic_1.txt')), + '--rpcuser=abc', + '--rpcpassword=abc', + '2of2-csv', + '--network=testnet', + '--key-search-depth={}'.format(key_depth), + '--search-subaccounts={}'.format(sub_depth), + ] + + # Raw tx + output = get_output(args).strip() + assert output == open(datafile("signed_2of2_csv_1")).read().strip() + + tx = txutil.from_hex(output) + assert wally.tx_get_num_inputs(tx) == 1 + + # Summary + args = ['--show-summary', ] + args + output = get_output(args) + summary = parse_summary(output) + assert len(summary) == 1 + + # Use scantxoutset instead of importmulti + listunspent + scantxoutset_result = { + 'success': True, + 'unspents': [{ + 'txid': '0ab5d70ef25a601de455155fdcb8c492d21a9b3063211dc8a969568d9d0fe15b', + 'vout': 0, + 'scriptPubKey': 'a91458ce12e1773dd078940a9dc855b94c3c9a343b8587', + 'desc': 'addr(2N1LnKRLTCWr8H9UdwoREazuFDXHMEgZj9g)#ztm9gzsm', + 'amount': 0.001, + 'height': 0, + }], + } + mock_bitcoincore.return_value.scantxoutset = mock.Mock(return_value=scantxoutset_result) + # output not expired yet + mock_bitcoincore.return_value.getblockcount.return_value = 143 + + args = [ + '--mnemonic-file={}'.format(datafile('mnemonic_1.txt')), + '--rpcuser=abc', + '--rpcpassword=abc', + '2of2-csv', + '--network=testnet', + '--key-search-depth={}'.format(key_depth), + '--search-subaccounts={}'.format(sub_depth), + '--ignore-mempool', + ] + + # Raw tx + raw_tx = get_output(args).strip() + assert raw_tx == '' + + # output expired + mock_bitcoincore.return_value.getblockcount.return_value = 144 + + # Raw tx + output = get_output(args).strip() + assert output == open(datafile("signed_2of2_csv_1")).read().strip() + + # Check replace by fee is set + tx = txutil.from_hex(output) + assert wally.tx_get_num_inputs(tx) == 1 + + # Summary + args = ['--show-summary', ] + args + output = get_output(args) + summary = parse_summary(output) + assert len(summary) == 1 diff --git a/garecovery/tests/util.py b/garecovery/tests/util.py index 1fb80b6..782d30a 100644 --- a/garecovery/tests/util.py +++ b/garecovery/tests/util.py @@ -15,12 +15,36 @@ from garecovery.recoverycli import main from garecovery.clargs import DEFAULT_OFILE -from gaservices.utils import txutil, gaconstants +from gaservices.utils import txutil, gaconstants, b2h, h2b, h2b_rev TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'test_data') +mock_addresses_bitcoin = [ + { + 'address': '2MsFDzHRUAMpjHxKyoEHU3aMCMsVtMqs1PV', + }, + { + 'address': '2MsFDzHRUAMpjHxKyoEHU3aMCMsVtXMsfu8', + }, +] +mock_addresses_liquid = [ + { + 'address': 'AzpjuSThJLcyJFrXZDXstHU8uvq8EDDPBTzQvAQuNVbZaisSjYX1HMEMjaoLz2AphkJ2wfmFwRGhyako', + 'unconfidential_address': 'XKMc3Zqa2eqzDCJ2u9y1vUBnC7o1p3WnKh', + 'scriptpubkey': 'a91457d2b3591fe27661d0a785967126abf9ed19999d87', + 'public_blinding_key': '02227fe08e840120991db5115cce32162897bdd73012e3858715d5f94ad74113cb', + }, + { + 'address': 'Azppoxtn14x4niJRxnhEmQNY7qiEoUM8gcVrc6MASVTey9e3ZzxK7Q4riJyxgfq2ySjM6qh2L2TetpvL', + 'unconfidential_address': 'XWypf6BiGS9DW72gJ3unU1vp41PA7z4ZJx', + 'scriptpubkey': 'a914d7559651b007d4971de281f81347653fa8cf9e0087', + 'public_blinding_key': '02d7a477cbcd56ae33a7b6917e10e41a80168b590b23775546a4ea26a977f178e7', + }, +] + + # Patch os.path.exists so that it returns False whenever asked if the default output file exists, # otherwise all tests will fail by default def path_exists(filename): @@ -106,7 +130,7 @@ def __exit__(self, *args): return False -def get_output_ex(args, expect_error=False): +def get_output_ex(args, expect_error=False, is_liquid=False): ofiles = {} def recovery_open_(filename, mode=None): @@ -120,7 +144,7 @@ def recovery_open_(filename, mode=None): with mock.patch('garecovery.recoverycli.open', side_effect=recovery_open_): with mock.patch('sys.stdout', io.StringIO()) as output: - result = main([sys.argv[0], ] + args) + result = main([sys.argv[0], ] + args, is_liquid) if expect_error: assert result != 0, output.getvalue() else: @@ -131,23 +155,23 @@ def recovery_open_(filename, mode=None): return output.getvalue(), ofiles -def get_output(args, expect_error=False): +def get_output(args, expect_error=False, is_liquid=False): """Patch sys.stdout and call main with args capturing the output This is now a legacy call for backwards compatibility of old tests """ filtered_args = [] - two_of_three = False + nlocktime = False show_summary = False for arg in args: if arg in ['--show-summary', '-s']: show_summary = True else: - if arg == '2of3': - two_of_three = True + if arg == '2of2': + nlocktime = True filtered_args.append(arg) - output, ofiles = get_output_ex(filtered_args, expect_error) + output, ofiles = get_output_ex(filtered_args, expect_error, is_liquid) if expect_error: return output @@ -160,12 +184,12 @@ def get_output(args, expect_error=False): # recovery mode. Can use the csv output to reconstruct it csv_content = ofiles[DEFAULT_OFILE] csv_ = csv.DictReader(io.StringIO(csv_content)) - if two_of_three: + if not nlocktime: # simply the raw transactions lines = [row['raw tx'] for row in csv_] return '\n'.join(lines) else: - # in 2of2 you got the raw tx but also the private key + # in 2of2 nlocktime you got the raw tx but also the private key lines = ["{} {}".format(row['raw tx'], row['private key']) for row in csv_] return '\n'.join(lines) @@ -193,16 +217,44 @@ def verify_txs(txs, utxos, expect_witness): class AuthServiceProxy: """Mock bitcoincore""" - def __init__(self, txfile): + lbtc_hex = 'b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23' + + def tx_from_hex(self, tx_hex): + flags = wally.WALLY_TX_FLAG_USE_WITNESS + if self.is_liquid: + flags |= wally.WALLY_TX_FLAG_USE_ELEMENTS + return wally.tx_from_hex(tx_hex, flags) + + @staticmethod + def tx_to_hex(tx): + return wally.tx_to_hex(tx, wally.WALLY_TX_FLAG_USE_WITNESS) + + def __init__(self, txfile, is_liquid=False): + self.is_liquid = is_liquid + versions = gaconstants.ADDR_VERSIONS_LIQUID_REGTEST if is_liquid else \ + gaconstants.ADDR_VERSIONS_TESTNET + family = gaconstants.ADDR_FAMILY_LIQUID_REGTEST if is_liquid else \ + gaconstants.ADDR_FAMILY_TESTNET + self.given_addresses = 0 self.tx_by_id = {} self.txout_by_address = {} for line in open(datafile(txfile)).readlines(): - tx = txutil.from_hex(line.strip()) - self.tx_by_id[txutil.get_txhash_bin(tx)] = tx + tx_data = line.strip().split() + tx_hex = tx_data[0] + tx = self.tx_from_hex(tx_hex) + self.tx_by_id[txutil.get_txhash_hex(tx)] = tx for i in range(wally.tx_get_num_outputs(tx)): - addr = txutil.get_output_address(tx, i, gaconstants.ADDR_VERSIONS_TESTNET, - gaconstants.ADDR_FAMILY_TESTNET) - self.txout_by_address[addr] = (tx, i) + addr = txutil.get_output_address(tx, i, versions, family) + self.txout_by_address[addr] = [tx, i] + if is_liquid: + _, amount, asset, a_bl, v_bl, conf = tx_data + self.txout_by_address[addr] += [amount, asset, a_bl, v_bl, conf] + # for simplicity liquid mockup tx have one output + assert wally.tx_get_num_outputs(tx) == 1 + else: + conf = tx_data[1] if len(tx_data) > 1 else 0 + self.txout_by_address[addr] += [conf] + self.imported = {} # This is something of a workaround because all the existing tests are based on generating @@ -215,8 +267,15 @@ def __init__(self, txfile): def importmulti(self, requests): result = [] for request in requests: - assert request['watchonly'] is True - address = request['scriptPubKey']['address'] + if not self.is_liquid: + assert request['watchonly'] is True + address = request['scriptPubKey']['address'] + else: + scriptpubkey = h2b(request['scriptPubKey']) + assert wally.scriptpubkey_get_type(scriptpubkey) == wally.WALLY_SCRIPT_TYPE_P2SH + address = wally.base58check_from_bytes( + bytearray([gaconstants.ADDR_VERSIONS_LIQUID_REGTEST[1]]) + scriptpubkey[2:22]) + if address in self.txout_by_address: self.imported[address] = self.txout_by_address[address] result.append({'success': True}) @@ -226,15 +285,40 @@ def _get_unspent(self, address): imported = self.imported.get(address, None) if imported is None: return None - tx, i = imported - script = wally.tx_get_output_script(tx, i) - return { - "txid": txutil.get_txhash_bin(tx), + tx, i, conf = imported[:3] + scriptpubkey = wally.tx_get_output_script(tx, i) + satoshi = wally.tx_get_output_satoshi(tx, i) + ret = { + "txid": txutil.get_txhash_hex(tx), "vout": i, "address": address, - "scriptPubKey": wally.hex_from_bytes(script) + "scriptPubKey": b2h(scriptpubkey), + "amount": round(satoshi * 10**-8, 8), + "confirmations": int(conf), } + if self.is_liquid: + # TODO: also mockup the unblinded case + amount, asset, a_bl, v_bl, conf = imported[2:] + generator = wally.asset_generator_from_bytes( + h2b_rev(asset), h2b_rev(a_bl)) + value_commitment = wally.asset_value_commitment( + round(float(amount) * 10 ** 8), h2b_rev(v_bl), generator) + a_com = b2h(generator) + v_com = b2h(value_commitment) + ret.update({ + "txid": txutil.get_txhash_hex(tx), + "amount": float(amount), + "assetcommitment": a_com, + "asset": asset, + "amountcommitment": v_com, + "amountblinder": v_bl, + "assetblinder": a_bl, + "confirmations": int(conf), + }) + + return ret + def listunspent(self, minconf, maxconf, addresses): unspent = [self._get_unspent(address) for address in addresses] return [x for x in unspent if x] @@ -245,7 +329,128 @@ def getrawtransaction(self, txid): def batch_(self, requests): return [getattr(self, call)(params) for call, params in requests] + def dumpassetlabels(self): + return {'bitcoin': 'b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23'} + + def getnewaddress(self): + mock_addresses = mock_addresses_liquid if self.is_liquid else mock_addresses_bitcoin + self.given_addresses += 1 + return mock_addresses[self.given_addresses % len(mock_addresses)]['address'] + + def testmempoolaccept(self, tx_list): + assert isinstance(tx_list, list) and len(tx_list) == 1 + tx = self.tx_from_hex(tx_list[0]) + return [{'txid': txutil.get_txhash_hex(tx), 'allowed': True}] + + def createrawtransaction(self, inputs, map_amount, nlocktime, is_replaceable, map_asset): + tx = wally.tx_init(wally.WALLY_TX_VERSION_2, nlocktime, len(inputs), len(map_amount)) + sequence = gaconstants.MAX_BIP125_RBF_SEQUENCE + if not is_replaceable: + sequence += 1 + + for _input in inputs: + txid = h2b_rev(_input['txid']) + wally.tx_add_elements_raw_input(tx, txid, _input['vout'], sequence, None, + None, None, None, None, None, None, None, None, 0) + + fee_value = None + for address, amount in map_amount.items(): + value = wally.tx_confidential_value_from_satoshi(round(amount * 10 ** 8)) + if address == 'fee': + fee_value = value + else: + mock_address = [e for e in mock_addresses_liquid if e['address'] == address][0] + scriptpubkey = h2b(mock_address['scriptpubkey']) + blindingpubkey = h2b(mock_address['public_blinding_key']) + asset = b'\x01' + h2b_rev(map_asset[address]) + wally.tx_add_elements_raw_output( + tx, scriptpubkey, asset, value, blindingpubkey, None, None, 0) + # force fee to be the last output, to make things simpler + lbtc = b'\x01' + h2b_rev(self.lbtc_hex) + wally.tx_add_elements_raw_output(tx, None, lbtc, fee_value, None, None, None, 0) + + return self.tx_to_hex(tx) + + def rawblindrawtransaction( + self, tx_hex, input_vbfs_hex, input_values, input_assets_hex, input_abfs_hex): + tx = self.tx_from_hex(tx_hex) + + input_values = [round(i * 10 ** 8) for i in input_values] + input_assets = [h2b_rev(h) for h in input_assets_hex] + input_abfs = [h2b_rev(h) for h in input_abfs_hex] + input_vbfs = [h2b_rev(h) for h in input_vbfs_hex] + input_ags = [ + wally.asset_generator_from_bytes(a, bf) for a, bf in zip(input_assets, input_abfs)] + + input_assets_concat = b''.join(input_assets) + input_abfs_concat = b''.join(input_abfs) + input_ags_concat = b''.join(input_ags) + + fake_random_bytes = b'\x77' * 32 + # ephemeral keypair + fake_eph_key_prv = b'\x00' * 31 + b'\x01' + fake_eph_key_pub = wally.ec_public_key_from_private_key(fake_eph_key_prv) + + min_value = 1 + ct_exp = 0 + ct_bits = 36 + + out_num = wally.tx_get_num_outputs(tx) + output_blinded_values = [] + for out_idx in range(out_num): + if wally.tx_get_output_script(tx, out_idx): + value_bytes = wally.tx_get_output_value(tx, out_idx) + value_satoshi = wally.tx_confidential_value_to_satoshi(value_bytes) + output_blinded_values.append(value_satoshi) + else: + # fee, make sure it is the last output, to simplify things + assert out_idx == out_num - 1 + + output_abfs = [fake_random_bytes for i in range(out_num - 1)] + output_vbfs = [fake_random_bytes for i in range(out_num - 2)] + output_vbfs.append(wally.asset_final_vbf( + input_values + output_blinded_values, wally.tx_get_num_inputs(tx), + b''.join(input_abfs + output_abfs), b''.join(input_vbfs + output_vbfs))) + + for out_idx in range(out_num - 1): + # To be accurate, if an output can't be blinded, we should not set + # the *proof and set nonce empty, however it is not strictly + # necessary to mimic the exact behavior in the mockup. + asset_prefixed = wally.tx_get_output_asset(tx, out_idx) + value_bytes = wally.tx_get_output_value(tx, out_idx) + blinding_pubkey = wally.tx_get_output_nonce(tx, out_idx) + scriptpubkey = wally.tx_get_output_script(tx, out_idx) + assert scriptpubkey + + assert asset_prefixed[0] == 1 and value_bytes[0] == 1 + value_satoshi = wally.tx_confidential_value_to_satoshi(value_bytes) + asset = asset_prefixed[1:] + + blinding_nonce = wally.sha256(wally.ecdh(blinding_pubkey, fake_eph_key_prv)) + + output_abf = output_abfs[out_idx] + output_vbf = output_vbfs[out_idx] + output_generator = wally.asset_generator_from_bytes(asset, output_abf) + output_value_commitment = wally.asset_value_commitment( + value_satoshi, output_vbf, output_generator) + + rangeproof = wally.asset_rangeproof_with_nonce( + value_satoshi, blinding_nonce, asset, output_abf, output_vbf, + output_value_commitment, scriptpubkey, output_generator, min_value, + ct_exp, ct_bits) + + surjectionproof = wally.asset_surjectionproof( + asset, output_abf, output_generator, fake_random_bytes, + input_assets_concat, input_abfs_concat, input_ags_concat) + + wally.tx_set_output_asset(tx, out_idx, output_generator) + wally.tx_set_output_value(tx, out_idx, output_value_commitment) + wally.tx_set_output_nonce(tx, out_idx, fake_eph_key_pub) + wally.tx_set_output_surjectionproof(tx, out_idx, surjectionproof) + wally.tx_set_output_rangeproof(tx, out_idx, rangeproof) + + return self.tx_to_hex(tx) + estimatesmartfee = mock.Mock() getblockcount = mock.Mock() - getnewaddress = mock.Mock() getnetworkinfo = mock.Mock() diff --git a/garecovery/two_of_three.py b/garecovery/two_of_three.py index c8dc18c..a16194b 100644 --- a/garecovery/two_of_three.py +++ b/garecovery/two_of_three.py @@ -6,7 +6,7 @@ import shutil import time -from gaservices.utils import gacommon, gaconstants, txutil +from gaservices.utils import gacommon, gaconstants, txutil, b2h, h2b import wallycore as wally @@ -24,9 +24,9 @@ def get_scriptpubkey_hex(redeem_script_hash_hex): def get_redeem_script(keys): """Return a 2of3 multisig redeem script as a hex string""" - keys = [wally.hex_from_bytes(key) for key in keys] + keys = [b2h(key) for key in keys] logging.debug("get_redeem_script public keys = {}".format(keys)) - return wally.hex_to_bytes("5221{}21{}21{}53ae".format(*keys)) + return h2b("5221{}21{}21{}53ae".format(*keys)) def bip32_key_from_base58check(base58check): @@ -34,8 +34,8 @@ def bip32_key_from_base58check(base58check): return wally.bip32_key_unserialize(raw) -def derive_user_key(wallet, subaccount, branch=1): - subaccount_path = gacommon.get_subaccount_path(subaccount) +def derive_user_key(wallet, subaccount, branch=1, gdk_path=False): + subaccount_path = [1, subaccount] if gdk_path else gacommon.get_subaccount_path(subaccount) return gacommon.derive_hd_key(wallet, subaccount_path + [branch]) @@ -45,10 +45,10 @@ class P2SH: def __init__(self, pubkeys, network): self.redeem_script = get_redeem_script(pubkeys) - self.redeem_script_hex = wally.hex_from_bytes(self.redeem_script) + self.redeem_script_hex = b2h(self.redeem_script) script_hash = wally.hash160(self.get_witness_script()) - script_hash_hex = wally.hex_from_bytes(script_hash) + script_hash_hex = b2h(script_hash) self.scriptPubKey = get_scriptpubkey_hex(script_hash_hex) ver = {'testnet': b'\xc4', 'mainnet': b'\x05'}[network] @@ -69,7 +69,7 @@ def get_witness_script(self): return wally.witness_program_from_bytes(self.redeem_script, wally.WALLY_SCRIPT_SHA256) -def createDerivedKeySet(ga_xpub, wallets, custom_xprv, network): +def createDerivedKeySet(ga_xpub, wallets, custom_xprv, network, gdk_path=False): """Return class instances which represent sets of derived keys Given a user's key material call createDerivedKeySet to create a class @@ -83,13 +83,15 @@ def createDerivedKeySet(ga_xpub, wallets, custom_xprv, network): # Given the subaccount the user keys can be derived. Optionally the user may provide a custom # extended private key as the backup - user_keys = [derive_user_key(wallet, subaccount) for wallet in wallets] + user_keys = [derive_user_key(wallets[0], subaccount)] if custom_xprv: logging.debug("Using custom xprv") root_xprv = bip32_key_from_base58check(custom_xprv) branch = 1 xprv = gacommon.derive_hd_key(root_xprv, [branch], wally.BIP32_FLAG_KEY_PRIVATE) user_keys.append(xprv) + else: + user_keys.append(derive_user_key(wallets[1], subaccount, gdk_path=gdk_path)) assert len(user_keys) == 2 class DerivedKeySet: @@ -105,7 +107,7 @@ def __init__(self, pointer): # Derive the GreenAddress public key for this pointer value ga_key = gacommon.derive_hd_key(ga_xpub, [pointer], wally.BIP32_FLAG_KEY_PUBLIC) self.ga_key = wally.bip32_key_get_pub_key(ga_key) - logging.debug("ga_key = {}".format(wally.hex_from_bytes(self.ga_key))) + logging.debug("ga_key = {}".format(b2h(self.ga_key))) # Derive the user private keys for this pointer value flags = wally.BIP32_FLAG_KEY_PRIVATE @@ -137,42 +139,6 @@ def __init__(self, keyset, witness_type, vout, tx, dest_address): self.dest_address = dest_address self.witness = self.keyset.witnesses[witness_type] - def get_default_feerate(self): - """Get a value for default feerate. - - On testnet only it is possible to pass --default-feerate as an option. On mainnet this is - not supported as it is too error prone. - """ - if clargs.args.default_feerate is None: - msg = 'Unable to get fee rate from core, you must pass --default-feerate' - raise exceptions.NoFeeRate(msg) - - fee_satoshi_byte = decimal.Decimal(clargs.args.default_feerate) - return fee_satoshi_byte - - def get_feerate(self): - """Return the required fee rate in satoshis per byte""" - logging.debug("Connecting to bitcoinrpc to get feerate") - core = bitcoincore.Connection(clargs.args) - - blocks = clargs.args.fee_estimate_blocks - - estimate = core.estimatesmartfee(blocks) - if 'errors' in estimate: - fee_satoshi_byte = self.get_default_feerate() - else: - fee_btc_kb = estimate['feerate'] - fee_satoshi_kb = fee_btc_kb * gaconstants.SATOSHI_PER_BTC - fee_satoshi_byte = round(fee_satoshi_kb / 1000) - - logging.debug('feerate = {} BTC/kb'.format(fee_btc_kb)) - logging.debug('feerate = {} satoshis/kb'.format(fee_satoshi_kb)) - - logging.info('Fee estimate for confirmation in {} blocks is ' - '{} satoshis/byte'.format(blocks, fee_satoshi_byte)) - - return fee_satoshi_byte - def get_raw_unsigned(self, fee_satoshi): """Return raw transaction ready for signing @@ -205,7 +171,7 @@ def get_fee(self, tx): """Given a raw transaction return the fee""" virtual_tx_size = wally.tx_get_vsize(tx) logging.debug("virtual transaction size = {}".format(virtual_tx_size)) - fee_satoshi_byte = self.get_feerate() + fee_satoshi_byte = util.get_feerate() fee_satoshi = fee_satoshi_byte * virtual_tx_size logging.debug('Calculating fee over {} (virtual) byte tx @{} satoshi per ' 'byte = {} satoshis'.format(virtual_tx_size, fee_satoshi_byte, fee_satoshi)) @@ -360,7 +326,8 @@ def scan_blockchain(self, keysets): def _derived_keyset(self, ga_xpub): """Call createDerivedKeySet with ga_xpub""" - return createDerivedKeySet(ga_xpub, self.wallets, self.custom_xprv, clargs.args.network) + return createDerivedKeySet( + ga_xpub, self.wallets, self.custom_xprv, clargs.args.network, clargs.args.gdk_path) def get_keysets(self, subaccounts, pointers): """Return the keysets for a set of subaccounts/pointers""" diff --git a/garecovery/two_of_two.py b/garecovery/two_of_two.py index b6feac1..4563a5a 100644 --- a/garecovery/two_of_two.py +++ b/garecovery/two_of_two.py @@ -2,7 +2,7 @@ import json import sys -from gaservices.utils import gacommon, gaconstants, txutil +from gaservices.utils import gacommon, gaconstants, txutil, b2h import wallycore as wally from . import clargs @@ -52,8 +52,8 @@ def fixup_old_nlocktimes(self): txdata['prevout_scripts'] = [] for i in range(wally.tx_get_num_inputs(tx)): inp = wally.tx_get_input_script(tx, i) - ga_signature = wally.hex_from_bytes(inp[2:inp[1]+2]) - redeem_script = wally.hex_from_bytes(inp[-71:]) + ga_signature = b2h(inp[2:inp[1]+2]) + redeem_script = b2h(inp[-71:]) txdata['prevout_signatures'].append(ga_signature) txdata['prevout_scripts'].append(redeem_script) txdata['prevout_script_types'].append(gaconstants.P2SH_FORTIFIED_OUT) @@ -71,7 +71,7 @@ def infer_network(self): def get_pubkey_for_pointer_hex(xpub): """Return hex encoded public key derived from xpub for pointer""" xpub = gacommon.derive_hd_key(xpub, [pointer], wally.BIP32_FLAG_KEY_PUBLIC) - return wally.hex_from_bytes(wally.bip32_key_get_pub_key(xpub)) + return b2h(wally.bip32_key_get_pub_key(xpub)) def get_pubkeys_hex(fn, keys_material, network): """Return a list of hex-encoded public key given either a seed or a mnemonic""" diff --git a/garecovery/two_of_two_csv.py b/garecovery/two_of_two_csv.py new file mode 100644 index 0000000..1faf8e8 --- /dev/null +++ b/garecovery/two_of_two_csv.py @@ -0,0 +1,212 @@ +import logging + +import wallycore as wally + +from garecovery import bitcoincore +from garecovery import clargs +from garecovery.exceptions import BitcoinCoreConnectionError, InsufficientFee, \ + MempoolRejectionError +from garecovery.ga_xpub import gait_paths_from_seed, gait_path_from_mnemonic +from garecovery.key import Bip32Key +from garecovery.subaccount import Green2of2Subaccount +from garecovery.utxo import SpendableUTXO +from garecovery.util import get_current_blockcount, get_feerate, scriptpubkey_from_address +from gaservices.utils import b2h_rev, h2b, h2b_rev +from gaservices.utils.gaconstants import CSV_BUCKETS, DUST_SATOSHI, EMPTY_TX_SIZE, INPUT_SIZE, \ + MAX_BIP125_RBF_SEQUENCE + + +class TwoOfTwoCSV(object): + + def __init__(self, mnemonic, seed): + self.master_xprv = Bip32Key.from_seed(seed) + self.gait_paths = gait_paths_from_seed(seed) + if mnemonic: + self.gait_paths.append(gait_path_from_mnemonic(mnemonic)) + + def get_utxos(self, outputs): + """Get utxos from a list of possible outputs""" + core = bitcoincore.Connection(clargs.args) + + version = core.getnetworkinfo()["version"] + if version < 190100: + raise BitcoinCoreConnectionError('Unsupported version') + + if clargs.args.ignore_mempool: + # using a descriptor with CSV is not possible + scanobjects = [{'desc': 'addr({})'.format(o.address)} for o in outputs] + result = core.scantxoutset('start', scanobjects) + if not result['success']: + raise BitcoinCoreConnectionError('scantxoutset failed') + unspents = result['unspents'] + else: + logging.info("Scanning from '{}'".format(clargs.args.scan_from)) + logging.warning('This step may take 10 minutes or more') + + # Need to import our keysets into core so that it will recognise the + # utxos we are looking for + addresses = [o.address for o in outputs] + requests = [{ + 'scriptPubKey': {'address': o.address}, + 'timestamp': clargs.args.scan_from, + 'watchonly': True, + } for o in outputs] + logging.info('Importing {} derived addresses into bitcoind'.format(len(requests))) + result = core.importmulti(requests) + if result != [{'success': True}] * len(requests): + raise exceptions.ImportMultiError('Unexpected result from importmulti') + logging.info('Successfully imported {} derived addresses'.format(len(result))) + + current_blockcount = core.getblockcount() + unspents = core.listunspent(0, 9999999, addresses) + for u in unspents: + # This may be inaccurate + u['height'] = current_blockcount - u['confirmations'] + + # match keys with utxos + utxos = [SpendableUTXO(u, o) + for u in unspents + for o in outputs + if h2b(u['scriptPubKey']) == o.script_pubkey] + + logging.info('found {} utxos'.format(len(utxos))) + return utxos + + def scan_subaccount(self, subaccount_pointer, pointer_search_depth): + """Scan for utxos in a subaccount""" + logging.info('subaccount {}: start scanning'.format(subaccount_pointer)) + utxos = [] + for gait_path in self.gait_paths: + gait_path_hex = ''.join(hex(i+2**32)[-4:] for i in gait_path) + logging.info('Using gait_path: {}'.format(gait_path_hex)) + subaccount = Green2of2Subaccount.from_master_xprv( + self.master_xprv.xprv, gait_path, subaccount_pointer, clargs.args.network) + + start = 0 + while True: + logging.info('subaccount {}: range {}-{}'.format( + subaccount_pointer, start, start + pointer_search_depth)) + outputs = [] + for pointer in range(start, start + pointer_search_depth): + for csv_blocks in CSV_BUCKETS[clargs.args.network]: + outputs.append(subaccount.get_csv_output(pointer, csv_blocks)) + + new_utxos = self.get_utxos(outputs) + logging.info('subaccount {}: found {} new utxos'.format( + subaccount_pointer, len(new_utxos))) + + if not new_utxos: + break + + utxos += new_utxos + start += pointer_search_depth + + logging.info('subaccount {}: stop scanning'.format(subaccount_pointer)) + return utxos + + # TODO: transaction may be too big, allow to split it + @staticmethod + def create_transaction(utxos): + core = bitcoincore.Connection(clargs.args) + + nlocktime = blockcount = get_current_blockcount() or 0 + is_replaceable = True + + estimated_vsize = EMPTY_TX_SIZE + inputs, used_utxos = [], [] + + for u in utxos: + if not u.is_expired(blockcount): + blocks_left = u.output.csv_blocks + u.height - blockcount + logging.info('Skipping utxo ({}:{}) not expired ({} blocks left)'.format( + b2h_rev(u.txid), u.vout, blocks_left)) + continue + + estimated_vsize += INPUT_SIZE + + inputs.append({'txid': b2h_rev(u.txid), 'vout': u.vout}) + used_utxos.append(u) + + if len(used_utxos) == 0: + return '', [] + + logging.info('num used utxos: {}'.format(len(used_utxos))) + + feerate = get_feerate() + satoshi_fee = round(feerate * estimated_vsize) + + satoshi_send = sum(u.satoshi for u in used_utxos) - satoshi_fee + if satoshi_send < DUST_SATOSHI: + raise InsufficientFee + + address = core.getnewaddress() + scriptpubkey = scriptpubkey_from_address(address) + + tx = wally.tx_init(wally.WALLY_TX_VERSION_2, nlocktime, len(inputs), 1) + sequence = MAX_BIP125_RBF_SEQUENCE + if not is_replaceable: + # A transaction is considered to have opted in to allowing + # replacement of itself if any of its inputs have an nSequence + # number less than or equal to MAX_BIP125_RBF_SEQUENCE + sequence += 1 + + for _input in inputs: + txid = h2b_rev(_input['txid']) + wally.tx_add_raw_input(tx, txid, _input['vout'], sequence, None, None, 0) + + wally.tx_add_raw_output(tx, satoshi_send, scriptpubkey, 0) + + transaction = wally.tx_to_hex(tx, wally.WALLY_TX_FLAG_USE_WITNESS) + + return transaction, used_utxos + + @staticmethod + def sign_transaction(transaction, used_utxos): + # TODO: use a wally_tx wrapper + flags = wally.WALLY_TX_FLAG_USE_WITNESS + tx = wally.tx_from_hex(transaction, flags) + + # All sequence numbers must be set before signing + for index, u in enumerate(used_utxos): + u.set_csv_sequence(tx, index) + + blockcount = get_current_blockcount() or 0 + for index, u in enumerate(used_utxos): + assert u.is_expired(blockcount) + logging.debug('signing {}-th input'.format(index)) + u.sign(tx, index) + + logging.debug('signed tx: {}'.format(wally.tx_to_hex(tx, flags))) + return wally.tx_to_hex(tx, flags) + + @staticmethod + def test_transactions(transactions): + logging.info('testing {} transactions against mempool'.format(len(transactions))) + core = bitcoincore.Connection(clargs.args) + results = core.testmempoolaccept(transactions) + logging.info('testmempoolaccept results {}'.format(results)) + # FIXME: consider filtering the unaccepted transactions instead of raising an error + if not all(d.get('allowed') for d in results): + raise MempoolRejectionError( + 'Transactions rejected from mempool ({})'.format(transactions)) + + def get_transactions(self): + """Get one transaction per subaccount which includes at least one recovered utxo and it is + able to pay the fees""" + transactions = [] + for subaccount_pointer in range((clargs.args.search_subaccounts or 0) + 1): + utxos = self.scan_subaccount(subaccount_pointer, clargs.args.key_search_depth) + if len(utxos) == 0: + continue + + transaction, used_utxo = self.create_transaction(utxos) + if transaction: + signed_transaction = self.sign_transaction(transaction, used_utxo) + transactions.append(signed_transaction) + + if transactions: + self.test_transactions(transactions) + + logging.debug('transactions: {}'.format(transactions)) + flags = wally.WALLY_TX_FLAG_USE_WITNESS + return [(wally.tx_from_hex(transaction, flags), None) for transaction in transactions] diff --git a/garecovery/util.py b/garecovery/util.py index 5a8a9ab..f983157 100644 --- a/garecovery/util.py +++ b/garecovery/util.py @@ -1,3 +1,4 @@ +import decimal import logging from gaservices.utils import gacommon, gaconstants @@ -23,6 +24,44 @@ def get_current_blockcount(): return None +def get_default_feerate(): + """Get a value for default feerate. + + On testnet only it is possible to pass --default-feerate as an option. On mainnet this is + not supported as it is too error prone. + """ + if clargs.args.default_feerate is None: + msg = 'Unable to get fee rate from core, you must pass --default-feerate' + raise exceptions.NoFeeRate(msg) + + fee_satoshi_byte = decimal.Decimal(clargs.args.default_feerate) + return fee_satoshi_byte + + +def get_feerate(): + """Return the required fee rate in satoshis per byte""" + logging.debug("Connecting to bitcoinrpc to get feerate") + core = bitcoincore.Connection(clargs.args) + + blocks = clargs.args.fee_estimate_blocks + + estimate = core.estimatesmartfee(blocks) + if 'errors' in estimate: + fee_satoshi_byte = get_default_feerate() + else: + fee_btc_kb = estimate['feerate'] + fee_satoshi_kb = fee_btc_kb * gaconstants.SATOSHI_PER_BTC + fee_satoshi_byte = round(fee_satoshi_kb / 1000) + + logging.debug('feerate = {} BTC/kb'.format(fee_btc_kb)) + logging.debug('feerate = {} satoshis/kb'.format(fee_satoshi_kb)) + + logging.info('Fee estimate for confirmation in {} blocks is ' + '{} satoshis/byte'.format(blocks, fee_satoshi_byte)) + + return fee_satoshi_byte + + def decode_base58_address(address): try: decoded = wally.base58check_to_bytes(address) diff --git a/garecovery/utxo.py b/garecovery/utxo.py new file mode 100644 index 0000000..395cae1 --- /dev/null +++ b/garecovery/utxo.py @@ -0,0 +1,114 @@ +import wallycore as wally + +from gaservices.utils import b2h, h2b, h2b_rev + + +class UTXO(object): + """UTXO""" + + def __init__(self, unspent): + """Create UTXO from scanutxoset output""" + self.txid = h2b_rev(unspent.get('txid')) + self.vout = unspent.get('vout') + self.script_pubkey = h2b(unspent.get('scriptPubKey')) + self.height = unspent.get('height') + self.satoshi = round(unspent['amount'] * 10 ** 8) + + +class SpendableUTXO(UTXO): + """Utxo able to spend itself""" + + def __init__(self, unspent, output): + super().__init__(unspent) + if output.script_pubkey != self.script_pubkey: + raise ValueError('scriptpubkey must match: {}, {}'.format( + b2h(output.script_pubkey), + b2h(self.script_pubkey))) + self.output = output + + def is_expired(self, blockcount): + if not hasattr(self.output, 'csv_blocks'): + return True + return (blockcount - self.height) >= self.output.csv_blocks + + def set_csv_sequence(self, tx, index): + """Set the sequence number with csv_blocks""" + wally.tx_set_input_sequence(tx, index, self.output.csv_blocks) + + def _get_signature_hash(self, tx, index): + return wally.tx_get_btc_signature_hash( + tx, + index, + self.output.witness_script, + self.satoshi, + wally.WALLY_SIGHASH_ALL, + wally.WALLY_TX_FLAG_USE_WITNESS) + + def sign(self, tx, index): + """Sign the index-th input of tx, fill its witness and scriptSig assuming CSV time is + expired""" + txhash = self._get_signature_hash(tx, index) + wally.tx_set_input_witness(tx, index, self.output.get_signed_witness(txhash)) + wally.tx_set_input_script(tx, index, self.output.script_sig) + + +class ElementsUTXO(object): + """Elements UTXO""" + + def __init__(self, unspent): + """Create ElementsUTXO from scanutxoset (processed) output""" + self.txid = h2b_rev(unspent.get('txid')) + self.vout = unspent.get('vout') + self.script_pubkey = h2b(unspent.get('scriptPubKey')) + self.address = unspent.get('address') + self.height = unspent.get('height') + + # blinded data + is_unblinded = 'asset' in unspent and 'amount' in unspent + self.asset = h2b_rev(unspent['asset']) if is_unblinded else None + self.value = round(unspent['amount'] * 10**8) if is_unblinded else None + self.abf = b'\x00' * 32 if self.asset else None + self.vbf = b'\x00' * 32 if self.value else None + + self.asset_commitment = \ + b'\x01' + self.asset if is_unblinded else \ + h2b(unspent.get('assetcommitment')) + self.value_commitment = \ + wally.tx_confidential_value_from_satoshi(self.value) if is_unblinded else \ + h2b(unspent.get('amountcommitment')) + self.nonce_commitment = \ + b'' if is_unblinded else \ + h2b(unspent.get('noncecommitment')) + self.rangeproof = \ + b'' if is_unblinded else \ + h2b(unspent.get('rangeproof')) + + def unblind(self, private_blinding_key): + if self.is_unblinded(): + return + self.value, self.asset, self.abf, self.vbf = wally.asset_unblind( + self.nonce_commitment, private_blinding_key, self.rangeproof, self.value_commitment, + self.script_pubkey, self.asset_commitment) + + def is_unblinded(self): + return 1 == self.asset_commitment[0] == self.value_commitment[0] + + +class SpendableElementsUTXO(ElementsUTXO, SpendableUTXO): + """Elements unblinded UTXO able to spend itself""" + + def __init__(self, unspent, output, seed): + super().__init__(unspent) + if output.address != self.address: + raise ValueError('addresses must match: {}, {}'.format(output.address, self.address)) + self.output = output + self.unblind(output.get_private_blinding_key(seed)) + + def _get_signature_hash(self, tx, index): + return wally.tx_get_elements_signature_hash( + tx, + index, + self.output.witness_script, + self.value_commitment, + wally.WALLY_SIGHASH_ALL, + wally.WALLY_TX_FLAG_USE_WITNESS) diff --git a/gaservices/utils/__init__.py b/gaservices/utils/__init__.py index e69de29..93fef7f 100644 --- a/gaservices/utils/__init__.py +++ b/gaservices/utils/__init__.py @@ -0,0 +1,8 @@ +import wallycore as wally + + +h2b = wally.hex_to_bytes +b2h = wally.hex_from_bytes + +h2b_rev = lambda h : wally.hex_to_bytes(h)[::-1] +b2h_rev = lambda b : wally.hex_from_bytes(b[::-1]) diff --git a/gaservices/utils/gacommon.py b/gaservices/utils/gacommon.py index dce9efe..36b008e 100644 --- a/gaservices/utils/gacommon.py +++ b/gaservices/utils/gacommon.py @@ -3,7 +3,7 @@ import base64 -from . import gaconstants, txutil +from . import gaconstants, txutil, h2b import wallycore as wally @@ -78,7 +78,7 @@ def _to_der(sig): def sign(txdata, signatories): tx = txutil.from_hex(txdata['tx']) for i in range(wally.tx_get_num_inputs(tx)): - script = wally.hex_to_bytes(txdata['prevout_scripts'][i]) + script = h2b(txdata['prevout_scripts'][i]) script_type = txdata['prevout_script_types'][i] flags, value, sighash = 0, 0, wally.WALLY_SIGHASH_ALL @@ -102,7 +102,7 @@ def sign(txdata, signatories): def countersign(txdata, private_key): - GreenAddress = PassiveSignatory(wally.hex_to_bytes(txdata['prevout_signatures'][0])) + GreenAddress = PassiveSignatory(h2b(txdata['prevout_signatures'][0])) user = ActiveSignatory(wally.bip32_key_get_priv_key(private_key)) return sign(txdata, [GreenAddress, user]) @@ -122,3 +122,7 @@ def derive_user_private_key(txdata, wallet, branch): pointer = txdata['prevout_pointers'][0] or 0 path = get_subaccount_path(subaccount) return derive_hd_key(wallet, path + [branch, pointer]) + + +def is_liquid(network): + return network in ['liquid', 'localtest-liquid'] diff --git a/gaservices/utils/gaconstants.py b/gaservices/utils/gaconstants.py index ad48860..3d519f5 100644 --- a/gaservices/utils/gaconstants.py +++ b/gaservices/utils/gaconstants.py @@ -1,17 +1,23 @@ """ Constant values for recovery/BTC """ import decimal import sys +import wallycore as wally PY3 = sys.version_info.major > 2 SATOSHI_PER_BTC = decimal.Decimal(1e8) +DUST_SATOSHI = 546 + MAX_BIP125_RBF_SEQUENCE = 0xfffffffd # BIP32 hardened derivation flag HARDENED = 0x80000000 -SUPPORTED_NETWORKS = ['mainnet', 'testnet'] +SUPPORTED_NETWORKS = { + False: ['mainnet', 'testnet'], + True: ['liquid', 'localtest-liquid'] +} P2PKH_MAINNET = 0x00 P2SH_MAINNET = 0x05 @@ -19,17 +25,60 @@ P2PKH_TESTNET = 0x6f P2SH_TESTNET = 0xc4 +P2PKH_LIQUID = 0x39 +P2SH_LIQUID = 0x27 + +P2PKH_LIQUID_REGTEST = 0xeb +P2SH_LIQUID_REGTEST = 0x4b + ADDR_VERSIONS_MAINNET = [P2PKH_MAINNET, P2SH_MAINNET] ADDR_VERSIONS_TESTNET = [P2PKH_TESTNET, P2SH_TESTNET] +ADDR_VERSIONS_LIQUID = [P2PKH_LIQUID, P2SH_LIQUID] +ADDR_VERSIONS_LIQUID_REGTEST = [P2PKH_LIQUID_REGTEST, P2SH_LIQUID_REGTEST] ADDR_FAMILY_MAINNET = 'bc' ADDR_FAMILY_TESTNET = 'tb' +ADDR_FAMILY_LIQUID = 'lq' +ADDR_FAMILY_LIQUID_REGTEST = 'tb' + +CA_PREFIX = { + 'liquid': wally.WALLY_CA_PREFIX_LIQUID, + 'localtest-liquid': wally.WALLY_CA_PREFIX_LIQUID_REGTEST, +} + +CSV_BUCKETS = { + 'liquid': [65535], + 'localtest-liquid': [144, 4320, 25920, 51840, 65535], + 'testnet': [144, 4320, 51840], + 'mainnet': [25920, 51840, 65535], +} + +EMPTY_TX_SIZE = 106 # 0 inputs 1 p2wsh (longest) output +INPUT_SIZE = 159 + +# TODO: correctly estimate transaction vsize +# The following values overestimate the impact of adding an input or output to the transaction vsize, in order to +# produce a valid feerate. A temporary solution can be tweaking such values in order to match the desired feerate. +LIQUID_EMPTY_TX_SIZE = 40 +LIQUID_INPUT_SIZE = 150 +LIQUID_OUTPUT_SIZE = 1200 + def get_address_versions(network): - return {'testnet': ADDR_VERSIONS_TESTNET, 'mainnet': ADDR_VERSIONS_MAINNET}[network] + return { + 'testnet': ADDR_VERSIONS_TESTNET, + 'mainnet': ADDR_VERSIONS_MAINNET, + 'liquid': ADDR_VERSIONS_LIQUID, + 'localtest-liquid': ADDR_VERSIONS_LIQUID_REGTEST, + }[network] def get_address_family(network): - return {'testnet': ADDR_FAMILY_TESTNET, 'mainnet': ADDR_FAMILY_MAINNET}[network] + return { + 'testnet': ADDR_FAMILY_TESTNET, + 'mainnet': ADDR_FAMILY_MAINNET, + 'liquid': ADDR_FAMILY_LIQUID, + 'localtest-liquid': ADDR_FAMILY_LIQUID_REGTEST, + }[network] # GreenAddress script type for standard p2sh multisig UTXOs P2SH_FORTIFIED_OUT = 10 @@ -47,5 +96,17 @@ def get_address_family(network): 'pubkey': '036307e560072ed6ce0aa5465534fb5c258a2ccfbc257f369e8e7a181b16d897b3', } +GA_KEY_DATA_LIQUID = { + 'chaincode': '02721cc509aa0c2f4a90628e9da0391b196abeabc6393ed4789dd6222c43c489', + 'pubkey': '02c408c3bb8a3d526103fb93246f54897bdd997904d3e18295b49a26965cb41b7f' +} + +GA_KEY_DATA_LIQUID_REGTEST = GA_KEY_DATA_TESTNET + def get_ga_key_data(network): - return {'testnet': GA_KEY_DATA_TESTNET, 'mainnet': GA_KEY_DATA_MAINNET}[network] + return { + 'testnet': GA_KEY_DATA_TESTNET, + 'mainnet': GA_KEY_DATA_MAINNET, + 'liquid': GA_KEY_DATA_LIQUID, + 'localtest-liquid': GA_KEY_DATA_LIQUID_REGTEST, + }[network] diff --git a/gaservices/utils/txutil.py b/gaservices/utils/txutil.py index a398ac0..0ded88d 100644 --- a/gaservices/utils/txutil.py +++ b/gaservices/utils/txutil.py @@ -1,5 +1,5 @@ """ Transaction utility functions """ -from . import gaconstants +from . import gaconstants, b2h_rev import wallycore as wally if gaconstants.PY3: @@ -21,7 +21,7 @@ def get_txhash_bin(tx): def get_txhash_hex(tx): - return wally.hex_from_bytes(get_txhash_bin(tx)[::-1]) + return b2h_rev(get_txhash_bin(tx)) def new(nlocktime=0, inputs=8, outputs=8, version=wally.WALLY_TX_VERSION_2): @@ -43,6 +43,8 @@ def set_witness(tx, i, witness): def get_output_address(tx, i, versions, family): script = wally.tx_get_output_script(tx, i) + if len(script) == 0: # liquid only + return 'fee' script_type = wally.scriptpubkey_get_type(script) if script_type == wally.WALLY_SCRIPT_TYPE_P2PKH: return wally.base58check_from_bytes(bytearray([versions[0]]) + script[3:23]) diff --git a/setup.py b/setup.py index f620160..a924334 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,9 @@ packages=find_packages(), tests_require=['pytest', 'pytest-cov', 'mock', 'pycodestyle', ], setup_requires=['pytest-runner', ], - scripts=['garecovery/bin/garecovery-cli', ], + scripts=[ + 'garecovery/bin/garecovery-cli', + 'garecovery/bin/garecovery-liquid-cli', + ], include_package_data=True, zip_safe=False) diff --git a/tools/requirements.txt b/tools/requirements.txt index 54bd354..b140efa 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,8 +1,8 @@ argcomplete==1.8.2 --hash=sha256:c2a0a88492c31a42dde41636fbd4c406cde78fe7fb938ab511b733bff6358782 python-bitcoinrpc==1.0 --hash=sha256:a6a6f35672635163bc491c25fe29520bdd063dedbeda3b37bf5be97aa038c6e7 -https://github.com/ElementsProject/libwally-core/releases/download/release_0.7.8/wallycore-0.7.8-cp36-cp36m-linux_x86_64.whl; 'linux' in sys_platform and python_version == '3.6' --hash=sha256:b973fbabf7c30952d6ab5f626ea5231f06200c5f4f35f07bea19308ab87c8162 -https://github.com/ElementsProject/libwally-core/releases/download/release_0.7.8/wallycore-0.7.8-cp37-cp37m-linux_x86_64.whl; 'linux' in sys_platform and python_version == '3.7' --hash=sha256:77bebee316219c4be5069722e9aa08043278934641c7f677627194b4dfeabfd0 -https://github.com/ElementsProject/libwally-core/releases/download/release_0.7.8/wallycore-0.7.8-cp37-cp37m-macosx_10_14_x86_64.whl; sys_platform == 'darwin' and python_version == '3.7' --hash=sha256:bae6656aa86ed1197e636f69905e10987ef4fd3ee8ba18d16e0f62fcd8aaaaf1 -https://github.com/ElementsProject/libwally-core/releases/download/release_0.7.8/wallycore-0.7.8-cp36-cp36m-win_amd64.whl; sys_platform == 'win32' and python_version == '3.6' --hash=sha256:62f4d36a3a94f5e8d9fc48f641f5345df9088632d5d01554bbcdfb751b22fca6 -https://github.com/ElementsProject/libwally-core/archive/release_0.7.8.tar.gz --hash=sha256:b36a9e4e7db155d5a435b43c08e74ed9d812b05ea2becce454700a18217c2296 +https://github.com/ElementsProject/libwally-core/releases/download/release_0.7.9/wallycore-0.7.9-cp36-cp36m-linux_x86_64.whl; 'linux' in sys_platform and python_version == '3.6' --hash=sha256:7b83e33630330ce7579af58d675429c744557b4e8f13af2fc164af827aba7dbd +https://github.com/ElementsProject/libwally-core/releases/download/release_0.7.9/wallycore-0.7.9-cp37-cp37m-linux_x86_64.whl; 'linux' in sys_platform and python_version == '3.7' --hash=sha256:f5e82cc5b4affcce354da493e2076cc007fd4ac7c7dd3f6970cde4deebf2c179 +https://github.com/ElementsProject/libwally-core/releases/download/release_0.7.9/wallycore-0.7.9-cp37-cp37m-macosx_10_14_x86_64.whl; sys_platform == 'darwin' and python_version == '3.7' --hash=sha256:667d7a1b6eb7bd068fc819645bfab9df4c7760f83469012d15ecf168dd435a36 +https://github.com/ElementsProject/libwally-core/releases/download/release_0.7.9/wallycore-0.7.9-cp36-cp36m-win_amd64.whl; sys_platform == 'win32' and python_version == '3.6' --hash=sha256:30343164d146be1011e665245f66e55dfd8f82caa57e2cff130951cb380835cc +https://github.com/ElementsProject/libwally-core/archive/release_0.7.9.tar.gz --hash=sha256:339ac53546f0372f11c186d4d8e06a894fc3102a89ec11180d5e7bdd5ff34eab