diff --git a/ArmoryQt.py b/ArmoryQt.py index abdfb12f8..ad7892ead 100644 --- a/ArmoryQt.py +++ b/ArmoryQt.py @@ -1315,6 +1315,8 @@ def execImportWallet(self): print 'The highest index used was:', highestIdx self.addWalletToApplication(dlgPaper.newWallet, walletIsNew=False) print 'Import Complete!' + elif dlg.importType_migrate: + DlgMigrateSatoshiWallet(self, self).exec_() else: return diff --git a/armoryengine.py b/armoryengine.py index 95450feb2..977cc867f 100644 --- a/armoryengine.py +++ b/armoryengine.py @@ -7195,7 +7195,7 @@ def deleteImportedAddress(self, addr160): def importExternalAddressData(self, privKey=None, privChk=None, \ pubKey=None, pubChk=None, \ addr20=None, addrChk=None, \ - firstTime=0, firstBlk=0, \ + firstTime=UINT32_MAX, firstBlk=UINT32_MAX, \ lastTime=0, lastBlk=0): """ This wallet fully supports importing external keys, even though it is @@ -8939,7 +8939,6 @@ def castVal(v): import json import struct - class BCDataStream(object): def __init__(self): self.input = None @@ -9096,14 +9095,9 @@ def GetKeyFromPassphraseSatoshi(vKeyData, vSalt, nIter, deriveMethod): ################################################################################ -def read_wallet(json_db, db_env, wltFile): +def read_wallet(db_env, wltFile): db = open_wallet(db_env, wltFile) - json_db['keys'] = [] - json_db['pool'] = [] - json_db['names'] = {} - - # Moved parse_wallet code inline here kds = BCDataStream() vds = BCDataStream() @@ -9112,6 +9106,7 @@ def read_wallet(json_db, db_env, wltFile): cryptPrivList = [] masterEncrKey = {} poolKeysList = [] + addrNames = {} for (key, value) in db.items(): d = { } @@ -9129,8 +9124,6 @@ def read_wallet(json_db, db_env, wltFile): try: - # This is a weird merge of two if-then-else blocks... - # It didn't seem to be necessary to keep them separate if dType == "key": priv = SecureBinaryData(vds.read_bytes(vds.read_compact_size())[9:9+32]) plainPrivList.append(priv) @@ -9138,27 +9131,23 @@ def read_wallet(json_db, db_env, wltFile): pub = kds.read_bytes(kds.read_compact_size()) ckey = vds.read_bytes(vds.read_compact_size()) cryptPrivList.append( [pub, ckey] ) - elif dType == "mkey": masterEncrKey['mkey'] = vds.read_bytes(vds.read_compact_size()) masterEncrKey['salt'] = vds.read_bytes(vds.read_compact_size()) masterEncrKey['mthd'] = vds.read_int32() masterEncrKey['iter'] = vds.read_int32() masterEncrKey['othr'] = vds.read_bytes(vds.read_compact_size()) - - print len(masterEncrKey['mkey']) - print len(masterEncrKey['salt']) - print len(masterEncrKey['othr']) - elif dType == "pool": d['n'] = kds.read_int64() ver = vds.read_int32() ntime = vds.read_int64() pubkey = vds.read_bytes(vds.read_compact_size()) poolKeysList.append(pubkey_to_addrStr(pubkey)) - + elif dType == "name": + addrB58 = kds.read_string() + name = vds.read_string() + addrNames[addrB58] = name except Exception, e: - #traceback.print_exc() print("ERROR parsing wallet.dat, type %s" % dType) print("key data in hex: %s"%key.encode('hex_codec')) print("value data in hex: %s"%value.encode('hex_codec')) @@ -9166,34 +9155,23 @@ def read_wallet(json_db, db_env, wltFile): db.close() - return (plainPrivList, masterEncrKey, cryptPrivList, poolKeysList) - - - -def checkSatoshiEncrypted(wltPath): - if not os.path.exists(wltPath): - raise FileExistsError, 'Specified Satoshi wallet does not exist!' + return (plainPrivList, masterEncrKey, cryptPrivList, poolKeysList, addrNames) - wltDir,wltFile = os.path.split(wltPath) - db_env = create_env(wltDir) - json_db = {} - plain,mkey,crypt,pool = read_wallet(json_db, db_env, wltFile) - return len(crypt)>0 def extractSatoshiKeys(wltPath, passphrase=None): + # Returns a list of [privKey, usedYet] pairs if not os.path.exists(wltPath): raise FileExistsError, 'Specified Satoshi wallet does not exist!' wltDir,wltFile = os.path.split(wltPath) db_env = create_env(wltDir) - json_db = {} - plainkeys,mkey,crypt,pool = read_wallet(json_db, db_env, wltFile) + plainkeys,mkey,crypt,pool,names = read_wallet(db_env, wltFile) if len(crypt)>0: # Satoshi Wallet is encrypted! @@ -9209,22 +9187,38 @@ def extractSatoshiKeys(wltPath, passphrase=None): masterKey = CryptoAES().DecryptCBC( SecureBinaryData(mkey['mkey']), \ SecureBinaryData(pKey), \ SecureBinaryData(IV) ) - - print masterKey.toHexStr() masterKey.resize(32) + checkedCorrectPassphrase = False for pub,ckey in crypt: iv = hash256(pub)[:16] privKey = CryptoAES().DecryptCBC( SecureBinaryData(ckey), \ SecureBinaryData(masterKey), \ SecureBinaryData(iv)) privKey.resize(32) + if not checkedCorrectPassphrase: + checkedCorrectPassphrase = True + if not CryptoECDSA().CheckPubPrivKeyMatch(privKey, SecureBinaryData(pub)): + raise EncryptionError, 'Incorrect Passphrase!' plainkeys.append(privKey) - return plainkeys + outputList = [] + for key in plainkeys: + addr = hash160_to_addrStr(convertKeyDataToAddress(key.toBinStr())) + strName = '' + if names.has_key(addr): + strName = names[addr] + outputList.append( [addr, key, (not addr in pool), strName] ) + return outputList +def checkSatoshiEncrypted(wltPath): + try: + extractSatoshiKeys(wltPath, '') + return False + except EncryptionError: + return True diff --git a/qtdialogs.py b/qtdialogs.py index a94a1d2cb..b59383608 100755 --- a/qtdialogs.py +++ b/qtdialogs.py @@ -23,7 +23,6 @@ def __init__(self, wlt, parent=None, main=None, unlockMsg='Unlock Wallet'): self.edtPasswd = QLineEdit() self.edtPasswd.setEchoMode(QLineEdit.Password) self.edtPasswd.setMinimumWidth(MIN_PASSWD_WIDTH(self)) - fm = QFontMetricsF(QFont(self.font())) self.edtPasswd.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self.btnAccept = QPushButton("Unlock") @@ -55,6 +54,38 @@ def acceptPassphrase(self): return +################################################################################ +class DlgGenericGetPassword(QDialog): + def __init__(self, wlt, descriptionStr, parent=None, main=None): + super(DlgUnlockWallet, self).__init__(parent) + + self.parent = parent + self.main = main + + lblDescr = QRichLabel(descriptionStr) + lblPasswd = QRichLabel("Password:") + self.edtPasswd = QLineEdit() + self.edtPasswd.setEchoMode(QLineEdit.Password) + self.edtPasswd.setMinimumWidth(MIN_PASSWD_WIDTH(self)) + self.edtPasswd.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) + + self.btnAccept = QPushButton("OK") + self.btnCancel = QPushButton("Cancel") + self.connect(self.btnAccept, SIGNAL('clicked()'), self.accept) + self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) + buttonBox = QDialogButtonBox() + buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) + buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) + + layout = QGridLayout() + layout.addWidget(lblDescr, 1, 0, 1, 2) + layout.addWidget(lblPasswd, 2, 0, 1, 1) + layout.addWidget(self.edtPasswd, 2, 1, 1, 1) + layout.addWidget(buttonBox, 3, 1, 1, 2) + + self.setLayout(layout) + self.setWindowTitle('Enter Password') + self.setWindowIcon(QIcon(self.main.iconfile)) ################################################################################ @@ -479,7 +510,6 @@ def __init__(self, currName='', currDescr='', parent=None, main=None): self.edtDescr = QTextEdit() tightHeight = tightSizeNChar(self.edtDescr, 1)[1] - #fm = QFontMetricsF(QFont(self.edtDescr.font())) self.edtDescr.setMaximumHeight(tightHeight*4.2) lblDescr = QLabel("Wallet &description:") lblDescr.setAlignment(Qt.AlignVCenter) @@ -1493,13 +1523,24 @@ def __init__(self, wlt, parent=None, main=None): ## Import option - self.radioSweep = QRadioButton('Sweep any funds owned by this address ' - 'into your wallet\n' - 'Select this option if someone else gave you this key') self.radioImport = QRadioButton('Import this address to your wallet\n' 'Only select this option if you are positive ' 'that no one else has access to this key') + ## Sweep option (only available when online) + if self.main.isOnline: + self.radioSweep = QRadioButton('Sweep any funds owned by this address ' + 'into your wallet\n' + 'Select this option if someone else gave you this key') + self.radioSweep.setChecked(True) + else: + self.radioSweep = QRadioButton('Sweep any funds owned by this address ' + 'into your wallet\n' + '(Not available in offline mode)') + self.radioImport.setChecked(True) + self.radioSweep.setEnabled(False) + + sweepTooltip = createToolTipObject( \ 'You should never add an untrusted key to your wallet. By choosing this ' 'option, you are only moving the funds into your wallet, but not the key ' @@ -1516,7 +1557,7 @@ def __init__(self, wlt, parent=None, main=None): btngrp.addButton(self.radioSweep) btngrp.addButton(self.radioImport) btngrp.setExclusive(True) - self.radioSweep.setChecked(True) + frmWarn = QFrame() @@ -1708,6 +1749,7 @@ def processUserString(self): +############################################################################# class DlgVerifySweep(QDialog): def __init__(self, inputStr, outputStr, outVal, fee, parent=None, main=None): super(DlgVerifySweep, self).__init__(parent) @@ -1763,6 +1805,7 @@ def __init__(self, parent=None, main=None): lblImportDescr = QLabel('Chose the wallet import source:') self.btnImportFile = QPushButton("Import from &file") self.btnImportPaper = QPushButton("Import from &paper backup") + self.btnMigrate = QPushButton("Import wallet.dat from main Bitcoin client") self.btnImportFile.setMinimumWidth(300) @@ -1772,6 +1815,9 @@ def __init__(self, parent=None, main=None): self.connect( self.btnImportPaper, SIGNAL('clicked()'), \ self.acceptPaper) + self.connect( self.btnMigrate, SIGNAL('clicked()'), \ + self.acceptMigrate) + ttip1 = createToolTipObject('Import an existing Armory wallet, usually with a ' '*.wallet extension. Any wallet that you import will ' 'be copied into your settings directory, and maintained ' @@ -1781,6 +1827,9 @@ def __init__(self, parent=None, main=None): 'a wallet, you can manually enter the wallet ' 'data into Armory to recover the wallet.') + ttip3 = createToolTipObject('Migrate all your wallet.dat addresses ' + 'from the regular Bitcoin client to an Armory ' + 'wallet.') w,h = relaxedSizeStr(ttip1, '(?)') for ttip in (ttip1, ttip2): @@ -1792,6 +1841,7 @@ def __init__(self, parent=None, main=None): layout.addWidget(lblImportDescr, 0,0, 1, 2) layout.addWidget(self.btnImportFile, 1,0, 1, 2); layout.addWidget(ttip1, 1,2,1,1) layout.addWidget(self.btnImportPaper, 2,0, 1, 2); layout.addWidget(ttip2, 2,2,1,1) + layout.addWidget(self.btnMigrate, 3,0, 1, 2); layout.addWidget(ttip3, 3,2,1,1) if self.main.usermode in (USERMODE.Advanced, USERMODE.Developer): lbl = QLabel('You can manually add wallets to armory by copying them ' @@ -1829,10 +1879,295 @@ def acceptPaper(self): We will accept this dialog but signal to the caller that paper-import was selected so that it can open the dialog for itself """ - self.importType_file = False - self.importType_paper = True + self.importType_file = False + self.importType_paper = True + self.importType_migrate = False self.accept() + def acceptMigrate(self): + self.importType_file = False + self.importType_paper = False + self.importType_migrate = True + self.accept() + +############################################################################# +class DlgMigrateSatoshiWallet(QDialog): + def __init__(self, parent=None, main=None): + super(DlgMigrateSatoshiWallet, self).__init__(parent) + + self.parent = parent + self.main = main + + lblDescr = QRichLabel( \ + 'Specify the location of your regular Bitcoin wallet (wallet.dat) ' + 'to be imported into Armory. All private ' + 'keys will be migrated from the wallet.dat allowing you to use the ' + 'same addresses in Armory as you use with the regular ' + 'Bitcoin client.\n\nNOTE: It is strongly recommended that all ' + 'Bitcoin addresses be used in only one program at a time. If you ' + 'import your entire wallet.dat, it is recommended to stop using the ' + 'regular Bitcoin client, and only use Armory to send transactions. ' + 'Armory developers will not be responsible for coins getting "locked" ' + 'or "stuck" due to multiple applications attempting to spend coins ' + 'from the same addresses.') + + lblSatoshiWlt = QRichLabel('Wallet File to be Migrated (usually "wallet.dat")') + self.txtWalletPath = QLineEdit() + self.chkAllKeys = QCheckBox('Include Address Pool (unused keys)') + + btnGetFilename = QPushButton('Find...') + self.connect(btnGetFilename, SIGNAL('clicked()'), self.getSatoshiFilename) + + defaultWalletPath = os.path.join(BTC_HOME_DIR,'wallet.dat') + if os.path.exists(defaultWalletPath): + self.txtWalletPath.setText(defaultWalletPath) + + buttonBox = QDialogButtonBox() + self.btnAccept = QPushButton("Import") + self.btnReject = QPushButton("Cancel") + self.connect(self.btnAccept, SIGNAL('clicked()'), self.execMigrate) + self.connect(self.btnReject, SIGNAL('clicked()'), self.reject) + buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) + buttonBox.addButton(self.btnReject, QDialogButtonBox.RejectRole) + + + # Select the wallet into which you want to import + self.wltidlist = [''] + self.lstWallets = QListWidget() + self.lstWallets.addItem(QListWidgetItem('New Wallet...')) + for wltID in self.main.walletIDList: + wlt = self.main.walletMap[wltID] + wlttype = determineWalletType(self.main.walletMap[wltID], self.main)[0] + if wlttype in (WLTTYPES.WatchOnly, WLTTYPES.Offline): + continue + self.lstWallets.addItem( \ + QListWidgetItem('%s (%s)' % (wlt.labelName, wlt.uniqueIDB58) )) + self.wltidlist.append(wlt.uniqueIDB58) + self.lstWallets.setCurrentRow(0) + + + dlgLayout = QVBoxLayout() + dlgLayout.addWidget(lblDescr) + dlgLayout.addWidget(HLINE()) + dlgLayout.addWidget(lblSatoshiWlt) + dlgLayout.addWidget(makeHorizFrame([self.txtWalletPath, btnGetFilename])) + dlgLayout.addWidget(makeHorizFrame([self.chkAllKeys, 'Stretch'])) + dlgLayout.addWidget(HLINE()) + dlgLayout.addWidget(buttonBox) + + self.setLayout(dlgLayout) + + self.setWindowTitle('Migrate Satoshi Wallet') + self.setWindowIcon(QIcon( self.main.iconfile)) + + + def getSatoshiFilename(self): + # Temporarily reset the "LastDir" to where the default wallet.dat is + prevLastDir = self.settings.get('LastDirectory') + self.settings.set('LastDirectory', BTC_HOME_DIR) + satoshiWltFile = getFileLoad('Load Bitcoin Wallet File', ['Bitcoin Wallets (*.dat)']) + self.settings.set('LastDirectory', prevLastDir) + if len(str(satoshiWltFile))>0: + self.txtWalletPath.setText(satoshiWltFile) + + + + def execMigrate(self): + satoshiWltFile = str(self.txtWalletPath.text()) + if not os.path.exists(satoshiWltFile): + QMessageBox.critical(self, 'File does not exist!', \ + 'The specified file does not exist:\n\n' + satoshiWltFile, + QMessageBox.Ok) + return + + selectedRow = self.lstWallets.currentRow() + toWalletID = None + if selectedRow>0: + toWalletID = self.wltidlist[selectedRow-1] + + # KeyList is [addrB58, privKey, usedYet, acctName] + keyList = [] + if not checkSatoshiEncrypted(satoshiWltFile): + keyList = extractSatoshiKeys(satoshiWltFile, str(dlg.edtPasswd.text())) + else: + correctPassphrase = False + firstAsk = True + while not correctPassphrase: + redText = '' + if not firstAsk: + redText = 'Incorrect passphrase.\n\n' + firstAsk = False + + dlg = DlgGenericGetPassword( \ + redText + 'The wallet.dat file you specified is encrypted. ' + 'Please provide the passphrase to decrypt it.') + + if not dlg.exec_(): + return + else: + try: + keyList = extractSatoshiKeys(satoshiWltFile, str(dlg.edtPasswd.text())) + correctPassphrase = True + except EncryptionError: + pass + + + if not self.chkAllKeys.isChecked(): + keyList = filter(lambda x: x[2], keyList) + + + # Warn about addresses that would be duplicates. + # This filters the list down to addresses already in a wallet that isn't selected + # Addresses already in the selected wallet will simply be skipped, no need to + # do anything about that + addr_to_wltID = lambda a: self.main.getWalletForAddr160(addrStr_to_hash160(a)) + allWltList = [[addr_to_wltID(k[0]), k[0]] for k in keyList] + + dupeWltList = filter(lambda a: (len(a[0])>0 and a[0]!=toWalletID), allWltList) + dispStrList = [d[0].ljust(40) + d[0] for d in dupeWltList] + + if len(dispStrList)>0: + dlg = DlgDuplicateAddr(dispStrList, self, self.main) + if dlg.exec_(): + dupeAddrList = [a[1] for a in dupeWltList] + keyList = filter(lambda x: (x[0] not in dupeAddrList), keyList) + + + # Confirm import + addrList = [k[0].ljust(40)+k[3] for k in keyList] + dlg = DlgConfirmBulkImport(addrList, toWallet, self, self.main) + if not dlg.exec_(): + return + + # Okay, let's do it! + if self.wlt.useEncryption and self.wlt.isLocked: + # Target wallet is encrypted... + unlockdlg = DlgUnlockWallet(self.wlt, self, self.main, 'Unlock Wallet to Import') + if not unlockdlg.exec_(): + QMessageBox.critical(self, 'Wallet is Locked', \ + 'Cannot import private keys without unlocking wallet!', \ + QMessageBox.Ok) + return + + + nImport = 0 + for i,key4 in enumerate(keyList): + addrB58, sbdKey, isUsed, addrName = key4[:] + try: + a160 = addrStr_to_hash160(addrB58) + self.wlt.importExternalAddressData(privKey=sbdKey) + cmt = 'Imported #%03d'%i + if len(addrName)>0: + cmt += ': %s' % addrName + self.wlt.setComment(a160, cmt) + nImport += 1 + except Exception,msg: + print '***ERROR importing:', addrB58 + print ' Error Msg:', msg + + MsgBoxCustom(MSGBOX.Good, 'Success!', \ + 'Success: %d private keys were imported into your wallet. ' + 'Please restart Armory to guarantee that balances are computed ' + 'correctly') + self.accept() + + + + + + + + + +############################################################################# +class DlgConfirmBulkImport(QDialog): + def __init__(self, addrList, wlt, parent=None, main=None): + super(DlgConfirmBulkImport, self).__init__(parent) + + self.parent = parent + self.main = main + self.wlt = wlt + + if len(addrList)==0: + QMessageBox.warning(self, 'No Addresses to Import', \ + 'There are no addresses to import!', QMessageBox.Ok) + self.reject() + + lblDescr = QRichLabel( \ + 'You are about to import %d private keys into wallet, %s (%s). ' + 'The following is a list of addresses corresponding to the keys to ' + 'be imported:' % (len(addrList), wlt.uniqueIDB58, wlt.labelName)) + + fnt = GETFONT('Fixed',8) + w,h = tightSizeNChar(fnt, 50) + txtDispAddr = QTextEdit() + txtDispAddr.setFont(fnt) + txtDispAddr.setReadOnly(True) + txtDispAddr.setMinimumWidth(w) + txtDispAddr.setMinimumHeight(16.2*h) + txtDispAddr.setText( '\n'.join(addrList) ) + + buttonBox = QDialogButtonBox() + self.btnAccept = QPushButton("Import") + self.btnReject = QPushButton("Cancel") + self.connect(self.btnAccept, SIGNAL('clicked()'), self.accept) + self.connect(self.btnReject, SIGNAL('clicked()'), self.reject) + buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) + buttonBox.addButton(self.btnReject, QDialogButtonBox.RejectRole) + + dlgLayout = QVBoxLayout() + dlgLayout.addWidget(lblDescr) + dlgLayout.addWidget(txtDispAddr) + dlgLayout.addWidget(buttonBox) + self.setLayout(dlgLayout) + + self.setWindowTitle('Greetings!') + self.setWindowIcon(QIcon( self.main.iconfile)) + + +############################################################################# +class DlgDuplicateAddr(QDialog): + def __init__(self, addrList, wlt, parent=None, main=None): + super(DlgDuplicateAddr, self).__init__(parent) + + self.parent = parent + self.main = main + self.wlt = wlt + + if len(addrList)==0: + QMessageBox.warning(self, 'No Addresses to Import', \ + 'There are no addresses to import!', QMessageBox.Ok) + self.reject() + + lblDescr = QRichLabel( \ + 'Duplicate addresses detected! ' + 'The following addresses already exist in other Armory wallets:') + + fnt = GETFONT('Fixed',8) + w,h = tightSizeNChar(fnt, 50) + txtDispAddr = QTextEdit() + txtDispAddr.setFont(fnt) + txtDispAddr.setReadOnly(True) + txtDispAddr.setMinimumWidth(w) + txtDispAddr.setMinimumHeight(8.2*h) + txtDispAddr.setText( '\n'.join(addrList) ) + + buttonBox = QDialogButtonBox() + self.btnAccept = QPushButton("Import") + self.btnReject = QPushButton("Cancel") + self.connect(self.btnAccept, SIGNAL('clicked()'), self.accept) + self.connect(self.btnReject, SIGNAL('clicked()'), self.reject) + buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) + buttonBox.addButton(self.btnReject, QDialogButtonBox.RejectRole) + + dlgLayout = QVBoxLayout() + dlgLayout.addWidget(lblDescr) + dlgLayout.addWidget(txtDispAddr) + dlgLayout.addWidget(buttonBox) + self.setLayout(dlgLayout) + + self.setWindowTitle('Greetings!') + self.setWindowIcon(QIcon( self.main.iconfile)) ############################################################################# class DlgAddressInfo(QDialog): @@ -3144,11 +3479,8 @@ def __init__(self, parent=None, main=None, title='Select Wallet:', \ self.accept() return - - if wltIDList==None: wltIDList = list(self.main.walletIDList) - self.rowList = [] diff --git a/unittest.py b/unittest.py index ffbca61f0..66f8ececb 100644 --- a/unittest.py +++ b/unittest.py @@ -14,13 +14,14 @@ Test_TxSimpleCreate = False Test_EncryptedAddress = False Test_EncryptedWallet = False -Test_TxDistProposals = True +Test_TxDistProposals = False Test_SelectCoins = False Test_CryptoTiming = False Test_NetworkObjects = False Test_ReactorLoop = False Test_SettingsFile = False +Test_WalletMigrate = True ''' import optparse @@ -1802,5 +1803,25 @@ def printstat(): os.remove(testFile2) +if Test_WalletMigrate: + + import getpass + p = '/home/alan/winlinshare/wallet_plain.dat' + print 'Encrypted? ', checkSatoshiEncrypted(p) + plain = extractSatoshiKeys(p) + + print len(plain) + print sum([1 if p[2] else 0 for p in plain]) + print sum([0 if p[2] else 1 for p in plain]) + + p = '/home/alan/.bitcoin/wallet.dat' + print 'Encrypted? ', checkSatoshiEncrypted(p) + k = getpass.getpass('decrypt passphrase:') + crypt = extractSatoshiKeys(p, k) + + + print len(crypt) + print sum([1 if p[2] else 0 for p in crypt]) + print sum([0 if p[2] else 1 for p in crypt])